{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Irc.Internal where import Data.String ( IsString(..) ) import Data.Default ( Default(..) ) import Control.Applicative (Applicative) import Control.Monad.Trans.State as State ( State , modify , execState ) import Control.Exception import Control.Monad.Reader import Data.List ( find , isPrefixOf) import Network ( PortID(PortNumber) , connectTo) import System.IO ( Handle , hClose , hSetBuffering , BufferMode(..) , hFlush , stdout , hGetLine ) import Text.Printf( hPrintf , printf) -- $setup -- >>> :set -XOverloadedStrings -- >>> -- -- | The Irc monad is an IO monad wrapped by a readerT, which -- contains the bot's immutable state (Rule, socket connection, -- and config) -- type Irc = ReaderT Bot IO data Bot = Bot { rules :: [Rule] , config :: Config , socket :: Handle} -- -- | The Config struct represents the Irc configuration -- data Config = Config { server :: String , port :: Integer , chan :: String , nick :: String} -- -- | Set up actions to run on start and end, and run the main loop. -- as an example: -- -- > main :: IO () -- > main = mainWithConfigAndBehavior (Config -- > "irc.freenode.org" -- > 6667 -- > "#yunbot-testing" -- > "yunbot") $ do -- > "!echo " |! return . drop 6 -- > "!reverse " |! return . reverse . drop 9 -- mainWithConfigAndBehavior :: Config -> Behavior -> IO () mainWithConfigAndBehavior conf bev = bracket (connect conf bev) disconnect loop where disconnect = hClose . socket loop = runReaderT run -- -- | Connect to the server and return the initial bot state -- connect :: Config -> Behavior -> IO Bot connect conf bev = notify $ do h <- connectTo (server conf) (PortNumber (fromIntegral (port conf))) hSetBuffering h NoBuffering return (Bot (runBevhavior bev) conf h) where notify = bracket_ (printf "Connecting to %s ... " (server conf) >> hFlush stdout) (putStrLn "done.") -- -- | We're in the Irc monad now, so we've connected successfully -- Join a channel, and start processing commands -- run :: Irc () run = do conf <- asks config write "NICK" $ nick conf write "USER" $ nick conf ++ " 0 * :bot" write "JOIN" $ chan conf asks socket >>= listen -- -- | Process each line from the server -- listen :: Handle -> Irc () listen h = forever $ do s <- init `fmap` io (hGetLine h) io (putStrLn s) if ping s then pong s else eval (clean s) where clean = drop 1 . dropWhile (/= ':') . drop 1 ping x = "PING :" `isPrefixOf` x pong x = write "PONG" (':' : drop 6 x) -- -- | Dispatch a command -- eval :: String -> Irc () eval s = do r <- asks rules liftAction (findAction s r) s findAction :: String -> [Rule] -> Action findAction s l = maybe doNothing action $ find (\x -> pattern x `isPrefixOf` s) l where doNothing _ = return "" -- -- | Send a privmsg to the current chan + server -- privmsg :: String -> Irc () privmsg s = do conf <- asks config write "PRIVMSG" (chan conf ++ " :" ++ s) -- -- | Send a message out to the server we're currently connected to -- write :: String -> String -> Irc () write s t = do h <- asks socket io $ hPrintf h "%s %s\r\n" s t io $ printf "> %s %s\n" s t -- -- | Convenience. -- io :: IO a -> Irc a io = liftIO type Pattern = String type Action = String -> IO String data Rule = Rule { pattern :: Pattern , action :: Action } instance Default Rule where def = Rule { pattern = "" , action = def } instance IsString Rule where fromString x = def { pattern = x} liftAction :: Action -> String -> Irc () liftAction a s = do h <- asks socket conf <- asks config r <- io (a s) p r h (chan conf) where p [] _ _ = return () p r h c = io $ hPrintf h "PRIVMSG %s\r\n" (c ++ " :" ++ r) newtype BehaviorM a = BehaviorM {unBehaviorM :: State [Rule] a} deriving (Functor , Applicative , Monad) type Behavior = BehaviorM () instance IsString Behavior where fromString = addRule . fromString instance Show Behavior where show = show . map pattern . runBevhavior runBevhavior :: Behavior -> [Rule] runBevhavior bev = execState (unBehaviorM bev) [] addRule :: Rule -> Behavior addRule r = BehaviorM $ modify (r :) -- | modHeadRule modifies the first rule of a behavior -- -- >>> "pattern" `modHeadRule` (\x -> x {pattern = (reverse.pattern) x}) -- ["nrettap"] -- modHeadRule :: Behavior -> (Rule -> Rule) -> Behavior modHeadRule bev f = do let rs = runBevhavior bev BehaviorM $ case rs of x:_ -> modify (reverse.(f x:).reverse) [] -> modify id ruleAddAction :: Action -> Rule -> Rule ruleAddAction f r = r {action = f} infixl 8 |! -- | (|!) is a infix API to add a rule to a Behavior monad -- -- >>> "pattern1" |! return >> "pattern2" |! return -- ["pattern1","pattern2"] -- -- >>> "pattern1" |! return >> "pattern2" |! return >> "pattern3" |! return -- ["pattern1","pattern2","pattern3"] (|!) :: Behavior -> (String -> IO String) -> Behavior bev |! f = modHeadRule bev $ ruleAddAction f