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 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 (
-- Creating a bot
-- * Creating a bot
Bot
, BotConfig(..)
, runBot
-- Utilities
-- * Bot commands
-- * Utilities
, fork
, defaultReconnectPolicy
-- ** Context info
, getBotInfo
, getStartTime
, getConnection
, getConnectionInfo
, defaultReconnectPolicy
, respondToPing
, getConnectTime
) where
import Control.Concurrent
@ -22,6 +81,7 @@ import Control.Monad.IO.Class
import Control.Concurrent.STM
import Control.Monad.Trans.Reader
import qualified Data.Text as T
import Data.Time
import qualified System.Log.Logger as L
import qualified EuphApi.Connection as E
@ -56,47 +116,96 @@ data BotState b c = BotState
, bNewConnectionInfo :: IO c
, bReconnectPolicy :: Integer -> Maybe Int
, bStopping :: TVar Bool
, bStartTime :: UTCTime
-- connection specific
, bConnection :: E.Connection
, 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
-- | Configuration for a bot.
--
-- Create one of these and use them with 'runBot' to run your bot.
data BotConfig b c = BotConfig
{ 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
-- ^ Name of the room to connect to.
, botPassword :: Maybe String
-- ^ Password of the room, if any.
, botNick :: String
-- ^ Nick that the bot should use.
, 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
-- ^ Bot specific data.
, botNewConnectionInfo :: IO c
-- ^ Create new instance of $c$ to use with a new connection.
, 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 = do
state <- ask
void $ liftIO $ forkIO $ void $ runReaderT bot state
-- | Returns the @Bot b c a@'s bot specific info @b@.
getBotInfo :: Bot b c b
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 = asks bConnection
-- | Returns the @Bot b c a@'s connection specific info @c@.
getConnectionInfo :: Bot b c c
getConnectionInfo = asks bConnectionInfo
-- | A default reconnect policy that waits an exponentially increasing amount
-- of seconds between retries.
defaultReconnectPolicy :: Integer -> Maybe Int
defaultReconnectPolicy n = Just $ (2 ^ n) * 1000 * 1000 -- in microseconds
respondToPing :: E.EventType -> Bot b c ()
respondToPing (E.EuphEvent (E.PingEvent time _)) = do
con <- getConnection
liftIO $ E.pingReply con time -- TODO: Replace with bot version of the command
respondToPing _ = return ()
{-
- Running a bot
-}
-- | Execute a bot in this thread.
runBot :: BotConfig b c -> IO ()
runBot BotConfig{..} = do
bAddress <- atomically $ newTVar botAddress
@ -105,12 +214,14 @@ runBot BotConfig{..} = do
bNick <- atomically $ newTVar $ T.pack botNick
bStopping <- atomically $ newTVar False
noticeM $ "Connecting to &" ++ botRoom ++ " on " ++ show botAddress ++ "."
bStartTime <- getCurrentTime
bConnection <- E.startEuphConnection botAddress botRoom
bConnectionInfo <- botNewConnectionInfo
let bHandler = botHandler
bBotInfo = botInfo
bNewConnectionInfo = botNewConnectionInfo
bReconnectPolicy = botReconnectPolicy
bConnectTime = bStartTime
state = BotState{..}
runReaderT (eventLoop 0) state
@ -129,10 +240,11 @@ reconnect retries = do
liftIO $ threadDelay delay
address <- liftIO $ atomically $ readTVar $ bAddress state
room <- liftIO $ atomically $ readTVar $ bRoom state
now <- liftIO $ getCurrentTime
con <- liftIO $ E.startEuphConnection address room
liftIO $ infoM $ "Reconnecting to &" ++ room ++ " on " ++ show address ++ "."
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)
-- lift $ runReaderT (eventLoop retries) newState
@ -147,10 +259,17 @@ eventLoop retries = do
E.ConnectionFailed -> reconnect (retries + 1)
E.Disconnected -> reconnect 0
E.EuphEvent e -> do
handlePingStuff e
handlePasswordStuff e
handleNickStuff e
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.SnapshotEvent _ _ _ maybeNick) = do
myNickVar <- asks bNick
@ -173,5 +292,9 @@ handlePasswordStuff (E.BounceEvent _ (Just options))
case myPassword of
Nothing -> fork $ liftIO $ E.disconnect con -- TODO: Do something here
Just p -> fork $ liftIO $ E.auth con p
| otherwise = return ()
| otherwise = return () -- TODO: And also here
handlePasswordStuff _ = return ()
{-
- Commands
-}

View file

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