marvin 0.0.6 → 0.0.7
raw patch · 8 files changed
+214/−126 lines, 8 filesdep +haskelinedep +monad-loopsdep +timedep ~marvin
Dependencies added: haskeline, monad-loops, time
Dependency ranges changed: marvin
Files
- marvin.cabal +9/−6
- preprocessor/Main.hs +1/−0
- src/Marvin/Adapter/Shell.hs +59/−0
- src/Marvin/Adapter/Slack.hs +30/−9
- src/Marvin/Adapter/Telegram.hs +6/−6
- src/Marvin/Internal.hs +65/−56
- src/Marvin/Internal/Types.hs +14/−20
- src/Marvin/Run.hs +30/−29
marvin.cabal view
@@ -1,5 +1,5 @@ name: marvin-version: 0.0.6+version: 0.0.7 cabal-version: >=1.10 build-type: Simple license: BSD3@@ -9,7 +9,7 @@ homepage: https://marvin.readthedocs.io synopsis: A modular chat bot description:- The proper documentation is on readthedocs: https://marvin.readthedocs.io+ A framework for writing portable chat bots. Inspired by hubot. The documentation is on readthedocs: <https://marvin.readthedocs.io> category: Development author: JustusAdam data-files:@@ -37,6 +37,7 @@ Marvin.Util.JSON Marvin.Util.HTTP Marvin.Adapter+ Marvin.Adapter.Shell Marvin.Adapter.Slack Marvin.Adapter.Telegram Marvin.Internal@@ -60,15 +61,17 @@ random ==1.1.*, hashable >=1.2.4.0 && <1.3, text >=1.2.2.1 && <1.3,- mtl >=2.2.1 && <2.3, unordered-containers >=0.2.7.1 && <0.3,- mono-traversable >=1.0.0.1 && <1.1, stm >=2.4.4.1 && <2.5, marvin-interpolate >0.4 && <0.5, lifted-base >=0.2.3.8 && <0.3, lifted-async >=0.9.0 && <0.10, wai >=3.2.1.1 && <3.3,- warp >=3.2.8 && <3.3+ warp >=3.2.8 && <3.3,+ haskeline >=0.7.2.3 && <0.8,+ monad-loops >=0.4.3 && <0.5,+ mono-traversable >=1.0.0.1 && <1.1,+ time >=1.6.0.1 && <1.7 default-language: Haskell2010 default-extensions: OverloadedStrings TypeFamilies MultiParamTypeClasses TupleSections GADTs TemplateHaskell@@ -84,7 +87,7 @@ mustache ==2.1.*, directory >=1.2.6.2 && <1.3, filepath >=1.4.1.0 && <1.5,- marvin >=0.0.6 && <0.1,+ marvin >=0.0.7 && <0.1, configurator >=0.3.0.0 && <0.4, optparse-applicative >=0.12.1.0 && <0.13, bytestring >=0.10.8.1 && <0.11,
preprocessor/Main.hs view
@@ -35,6 +35,7 @@ [ ("slack-rtm", slackRtmData) , ("telegram-poll", ("Marvin.Adapter.Telegram", "(TelegramAdapter Poll)")) , ("telegram-push", ("Marvin.Adapter.Telegram", "(TelegramAdapter Push)"))+ , ("shell", ("Marvin.Adapter.Shell", "ShellAdapter")) ]
+ src/Marvin/Adapter/Shell.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE NamedFieldPuns #-}+module Marvin.Adapter.Shell where+++import Control.Concurrent.Async.Lifted+import Control.Concurrent.MVar.Lifted+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.Loops+import Data.Char (isSpace)+import Data.Maybe (fromMaybe)+import qualified Data.Text as T+import qualified Data.Text.Lazy as L+import Data.Time.Clock (getCurrentTime)+import Marvin.Adapter+import Marvin.Internal (defaultBotName)+import Marvin.Internal.Types+import Marvin.Interpolate.String+import Marvin.Run (lookupFromAppConfig)+import System.Console.Haskeline+import qualified Data.Configurator as C+++data ShellAdapter = ShellAdapter+ { output :: MVar (Maybe L.Text)+ }+++instance IsAdapter ShellAdapter where+ type User ShellAdapter = ()+ type Channel ShellAdapter = ()++ adapterId = "shell"+ messageChannel ShellAdapter{output} _ = putMVar output . Just+ getUsername _ _ = return "shell"+ getChannelName _ _ = return "shell"+ resolveChannel _ _ = return $ Just ()+ runWithAdapter cfg initializer = do+ bot <- liftIO $ fromMaybe defaultBotName <$> lookupFromAppConfig cfg "name"+ histfile <- liftIO $ C.lookup cfg "history-file"+ out <- liftIO newEmptyMVar+ let ada = ShellAdapter out+ handler <- liftIO $ initializer ada+ liftIO $ runInputT defaultSettings {historyFile=histfile} $ forever $ do+ input <- getInputLine $(isS "#{bot}> ")+ case input of+ Nothing -> return ()+ Just i -> do+ h <- liftIO $ async $ do+ botname <- L.toLower . fromMaybe defaultBotName <$> liftIO (lookupFromAppConfig cfg "name")+ let mtext = L.pack i+ ts <- TimeStamp <$> getCurrentTime+ handler $ case L.stripPrefix botname $ L.stripStart mtext of+ Just cmd | fmap (isSpace . fst) (L.uncons cmd) == Just True ->+ CommandEvent () () (L.stripStart cmd) ts+ _ -> MessageEvent () () mtext ts+ putMVar out Nothing+ whileJust_ (liftIO $ takeMVar out) $ outputStrLn . L.unpack+ liftIO $ wait h
src/Marvin/Adapter/Slack.hs view
@@ -30,10 +30,11 @@ import Data.Aeson.TH import Data.Aeson.Types hiding (Error) import qualified Data.ByteString.Lazy.Char8 as BS+import Data.Char (isSpace) import qualified Data.Configurator as C import qualified Data.Configurator.Types as C import Data.Containers-import Data.Foldable (toList)+import Data.Foldable (asum, toList) import Data.Hashable import Data.HashMap.Strict (HashMap) import Data.IORef.Lifted@@ -43,8 +44,10 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as L import Marvin.Adapter+import Marvin.Internal+import Marvin.Internal.Types as Types import Marvin.Interpolate.Text-import Marvin.Types as Types+import Marvin.Run import Network.URI import Network.Wai import Network.Wai.Handler.Warp@@ -125,12 +128,12 @@ deriveJSON defaultOptions { fieldLabelModifier = camelTo2 '_' } ''RTMData -messageParser :: Value -> Parser (Types.Message (SlackAdapter a))-messageParser (Object o) = Message+messageParser :: Value -> Parser (Event (SlackAdapter a))+messageParser (Object o) = MessageEvent <$> o .: "user" <*> o .: "channel" <*> o .: "text"- <*> o .: "ts"+ <*> (o .: "ts" >>= timestampFromNumber) messageParser _ = mzero @@ -162,20 +165,21 @@ "channel_leave" -> cLeave "group_leave" -> cLeave "channel_topic" -> do- t <- TopicChangeEvent <$> o .: "topic" <*> channel+ t <- TopicChangeEvent <$> user <*> channel <*> o .: "topic" <*> ts return $ Right t _ -> msgEv _ -> msgEv where- msgEv = Right . MessageEvent <$> messageParser v+ ts = o .: "ts" >>= timestampFromNumber+ msgEv = Right <$> messageParser v user = o .: "user" channel = o .: "channel" cJoin = do- ev <- ChannelJoinEvent <$> user <*> channel+ ev <- ChannelJoinEvent <$> user <*> channel <*> ts return $ Right ev cLeave = do- ev <- ChannelLeaveEvent <$> user <*> channel+ ev <- ChannelLeaveEvent <$> user <*> channel <*> ts return $ Right ev "reconnect_url" -> return $ Left Ignored "channel_archive" -> do@@ -269,12 +273,27 @@ where cfg = userConfig ada +stripWhiteSpaceMay :: L.Text -> Maybe L.Text+stripWhiteSpaceMay t =+ case L.uncons t of+ Just (c, _) | isSpace c -> Just $ L.stripStart t+ _ -> Nothing++ runHandlerLoop :: SlackAdapter a -> Chan BS.ByteString -> EventHandler (SlackAdapter a) -> RunnerM () runHandlerLoop adapter messageChan handler = forever $ do d <- readChan messageChan case eitherDecode d >>= parseEither eventParser of Left err -> logErrorN $(isT "Error parsing json: #{err} original data: #{rawBS d}")+ Right (Right ev@(MessageEvent u c m t)) -> do++ botname <- L.toLower . fromMaybe defaultBotName <$> liftIO (lookupFromAppConfig cfg "name")+ let lmsg = L.stripStart $ L.toLower m+ liftIO $ handler $ case asum $ map ((`L.stripPrefix` lmsg) >=> stripWhiteSpaceMay) [botname, L.cons '@' botname, L.cons '/' botname] of+ Nothing -> ev+ Just m' -> CommandEvent u c m' t+ Right (Right event) -> liftIO $ handler event Right (Left internalEvent) -> case internalEvent of@@ -291,6 +310,8 @@ ChannelDeleted chan -> deleteChannel adapter chan ChannelRename info -> renameChannel adapter info UserChange ui -> void $ refreshUserInfo adapter (ui^.idValue)+ where+ cfg = userConfig adapter sendMessageImpl :: MVar Connection -> BS.ByteString -> RunnerM ()
src/Marvin/Adapter/Telegram.hs view
@@ -25,9 +25,9 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as L import Marvin.Adapter+import Marvin.Internal.Types import Marvin.Interpolate.String import Marvin.Interpolate.Text-import Marvin.Types import Network.Wai import Network.Wai.Handler.Warp import Network.Wreq@@ -109,18 +109,18 @@ where isMessage = do msg <- o .: "message" >>= msgParser- return $ Ev $ MessageEvent msg- isPost = Ev . MessageEvent <$> (o .: "channel_post" >>= msgParser)+ return $ Ev msg+ isPost = Ev <$> (o .: "channel_post" >>= msgParser) isUnhandeled = return Unhandeled -msgParser :: Value -> Parser (Message (TelegramAdapter a))+msgParser :: Value -> Parser (Event (TelegramAdapter a)) msgParser = withObject "expected message object" $ \o ->- Message+ MessageEvent <$> o .: "from" <*> o .: "chat" <*> o .: "text"- <*> o .: "date"+ <*> (o .: "date" >>= timestampFromNumber)
src/Marvin/Internal.hs view
@@ -62,6 +62,7 @@ import Marvin.Internal.Types hiding (getChannelName, getUsername, messageChannel, resolveChannel, resolveChannel) import Marvin.Interpolate.Text+import Marvin.Interpolate.String import Marvin.Util.Regex (Match, Regex) import Util @@ -80,20 +81,17 @@ |] -type Topic = L.Text-- declareFields [d| data Handlers a = Handlers- { handlersResponds :: Vector (Regex, Match -> Message a -> RunnerM ())- , handlersHears :: Vector (Regex, Match -> Message a -> RunnerM ())+ { handlersResponds :: Vector (Regex, (User' a, Channel' a, Match, Message, TimeStamp) -> RunnerM ())+ , handlersHears :: Vector (Regex, (User' a, Channel' a, Match, Message, TimeStamp) -> RunnerM ()) , handlersCustoms :: Vector (Event a -> Maybe (RunnerM ()))- , handlersJoins :: Vector ((User' a, Channel' a) -> RunnerM ())- , handlersLeaves :: Vector ((User' a, Channel' a) -> RunnerM ())- , handlersTopicChange :: Vector ((Topic, Channel' a) -> RunnerM ())- , handlersJoinsIn :: HM.HashMap L.Text (Vector ((User' a, Channel' a) -> RunnerM ()))- , handlersLeavesFrom :: HM.HashMap L.Text (Vector ((User' a, Channel' a) -> RunnerM ()))- , handlersTopicChangeIn :: HM.HashMap L.Text (Vector ((Topic, Channel' a) -> RunnerM ()))+ , handlersJoins :: Vector ((User' a, Channel' a, TimeStamp) -> RunnerM ())+ , handlersLeaves :: Vector ((User' a, Channel' a, TimeStamp) -> RunnerM ())+ , handlersTopicChange :: Vector ((User' a, Channel' a, Topic, TimeStamp) -> RunnerM ())+ , handlersJoinsIn :: HM.HashMap L.Text (Vector ((User' a, Channel' a, TimeStamp) -> RunnerM ()))+ , handlersLeavesFrom :: HM.HashMap L.Text (Vector ((User' a, Channel' a, TimeStamp) -> RunnerM ()))+ , handlersTopicChangeIn :: HM.HashMap L.Text (Vector ((User' a, Channel' a, Topic, TimeStamp) -> RunnerM ())) } |] @@ -102,7 +100,7 @@ mempty = Handlers mempty mempty mempty mempty mempty mempty mempty mempty mempty mappend (Handlers r1 h1 c1 j1 l1 t1 ji1 li1 ti1) (Handlers r2 h2 c2 j2 l2 t2 ji2 li2 ti2)- = Handlers (r1 <> r2) (h1 <> h2) (c1 <> c2) (j1 <> j2) (l1 <> l2) (t1 <> t2) (ji1 <> ji2) (li1 <> li2) (ti1 <> ti2)+ = Handlers (r1 <> r2) (h1 <> h2) (c1 <> c2) (j1 <> j2) (l1 <> l2) (t1 <> t2) (HM.unionWith mappend ji1 ji2) (HM.unionWith mappend li1 li2) (HM.unionWith mappend ti1 ti2) -- | Monad for reacting in the bot. Allows use of functions like 'send', 'reply' and 'messageChannel' as well as any arbitrary 'IO' action using 'liftIO'.@@ -144,33 +142,42 @@ class Get a b where getLens :: Lens' a b -instance Get (b, Message a) (Message a) where- getLens = _2+instance Get (User' a, b, c) (User' a) where+ getLens = _1 -instance Get (Match, b) Match where+instance Get (User' a, b, c, d) (User' a) where getLens = _1 -instance Get (Topic, a) Topic where+instance Get (User' a, b, c, d, e) (User' a) where getLens = _1 -idLens :: Lens' a a-idLens = lens id (flip const)+instance Get (a, Channel' b, c) (Channel' b) where+ getLens = _2 -instance Get (b, Channel' a) (Channel' a) where+instance Get (a, Channel' b, c, d) (Channel' b) where getLens = _2 -instance Get (b, Message a) (Channel' a) where- getLens = _2 . lens (Channel' . channel) (\a (Channel' b) -> a {channel = b})+instance Get (a, Channel' b, c, d, e) (Channel' b) where+ getLens = _2 -instance Get a a where- getLens = idLens+instance Get (a, b, TimeStamp) TimeStamp where+ getLens = _3 -instance Get (User' a, b) (User' a) where- getLens = _1+instance Get (a, b, c, TimeStamp) TimeStamp where+ getLens = _4 -instance Get (b, Message a) (User' a) where- getLens = _2 . lens (User' . sender) (\a (User' b) -> a {sender = b})+instance Get (a, b, c, d, TimeStamp) TimeStamp where+ getLens = _5 +instance Get (a, b, Match, d, e) Match where+ getLens = _3++instance Get (a, b, c, Message, e) Message where+ getLens = _4++instance Get (a, b, Topic, d) Topic where+ getLens = _3+ instance HasConfigAccess (ScriptDefinition a) where getConfigInternal = ScriptDefinition $ use config @@ -225,34 +232,34 @@ onScriptExcept id trigger e = do case trigger of Just t ->- err $(isT "Unhandled exception during execution of script #{id} with trigger #{t}")+ err $(isT "Unhandled exception during execution of script \"#{id}\" with trigger \"#{t}\"") Nothing ->- err $(isT "Unhandled exception during execution of script #{id}")+ err $(isT "Unhandled exception during execution of script \"#{id}\"") err $(isT "#{e}") where- err = logErrorNS "#{applicationScriptId}.dispatch"+ err = logErrorNS $(isT "#{applicationScriptId}.dispatch") -- | Whenever any message matches the provided regex this handler gets run. -- -- Equivalent to "robot.hear" in hubot-hear :: Regex -> BotReacting a (Match, Message a) () -> ScriptDefinition a ()+hear :: Regex -> BotReacting a (User' a, Channel' a, Match, Message, TimeStamp) () -> ScriptDefinition a () hear !re ac = ScriptDefinition $ do pac <- prepareAction (Just re) ac- actions . hears %= V.cons (re, curry pac)+ actions . hears %= V.cons (re, pac) -- | Runs the handler only if the bot was directly addressed. -- -- Equivalent to "robot.respond" in hubot-respond :: Regex -> BotReacting a (Match, Message a) () -> ScriptDefinition a ()+respond :: Regex -> BotReacting a (User' a, Channel' a, Match, Message, TimeStamp) () -> ScriptDefinition a () respond !re ac = ScriptDefinition $ do pac <- prepareAction (Just re) ac- actions . responds %= V.cons (re, curry pac)+ actions . responds %= V.cons (re, pac) -- | This handler runs whenever a user enters __any channel__ (which the bot is subscribed to) -- -- The payload contains the entering user and the channel which was entered.-enter :: BotReacting a (User' a, Channel' a) () -> ScriptDefinition a ()+enter :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () enter ac = ScriptDefinition $ do pac <- prepareAction (Just "enter event" :: Maybe T.Text) ac actions . joins %= V.cons pac@@ -261,7 +268,7 @@ -- | This handler runs whenever a user exits __any channel__ (which the bot is subscribed to) -- -- The payload contains the exiting user and the channel which was exited.-exit :: BotReacting a (User' a, Channel' a) () -> ScriptDefinition a ()+exit :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () exit ac = ScriptDefinition $ do pac <- prepareAction (Just "exit event" :: Maybe T.Text) ac actions . leaves %= V.cons pac@@ -276,9 +283,9 @@ -- The argument is the human readable name for the channel. -- -- The payload contains the entering user.-enterIn :: L.Text -> BotReacting a (User' a, Channel' a) () -> ScriptDefinition a ()+enterIn :: L.Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () enterIn !chanName ac = ScriptDefinition $ do- pac <- prepareAction (Just $(isT "anter event in #{chanName}")) ac+ pac <- prepareAction (Just $(isT "enter event in #{chanName}")) ac actions . joinsIn %= HM.alter (alterHelper pac) chanName @@ -287,7 +294,7 @@ -- The argument is the human readable name for the channel. -- -- The payload contains the exting user.-exitFrom :: L.Text -> BotReacting a (User' a, Channel' a) () -> ScriptDefinition a ()+exitFrom :: L.Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () exitFrom !chanName ac = ScriptDefinition $ do pac <- prepareAction (Just $(isT "exit event in #{chanName}")) ac actions . leavesFrom %= HM.alter (alterHelper pac) chanName@@ -296,7 +303,7 @@ -- | This handler runs when the topic in __any channel__ the bot is subscribed to changes. -- -- The payload contains the new topic and the channel in which it was set.-topic :: BotReacting a (Topic, Channel' a) () -> ScriptDefinition a ()+topic :: BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a () topic ac = ScriptDefinition $ do pac <- prepareAction (Just "topic event" :: Maybe T.Text) ac actions . topicChange %= V.cons pac@@ -305,7 +312,7 @@ -- | This handler runs when the topic in __the specified channel__ is changed, provided the bot is subscribed to the channel in question. -- -- The argument is the human readable channel name.-topicIn :: L.Text -> BotReacting a (Topic, Channel' a) () -> ScriptDefinition a ()+topicIn :: L.Text -> BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a () topicIn !chanName ac = ScriptDefinition $ do pac <- prepareAction (Just $(isT "topic event in #{chanName}")) ac actions . topicChangeIn %= HM.alter (alterHelper pac) chanName@@ -325,7 +332,7 @@ -- | Send a message to the channel the triggering message came from. -- -- Equivalent to "robot.send" in hubot-send :: (IsAdapter a, Get m (Channel' a)) => L.Text -> BotReacting a m ()+send :: (IsAdapter a, Get d (Channel' a)) => L.Text -> BotReacting a d () send msg = do o <- getChannel messageChannel' o msg@@ -354,11 +361,11 @@ -- | Send a message to the channel the original message came from and address the user that sent the original message. -- -- Equivalent to "robot.reply" in hubot-reply :: (IsAdapter a, Get m (Message a)) => L.Text -> BotReacting a m ()+reply :: (IsAdapter a, Get d (User' a), Get d (Channel' a)) => L.Text -> BotReacting a d () reply msg = do- om <- getMessage- user <- getUsername $ sender om- messageChannel' (channel om) $ user <> " " <> msg+ chan <- getChannel+ user <- getUser >>= getUsername+ messageChannel' chan $ user <> " " <> msg -- | Send a message to a Channel (by name)@@ -408,7 +415,7 @@ -- | Get the message that triggered this action -- Includes sender, target channel, as well as the full, untruncated text of the original message-getMessage :: Get m (Message a) => BotReacting a m (Message a)+getMessage :: Get m Message => BotReacting a m Message getMessage = view (payload . getLens) @@ -446,7 +453,12 @@ requireConfigVal :: (C.Configured a, HasConfigAccess m) => C.Name -> m a requireConfigVal name = do cfg <- getConfig- liftIO $ C.require cfg name+ l <- liftIO $ C.lookup cfg name+ case l of+ Just v -> return v+ _ -> do+ sid <- getScriptId+ error $(isS "Could not find required config value \"#{name}\" in script \"#{sid}\"") -- | INTERNAL, USE WITH CARE@@ -483,17 +495,14 @@ -- Useful for creating actions which can be scheduled to execute a certain time or asynchronous. -- The idea is that one can conveniently send messages from inside a schedulable action. extractReaction :: BotReacting a s o -> BotReacting a s (IO o)-extractReaction reac = BotReacting $ do- s <- ask- return $ runStderrLoggingT $ runReaderT (runReaction reac) s+extractReaction reac = BotReacting $+ runStderrLoggingT . runReaderT (runReaction reac) <$> ask -- | Take an action and produce an IO action with the same effect. -- Useful for creating actions which can be scheduled to execute a certain time or asynchronous. -- The idea is that one can conveniently send messages from inside a schedulable action. extractAction :: BotReacting a () o -> ScriptDefinition a (IO o)-extractAction ac = ScriptDefinition $ do- a <- use adapter- sid <- use scriptId- cfg <- use config- return $ runStderrLoggingT $ runReaderT (runReaction ac) (BotActionState sid cfg a ())+extractAction ac = ScriptDefinition $+ fmap (runStderrLoggingT . runReaderT (runReaction ac)) $+ BotActionState <$> use scriptId <*> use config <*> use adapter <*> pure ()
src/Marvin/Internal/Types.hs view
@@ -9,21 +9,28 @@ import Control.Monad.IO.Class import Control.Monad.Logger import Data.Aeson+import Data.Aeson.Types import Data.Char (isAlphaNum, isLetter) import qualified Data.Configurator.Types as C import Data.String import qualified Data.Text as T import qualified Data.Text.Lazy as L+import Data.Time.Clock+import Data.Time.Clock.POSIX import Marvin.Interpolate.Text import Text.Read (readMaybe) +type Topic = L.Text+type Message = L.Text+ -- | Representation for the types of events which can occur data Event a- = MessageEvent (Message a)- | ChannelJoinEvent (User a) (Channel a)- | ChannelLeaveEvent (User a) (Channel a)- | TopicChangeEvent L.Text (Channel a)+ = MessageEvent (User a) (Channel a) Message TimeStamp+ | CommandEvent (User a) (Channel a) Message TimeStamp+ | ChannelJoinEvent (User a) (Channel a) TimeStamp+ | ChannelLeaveEvent (User a) (Channel a) TimeStamp+ | TopicChangeEvent (User a) (Channel a) Topic TimeStamp type EventHandler a = Event a -> IO ()@@ -51,24 +58,11 @@ newtype User' a = User' {unwrapUser' :: User a} newtype Channel' a = Channel' {unwrapChannel' :: Channel a} -newtype TimeStamp = TimeStamp { unwrapTimeStamp :: Double } deriving Show----- | contents and meta information of a recieved message-data Message a = Message- { sender :: User a- , channel :: Channel a- , content :: L.Text- , timestamp :: TimeStamp- }+newtype TimeStamp = TimeStamp { unwrapTimeStamp :: UTCTime } deriving Show -instance FromJSON TimeStamp where- parseJSON (String s) = maybe mzero (return . TimeStamp) $ readMaybe (T.unpack s)- parseJSON _ = mzero--instance ToJSON TimeStamp where- toJSON = toJSON . show . unwrapTimeStamp+timestampFromNumber :: Value -> Parser TimeStamp+timestampFromNumber = withScientific "expected number type" $ return . TimeStamp . posixSecondsToUTCTime . realToFrac -- | A type, basically a String, which identifies a script to the config and the logging facilities.
src/Marvin/Run.hs view
@@ -11,11 +11,9 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-} module Marvin.Run ( runMarvin, ScriptInit, IsAdapter , requireFromAppConfig, lookupFromAppConfig, defaultConfigName@@ -80,15 +78,16 @@ for_ asyncs wait handler ev wait generics- handler (MessageEvent msg) = handleMessage msg+ handler (MessageEvent user chan msg ts) = handleMessage user chan msg ts+ handler (CommandEvent user chan msg ts) = handleCommand user chan msg ts -- TODO implement other handlers- handler (ChannelJoinEvent user chan) = changeHandlerHelper joinsV joinsInV (User' user) chan- handler (ChannelLeaveEvent user chan) = changeHandlerHelper leavesV leavesFromV (User' user) chan- handler (TopicChangeEvent topic chan) = changeHandlerHelper topicsV topicsInV topic chan+ handler (ChannelJoinEvent user chan ts) = changeHandlerHelper joinsV joinsInV (User' user, , ts) chan+ handler (ChannelLeaveEvent user chan ts) = changeHandlerHelper leavesV leavesFromV (User' user, , ts) chan+ handler (TopicChangeEvent user chan topic ts) = changeHandlerHelper topicsV topicsInV (User' user, , topic, ts) chan - changeHandlerHelper :: Vector ((b, Channel' a) -> RunnerM ())- -> HM.HashMap L.Text (Vector ((b, Channel' a) -> RunnerM ()))- -> b+ changeHandlerHelper :: Vector (d -> RunnerM ())+ -> HM.HashMap L.Text (Vector (d -> RunnerM ()))+ -> (Channel' a -> d) -> Channel a -> RunnerM () changeHandlerHelper wildcards specifics other chan = do@@ -96,30 +95,31 @@ let applicables = fromMaybe mempty $ specifics^?ix cName - wildcardsRunning <- for wildcards (async . ($ (other, Channel' chan)))-- applicablesRunning <- for applicables (async . ($ (other, Channel' chan)))+ wildcardsRunning <- for wildcards (async . ($ other (Channel' chan))) - mapM_ wait $ wildcardsRunning `mappend` applicablesRunning+ applicablesRunning <- for applicables (async . ($ other (Channel' chan))) + mapM_ wait wildcardsRunning+ mapM_ wait applicablesRunning - handleMessage msg = do- lDispatches <- doIfMatch hearsV text- botname <- fromMaybe defaultBotName <$> liftIO (lookupFromAppConfig cfg "name")- let (trimmed, remainder) = L.splitAt (fromIntegral $ succ $ L.length botname) $ L.stripStart text- -- TODO At some point this needs to support derivations of the name. Maybe make that configurable?- rDispatches <- if L.stripEnd (L.toLower trimmed) == L.strip (L.toLower botname)- then doIfMatch respondsV remainder- else return mempty- mapM_ wait (lDispatches <> rDispatches)+ handleMessageLike :: Vector (Regex, (User' a, Channel' a, Match, Message, TimeStamp) -> RunnerM ())+ -> User a+ -> Channel a+ -> Message+ -> TimeStamp+ -> RunnerM ()+ handleMessageLike v user chan msg ts = do+ lDispatches <- doIfMatch v+ mapM_ wait lDispatches where- text = content msg- doIfMatch things toMatch =+ doIfMatch things = catMaybes <$> for things (\(trigger, action) ->- case match trigger toMatch of+ case match trigger msg of Nothing -> return Nothing- Just m -> Just <$> async (action m msg))+ Just m -> Just <$> async (action (User' user, Channel' chan, m, msg, ts)))+ handleCommand = handleMessageLike respondsV+ handleMessage = handleMessageLike hearsV Handlers respondsV hearsV customsV joinsV leavesV topicsV joinsInV leavesFromV topicsInV = foldMap (^.actions) scripts@@ -128,16 +128,17 @@ -- | Create a wai compliant application application :: IsAdapter a => LoggingFn -> [ScriptInit a] -> C.Config -> InitEventHandler a application log inits config ada = flip runLoggingT log $ do- $logInfoS "bot" "Initializing scripts"+ logInfoNS logSource "Initializing scripts" s <- catMaybes <$> mapM (\(ScriptInit (sid, s)) -> catch (Just <$> s ada config) (onInitExcept sid)) inits return $ mkApp log s config ada where+ logSource = $(isT "#{applicationScriptId}.init") onInitExcept :: ScriptId -> SomeException -> RunnerM (Maybe a') onInitExcept (ScriptId id) e = do err $(isT "Unhandled exception during initialization of script #{id}") err $(isT "#{e}") return Nothing- where err = logErrorNS $(isT "#{applicationScriptId}.init")+ where err = logErrorNS logSource setLoggingLevelIn :: LogLevel -> RunnerM a -> RunnerM a@@ -152,7 +153,7 @@ args <- liftIO $ execParser infoParser cfgLoc <- maybe- ($logInfoS $(isT "${applicationScriptId}") "Using default config: config.cfg" >> return defaultConfigName)+ ($logInfoS $(isT "#{applicationScriptId}") "Using default config: config.cfg" >> return defaultConfigName) return (configPath args) (cfg, _) <- liftIO $ C.autoReload C.autoConfig [C.Required cfgLoc]