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 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
|
||||||
|
-}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue