Add lenses to listing and use them in the example bot

This commit is contained in:
Joscha 2020-04-09 10:40:24 +00:00
parent 9a476d9371
commit 822bb9efad
2 changed files with 70 additions and 41 deletions

View file

@ -1,7 +1,13 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- | This module contains an example implementation of a small bot. It is a good -- | This module contains an example implementation of a small bot. It is a good
-- starting point if you want to create your own bot. -- starting point if you want to create your own bot.
--
-- The example bot uses lenses for its state because they vastly reduce the
-- amount of code required to update the 'Listing' inside the state. It is
-- entirely possible to use haboli without lenses though, should you want to do
-- that.
module Haboli.Euphoria.ExampleBot module Haboli.Euphoria.ExampleBot
( exampleBot ( exampleBot
@ -14,15 +20,19 @@ import Data.List
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import qualified Data.Text as T import qualified Data.Text as T
import Data.Time import Data.Time
import Lens.Micro
import Lens.Micro.TH
import Haboli.Euphoria import Haboli.Euphoria
import Haboli.Euphoria.Botrulez import Haboli.Euphoria.Botrulez
data BotState = BotState data BotState = BotState
{ botStartTime :: UTCTime { _botStartTime :: UTCTime
, botListing :: Listing , _botListing :: Listing
} deriving (Show) } deriving (Show)
makeLenses ''BotState
-- | A small example bot. Takes a room password as its first argument. You can -- | A small example bot. Takes a room password as its first argument. You can
-- run this bot in [&test](https://euphoria.io/room/test) like this: -- run this bot in [&test](https://euphoria.io/room/test) like this:
-- --
@ -33,27 +43,27 @@ exampleBot mPasswd = do
initialEvents <- untilConnected $ initialEvents <- untilConnected $
respondingToBounce mPasswd $ respondingToBounce mPasswd $
respondingToPing nextEvent respondingToPing nextEvent
listing <- preferNick "ExampleBot" $ newListing initialEvents let initialState = BotState startTime $ newListing initialEvents
stateVar <- liftIO $ newMVar $ BotState startTime listing stateVar <- liftIO $ newMVar initialState
preferNickVia botListing stateVar "ExampleBot"
botMain stateVar botMain stateVar
botMain :: MVar BotState -> Client T.Text () botMain :: MVar BotState -> Client T.Text ()
botMain stateVar = forever $ do botMain stateVar = forever $ do
event <- respondingToCommands (getCommands stateVar) $ event <- respondingToCommands (getCommands stateVar) $
respondingToPing nextEvent respondingToPing nextEvent
-- Update the listing updateFromEventVia botListing stateVar event
liftIO $ modifyMVar_ stateVar $ \state ->
pure state{botListing = updateFromEvent event $ botListing state}
getCommands :: MVar BotState -> Client e [Command T.Text] getCommands :: MVar BotState -> Client e [Command T.Text]
getCommands stateVar = do getCommands stateVar = do
state <- liftIO $ readMVar stateVar state <- liftIO $ readMVar stateVar
let name = svNick $ self $ botListing state let name = state ^. botListing . lsSelfL . svNickL
pure pure
[ botrulezPingGeneral [ botrulezPingGeneral
, botrulezPingSpecific name , botrulezPingSpecific name
, botrulezHelpSpecific name "I am an example bot for https://github.com/Garmelon/haboli/." , botrulezHelpSpecific name
, botrulezUptimeSpecific name $ botStartTime state "I am an example bot for https://github.com/Garmelon/haboli/."
, botrulezUptimeSpecific name $ state ^. botStartTime
, botrulezKillSpecific name , botrulezKillSpecific name
, cmdSpecific "hug" name $ \msg -> void $ reply msg "/me hugs back" , cmdSpecific "hug" name $ \msg -> void $ reply msg "/me hugs back"
, cmdHello , cmdHello
@ -68,15 +78,12 @@ cmdHello = cmdGeneral "hello" $ \msg -> do
cmdNick :: MVar BotState -> T.Text -> Command e cmdNick :: MVar BotState -> T.Text -> Command e
cmdNick stateVar name = cmdSpecificArgs "nick" name $ \msg args -> do cmdNick stateVar name = cmdSpecificArgs "nick" name $ \msg args -> do
-- Update the listing while updating the nick preferNickVia botListing stateVar args
state <- liftIO $ takeMVar stateVar
listing' <- preferNick args $ botListing state
liftIO $ putMVar stateVar state{botListing = listing'}
void $ reply msg "Is this better?" void $ reply msg "Is this better?"
cmdWho :: MVar BotState -> Command e cmdWho :: MVar BotState -> Command e
cmdWho stateVar = cmdGeneral "who" $ \msg -> do cmdWho stateVar = cmdGeneral "who" $ \msg -> do
state <- liftIO $ readMVar stateVar state <- liftIO $ readMVar stateVar
let people = others $ botListing state let people = state ^. botListing . lsOthersL
nicks = sort $ map svNick $ Map.elems people nicks = sort $ map svNick $ Map.elems people
void $ reply msg $ T.intercalate "\n" nicks void $ reply msg $ T.intercalate "\n" nicks

View file

@ -1,34 +1,49 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
-- | A 'Listing' helps keep track of a bot's own 'SessionView' as well as all -- | A 'Listing' helps keep track of a bot's own 'SessionView' as well as all
-- other clients connected to a room. It must be kept up-to-date manually. -- other clients connected to a room. It must be kept up-to-date manually.
module Haboli.Euphoria.Listing module Haboli.Euphoria.Listing
( Listing ( Listing(..)
, lsSelfL
, lsOthersL
, newListing , newListing
, self
, others
, updateOwnNick , updateOwnNick
, preferNick , preferNick
, preferNickVia
, updateFromList , updateFromList
, updateFromListVia
, updateFromEvent , updateFromEvent
, updateFromEventVia
) where ) where
import Control.Concurrent
import Control.Monad.IO.Class
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Data.Maybe import Data.Maybe
import qualified Data.Text as T import qualified Data.Text as T
import Lens.Micro
import Haboli.Euphoria.Api import Haboli.Euphoria.Api
import Haboli.Euphoria.Client import Haboli.Euphoria.Client
import Haboli.Euphoria.Lens
-- | A listing contains a bot's own 'SessionView' (accessible via 'self') and a -- | A listing contains a bot's own 'SessionView' (accessible via 'lsSelf') and a
-- map of all other clients currently connected to the room (accessible via -- map of all other clients currently connected to the room (accessible via
-- 'others'). The latter never includes the bot itself. -- 'lsOthers'). The latter never includes the bot itself.
data Listing = Listing data Listing = Listing
{ lsSelf :: SessionView { lsSelf :: SessionView
-- ^ The 'SessionView' describing the bot itself.
, lsOthers :: Map.Map UserId SessionView , lsOthers :: Map.Map UserId SessionView
-- ^ The 'SessionView's describing the other clients connected to the current
-- room. Does not include the bot's own 'SessionView' (use 'lsSelf' to access
-- that).
} deriving (Show) } deriving (Show)
makeLensesL ''Listing
othersFromList :: [SessionView] -> Map.Map UserId SessionView othersFromList :: [SessionView] -> Map.Map UserId SessionView
othersFromList sessions = Map.fromList [(svId sv, sv) | sv <- sessions] othersFromList sessions = Map.fromList [(svId sv, sv) | sv <- sessions]
@ -39,29 +54,28 @@ newListing (h, s) = Listing
, lsOthers = othersFromList $ snapshotListing s , lsOthers = othersFromList $ snapshotListing s
} }
-- | The 'SessionView' describing the bot itself.
self :: Listing -> SessionView
self = lsSelf
-- | The 'SessionView's describing the other clients connected to the current
-- room. Does not include the bot's own 'SessionView' (use 'self' to access
-- that).
others :: Listing -> Map.Map UserId SessionView
others = lsOthers
-- | Set the bot's own nick to a new nick. -- | Set the bot's own nick to a new nick.
updateOwnNick :: T.Text -> Listing -> Listing updateOwnNick :: T.Text -> Listing -> Listing
updateOwnNick name listing = listing{lsSelf = (lsSelf listing){svNick = name}} updateOwnNick name = lsSelfL . svNickL .~ name
-- | Set the bot's nick and update the 'Listing' with the server's reply in one -- | Set the bot's nick and update the 'Listing' with the server's reply in one
-- go. -- go.
preferNick :: T.Text -> Listing -> Client e Listing preferNick :: T.Text -> Listing -> Client e Listing
preferNick name listing preferNick name listing
| name == svNick (self listing) = pure listing | name == listing ^. lsSelfL . svNickL = pure listing
| otherwise = do | otherwise = do
(_, newNick) <- nick name (_, newNick) <- nick name
pure $ updateOwnNick newNick listing pure $ updateOwnNick newNick listing
-- | Like 'preferNick', but updates a 'Listing' inside a data type inside an
-- 'MVar'.
preferNickVia :: Lens' a Listing -> MVar a -> T.Text -> Client e ()
preferNickVia field mvar name = do
a <- liftIO $ takeMVar mvar
listing' <- preferNick name $ a ^. field
let a' = a & field .~ listing'
liftIO $ putMVar mvar a'
-- | Update a 'Listing' from a list of sessions currently connected to the room. -- | Update a 'Listing' from a list of sessions currently connected to the room.
-- Afterwards, the 'Listing' will contain only those sessions present in the -- Afterwards, the 'Listing' will contain only those sessions present in the
-- list. -- list.
@ -73,22 +87,30 @@ updateFromList sessions listing =
newOthers = Map.filterWithKey (\k _ -> k /= ownId) others' newOthers = Map.filterWithKey (\k _ -> k /= ownId) others'
in Listing newSelf newOthers in Listing newSelf newOthers
onJoin :: SessionView -> Listing -> Listing -- | Like 'updateFromList', but updates a 'Listing' inside a data type inside an
onJoin sv listing = listing{lsOthers = Map.insert (svId sv) sv $ lsOthers listing} -- 'MVar'.
updateFromListVia :: Lens' a Listing -> MVar a -> [SessionView] -> Client e ()
onPart :: SessionView -> Listing -> Listing updateFromListVia field mvar list =
onPart sv listing = listing{lsOthers = Map.delete (svId sv) $ lsOthers listing} liftIO $ modifyMVar_ mvar $ pure . (field %~ updateFromList list)
-- | Update a 'Listing' based on an 'Event'. Follows the euphoria documentation -- | Update a 'Listing' based on an 'Event'. Follows the euphoria documentation
-- for 'JoinEvent', 'PartEvent' and 'NetworkEvent'. -- for 'JoinEvent', 'PartEvent' and 'NetworkEvent'.
updateFromEvent :: Event -> Listing -> Listing updateFromEvent :: Event -> Listing -> Listing
updateFromEvent (EventJoin e) listing = onJoin (joinSession e) listing updateFromEvent (EventJoin e) listing =
updateFromEvent (EventPart e) listing = onPart (partSession e) listing let sv = joinSession e
in listing & lsOthersL %~ Map.insert (svId sv) sv
updateFromEvent (EventPart e) listing =
let sv = partSession e
in listing & lsOthersL %~ Map.delete (svId sv)
updateFromEvent (EventNetwork e) listing | networkType e == "partition" = updateFromEvent (EventNetwork e) listing | networkType e == "partition" =
let sId = networkServerId e let sId = networkServerId e
sEra = networkServerEra e sEra = networkServerEra e
isAffected sv = svServerId sv == sId && svServerEra sv == sEra isAffected sv = svServerId sv == sId && svServerEra sv == sEra
others' = Map.filter (not . isAffected) $ lsOthers listing in listing & lsOthersL %~ Map.filter (not . isAffected)
in listing{lsOthers = others'}
updateFromEvent _ listing = listing updateFromEvent _ listing = listing
-- | Like 'updateFromEvent', but updates a 'Listing' inside a data type inside
-- an 'MVar'.
updateFromEventVia :: Lens' a Listing -> MVar a -> Event -> Client e ()
updateFromEventVia field mvar event =
liftIO $ modifyMVar_ mvar $ pure . (field %~ updateFromEvent event)