packages feed

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