packages feed

marvin-0.0.2: src/Marvin/Run.hs

{-|
Module      : $Header$
Description : Running marvin.
Copyright   : (c) Justus Adam, 2016
License     : BSD3
Maintainer  : dev@justus.science
Stability   : experimental
Portability : POSIX
-}
{-# LANGUAGE DeriveGeneric          #-}
{-# LANGUAGE ExplicitForAll         #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE NamedFieldPuns         #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TemplateHaskell        #-}
module Marvin.Run
    ( runMarvin, ScriptInit, IsAdapter
    , requireFromAppConfig, lookupFromAppConfig, defaultConfigName
    ) where


import           Control.Concurrent.Async  (async, wait)
import           Control.Exception
import           Control.Lens              hiding (cons)
import           Control.Monad.State       hiding (mapM_)
import           Data.Char                 (isSpace)
import qualified Data.Configurator         as C
import qualified Data.Configurator.Types   as C
import           Data.Maybe                (fromMaybe)
import           Data.Sequences
import           Data.Traversable          (for)
import           Data.Vector               (Vector)
import           Marvin.Adapter
import           Marvin.Internal           hiding (match)
import           Marvin.Internal.Types     hiding (channel)
import           Marvin.Util.Regex
import           Options.Generic
import           Prelude                   hiding (dropWhile, splitAt, (++))
import qualified System.Log.Formatter      as L
import qualified System.Log.Handler.Simple as L
import qualified System.Log.Logger         as L

(++) :: Monoid a => a -> a -> a
(++) = mappend

data CmdOptions = CmdOptions
    { configPath :: Maybe FilePath
    , verbose    :: Bool
    , debug      :: Bool
    } deriving (Generic)


instance ParseRecord CmdOptions


defaultBotName :: String
defaultBotName = "marvin"


defaultConfigName :: FilePath
defaultConfigName = "config.cfg"


requireFromAppConfig :: C.Configured a => C.Config -> C.Name -> IO a
requireFromAppConfig cfg = C.require (C.subconfig (unwrapScriptId applicationScriptId) cfg)


lookupFromAppConfig :: C.Configured a => C.Config -> C.Name -> IO (Maybe a)
lookupFromAppConfig cfg = C.lookup (C.subconfig (unwrapScriptId applicationScriptId) cfg)


declareFields [d|
    data Handlers = Handlers
        { handlersResponds :: [(Regex, Message -> Match -> IO ())]
        , handlersHears :: [(Regex, Message -> Match -> IO ())]
        }
    |]


mkApp :: [Script a] -> C.Config -> a -> EventHandler a
mkApp scripts cfg adapter = handler
  where
    handler (MessageEvent msg) = handleMessage msg

    handleMessage msg = do
        lDispatches <- doIfMatch allListens text
        botname <- fromMaybe defaultBotName <$> lookupFromAppConfig cfg "name"
        let (trimmed, remainder) = splitAt (fromIntegral $ length botname) $ dropWhile isSpace text
        rDispatches <- if toLower trimmed == toLower botname
                            then doIfMatch allReactions remainder
                            else return mempty
        mapM_ wait (lDispatches ++ rDispatches)
      where
        text = content msg
        doIfMatch things toMatch  =
            catMaybes <$> for things (\(trigger, action) ->
                case match [] trigger toMatch of
                        Nothing -> return Nothing
                        Just m  -> Just <$> async (action msg m))

    flattenActions = foldr $ \script -> flip (foldr (addAction script adapter)) (script^.actions)

    allActions = flattenActions (Handlers mempty mempty) scripts

    allReactions :: Vector (Regex, Message -> Match -> IO ())
    allReactions = fromList $! allActions^.responds
    allListens :: Vector (Regex, Message -> Match -> IO ())
    allListens = fromList $! allActions^.hears


addAction :: Script a -> a -> WrappedAction a -> Handlers -> Handlers
addAction script adapter wa =
    case wa of
        (WrappedAction (Hear re) ac) -> hears %~ cons (re, runMessageAction script adapter re ac)
        (WrappedAction (Respond re) ac) -> responds %~ cons (re, runMessageAction script adapter re ac)


runMessageAction :: Script a -> a -> Regex -> BotReacting a MessageReactionData () -> Message -> Match -> IO ()
runMessageAction script adapter re ac msg mtch =
    catch
        (evalStateT (runReaction ac) (BotActionState (script^.scriptId) (script^.config) adapter (MessageReactionData msg mtch)))
        (onScriptExcept (script^.scriptId) re)


onScriptExcept :: ScriptId -> Regex -> SomeException -> IO ()
onScriptExcept (ScriptId id) r e = do
    err $ "Unhandled exception during execution of script " ++ show id ++ " with trigger " ++ show r
    err $ show e
  where
    err = L.errorM "bot.dispatch"


-- | Create a wai compliant application
application :: [ScriptInit a] -> C.Config -> InitEventHandler a
application inits config ada = do
    L.infoM "bot" "Initializing scripts"
    s <- catMaybes <$> mapM (\(ScriptInit (sid, s)) -> catch (Just <$> s ada config) (onInitExcept sid)) inits
    return $ mkApp s config ada
  where
    onInitExcept :: ScriptId -> SomeException -> IO (Maybe a')
    onInitExcept (ScriptId id) e = do
        err $ "Unhandled exception during initialization of script " ++ show id
        err $ show e
        return Nothing
      where err = L.errorM "bot.init"


prepareLogger :: IO ()
prepareLogger =
    L.updateGlobalLogger L.rootLoggerName (L.setHandlers [handler])
  where
    handler = L.GenericHandler { L.priority = L.DEBUG
                               , L.formatter = L.simpleLogFormatter "$time [$prio:$loggername] $msg"
                               , L.privData = ()
                               , L.writeFunc = const putStrLn
                               , L.closeFunc = const $ return ()
                               }



runMarvin :: forall a. IsAdapter a => [ScriptInit a] -> IO ()
runMarvin s' = do
    prepareLogger
    args <- getRecord "bot server"
    when (verbose args) $ L.updateGlobalLogger L.rootLoggerName (L.setLevel L.INFO)
    when (debug args) $ L.updateGlobalLogger L.rootLoggerName (L.setLevel L.DEBUG)
    cfgLoc <- maybe
                (L.noticeM "bot" "Using default config: config.cfg" >> return defaultConfigName)
                return
                (configPath args)
    (cfg, cfgTid) <- C.autoReload C.autoConfig [C.Required cfgLoc]
    unless (verbose args || debug args) $ C.lookup cfg "bot.logging" >>= maybe (return ()) (L.updateGlobalLogger L.rootLoggerName . L.setLevel)

    runWithAdapter
        (C.subconfig ("adapter." ++ unwrapAdapterId (adapterId :: AdapterId a)) cfg)
        $ application s' cfg