packages feed

irc-fun-bot-0.6.0.0: src/Network/IRC/Fun/Bot/Internal/Event.hs

{- This file is part of irc-fun-bot.
 -
 - 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 #-}

module Network.IRC.Fun.Bot.Internal.Event
    ( modId
    , modPrefix
    , modPrefixes
    , modPrefixCI
    , modPrefixesCI
    , modPleasePrefix
    , modPleasePrefix'
    , matchPrefixedCommand
    , matchPrefixedCommandFromSet
    , matchPrefixedCommandFromNames
    , matchRefCommand
    , matchRefCommandFromSet
    , matchRefCommandFromNames
    , matchPlainPrivateCommand
    , matchNotice
    , matchRef
    , defaultMatch
    , matchEvent
    , handleEvent
    )
where

import Control.Monad (when)
import Control.Monad.IO.Class (liftIO)
import Data.Char (isSpace)
import Data.Maybe
import Data.List (find)
import Data.Text (Text)
import Network.IRC.Fun.Bot.Internal.Chat (pong, sendBack)
import Network.IRC.Fun.Bot.Internal.Failure
import Network.IRC.Fun.Bot.Internal.Monad (gets)
import Network.IRC.Fun.Bot.Internal.Nicks
import Network.IRC.Fun.Bot.Internal.State
import Network.IRC.Fun.Bot.Internal.Types hiding (Logger)
import Network.IRC.Fun.Bot.Behavior (findCmd, findCmdInSet)
import Network.IRC.Fun.Client.ChannelLogger hiding (LogEvent (..))
import Network.IRC.Fun.Client.IO (connNickname)
import Network.IRC.Fun.Client.Util (mentions)
import Network.IRC.Fun.Types hiding (Command)

import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Lazy as M
import qualified Data.Text as T
import qualified Network.IRC.Fun.Client.ChannelLogger as L
import qualified Network.IRC.Fun.Client.Events as C (Event (..))

-------------------------------------------------------------------------------
-- Modifiers
-------------------------------------------------------------------------------

modId :: Text -> Text
modId = id

modPrefix :: Text -> Bool -> Text -> Text
modPrefix p d s =
    case T.stripPrefix p s of
        Just s' -> if d then T.stripStart s' else s'
        Nothing -> s

modPrefixes :: [Text] -> Bool -> Text -> Text
modPrefixes ps d s =
    case listToMaybe $ mapMaybe (flip T.stripPrefix s) ps of
        Just s' -> if d then T.stripStart s' else s'
        Nothing -> s

stripPrefixCI :: Text -> Text -> Maybe Text
stripPrefixCI pref t =
    let len = T.length pref
        (p, r) = T.splitAt len t
    in  if T.toCaseFold p == T.toCaseFold pref
            then Just r
            else Nothing

modPrefixCI :: Text -> Bool -> Text -> Text
modPrefixCI p d s =
    case stripPrefixCI p s of
        Just s' -> if d then T.stripStart s' else s'
        Nothing -> s

modPrefixesCI :: [Text] -> Bool -> Text -> Text
modPrefixesCI ps d s =
    case listToMaybe $ mapMaybe (flip stripPrefixCI s) ps of
        Just s' -> if d then T.stripStart s' else s'
        Nothing -> s

modPleasePrefix :: Text -> Text
modPleasePrefix = modPrefixCI "please" True

modPleasePrefix' :: Text -> Text
modPleasePrefix' = modPrefixesCI ["please", "plz", "pls"] True

-------------------------------------------------------------------------------
-- Make Events
-------------------------------------------------------------------------------

detectRef :: Config -> MsgContent -> Maybe MsgContent
detectRef conf msg =
    let bnick = unNickname $ connNickname (cfgConnection conf)
        dw = Just . MsgContent . T.stripStart
    in  case T.stripPrefix bnick (unMsgContent msg) >>= T.uncons of
            Nothing        -> Nothing
            Just (',', s) -> dw s
            Just (':', s) -> dw s
            Just (';', s) -> dw s
            Just (c, s)   -> if isSpace c then dw s else Nothing

mkCmd :: MsgContent -> (CommandName, [Text])
mkCmd message =
    let w = T.words $ unMsgContent message
        name = CommandName $ CI.mk $ if null w then T.empty else head w
        args = if null w then [] else tail w
    in  (name, args)

expand :: [CommandName] -> Maybe (CommandSet e s) -> [CommandName]
expand ns Nothing     = ns
expand ns (Just cset) =
    let ls = map cmdNames $ csetCommands cset
    in  concat $ mapMaybe (\ n -> find (n `elem`) ls) ns

makePrefixedCommand
    :: Maybe Config
    -> [CommandSet e s]
    -> MessageSource
    -> Char
    -> MsgContent
    -> Maybe Event
makePrefixedCommand mconf csets src pref (MsgContent msg) =
    let (pref', msg') = fromMaybe (pref, msg) $ do
            conf <- mconf
            rest <- detectRef conf (MsgContent $ pref `T.cons` msg)
            T.uncons $ unMsgContent rest
    in  if pref' `elem` map csetPrefix csets && not (T.null msg')
            then
                let (name, args) = mkCmd $ MsgContent msg'
                in  Just $ BotCommand src (Just pref') name args
            else Nothing

makePrefixedCommandFromSet
    :: Maybe Config
    -> CommandSet e s
    -> MessageSource
    -> Char
    -> MsgContent
    -> Maybe Event
makePrefixedCommandFromSet mconf cset =
    let names = concatMap cmdNames $ csetCommands cset
    in  makePrefixedCommandFromNames mconf (Left $ csetPrefix cset) names

makePrefixedCommandFromNames
    :: Maybe Config
    -> Either Char (CommandSet e s)
    -> [CommandName]
    -> MessageSource
    -> Char
    -> MsgContent
    -> Maybe Event
makePrefixedCommandFromNames mconf eith names src pref (MsgContent msg) =
    let (pref', msg') = fromMaybe (pref, msg) $ do
            conf <- mconf
            rest <- detectRef conf $ MsgContent $ pref `T.cons` msg
            T.uncons $ unMsgContent rest
    in  if pref' == either id csetPrefix eith && not (T.null msg')
            then
                let (name, args) = mkCmd $ MsgContent msg'
                    cset = either (const Nothing) Just eith
                in  if name `elem` expand names cset
                        then Just $ BotCommand src (Just pref') name args
                        else Nothing
            else Nothing

makeRefCommand :: Config
               -> MessageSource
               -> (Text -> Text)
               -> MsgContent
               -> Maybe Event
makeRefCommand conf src f msg =
    case detectRef conf msg of
        Just (MsgContent s) ->
            let (name, args) = mkCmd $ MsgContent $ f s
            in  Just $ BotCommand src Nothing name args
        Nothing -> Nothing

makeRefCommandFromSet :: Config
                      -> CommandSet e s
                      -> MessageSource
                      -> (Text -> Text)
                      -> MsgContent
                      -> Maybe Event
makeRefCommandFromSet conf cset =
    let names = concatMap cmdNames $ csetCommands cset
    in  makeRefCommandFromNames conf Nothing names

makeRefCommandFromNames :: Config
                        -> Maybe (CommandSet e s)
                        -> [CommandName]
                        -> MessageSource
                        -> (Text -> Text)
                        -> MsgContent
                        -> Maybe Event
makeRefCommandFromNames conf cset names src f msg =
    case detectRef conf msg of
        Just (MsgContent s) ->
            let (name, args) = mkCmd $ MsgContent $ f s
            in  if name `elem` expand names cset
                    then Just $ BotCommand src Nothing name args
                    else Nothing
        Nothing -> Nothing

makePlainCommand :: MessageSource
                 -> MsgContent
                 -> Maybe Event
makePlainCommand src msg =
    let (name, args) = mkCmd msg
    in  Just $ BotCommand src Nothing name args

makeRefC :: Config -> Channel -> Nickname -> MsgContent -> Maybe Event
makeRefC conf chan nick msg =
    case detectRef conf msg of
        Just s  -> Just $ BotMessage chan nick s msg
        Nothing -> Nothing

makeRefP :: Config -> Nickname -> MsgContent -> Maybe Event
makeRefP conf nick msg =
    case detectRef conf msg of
        Just s  -> Just $ PersonalMessage nick s
        Nothing -> Nothing

-------------------------------------------------------------------------------
-- Match Events
-------------------------------------------------------------------------------

ifPriv :: EventMatchSpace -> Maybe Event -> Maybe Event
ifPriv MatchInChannel _ = Nothing
ifPriv _              e = e

ifChan :: EventMatchSpace -> Maybe Event -> Maybe Event
ifChan MatchInPrivate _ = Nothing
ifChan _              e = e

unconsMsg :: MsgContent -> Maybe (Char, MsgContent)
unconsMsg msg =
    case T.uncons $ unMsgContent msg of
        Nothing     -> Nothing
        Just (c, t) -> Just (c, MsgContent t)

matchPrefixedCommand
    :: EventMatchSpace
    -> Bool
    -> EventMatcher e s
matchPrefixedCommand space ref event conf csets =
    case event of
        C.ChannelMessage chan nick msg False -> ifChan space $ do
            (c, t) <- unconsMsg msg
            makePrefixedCommand mconf csets (SrcChannel chan nick) c t
        C.PrivateMessage nick msg False -> ifPriv space $ do
            (c, t) <- unconsMsg msg
            makePrefixedCommand mconf csets (SrcUser nick) c t
        _ -> Nothing
    where
    mconf = if ref then Just conf else Nothing

matchPrefixedCommandFromSet
    :: EventMatchSpace
    -> Bool
    -> Maybe (CommandSet e s)
    -> EventMatcher e s
matchPrefixedCommandFromSet space ref mcset event conf csets =
    case maybe (listToMaybe csets) Just mcset of
        Nothing   -> Nothing
        Just cset ->
            case event of
                C.ChannelMessage chan nick msg False -> ifChan space $ do
                    (c, t) <- unconsMsg msg
                    makePrefixedCommandFromSet
                        mconf cset (SrcChannel chan nick) c t
                C.PrivateMessage nick msg False -> ifPriv space $ do
                    (c, t) <- unconsMsg msg
                    makePrefixedCommandFromSet
                        mconf cset (SrcUser nick) c t
                _ -> Nothing
    where
    mconf = if ref then Just conf else Nothing

matchPrefixedCommandFromNames
    :: EventMatchSpace
    -> Bool
    -> Either Char (CommandSet e s)
    -> [CommandName]
    -> EventMatcher e s
matchPrefixedCommandFromNames space ref eith names event conf _csets =
    case event of
        C.ChannelMessage chan nick msg False -> ifChan space $ do
            (c, t) <- unconsMsg msg
            makePrefixedCommandFromNames
                mconf eith names (SrcChannel chan nick) c t
        C.PrivateMessage nick msg False -> ifPriv space $ do
            (c, t) <- unconsMsg msg
            makePrefixedCommandFromNames
                mconf eith names (SrcUser nick) c t
        _ -> Nothing
    where
    mconf = if ref then Just conf else Nothing

matchRefCommand :: EventMatchSpace -> (Text -> Text) -> EventMatcher e s
matchRefCommand space f event conf _csets =
    case event of
        C.ChannelMessage chan nick msg False ->
            ifChan space $ makeRefCommand conf (SrcChannel chan nick) f msg
        C.PrivateMessage nick msg False ->
            ifPriv space $ makeRefCommand conf (SrcUser nick) f msg
        _ -> Nothing

matchRefCommandFromSet
    :: EventMatchSpace
    -> (Text -> Text)
    -> EventMatcher e s
matchRefCommandFromSet _     _ _     _    []       = Nothing
matchRefCommandFromSet space f event conf (cset:_) =
    case event of
        C.ChannelMessage chan nick msg False -> ifChan space $
            makeRefCommandFromSet conf cset (SrcChannel chan nick) f msg
        C.PrivateMessage nick msg False -> ifPriv space $
            makeRefCommandFromSet conf cset (SrcUser nick) f msg
        _ -> Nothing

matchRefCommandFromNames
    :: EventMatchSpace
    -> (Text -> Text)
    -> Bool
    -> [CommandName]
    -> EventMatcher e s
matchRefCommandFromNames space f ex names event conf csets =
    case event of
        C.ChannelMessage chan nick msg False -> ifChan space $
            makeRefCommandFromNames conf cset names (SrcChannel chan nick) f msg
        C.PrivateMessage nick msg False -> ifPriv space $
            makeRefCommandFromNames conf cset names (SrcUser nick) f msg
        _ -> Nothing
    where
    cset = if ex then listToMaybe csets else Nothing

matchPlainPrivateCommand :: EventMatcher e s
matchPlainPrivateCommand event _conf _csets =
    case event of
        C.PrivateMessage nick msg False ->
            makePlainCommand (SrcUser nick) msg
        _ -> Nothing

matchNotice :: EventMatchSpace -> EventMatcher e s
matchNotice space event _conf _csets =
    case event of
        C.ChannelMessage chan nick msg True ->
            ifChan space $ Just $ Notice (Just chan) nick msg
        C.PrivateMessage nick msg True ->
            ifPriv space $ Just $ Notice Nothing nick msg
        _ -> Nothing

matchRef :: EventMatchSpace -> EventMatcher e s
matchRef space event conf _csets =
    case event of
        C.ChannelMessage chan nick msg False ->
            ifChan space $ makeRefC conf chan nick msg
        C.PrivateMessage nick msg False ->
            ifPriv space $ makeRefP conf nick msg
        _ -> Nothing

defaultMatch :: EventMatcher e s
defaultMatch event conf _csets =
    case event of
        C.Ping server1 server2      -> Just $ Ping server1 server2
        C.Kick channel nicks reason -> Just $ Kick channel nicks reason
        C.Join channel nick         -> Just $ Join channel nick
        C.Part channel nick reason  -> Just $ Part channel nick reason
        C.Quit nick reason          -> Just $ Quit nick reason
        C.ChannelMessage channel nick msg False ->
            Just $ Message channel nick msg $ msg `mentions` bnick
        C.ChannelAction channel nick msg ->
            Just $ Action channel nick msg $ msg `mentions` bnick
        C.PrivateMessage nick msg False -> Just $ PersonalMessage nick msg
        C.PrivateAction nick msg -> Just $ PersonalAction nick msg
        C.NickChange oldnick newnick -> Just $ NickChange oldnick newnick
        C.Topic channel nick topic -> Just $ TopicChange channel nick topic
        C.Names priv chan pnicks -> Just $ Names chan priv pnicks
        _ -> Nothing
    where
    bnick = connNickname (cfgConnection conf)

combineMatchers :: [EventMatcher e s] -> EventMatcher e s
combineMatchers []     _event _conf _csets = Nothing
combineMatchers (m:ms) event  conf  csets  =
    case m event conf csets of
       ev@(Just _) -> ev
       Nothing     -> combineMatchers ms event conf csets

applyMatchers
    :: [EventMatcher e s]
    -> C.Event
    -> Config
    -> [CommandSet e s]
    -> Event
applyMatchers ms event conf csets =
    fromMaybe (OtherEvent $ T.pack $ show event) $
    combineMatchers ms event conf csets

matchEvent
    :: [EventMatcher e s]
    -> C.Event
    -> Config
    -> [CommandSet e s]
    -> Event
matchEvent = applyMatchers

findCommand
    :: Maybe Char
    -> CommandName
    -> [CommandSet e s]
    -> Maybe (Either (CommandSet e s) (Command e s))
findCommand (Just cpref) cname csets    = findCmd cpref cname csets
findCommand Nothing      _     []       = Nothing
findCommand Nothing      cname (cset:_) =
    Just $ maybe (Left cset) Right $ findCmdInSet cname cset

-- Run the command with the given prefix character, command name and list of
-- parameters. If a command with the given prefix and name isn't found, the bot
-- sends a default friendly response.
runCommand
    :: Maybe Char    -- Command prefix, 'Nothing' picks the default prefix
    -> CommandName   -- Command name
    -> [Text]        -- List of parameters
    -> Maybe Channel -- Channel in which the command was triggered
    -> Nickname      -- Nickname of user who triggered the command
    -> Session e s ()
runCommand cpref cname cparams mchan sender = do
    csets <- askBehaviorS commandSets
    chans <- getChans
    let defresp chan =
            fromMaybe True $ fmap csDefResponse $ M.lookup chan chans
    case findCommand cpref cname csets of
        Nothing          ->
            case mchan of
                Just chan ->
                    when (defresp chan) $
                    defaultRespondToChan chan cpref cname Nothing
                Nothing -> defaultRespondToUser sender cpref cname Nothing
        Just (Left cset) ->
            case mchan of
                Just chan ->
                    when (defresp chan) $
                    defaultRespondToChan
                        chan
                        (Just $ csetPrefix cset)
                        cname
                        (Just cset)
                Nothing ->
                    defaultRespondToUser
                        sender (Just $ csetPrefix cset) cname (Just cset)
        Just (Right cmd) ->
            cmdRespond cmd mchan sender cparams (sendBack mchan sender)

-- React to a bot event.
handleBotEvent :: Event -> Session e s ()
handleBotEvent event = do
    b <- askBehavior
    case event of
        Ping s1 s2 -> pong s1 s2
        Kick _chan _users _why -> return ()
        Join chan user -> do
            tracked <- channelIsTracked chan
            when tracked $ addMember chan user
            self <- askConfigS $ connNickname . cfgConnection
            when (user == self) $ addCurrChan chan
            handleJoin b chan user
        Part chan nick why -> do
            tracked <- channelIsTracked chan
            when tracked $ removeMemberOnce chan nick
            handlePart b chan nick why
        Quit nick why -> do
            removeMember nick
            handleQuit b nick why
        Message chan sender msg mentioned ->
            handleMsg b chan sender msg mentioned
        Action chan sender msg mentioned ->
            handleAction b chan sender msg mentioned
        Notice _mchan _sender _msg -> return ()
        BotMessage chan sender msg full -> handleBotMsg b chan sender msg full
        BotCommand (SrcChannel chan sender) cpref cname cargs ->
            runCommand cpref cname cargs (Just chan) sender
        BotCommand (SrcUser sender) cpref cname cargs ->
            runCommand cpref cname cargs Nothing sender
        PersonalMessage sender msg -> handlePersonalMsg b sender msg
        PersonalAction sender msg -> handlePersonalAction b sender msg
        NickChange oldnick newnick -> do
            changeNick oldnick newnick
            handleNickChange b oldnick newnick
        TopicChange chan nick topic -> handleTopicChange b chan nick topic
        Names chan priv pnicks -> do
            tracked <- channelIsTracked chan
            let nicks = map snd pnicks
            when tracked $ addChannel chan nicks
            handleNames b chan priv pnicks
        OtherEvent _t -> return ()

-- Using nick tracking and logging state, determine from a general log event a
-- set of channel loggers and channel-specific log events to write into them.
detectLogEvents :: L.LogEvent -> Session e s [(Logger, ChanLogEvent)]
detectLogEvents e =
    let detect event cstate = fmap (\ cl -> (cl, event)) $ csLogger cstate
        detectOne chan event = do
            cstates <- gets bsChannels
            return $ maybeToList $ M.lookup chan cstates >>= detect event
        detectMany nick event = do
            chans <- presence nick
            cstates <- gets bsChannels
            let cstatesP =
                    cstates `M.difference` M.fromList (zip chans (repeat ()))
            return $ catMaybes $ map (detect event) $ M.elems cstatesP
    in  case e of
            L.Enter nick chan       -> detectOne chan $ EnterChan nick
            L.Leave nick chan       -> detectOne chan $ LeaveChan nick
            L.LeaveAll nick         -> detectMany nick $ LeaveChan nick
            L.Message nick chan msg -> detectOne chan $ MessageChan nick msg
            L.Action nick chan msg  -> detectOne chan $ ActInChan nick msg
            L.Rename oldN newN      -> detectMany oldN $ RenameInChan oldN newN

-- Possibly write a log event into the right file(s), according to logging
-- settings.
handleLogEvent :: L.LogEvent -> Session e s ()
handleLogEvent e = do
    l <- detectLogEvents e
    liftIO $ mapM_ (\ (logger, event) -> logEvent logger event) l

-- | Handle a bot event, or log a log event into a file.
handleEvent :: Either L.LogEvent Event -> Session e s ()
handleEvent = either handleLogEvent handleBotEvent