Changed msg handler design to make them stateful, added core IRC msg handlers

master
Abhinav Sarkar 2014-05-11 14:01:09 +05:30
parent 964d2fbb35
commit 74be6dd162
10 changed files with 270 additions and 111 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ cabal.sandbox.config
dist dist
config.cfg config.cfg
*sublime* *sublime*
logs

View File

@ -41,8 +41,8 @@ loadBotConfig configFile = do
channel <- CF.require cfg "channel" channel <- CF.require cfg "channel"
botNick <- CF.require cfg "nick" botNick <- CF.require cfg "nick"
timeout <- CF.require cfg "timeout" timeout <- CF.require cfg "timeout"
handlers <- CF.require cfg "handlers" msghandlers <- CF.require cfg "msghandlers"
return $ BotConfig server port channel botNick timeout handlers cfg return $ BotConfig server port channel botNick timeout msghandlers cfg
case eBotConfig of case eBotConfig of
Left (KeyError k) -> error $ "Error while reading key from config: " ++ unpack k Left (KeyError k) -> error $ "Error while reading key from config: " ++ unpack k

View File

@ -5,10 +5,11 @@ module Network.IRC.Client (run) where
import qualified Data.Text.Format as TF import qualified Data.Text.Format as TF
import qualified Data.Text.Format.Params as TF import qualified Data.Text.Format.Params as TF
import ClassyPrelude hiding (log) import ClassyPrelude
import Control.Concurrent import Control.Concurrent
import Control.Monad.Reader hiding (forM_) import Control.Monad.Reader hiding (forM_, foldM)
import Control.Monad.State hiding (forM_) import Control.Monad.State hiding (forM_, foldM)
import Data.Maybe (fromJust)
import Network import Network
import System.IO (hSetBuffering, BufferMode(..)) import System.IO (hSetBuffering, BufferMode(..))
import System.Timeout import System.Timeout
@ -20,14 +21,17 @@ import Network.IRC.Types
oneSec :: Int oneSec :: Int
oneSec = 1000000 oneSec = 1000000
log :: Text -> IO () debug :: Text -> IO ()
log msg = getCurrentTime >>= \t -> TF.print "[{}] ** {}\n" $ TF.buildParams (t, msg) debug msg = do
time <- getCurrentTime
TF.print "[{}] {}\n" $ TF.buildParams (formatTime defaultTimeLocale "%F %T" time, msg)
sendCommand :: Bot -> Command -> IO () sendCommand :: Bot -> Command -> IO ()
sendCommand Bot { .. } reply = do sendCommand Bot { .. } reply = do
time <- getCurrentTime
let line = lineFromCommand botConfig reply let line = lineFromCommand botConfig reply
TF.hprint socket "{}\r\n" $ TF.Only line TF.hprint socket "{}\r\n" $ TF.Only line
TF.print "> {}\n" $ TF.Only line TF.print "[{}} > {}\n" $ TF.buildParams (formatTime defaultTimeLocale "%F %T" time, line)
listen :: IRC () listen :: IRC ()
listen = do listen = do
@ -44,64 +48,92 @@ listen = do
Nothing -> return Disconnected Nothing -> return Disconnected
Just line -> do Just line -> do
now <- getCurrentTime now <- getCurrentTime
TF.print "[{}] {}\n" $ TF.buildParams (now, line) debug $ "< " ++ line
let message = msgFromLine botConfig now line let message = msgFromLine botConfig now line
case message of case message of
JoinMsg { .. } | userNick user == nick -> log "Joined" >> return Joined JoinMsg { .. } | userNick user == nick -> debug "Joined" >> return Joined
KickMsg { .. } | kicked == nick -> log "Kicked" >> return Kicked KickMsg { .. } | kicked == nick -> debug "Kicked" >> return Kicked
_ -> do ModeMsg { user = Self, .. } -> sendCommand bot JoinCmd >> return status
forkIO $ case message of _ -> return status
Ping { .. } -> sendCommand bot $ Pong msg
ModeMsg { user = Self, .. } -> sendCommand bot JoinCmd forM_ (msgHandlers botConfig) $ \msgHandlerName -> forkIO $ do
msg -> forM_ (handlers botConfig) $ \handlerName -> forkIO $ do let mMsgHandler = getMsgHandler msgHandlerName
let mHandler = getHandler handlerName case mMsgHandler of
case mHandler of Nothing -> debug $ "No msg handler found with name: " ++ msgHandlerName
Nothing -> log $ "No handler found with name: " ++ handlerName Just msgHandler -> do
Just handler -> do let msgHandlerState = fromJust . lookup msgHandlerName $ msgHandlerStates
mCmd <- runHandler handler botConfig msg mCmd <- runMsgHandler msgHandler botConfig msgHandlerState message
case mCmd of case mCmd of
Nothing -> return () Nothing -> return ()
Just cmd -> sendCommand bot cmd Just cmd -> sendCommand bot cmd
return status return status
put nStatus put nStatus
when (nStatus /= Disconnected) listen when (nStatus /= Disconnected) listen
connect :: BotConfig -> IO Bot connect :: BotConfig -> IO Bot
connect botConfig@BotConfig { .. } = do connect botConfig@BotConfig { .. } = do
log "Connecting ..." debug "Connecting ..."
socket <- connectToWithRetry socket <- connectToWithRetry
hSetBuffering socket LineBuffering hSetBuffering socket LineBuffering
hSetBuffering stdout LineBuffering msgHandlerStates <- loadMsgHandlers botConfig
log "Connected" debug "Connected"
return $ Bot botConfig socket return $ Bot botConfig socket msgHandlerStates
where where
connectToWithRetry = connectTo server (PortNumber (fromIntegral port)) connectToWithRetry = connectTo (unpack server) (PortNumber (fromIntegral port))
`catch` (\(e :: SomeException) -> do `catch` (\(e :: SomeException) -> do
log ("Error while connecting: " ++ pack (show e) ++ ". Waiting.") debug ("Error while connecting: " ++ pack (show e) ++ ". Waiting.")
threadDelay (5 * oneSec) threadDelay (5 * oneSec)
connectToWithRetry) connectToWithRetry)
loadMsgHandlers :: BotConfig -> IO MsgHandlerStates
loadMsgHandlers botConfig@BotConfig { .. } =
flip (`foldM` mapFromList []) msgHandlers $ \hMap msgHandlerName -> do
debug $ "Loading msg handler: " ++ msgHandlerName
let mMsgHandler = getMsgHandler msgHandlerName
case mMsgHandler of
Nothing -> debug ("No msg handler found with name: " ++ msgHandlerName) >> return hMap
Just msgHandler -> do
msgHandlerState <- initMsgHandler msgHandler botConfig
return $ insertMap msgHandlerName msgHandlerState hMap
unloadMsgHandlers :: Bot -> IO ()
unloadMsgHandlers Bot { .. } =
forM_ (mapToList msgHandlerStates) $ \(msgHandlerName, msgHandlerState) -> do
debug $ "Unloading msg handler: " ++ msgHandlerName
let mMsgHandler = getMsgHandler msgHandlerName
case mMsgHandler of
Nothing -> debug ("No msg handler found with name: " ++ msgHandlerName)
Just msgHandler -> exitMsgHandler msgHandler botConfig msgHandlerState
disconnect :: Bot -> IO () disconnect :: Bot -> IO ()
disconnect bot = do disconnect bot@Bot { .. } = do
log "Disconnecting ..." debug "Disconnecting ..."
hClose . socket $ bot unloadMsgHandlers bot
log "Disconnected" hClose socket
debug "Disconnected"
addCoreMsgHandlers :: BotConfig -> BotConfig
addCoreMsgHandlers botConfig =
botConfig { msgHandlers = hashNub $ msgHandlers botConfig ++ coreMsgHandlerNames }
run :: BotConfig -> IO () run :: BotConfig -> IO ()
run botConfig = withSocketsDo $ do run botConfig' = withSocketsDo $ do
log "Running with config:" hSetBuffering stdout LineBuffering
debug "Running with config:"
print botConfig print botConfig
status <- run_ status <- run_
case status of case status of
Disconnected -> log "Connection timed out" >> run botConfig Disconnected -> debug "Connection timed out" >> run botConfig
Errored -> return () Errored -> return ()
_ -> error "Unsupported status" _ -> error "Unsupported status"
where where
botConfig = addCoreMsgHandlers botConfig'
run_ = bracket (connect botConfig) disconnect $ \bot -> run_ = bracket (connect botConfig) disconnect $ \bot ->
go bot `catch` \(e :: SomeException) -> do go bot `catch` \(e :: SomeException) -> do
log $ "Exception! " ++ pack (show e) debug $ "Exception! " ++ pack (show e)
return Errored return Errored
go bot = do go bot = do

View File

@ -1,33 +1,44 @@
{-# LANGUAGE RecordWildCards, OverloadedStrings, NoImplicitPrelude #-} {-# LANGUAGE RecordWildCards, OverloadedStrings, NoImplicitPrelude, FlexibleContexts #-}
module Network.IRC.Handlers (getHandler) where module Network.IRC.Handlers (coreMsgHandlerNames, getMsgHandler) where
import qualified Network.IRC.Handlers.Core as C
import qualified Network.IRC.Handlers.SongSearch as SS
import ClassyPrelude import ClassyPrelude
import Control.Monad.Reader
import Data.Text (strip) import Data.Text (strip)
import Network.IRC.Handlers.SongSearch
import Network.IRC.Types import Network.IRC.Types
clean :: Text -> Text clean :: Text -> Text
clean = toLower . strip clean = toLower . strip
getHandler :: HandlerName -> Maybe Handler coreMsgHandlerNames :: [Text]
getHandler "greeter" = Just $ Handler greeter coreMsgHandlerNames = ["pingpong", "messagelogger"]
getHandler "welcomer" = Just $ Handler welcomer
getHandler "songsearch" = Just $ Handler songSearch
getHandler _ = Nothing
greeter :: Monad m => BotConfig -> Message -> m (Maybe Command) getMsgHandler :: MsgHandlerName -> Maybe MsgHandler
greeter _ ChannelMsg { .. } = case find (== clean msg) greetings of getMsgHandler "greeter" = Just $ newMsgHandler { msgHandlerRun = greeter }
getMsgHandler "welcomer" = Just $ newMsgHandler { msgHandlerRun = welcomer }
getMsgHandler name = listToMaybe $ mapMaybe (\f -> f name)
[C.getMsgHandler, SS.getMsgHandler]
greeter :: MonadMsgHandler m => Message -> m (Maybe Command)
greeter ChannelMsg { .. } = case find (== clean msg) greetings of
Nothing -> return Nothing Nothing -> return Nothing
Just greeting -> return . Just . ChannelMsgReply $ greeting ++ " " ++ userNick user Just greeting -> return . Just . ChannelMsgReply $ greeting ++ " " ++ userNick user
where where
greetings = ["hi", "hello", "hey", "sup", "bye" greetings = ["hi", "hello", "hey", "sup", "bye"
, "good morning", "good evening", "good night" , "good morning", "good evening", "good night"
, "ohayo", "oyasumi"] , "ohayo", "oyasumi"]
greeter _ _ = return Nothing greeter _ = return Nothing
welcomer :: Monad m => BotConfig -> Message -> m (Maybe Command) welcomer :: MonadMsgHandler m => Message -> m (Maybe Command)
welcomer BotConfig { .. } JoinMsg { .. } welcomer JoinMsg { .. } = do
| userNick user /= botNick = return . Just . ChannelMsgReply $ "welcome back " ++ userNick user BotConfig { .. } <- ask
welcomer _ _ = return Nothing if userNick user /= botNick
then return . Just . ChannelMsgReply $ "welcome back " ++ userNick user
else return Nothing
welcomer _ = return Nothing

View File

@ -0,0 +1,55 @@
{-# LANGUAGE RecordWildCards, NoImplicitPrelude, OverloadedStrings, FlexibleContexts #-}
module Network.IRC.Handlers.Core (getMsgHandler) where
import qualified Data.Configurator as C
import qualified Data.Text.Format as TF
import qualified Data.Text.Format.Params as TF
import ClassyPrelude hiding (try, (</>), (<.>))
import Control.Monad.Reader
import Control.Monad.State
import Data.Dynamic
import System.Directory
import System.FilePath
import System.IO
import Network.IRC.Types
getMsgHandler :: MsgHandlerName -> Maybe MsgHandler
getMsgHandler "pingpong" = Just $ newMsgHandler { msgHandlerRun = pingPong }
getMsgHandler "messagelogger" = Just $ newMsgHandler { msgHandlerInit = initMessageLogger
, msgHandlerRun = messageLogger
, msgHandlerExit = exitMessageLogger }
getMsgHandler _ = Nothing
pingPong :: MonadMsgHandler m => Message -> m (Maybe Command)
pingPong Ping { .. } = return . Just $ Pong msg
pingPong _ = return Nothing
initMessageLogger :: MonadMsgHandler m => m ()
initMessageLogger = do
BotConfig { .. } <- ask
logFileHandle <- liftIO $ do
logFileDir <- C.require config "messagelogger.logdir"
createDirectoryIfMissing True logFileDir
let logFilePath = logFileDir </> unpack botNick <.> "log"
logFileHandle <- openFile logFilePath AppendMode
hSetBuffering logFileHandle LineBuffering
return logFileHandle
put $ toDyn logFileHandle
exitMessageLogger :: MonadMsgHandler m => m ()
exitMessageLogger = do
mHandle <- map fromDynamic get
case mHandle of
Nothing -> return ()
Just logFileHandle -> liftIO $ hClose logFileHandle
messageLogger :: MonadMsgHandler m => Message -> m (Maybe Command)
messageLogger ChannelMsg { .. } = do
logFileHandle <- map (`fromDyn` error "No log file set") get
let time = formatTime defaultTimeLocale "%F %T" msgTime
liftIO $ TF.hprint logFileHandle "[{}] {}: {}\n" $ TF.buildParams (time, userNick user, msg)
return Nothing
messageLogger _ = return Nothing

View File

@ -1,11 +1,12 @@
{-# LANGUAGE RecordWildCards, OverloadedStrings, ScopedTypeVariables, NoImplicitPrelude #-} {-# LANGUAGE RecordWildCards, OverloadedStrings, ScopedTypeVariables, NoImplicitPrelude, FlexibleContexts #-}
module Network.IRC.Handlers.SongSearch (songSearch) where module Network.IRC.Handlers.SongSearch (getMsgHandler) where
import qualified Data.Configurator as CF import qualified Data.Configurator as CF
import ClassyPrelude hiding (try) import ClassyPrelude hiding (try)
import Control.Exception import Control.Exception
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import Data.Aeson.Types (emptyArray) import Data.Aeson.Types (emptyArray)
import Data.Text (strip) import Data.Text (strip)
@ -14,6 +15,10 @@ import Network.HTTP.Base
import Network.IRC.Types import Network.IRC.Types
getMsgHandler :: MsgHandlerName -> Maybe MsgHandler
getMsgHandler "songsearch" = Just $ newMsgHandler { msgHandlerRun = songSearch }
getMsgHandler _ = Nothing
data Song = NoSong | Song { url :: Text, name :: Text, artist :: Text } data Song = NoSong | Song { url :: Text, name :: Text, artist :: Text }
deriving (Show, Eq) deriving (Show, Eq)
@ -22,9 +27,11 @@ instance FromJSON Song where
parseJSON a | a == emptyArray = return NoSong parseJSON a | a == emptyArray = return NoSong
parseJSON _ = mempty parseJSON _ = mempty
songSearch :: MonadIO m => BotConfig -> Message -> m (Maybe Command) songSearch :: MonadMsgHandler m => Message -> m (Maybe Command)
songSearch BotConfig { .. } ChannelMsg { .. } songSearch ChannelMsg { .. } = if "!m " `isPrefixOf` msg
| "!m " `isPrefixOf` msg = liftIO $ do then do
BotConfig { .. } <- ask
liftIO $ do
let query = strip . drop 3 $ msg let query = strip . drop 3 $ msg
mApiKey <- CF.lookup config "songsearch.tinysong_apikey" mApiKey <- CF.lookup config "songsearch.tinysong_apikey"
map (Just . ChannelMsgReply) $ case mApiKey of map (Just . ChannelMsgReply) $ case mApiKey of
@ -40,5 +47,5 @@ songSearch BotConfig { .. } ChannelMsg { .. }
Right song -> case song of Right song -> case song of
Song { .. } -> "Listen to " ++ artist ++ " - " ++ name ++ " at " ++ url Song { .. } -> "Listen to " ++ artist ++ " - " ++ name ++ " at " ++ url
NoSong -> "No song found for: " ++ query NoSong -> "No song found for: " ++ query
| otherwise = return Nothing else return Nothing
songSearch _ _ = return Nothing songSearch _ = return Nothing

View File

@ -10,20 +10,20 @@ import Network.IRC.Types
msgFromLine :: BotConfig -> UTCTime -> Text -> Message msgFromLine :: BotConfig -> UTCTime -> Text -> Message
msgFromLine (BotConfig { .. }) time line msgFromLine (BotConfig { .. }) time line
| "PING :" `isPrefixOf` line = Ping time . drop 6 $ line | "PING :" `isPrefixOf` line = Ping time (drop 6 line) line
| otherwise = case command of | otherwise = case command of
"JOIN" -> JoinMsg time user "JOIN" -> JoinMsg time user line
"QUIT" -> QuitMsg time user message "QUIT" -> QuitMsg time user message line
"PART" -> PartMsg time user message "PART" -> PartMsg time user message line
"KICK" -> KickMsg time user kicked kickReason "KICK" -> KickMsg time user kicked kickReason line
"MODE" -> if source == botNick "MODE" -> if source == botNick
then ModeMsg time Self target message [] then ModeMsg time Self target message [] line
else ModeMsg time user target mode modeArgs else ModeMsg time user target mode modeArgs line
"NICK" -> NickMsg time user (drop 1 target) "NICK" -> NickMsg time user (drop 1 target) line
"PRIVMSG" -> if target == channel "PRIVMSG" -> if target == channel
then ChannelMsg time user message then ChannelMsg time user message line
else PrivMsg time user message else PrivMsg time user message line
_ -> OtherMsg time source command target message _ -> OtherMsg time source command target message line
where where
isSpc = (== ' ') isSpc = (== ' ')
isNotSpc = not . isSpc isNotSpc = not . isSpc
@ -46,4 +46,3 @@ lineFromCommand (BotConfig { .. }) reply = case reply of
JoinCmd -> "JOIN " ++ channel JoinCmd -> "JOIN " ++ channel
ChannelMsgReply { .. } -> "PRIVMSG " ++ channel ++ " :" ++ rmsg ChannelMsgReply { .. } -> "PRIVMSG " ++ channel ++ " :" ++ rmsg
PrivMsgReply (User { .. }) rmsg -> "PRIVMSG " ++ botNick ++ " :" ++ rmsg PrivMsgReply (User { .. }) rmsg -> "PRIVMSG " ++ botNick ++ " :" ++ rmsg

View File

@ -1,53 +1,60 @@
{-# LANGUAGE RecordWildCards, RankNTypes, GeneralizedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards, RankNTypes, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude, OverloadedStrings #-} {-# LANGUAGE NoImplicitPrelude, OverloadedStrings, FlexibleContexts #-}
module Network.IRC.Types where module Network.IRC.Types
(Channel, Nick, MsgHandlerName,
User (..), Message (..), Command (..),
BotConfig (..), BotStatus (..), Bot (..),
IRC, runIRC,
MonadMsgHandler, runMsgHandler, initMsgHandler, exitMsgHandler,
MsgHandlerState, MsgHandlerStates, MsgHandler (..), newMsgHandler)
where
import ClassyPrelude import ClassyPrelude
import Control.Monad.Reader import Control.Monad.Reader
import Control.Monad.State import Control.Monad.State
import Data.Configurator.Types import Data.Configurator.Types
import Data.Dynamic
type Channel = Text type Channel = Text
type Nick = Text type Nick = Text
type HandlerName = Text type MsgHandlerName = Text
newtype Handler = Handler { data User = Self | User { userNick :: !Nick, userServer :: !Text }
runHandler :: forall m . (MonadIO m) => BotConfig -> Message -> m (Maybe Command)
}
data User = Self | User { userNick :: Nick, userServer :: Text }
deriving (Show, Eq) deriving (Show, Eq)
data Message = data Message =
ChannelMsg { time :: UTCTime, user :: User, msg :: Text } ChannelMsg { msgTime :: !UTCTime, user :: !User, msg :: !Text, msgLine :: !Text }
| PrivMsg { time :: UTCTime, user :: User, msg :: Text } | PrivMsg { msgTime :: !UTCTime, user :: !User, msg :: !Text, msgLine :: !Text }
| Ping { time :: UTCTime, msg :: Text } | Ping { msgTime :: !UTCTime, msg :: !Text, msgLine :: !Text }
| JoinMsg { time :: UTCTime, user :: User } | JoinMsg { msgTime :: !UTCTime, user :: !User, msgLine :: !Text }
| ModeMsg { time :: UTCTime, user :: User, target :: Text , mode :: Text, modeArgs :: [Text] } | ModeMsg { msgTime :: !UTCTime, user :: !User, target :: !Text, mode :: !Text
| NickMsg { time :: UTCTime, user :: User, nick :: Text } , modeArgs :: ![Text], msgLine :: !Text }
| QuitMsg { time :: UTCTime, user :: User, msg :: Text } | NickMsg { msgTime :: !UTCTime, user :: !User, nick :: !Text, msgLine :: !Text }
| PartMsg { time :: UTCTime, user :: User, msg :: Text } | QuitMsg { msgTime :: !UTCTime, user :: !User, msg :: !Text, msgLine :: !Text }
| KickMsg { time :: UTCTime, user :: User, kicked :: Text , msg :: Text } | PartMsg { msgTime :: !UTCTime, user :: !User, msg :: !Text, msgLine :: !Text }
| OtherMsg { time :: UTCTime, source :: Text, command :: Text , target :: Text, msg :: Text } | KickMsg { msgTime :: !UTCTime, user :: !User, kicked :: !Text, msg :: !Text
, msgLine :: !Text }
| OtherMsg { msgTime :: !UTCTime, source :: !Text, command :: !Text, target :: !Text
, msg :: !Text, msgLine :: !Text }
deriving (Show, Eq) deriving (Show, Eq)
data Command = data Command =
Pong { rmsg :: Text } Pong { rmsg :: !Text }
| ChannelMsgReply { rmsg :: Text } | ChannelMsgReply { rmsg :: !Text }
| PrivMsgReply { ruser :: User, rmsg :: Text } | PrivMsgReply { ruser :: !User, rmsg :: !Text }
| NickCmd | NickCmd
| UserCmd | UserCmd
| JoinCmd | JoinCmd
deriving (Show, Eq) deriving (Show, Eq)
data BotConfig = BotConfig { server :: String data BotConfig = BotConfig { server :: !Text
, port :: Int , port :: !Int
, channel :: Text , channel :: !Text
, botNick :: Text , botNick :: !Text
, botTimeout :: Int , botTimeout :: !Int
, handlers :: [HandlerName] , msgHandlers :: ![MsgHandlerName]
, config :: Config } , config :: !Config }
instance Show BotConfig where instance Show BotConfig where
show BotConfig { .. } = "server = " ++ show server ++ "\n" ++ show BotConfig { .. } = "server = " ++ show server ++ "\n" ++
@ -55,15 +62,62 @@ instance Show BotConfig where
"channel = " ++ show channel ++ "\n" ++ "channel = " ++ show channel ++ "\n" ++
"nick = " ++ show botNick ++ "\n" ++ "nick = " ++ show botNick ++ "\n" ++
"timeout = " ++ show botTimeout ++ "\n" ++ "timeout = " ++ show botTimeout ++ "\n" ++
"handlers = " ++ show handlers ++ "\n" "handlers = " ++ show msgHandlers
data Bot = Bot { botConfig :: BotConfig, socket :: Handle } deriving (Show) type MsgHandlerStates = Map MsgHandlerName MsgHandlerState
data Bot = Bot { botConfig :: !BotConfig
, socket :: !Handle
, msgHandlerStates :: !MsgHandlerStates}
data BotStatus = Connected | Disconnected | Joined | Kicked | Errored data BotStatus = Connected | Disconnected | Joined | Kicked | Errored
deriving (Show, Eq) deriving (Show, Eq)
newtype IRC a = IRC { _runIRC :: StateT BotStatus (ReaderT Bot IO) a } newtype IRC a = IRC { _runIRC :: StateT BotStatus (ReaderT Bot IO) a }
deriving (Functor, Monad, MonadIO, MonadReader Bot, MonadState BotStatus) deriving ( Functor
, Applicative
, Monad
, MonadIO
, MonadReader Bot
, MonadState BotStatus)
runIRC :: Bot -> BotStatus -> IRC a -> IO BotStatus runIRC :: Bot -> BotStatus -> IRC a -> IO BotStatus
runIRC bot botStatus = flip runReaderT bot . flip execStateT botStatus . _runIRC runIRC bot botStatus = flip runReaderT bot . flip execStateT botStatus . _runIRC
type MsgHandlerState = Dynamic
newtype MsgHandlerT a = MsgHandlerT { _runMsgHandler :: StateT MsgHandlerState (ReaderT BotConfig IO) a }
deriving ( Functor
, Applicative
, Monad
, MonadIO
, MonadState MsgHandlerState
, MonadReader BotConfig)
class ( MonadIO m, Applicative m
, MonadState MsgHandlerState m, MonadReader BotConfig m) => MonadMsgHandler m where
msgHandler :: MsgHandlerT a -> m a
instance MonadMsgHandler MsgHandlerT where
msgHandler = id
runMsgHandler :: MsgHandler -> BotConfig -> MsgHandlerState -> Message -> IO (Maybe Command)
runMsgHandler MsgHandler { .. } botConfig msgHandlerState =
flip runReaderT botConfig . flip evalStateT msgHandlerState . _runMsgHandler . msgHandlerRun
initMsgHandler :: MsgHandler -> BotConfig -> IO MsgHandlerState
initMsgHandler MsgHandler { .. } botConfig =
flip runReaderT botConfig . flip execStateT (toDyn ()) . _runMsgHandler $ msgHandlerInit
exitMsgHandler :: MsgHandler -> BotConfig -> MsgHandlerState -> IO ()
exitMsgHandler MsgHandler { .. } botConfig msgHandlerState =
flip runReaderT botConfig . flip evalStateT msgHandlerState . _runMsgHandler $ msgHandlerExit
data MsgHandler = MsgHandler { msgHandlerInit :: !(forall m . MonadMsgHandler m => m ())
, msgHandlerRun :: !(forall m . MonadMsgHandler m => Message -> m (Maybe Command))
, msgHandlerExit :: !(forall m . MonadMsgHandler m => m ()) }
newMsgHandler :: MsgHandler
newMsgHandler = MsgHandler { msgHandlerInit = return ()
, msgHandlerRun = const $ return Nothing
, msgHandlerExit = return () }

View File

@ -2,7 +2,7 @@ server = "irc.freenode.net"
port = 6667 port = 6667
channel = "#testtesttest" channel = "#testtesttest"
nick = "haskman" nick = "haskman"
handlers = ["greeter", "welcomer", "songsearch"] msghandlers = ["greeter", "welcomer", "songsearch"]
songsearch { songsearch {
tinysong_apikey = "xxxyyyzzz" tinysong_apikey = "xxxyyyzzz"

View File

@ -52,14 +52,14 @@ library
build-depends: base >=4.5 && <4.7, text >=0.11 && <0.12, mtl >=2.1 && <2.2, build-depends: base >=4.5 && <4.7, text >=0.11 && <0.12, mtl >=2.1 && <2.2,
network >=2.3 && <2.5, configurator >= 0.2, time >=1.4.0, network >=2.3 && <2.5, configurator >= 0.2, time >=1.4.0,
curl-aeson ==0.0.3, aeson >=0.6.0.0, HTTP, transformers >=0.3, curl-aeson ==0.0.3, aeson >=0.6.0.0, HTTP, transformers >=0.3,
classy-prelude ==0.9.1, text-format >= 0.3.1 classy-prelude ==0.9.1, text-format >= 0.3.1, filepath >=1.3, directory >=1.2
exposed-modules: Network.IRC.Types, Network.IRC.Protocol, exposed-modules: Network.IRC.Types, Network.IRC.Protocol,
Network.IRC.Handlers, Network.IRC.Client Network.IRC.Handlers, Network.IRC.Client
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall -fno-warn-unused-do-bind -O2 -funbox-strict-fields ghc-options: -Wall -fno-warn-unused-do-bind -O2 -funbox-strict-fields -fno-warn-orphans
executable hask-irc executable hask-irc
@ -76,7 +76,7 @@ executable hask-irc
build-depends: base >=4.5 && <4.7, text >=0.11 && <0.12, mtl >=2.1 && <2.2, build-depends: base >=4.5 && <4.7, text >=0.11 && <0.12, mtl >=2.1 && <2.2,
network >=2.3 && <2.5, configurator >= 0.2, time >=1.4.0, network >=2.3 && <2.5, configurator >= 0.2, time >=1.4.0,
curl-aeson ==0.0.3, aeson >=0.6.0.0, HTTP, transformers >=0.3, curl-aeson ==0.0.3, aeson >=0.6.0.0, HTTP, transformers >=0.3,
classy-prelude ==0.9.1, text-format >= 0.3.1 classy-prelude ==0.9.1, text-format >= 0.3.1, filepath >=1.3, directory >=1.2
-- Directories containing source files. -- Directories containing source files.
-- hs-source-dirs: -- hs-source-dirs:
@ -84,5 +84,5 @@ executable hask-irc
-- Base language which the package is written in. -- Base language which the package is written in.
default-language: Haskell2010 default-language: Haskell2010
ghc-options: -Wall -fno-warn-unused-do-bind -funbox-strict-fields ghc-options: -Wall -fno-warn-unused-do-bind -funbox-strict-fields -fno-warn-orphans