Add documentation for EuphApi.Bot

Also automatically respond to pings
This commit is contained in:
Joscha 2018-02-16 17:31:16 +00:00
parent 09944406ef
commit 6b80c0970e
2 changed files with 135 additions and 12 deletions

View file

@ -1,18 +1,77 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
-- | This module lets you create bots, although it only contains the bare minimum necessary.
-- It defines the 'Bot' monad which takes care of a few things common to most bots.
--
-- = The 'Bot' monad
--
-- This monad takes care of
--
-- - maintaining a 'E.Connection' to a specific room
-- - setting a nick (even after reconnecting)
-- - authenticating via password
-- - tracking starting and reconnect time
-- - keeping track of bot specific data and connection specific data
--
-- @Bot b c r@ stands for an action (within a bot) that returns @r@,
-- with bot specific data @b@ and connection specific data @c@.
--
-- Bot specific data is data that doesn't change while the bot runs.
-- It can either be used to keep track of bot configuration, or to keep
-- a few TVars/MVars for bot state (or even inter-bot communication).
--
-- Connection specific data is data that is specific to the current connection.
-- Examples for such data would be the listing of people or the 'E.SessionView'
-- describing the current session.
--
-- == Writing a bot
--
-- You will almost certainly want to write an own 'botHandler' so that your bot
-- can react to events from the server.
-- You don't need to respond to ping events or bounce events,
-- or make sure your nick is set correctly.
--
-- This means that a bot with @(const $ return ())@ as handler will still stay connected,
-- keep its nick and even authenticate if given a password.
--
-- __Important:__
-- Make sure to use 'fork' or 'forkIO' when using bot commands
-- or IO actions in your handler.
-- Otherwise the main bot thread will block while waiting for a response from the server.
--
-- If you want your bot to respond to commands, see "EuphApi.Utils.Commands".
--
-- If you want your bot to comply with the <https://github.com/jedevc/botrulez botrulez>,
-- see "EuphApi.Utils.Botulez".
--
-- For a simple example bot that just connects to <https://euphoria.io/room/test/ &test>,
-- see <https://github.com/Garmelon/EuphApi/blob/master/test/bot_simple_custom_logging.hs bot_simple_custom_logging.hs>
-- in the tests folder of the repository.
-- This example bot also configures the logger to use a custom output format.
--
-- = Logging
--
-- This library uses the hslogger package for logging.
--
-- See <https://github.com/Garmelon/EuphApi/blob/master/test/bot_simple_custom_logging.hs bot_simple_custom_logging.hs>
-- for an example of how to set the global format.
module EuphApi.Bot ( module EuphApi.Bot (
-- Creating a bot -- * Creating a bot
Bot Bot
, BotConfig(..) , BotConfig(..)
, runBot , runBot
-- Utilities -- * Bot commands
-- * Utilities
, fork , fork
, defaultReconnectPolicy
-- ** Context info
, getBotInfo , getBotInfo
, getStartTime
, getConnection , getConnection
, getConnectionInfo , getConnectionInfo
, defaultReconnectPolicy , getConnectTime
, respondToPing
) where ) where
import Control.Concurrent import Control.Concurrent
@ -22,6 +81,7 @@ import Control.Monad.IO.Class
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time
import qualified System.Log.Logger as L import qualified System.Log.Logger as L
import qualified EuphApi.Connection as E import qualified EuphApi.Connection as E
@ -56,47 +116,96 @@ data BotState b c = BotState
, bNewConnectionInfo :: IO c , bNewConnectionInfo :: IO c
, bReconnectPolicy :: Integer -> Maybe Int , bReconnectPolicy :: Integer -> Maybe Int
, bStopping :: TVar Bool , bStopping :: TVar Bool
, bStartTime :: UTCTime
-- connection specific -- connection specific
, bConnection :: E.Connection , bConnection :: E.Connection
, bConnectionInfo :: c -- connection specific, user-defined info type , bConnectionInfo :: c -- connection specific, user-defined info type
, bConnectTime :: UTCTime
} }
-- | The monad that bots are written in.
--
-- @Bot b c r@ stands for an action (within a bot) that returns @r@,
-- with bot specific data @b@ and connection specific data @c@.
--
-- Don't worry about what @BotState@ is.
-- I might one day add a newtype for Bot.
type Bot b c = ReaderT (BotState b c) IO type Bot b c = ReaderT (BotState b c) IO
-- | Configuration for a bot.
--
-- Create one of these and use them with 'runBot' to run your bot.
data BotConfig b c = BotConfig data BotConfig b c = BotConfig
{ botAddress :: String { botAddress :: String
-- ^ Address of the server you want to connect to.
--
-- Unless you have a heim clone lying around somewhere,
-- use @\"euphoria.io\"@.
, botRoom :: String , botRoom :: String
-- ^ Name of the room to connect to.
, botPassword :: Maybe String , botPassword :: Maybe String
-- ^ Password of the room, if any.
, botNick :: String , botNick :: String
-- ^ Nick that the bot should use.
, botHandler :: E.EventType -> Bot b c () , botHandler :: E.EventType -> Bot b c ()
-- ^ This handler gets called whenever the bot receives an event from the server.
-- Your bot logic goes here.
--
-- Make sure you use 'fork' when calling bot commands.
, botInfo :: b , botInfo :: b
-- ^ Bot specific data.
, botNewConnectionInfo :: IO c , botNewConnectionInfo :: IO c
-- ^ Create new instance of $c$ to use with a new connection.
, botReconnectPolicy :: Integer -> Maybe Int , botReconnectPolicy :: Integer -> Maybe Int
-- ^ A function that determines how long, if at all, the bot should wait
-- before attempting to reconnect after the n-th try.
-- Returns the time in µs (to get seconds, divide by 1000000).
--
-- > reconnectPolicy n (Maybe waitTime)
} }
{-
- Helpful functions
-}
-- | Run this bot action in a separate thread.
fork :: Bot b c a -> Bot b c () fork :: Bot b c a -> Bot b c ()
fork bot = do fork bot = do
state <- ask state <- ask
void $ liftIO $ forkIO $ void $ runReaderT bot state void $ liftIO $ forkIO $ void $ runReaderT bot state
-- | Returns the @Bot b c a@'s bot specific info @b@.
getBotInfo :: Bot b c b getBotInfo :: Bot b c b
getBotInfo = asks bBotInfo getBotInfo = asks bBotInfo
-- | Returns the time when the bot was first started.
getStartTime :: Bot b c UTCTime
getStartTime = asks bStartTime
-- | Returns the time when the bot last connected to the room.
getConnectTime :: Bot b c UTCTime
getConnectTime = asks bConnectTime
-- | Returns the 'E.Connection' the bot currently uses.
--
-- __Warning:__ Only use this if you know what you're doing.
getConnection :: Bot b c E.Connection getConnection :: Bot b c E.Connection
getConnection = asks bConnection getConnection = asks bConnection
-- | Returns the @Bot b c a@'s connection specific info @c@.
getConnectionInfo :: Bot b c c getConnectionInfo :: Bot b c c
getConnectionInfo = asks bConnectionInfo getConnectionInfo = asks bConnectionInfo
-- | A default reconnect policy that waits an exponentially increasing amount
-- of seconds between retries.
defaultReconnectPolicy :: Integer -> Maybe Int defaultReconnectPolicy :: Integer -> Maybe Int
defaultReconnectPolicy n = Just $ (2 ^ n) * 1000 * 1000 -- in microseconds defaultReconnectPolicy n = Just $ (2 ^ n) * 1000 * 1000 -- in microseconds
respondToPing :: E.EventType -> Bot b c () {-
respondToPing (E.EuphEvent (E.PingEvent time _)) = do - Running a bot
con <- getConnection -}
liftIO $ E.pingReply con time -- TODO: Replace with bot version of the command
respondToPing _ = return ()
-- | Execute a bot in this thread.
runBot :: BotConfig b c -> IO () runBot :: BotConfig b c -> IO ()
runBot BotConfig{..} = do runBot BotConfig{..} = do
bAddress <- atomically $ newTVar botAddress bAddress <- atomically $ newTVar botAddress
@ -105,12 +214,14 @@ runBot BotConfig{..} = do
bNick <- atomically $ newTVar $ T.pack botNick bNick <- atomically $ newTVar $ T.pack botNick
bStopping <- atomically $ newTVar False bStopping <- atomically $ newTVar False
noticeM $ "Connecting to &" ++ botRoom ++ " on " ++ show botAddress ++ "." noticeM $ "Connecting to &" ++ botRoom ++ " on " ++ show botAddress ++ "."
bStartTime <- getCurrentTime
bConnection <- E.startEuphConnection botAddress botRoom bConnection <- E.startEuphConnection botAddress botRoom
bConnectionInfo <- botNewConnectionInfo bConnectionInfo <- botNewConnectionInfo
let bHandler = botHandler let bHandler = botHandler
bBotInfo = botInfo bBotInfo = botInfo
bNewConnectionInfo = botNewConnectionInfo bNewConnectionInfo = botNewConnectionInfo
bReconnectPolicy = botReconnectPolicy bReconnectPolicy = botReconnectPolicy
bConnectTime = bStartTime
state = BotState{..} state = BotState{..}
runReaderT (eventLoop 0) state runReaderT (eventLoop 0) state
@ -129,10 +240,11 @@ reconnect retries = do
liftIO $ threadDelay delay liftIO $ threadDelay delay
address <- liftIO $ atomically $ readTVar $ bAddress state address <- liftIO $ atomically $ readTVar $ bAddress state
room <- liftIO $ atomically $ readTVar $ bRoom state room <- liftIO $ atomically $ readTVar $ bRoom state
now <- liftIO $ getCurrentTime
con <- liftIO $ E.startEuphConnection address room con <- liftIO $ E.startEuphConnection address room
liftIO $ infoM $ "Reconnecting to &" ++ room ++ " on " ++ show address ++ "." liftIO $ infoM $ "Reconnecting to &" ++ room ++ " on " ++ show address ++ "."
conInfo <- liftIO $ bNewConnectionInfo state conInfo <- liftIO $ bNewConnectionInfo state
let newState = state{bConnection=con, bConnectionInfo=conInfo} let newState = state{bConnection=con, bConnectionInfo=conInfo, bConnectTime=now}
local (const newState) (eventLoop retries) local (const newState) (eventLoop retries)
-- lift $ runReaderT (eventLoop retries) newState -- lift $ runReaderT (eventLoop retries) newState
@ -147,10 +259,17 @@ eventLoop retries = do
E.ConnectionFailed -> reconnect (retries + 1) E.ConnectionFailed -> reconnect (retries + 1)
E.Disconnected -> reconnect 0 E.Disconnected -> reconnect 0
E.EuphEvent e -> do E.EuphEvent e -> do
handlePingStuff e
handlePasswordStuff e handlePasswordStuff e
handleNickStuff e handleNickStuff e
eventLoop retries eventLoop retries
handlePingStuff :: E.Event -> Bot b c ()
handlePingStuff (E.PingEvent time _) = do
con <- getConnection
liftIO $ E.pingReply con time
handlePingStuff _ = return ()
handleNickStuff :: E.Event -> Bot b c () handleNickStuff :: E.Event -> Bot b c ()
handleNickStuff (E.SnapshotEvent _ _ _ maybeNick) = do handleNickStuff (E.SnapshotEvent _ _ _ maybeNick) = do
myNickVar <- asks bNick myNickVar <- asks bNick
@ -173,5 +292,9 @@ handlePasswordStuff (E.BounceEvent _ (Just options))
case myPassword of case myPassword of
Nothing -> fork $ liftIO $ E.disconnect con -- TODO: Do something here Nothing -> fork $ liftIO $ E.disconnect con -- TODO: Do something here
Just p -> fork $ liftIO $ E.auth con p Just p -> fork $ liftIO $ E.auth con p
| otherwise = return () | otherwise = return () -- TODO: And also here
handlePasswordStuff _ = return () handlePasswordStuff _ = return ()
{-
- Commands
-}

View file

@ -14,7 +14,7 @@ myBotConfig = E.BotConfig
, E.botRoom = "test" , E.botRoom = "test"
, E.botPassword = Nothing , E.botPassword = Nothing
, E.botNick = "EuphApiTestBot" , E.botNick = "EuphApiTestBot"
, E.botHandler = E.respondToPing , E.botHandler = const $ return ()
, E.botInfo = () , E.botInfo = ()
, E.botNewConnectionInfo = return () , E.botNewConnectionInfo = return ()
, E.botReconnectPolicy = E.defaultReconnectPolicy , E.botReconnectPolicy = E.defaultReconnectPolicy