Writing marvin scripts¶
Each script in marvin is a Haskell module that defines a value
script with the type
This value contains the code necessary to set up the script and will be run automatically by the marvin runner at startup.
It returns the script.
It does not matter where in the module you define this value, only that it sits at the top level so that the main file can import it. You can define arbitrary other values in the top level of your script, such as mutable variables and you can import any Haskell library you like including other marvin scripts (for Data sharing).
Since each script is a Haskell module the module name and the file name must match.
I.e. a script module MyScript must be in a file called
Furthermore the module and file name may only contain word characters and the underscore
_ and must begin with an upper case letter.
A file which starts with an underscore
_ or dot
. is ignored by the automatic script discovery of the main file.
This is a way to hide unfinished scripts from being included in the program.
When you have created your source file you should first import marvins prelude
Marvin.Prelude (something like marvins standard library).
It contains all the marvin related functions you will need.
You dont have to use
The prelude is just a convenient collection of other modules, you can also import just the ones you need directly, but this is only recommended for people experienced with Haskell.
-- File: MyScript.hs module MyScript where -- Module definition (must match filename) -- import the prelude import Marvin.Prelude -- import other modules and libraries you need script :: IsAdapter a => ScriptInit a script = defineScript "my-script" $ do -- here follows the actual scripting part
Lastly we define a value called
script with the type signature
IsAdapter a => ScriptInit a.
This complicated looking type signature ensures our script will work with any adapter that satisfies the adapter type class (adapter interface).
Here we call the function
defineScript which takes an id string and an initializer block.
The id string is used fo two things
- Scoping the config, i.e. the config for this script will be stored in the
- Logging. All logging messages from this script will be prefixed with
Usually the id string is some variation on the name of the script file and module.
The initializer block is where the actual scripting starts.
The initializer block¶
The initializer block is the code that is run when you start marvin.
First and foremost this block is used to add new reactions to your marvin script, which is most likely the main part of your scripts functionality.
The reaction Monad¶
data BotReacting a d r = ... deriving (Monad, MonadIO, MonadReader (BotActionState a d) , MonadLogger, MonadLoggerIO)
The reaction monad offers basically four different capabilities.
MonadIOallows the user to execute arbitrary
IOactions by lifting them with
- This can be things such as performing HTTP requests, reading files etc.
MonadReader (BotActionState a d)allows read access to the data carried by the monad.
- In general you dont need to use this directly as functions such as getUser are much more convenient to use.
However the readable data you get by using
askcontains not only the payload which is of type
dand different depending on each handler function, but also access to the adapter, the config and script id. And is therefore capable of
MonadLogger(IO)Allows you to write log messages using functions from the monad-logger package by importing
There are several functions for reacting to some event happening in you chat application.
The type of reaction influences the kind of data available in the reaction handler.
The data available in the handler can be seen listed in a tuple in the
BotReacting a (User' a, Channel' a, Message, Match, TimeStamp) () will have access to a user, a channel, a message and so on.
Functions for getting access to this data are listed in Functions for Handlers
The basic structure of a reaction is
<reaction-type> <matcher> <handler>.
This also determines the type of data available in the handler.
Is some selection criterium for which events you wish to handle, and also often influences the contents of the data available to the handler.
For instance for hear and respond this is a regex. The message will only be handled if the regex matches, and the result of the match, as well as the original message is available to the handler later.
Arbitrary code which runs whenever a matched event occurs.
Has access to message specific data (like a regex match of the message). Can communicate with the chat (send messages to people or channels).
There are currently nine reaction functions available:
hear :: Regex -> BotReacting a (User' a, Channel' a, Match, Message, TimeStamp) () -> ScriptDefinition a () hear regex handler = ...
hear triggers on any message posted which matches the regular expression.
The type of Handler is
BotReacting a (User' a, Channel' a, Message, Match, TimeStamp) (), which means in addition to the :ref`normal reaction capabilities <reaction monad>` it has access to the message with the getMessage function and to the regex match with getMatch.
Since this is a reaction to a message we additionally have can use the send function in this handler to post a message to the same channel the triggering message was posted to and also the reply function to send a message to the sender of the original message (also posted to the same channel).
respond :: Regex -> BotReacting a (User' a, Channel' a, Match, Message, TimeStamp) () -> ScriptDefinition a () respond regex handler = ...
respond triggers only on messages which are directed at the bot itself, i.e. the message starts with the name of the bot.
The rest of the message is matched against the provided regular expression like in hear.
topic :: BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a () topic handler = ...
topic triggers whenever the topic in a channel which the bot is subscribed to changes.
The new topic is available via getTopic
The channel in which the topic was changed is available via the getChannel function.
Topic type is just for readability, it is just an alternate name for
topicIn :: Text -> BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a () topicIn channelName handler = ...
Like topic but only triggers when the topic changes in the channel with the human readable
enter :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () enter handler = ...
enter triggers whenever a user enters in a channel which the bot is subscribed to.
The entering user is available via getUser
The channel in which user entered is available via the getChannel function.
enterIn :: Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () enterIn channelName handler = ...
Like enter but only triggers when a user enters the channel with the human readable
exit :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a () exit handler = ...
exit triggers whenever a user exits a channel which the bot is subscribed to.
The exiting user is available via getUser
The channel from which user exited is available via the getChannel function.
Functions for Handlers¶
send :: (IsAdapter a, Get m (Channel' a)) => Text -> BotReacting a m () send msg = ...
send function is used to post messages to the same channel from which the event that triggered the handler came.
Explanation of the type signature:
- We require the saved
BotReactingto be an adapter. This means this function actually interacts with the chat service (sends a message in this case).
Get m (Channel' a)
- The data in the monad must have an originating
Channelin it somewhere to which the message will be posted. This is true for most handler functions, for instance hear, respond, enter all enter, exit and topic handlers.
reply :: (IsAdapter a, Get m (User' a), Get m (Channel' a)) => Text -> BotReacting a m () reply msg = ...
Reply is similar to send. It posts back to the same channel the original message came from, but it also references the author of the original message.
messageChannel :: (HasConfigAccess m, AccessAdapter m, IsAdapter (AdapterT m)) => L.Text -> L.Text -> m () messageChannel channelName message = ...
messageChannel' :: (HasConfigAccess m, AccessAdapter m, IsAdapter (AdapterT m), MonadIO m) => Channel (AdapterT m) -> L.Text -> m () messageChannel' channel message = ...
Like messgeChannel but references the channel by channel object, rather than name.
getMatch :: HasMatch m => BotReacting a m Match
Regex matches are a list of strings. The 0’th index is the full match, the following indexes are matched groups.
getMessage :: Get m (Message a) => BotReacting a m (Message a)
getTopic :: HasTopic m => BotReacting a m Topic
This function is usable in handlers which react to changes of the topic of a channel. It returns the new topic.
Topic type is just for readability, it is just an alternate name for
getChannel :: Get m (Channel' a) => BotReacting a m (Channel a)
Usable in most handler functions, this function returns the channel in which some event occurred.
getUser :: Get m (User' a) => BotReacting a m User
Usable in all handler functions which involve an acting user (most). Returns the user who triggered an event.