Writing marvin scripts

Each script in marvin is a Haskell module that defines a value script with the type ScriptInit. 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).

Script boilerplate

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 MyScript.hs. Furthermore the module and file name may only contain word characters and the underscore _ and must begin with an upper case letter.

Note

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.

Hint

You dont have to use Marvin.Prelude. 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

  1. Scoping the config, i.e. the config for this script will be stored in the scripts.<id-string> key.
  2. Logging. All logging messages from this script will be prefixed with scripts.<id-string>.

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.

But you can do a variety of other things here such as define periodic tasks, read data and define mutable variables for state tracking or data sharing.

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.

  1. MonadIO allows the user to execute arbitrary IO actions by lifting them with liftIO.
    This can be things such as performing HTTP requests, reading files etc.
  2. 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 ask contains not only the payload which is of type d and different depending on each handler function, but also access to the adapter, the config and script id. And is therefore capable of
  3. Accessing the adapter.
    This enables the handler to communicate. Functions such as send and messageChannel can be used to send messages to the chat application.
  4. MonadLogger(IO) Allows you to write log messages using functions from the monad-logger package by importing Control.Monad.Logging.

Reaction functions

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 monad. For instance 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>.

<reaction-type>

Is one of the reaction functions, like hear or respond (more are to follow).

This also determines the type of data available in the handler.

<matcher>

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.

<handler>

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).

Reacting to messages

There are two ways to react to a text message. A reaction defined with hear will trigger on any incoming message which matches its defined pattern (a regular expresion). By contrast reactions defined with respond will only trigger if the bot itself is being adressed. How one adresses the bot depends on the concrete adapter. However typically prefixing the message with the bots name or sending a direct message (if the adapter supports this) to the bot ususally trigger these reactions.

In the handler that is being attached you have access to the match groups of the regex with getMatch, the user who sent the message (getUser), the full text of the message (getMessage), the channel to which the message was posted (getChannel) and a timestamp for when the message arrived (getTimeStamp).

The type signature for both is the same.

hear, respond :: Regex -> BotReacting a (User' a, Channel' a, Match, Message, TimeStamp) () -> ScriptDefinition a ()
hear regex handler = ...
respond regex handler = ...

A working example could be something like this:

defineScript "test" $ do

    hear "\\bmarvin\\b" $ do
        user <- getUser

        send $(isL "Yes #{user^.username}, that is my name")

    respond "^\\bsudo\\b(.+)" $ do
        match <- getMatch
        send #(isL "I will do #{match !! 1} immediately!"

    hear ".*" $ do
        channel <- getChannel
        unless (channel^.name == "#nsa") $ do
            message <- getMessage
            messageChannel "#nsa" $(isL "Psst, this message was just posted in #{channel^.name}: #{message}")

Reacting to the topic

You can react to changes in the topic in two different ways. Using topic the handler will trigger whenever the topic in any channel changes. Using topicIn you can provide the name of a channel which you wish to watch for changes in the topic and the handler will only be run for changes to the topic in the specified channel.

In the handler you have access to the user which triggered the change (getUser), the channel in which the topic was changed (getChannel), the new topic (getTopic) and a timestamp for when this change occurred (getTimeStamp).

topic :: BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a ()
topic handler = ...

topicIn :: Text -> BotReacting a (User' a, Channel' a, Topic, TimeStamp) () -> ScriptDefinition a ()
topicIn channelName handler = ...

Note

The Topic type is just for readability, it is just an alternate name for Text.

Reacting to changes in channel participants

Marvin can react both to people joining and leaving channels. enter triggers when a user enters any channel in which the bot is also participating. enterIn takes as an argument the name of a channel and ony triggers if a user joins that specific channel. exit triggers when a user leaves any channel in which the bot is also participating. exitFrom takes as an argument the name of a channel and ony triggers if a user leaves that specific channel.

All of these handlers have access to the channel which the user joined/left (getChannel), the user that joined/left (getUser) and a timestamp for when this occurred (getTimeStamp)

enter :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
enter handler = ...

enterIn :: Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
enterIn channelName handler = ...

exit :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
exit handler = ...

exitFrom :: Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
exitFrom channelName handler = ...

Reacting to files

The fileShared handler is invoked any time a file is shared in any channel the bot is participating in. By contrast the fileSharedIn handler takes as its first argument a channel name and only reacts to files being shared in that channel.

The handlers provide access to the user who shared the file (getUser), the channel in which the file was shared (getChannel), the RemoteFile object, containing information about the file being shared (getRemoteFile) and a timestamp for when the file was shared getTimeStamp).

fileShared :: BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
fileShared handler = ...

fileSharedFrom :: Text -> BotReacting a (User' a, Channel' a, TimeStamp) () -> ScriptDefinition a ()
fileSharedFrom channelName handler = ...

Generic functions for handlers

The send function

send :: (IsAdapter a, Get m (Channel' a)) => Text -> BotReacting a m ()
send msg = ...

The 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:

IsAdapter a
We require the saved a in BotReacting to 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 Channel in 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.

The reply function

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.

The messageChannel function

messageChannel :: (HasConfigAccess m, AccessAdapter m, IsAdapter (AdapterT m)) => L.Text -> L.Text -> m ()
messageChannel channelName message = ...

Similar to send and reply this functions sends a message to the channel with the (human readable) channelName. If instead of a name you have a Channel a object, you can use messageChannel’.

The messageChannel' function

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.

The getMatch function

getMatch :: HasMatch m => BotReacting a m Match

Retrieves the result of a regex match inside a handler monad whos state supports it. Examples are the handlers for hear and respond.

Regex matches are a list of strings. The 0’th index is the full match, the following indexes are matched groups.

The getMessage function

getMessage :: Get m (Message a) => BotReacting a m (Message a)

Retrieves the respond structure for the message this handler is reacting to inside a handler monad whos state supports it. Examples are the handlers for hear and respond.

The getTopic function

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.

Note

The Topic type is just for readability, it is just an alternate name for Text.

The getChannel function

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.

The getUser function

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.

Persistence

In memory

On disk

Periodic tasks

Data sharing