packages feed

irc-fun-client-0.5.0.0: src/Network/IRC/Fun/Client/Events.hs

{- This file is part of irc-fun-client.
 -
 - Written in 2015 by fr33domlover <fr33domlover@rel4tion.org>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

{-# LANGUAGE OverloadedStrings #-}

-- | This module allows you to identify IRC events. Events are a wrapper around
-- IRC messages, which provide a logical layer convenient to work with (while
-- IRC messages have their lower level aspects, being plain protocol messages).
module Network.IRC.Fun.Client.Events
    ( Event (..)
    , detectEvents
    , hGetIrcEventsOnce
    , hGetIrcEvents
    )
where

import Control.Monad (unless)
import Data.Maybe (fromMaybe, isNothing)
import Data.Text (Text)
import Network.IRC.Fun.Client.IO (Connection, Result (..), hGetIrcOnce)
import Network.IRC.Fun.Messages
import Network.IRC.Fun.Types

-- | An event triggered by an IRC message sent from the server.
data Event
    -- | A ping was sent to the bot. The parameters are the server name and
    -- optionally a server to forward the response to. They can be passed as-is
    -- directly to the PONG response.
    = Ping Hostname (Maybe Hostname)
    -- | A ping response sent by the server.
    | Pong Hostname (Maybe Hostname)
    -- | One or more users have been kicked from a channel for an optionally
    -- given reason. Parameters: Channel, nicknames, reason.
    | Kick Channel [Nickname] (Maybe Comment)
    -- | A user joined a channel. Parameters: Channel, nickname.
    | Join Channel Nickname
    -- | A user left a channel, optionally with a given reason. Parameters:
    -- Channel, nickname, reason.
    | Part Channel Nickname (Maybe Comment)
    -- | A user left the network, optonally for the given reason. Parameters:
    -- Nickname, reason.
    | Quit Nickname (Maybe Comment)
    -- | TODO
    | Mode
    -- | Message sent in a channel by a user. The last parameter indicates
    -- whether the message is actually a notice. If yes, the client shouldn't
    -- automatically send a response. Parameters: Channel, nickname, message,
    -- whether notice.
    | ChannelMessage Channel Nickname MsgContent Bool
    -- | A channel message that is a virtual action (/me).
    | ChannelAction Channel Nickname MsgContent
    -- | A private message sent specifically to the client from a user. The
    -- last parameter indicates whether the message is actually a notice. If
    -- yes, the client shouldn't send any automatic response. Parameters:
    -- Nickname, message, whether notice.
    | PrivateMessage Nickname MsgContent Bool
    -- | A private message that is a virtual action (/me).
    | PrivateAction Nickname MsgContent
    -- | A user's nickname has changed. Parameters: Old nick, new nick.
    | NickChange Nickname Nickname
    -- | Channel topic change. Parameterss: Channel, nickname of user who
    -- changed the topic, new topic content.
    | Topic Channel Nickname ChannelTopic
    -- | The client has been invited to a channel by another user. Parameters:
    -- Channel, nickname of inviting user.
    | Invite Channel Nickname
    -- | The server sent a list of nicknames present in a channel. Parameters:
    -- Channel privacy mode, channel name, list of users. Each list item is a
    -- pair of a user privilege level in the channel, and the user's nickname.
    | Names ChannelPrivacy Channel [(Privilege, Nickname)]
    -- | Unused, unrecognized or unimplemented event. A successfuly analyzed
    -- IRC message not (yet) recognized by this library as any of the client
    -- events above. Error message provided too. This event can be a result of
    -- an error or bug, missing features in this library or simply an IRC
    -- message that isn't interesting to an IRC client. Or perhaps the IRC
    -- server sent an invalid/empty message.
    | OtherSpecific (Either SpecificReply SpecificMessage) Text
    -- | Unrecognized or unimplemented event. An IRC protocol message not known
    -- to the message analyzer or whose analysis has failed. Error message
    -- provided too.
    | OtherGeneric GenericMessage Text
    -- | An IRC protocol message whose parsing has failed.
    | OtherRaw Text
    -- | Some other unrecognized event. The parameter contains possibly empty
    -- input.
    | OtherEvent Text
    deriving Show

detectMessageEvents :: SpecificMessage -> [Event]
detectMessageEvents sm@(SpecificMessage mpref msg) =
    case msg of
        NickMessage newnick -> withSender $ \ sender ->
            one $ NickChange sender newnick
        QuitMessage reason -> withSender $ \ sender ->
            one $ Quit sender reason
        JoinMessage (Just ([chan], [])) -> withSender $ \ sender ->
            one $ Join chan sender
        JoinMessage _ -> other "Expected JOIN with a single channel and no key"
        PartMessage [chan] reason -> withSender $ \ sender ->
            one $ Part chan sender reason
        PartMessage _ _ -> other "Expected PART with a single channel"
        TopicMessage _ Nothing -> other "Expected a topic in TOPIC"
        TopicMessage chan (Just topic) -> withSender $ \ sender ->
            one $ Topic chan sender topic
        PingMessage s ms ->
            one $ Ping s ms
        PongMessage s ms ->
            one $ Pong s ms
        KickMessage [] _ _ -> other "Empty channel list"
        KickMessage _ [] _ -> other "Empty nickname list"
        KickMessage [chan] nicks comment ->
            one $ Kick chan (map u2n nicks) comment
        KickMessage chans nicks comment ->
            if length chans == length nicks
                then map (\ (c, n) -> Kick c [u2n n] comment) $ zip chans nicks
                else other "Channel and nickname lists not in same length"
        PrivMsgMessage (ChannelTarget chan) mc -> withSender $ \ sender ->
            one $ ChannelMessage chan sender mc False
        PrivMsgMessage (UserTarget _ _ _) mc -> withSender $ \ sender ->
            one $ PrivateMessage sender mc False
        PrivMsgMessage (MaskTarget _) _ -> other "Expected user/channel target"
        PrivActionMessage (ChannelTarget chan) mc -> withSender $ \ sender ->
            one $ ChannelAction chan sender mc
        PrivActionMessage (UserTarget _ _ _) mc -> withSender $ \ sender ->
            one $ PrivateAction sender mc
        PrivActionMessage (MaskTarget _) _ -> other "Expected user/chan target"
        _ -> other "Currently not recognized as a client event"
    where
    one e = [e]
    other = one . OtherSpecific (Right sm)
    withSender f =
        case mpref of
            Just (PrefixNick n _ _) -> f n
            Just (PrefixServer _) ->
                other "Expected nickname in prefix, but found server"
            Nothing ->
                other "Expected nickname in prefix, but no prefix found"
    u2n (Username u) = Nickname u

detectReplyEvents :: SpecificReply -> [Event]
detectReplyEvents sr@(SpecificReply _sender _target rpl) =
    case rpl of
        NamesReply priv chan pnicks -> one $ Names priv chan pnicks
        _ -> other "Currently not recognized as a client event"
    where
    one e = [e]
    other = one . OtherSpecific (Left sr)

-- | Try to generate events from an IRC message.
detectEvents :: Either SpecificReply SpecificMessage -> [Event]
detectEvents = either detectReplyEvents detectMessageEvents

-- | Receive IRC events. If parsing the message fails, one of the @Other*@
-- events is returned according to the error.
hGetIrcEventsOnce :: Connection -> IO [Event]
hGetIrcEventsOnce c = do
    res <- hGetIrcOnce c
    return $ case res of
        ParsingFailed t       -> [OtherRaw t]
        AnalysisFailed gm err -> [OtherGeneric gm err]
        GotReply sr           -> detectReplyEvents sr
        GotMessage sm         -> detectMessageEvents sm

-- | Receive the next valid (successfully parsed and detected) series of IRC
-- events, in the second element. The first element is failure events (i.e. one
-- of the @Other*@ constructors) received before that, if there were any.
hGetIrcEvents :: Connection -> IO ([Event], [Event])
hGetIrcEvents c = do
    (l, m) <- f []
    return (reverse l, m)
    where
    f errs = do
        res <- hGetIrcEventsOnce c
        case res of
            [o@OtherSpecific {}] -> f $ o : errs
            [o@OtherGeneric {}]  -> f $ o : errs
            [o@OtherRaw {}]      -> f $ o : errs
            [o@OtherEvent {}]    -> f $ o : errs
            _                    -> return (errs, res)