Add documentation for EuphApi.Bot
Also automatically respond to pings
This commit is contained in:
parent
09944406ef
commit
6b80c0970e
2 changed files with 135 additions and 12 deletions
|
|
@ -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
|
||||
-}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue