packages feed

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 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]