replicant-0.1.0.0: src/Replicant/Plugin.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Replicant.Plugin
( Adapter(..)
, BotSpec(..)
, Example(..)
, H
, Handler
, HandlerCtx(..)
, Plugin(..)
, PluginData(..)
, botHandlers
, mkHandler
, handlerApplies
, handlerCommandOnly
, handlerExamples
, handlerName
, pluginExamples
, runHandler
, sendToUser
) where
import Replicant.Base
import Control.Monad.Reader (ReaderT, runReaderT, lift)
import Data.Attoparsec.Text (Parser, parseOnly, endOfInput)
import Data.ByteString (ByteString)
import Data.List (find)
import Data.Maybe (isNothing)
import qualified Database.Redis as Redis (Connection)
data HandlerCtx m = HandlerCtx
{ handlerBot :: BotSpec m
, handlerMessage :: Message
, handlerPluginName :: ByteString
, handlerRedisConn :: Redis.Connection
}
type H m a = ReaderT (HandlerCtx m) m a
instance Replicant e m => Replicant e (ReaderT (HandlerCtx m) m) where
redisPool = lift redisPool
redisError = lift . redisError
redisNamespace = do
base <- lift redisNamespace
plugin <- asks handlerPluginName
BotSpec{..} <- asks handlerBot
return $ base <> ":bot:" <> encodeUtf8 (botId botRecord) <> ":plugin:" <> plugin
data Example = Example
{ exampleText :: Text
, exampleDescription :: Text
} deriving Show
data Plugin m = Plugin
{ pluginName :: Text
, pluginHandlers :: [Handler m]
}
data PluginData = PluginData
{ pdName :: Text
, pdExamples :: [Example]
}
data Handler m = Handler
{ handlerName :: Text
, handlerExamples :: [Example]
, handlerCommand :: Bool
-- The odd version of Bool is so we can have a consistent
-- monadic interface to both these helpers
-- We always `return ()` if the command check fails
-- These aren't exposed, so it's not that bad, but ...
-- TODO: make this more intuitive
, handlerTest :: BotSpec m -> Message -> Maybe ()
, handlerRun :: BotSpec m -> Message -> m ()
}
-- Idea:
-- * enforce that plugins parse their examples
-- * (optional) enforce that other plugins _don't_ parse
data BotSpec m = BotSpec
{ botRecord :: !Bot
, botAdapter :: !(Adapter m)
, botPlugins :: ![Plugin m]
}
data Adapter m = Adapter
{ bootBot :: BotSpec m -> m ()
, sendToRoom :: Bot -> Room -> Text -> m ()
, sendToUserId :: Bot -> UserId -> Text -> m ()
, parseCommand :: Bot -> Message -> Maybe Text
, getRoomByName :: Bot -> Text -> m (Maybe Room)
, getRoomMembers :: Bot -> Room -> m [User]
}
sendToUser :: Adapter m -> Bot -> User -> Text -> m ()
sendToUser a b User{..} = sendToUserId a b userId
checkParser :: Parser a -> BotSpec m -> Message -> Maybe ()
checkParser parser _ Message{..} =
case parseOnly (parser <* endOfInput) messageText of
Right _ -> Nothing
Left _ -> Just ()
runParser :: Monad m => Parser a -> (BotSpec m -> Message -> a -> m ()) -> BotSpec m -> Message -> m ()
runParser parser handler bot msg@Message{..} =
case parseOnly parser messageText of
Right r -> handler bot msg r
Left _ -> return ()
handlerApplies :: Monad m => Handler m -> BotSpec m -> Message -> Bool
handlerApplies p b m = isNothing $ handlerTest p b m
handlerCommandOnly :: Handler m -> Bool
handlerCommandOnly = handlerCommand
runHandler :: Monad m => Handler m -> BotSpec m -> Message -> m ()
runHandler = handlerRun
mkHandler :: Replicant e m
=> Text
-> Bool
-> Parser a
-> [Example]
-> (a -> H m ())
-> Handler m
mkHandler name commandOnly parser examples handler = Handler
{ handlerName = name
, handlerExamples = map verify examples
, handlerCommand = commandOnly
, handlerTest = withCommand $ checkParser parser
, handlerRun = withCommand . runParser parser $ run handler
}
where
run :: Replicant e m => (a -> H m ()) -> BotSpec m -> Message -> a -> m ()
run h b msg a = do
conn <- redisPool
let
Bot{..} = botRecord b
ctx = HandlerCtx
{ handlerBot = b
, handlerMessage = msg
, handlerPluginName = encodeUtf8 $ pluginNamespace b name
, handlerRedisConn = conn
}
runReaderT (h a) ctx
withCommand :: (Monad m, Monad a) => (BotSpec a -> Message -> m ()) -> BotSpec a -> Message -> m ()
withCommand f b m = case parseCommand (botAdapter b) (botRecord b) m of
Just text -> f b $ m { messageText = text }
_ -> -- This didn't match the command format for the given adapter
if commandOnly
then return ()
else f b m
-- TODO: check that each example does match the given parser (what about commands, users, room, &c)
verify = id
botHandlers :: BotSpec m -> [Handler m]
botHandlers bot = concatMap pluginHandlers $ botPlugins bot
-- TODO: plugins should probably just hold on to their namespace. How should
-- we enforce consistency?
pluginNamespace :: BotSpec m -> Text -> Text
pluginNamespace BotSpec{..} handler =
case find container botPlugins of
Just plugin -> pluginName plugin
-- This handler isn't in the bots' installed plugins
Nothing -> "handler:" <> handler
where
container Plugin{..} = any (\h -> handlerName h == handler) pluginHandlers
pluginExamples :: Plugin m -> [Example]
pluginExamples = concatMap handlerExamples . pluginHandlers