Menu-driven Matrix Bot Interaction

· 398 words · 2 minute read

I’m a huge fan of Matrix. A lot of the user value of modern chat platforms like Slack, Matrix and Discord (even IRC) comes from integrations to other services via bots. I had high hopes for MSC3006: Bot Interactions, but unfortunately it isn’t currently being pushed further. However, there exists an implementation of MSC3381: Polls.

Early poll bots used reactions for interaction instead of traditional text commands. Now that there exists an MSC for polls with implementations in the Element clients, I realized it can be used as a menu-driven interface, like dialog(1), whiptail(1) or zenity(1).

You can try out the bot by opening a new chat with @menubot:matrix.org. Please be aware that federation latency may affect the snappiness of your experience.

The code is available on gitlab. I don’t claim that it’s pretty, or even good, but it does seem to work.

Some notes on how I used the excellent mautrix-go library to achieve this:

Mautrix-go doesn’t have support for all experimental MSCs, or probably even everything that’s in the stable specification. In particular it took me a while to realize that the library was ignoring the poll related events. In order to log unknown events, I added:

syncer.ParseErrorHandler = func(e *event.Event, err error) bool {
        log.Printf("unknown type %+v", e)

        return false
}

Being aware of the needed types, I started adding them:

var (
        EventPollStart = event.Type{
                "org.matrix.msc3381.poll.start",
                event.MessageEventType,
        }
        EventPollResponse = event.Type{
                "org.matrix.msc3381.poll.response",
                event.MessageEventType,
        }
        EventPollEnd = event.Type{
                "org.matrix.msc3381.poll.end",
                event.MessageEventType,
        }
)

And corresponding structs:

type PollKind string

const (
        KindDisclosed   PollKind = "org.matrix.msc3381.poll.disclosed"
        KindUndisclosed PollKind = "org.matrix.msc3381.poll.undisclosed"
)

type PollQuestion struct {
        FallbackText string            `json:"org.matrix.msc1767.text,omitempty"`
        Body         string            `json:"body"`
        MsgType      event.MessageType `json:"msgtype"`
}

type PollAnswer struct {
        ID   string `json:"id"`
        Text string `json:"org.matrix.msc1767.text"`
}

type PollStart struct {
        Question      PollQuestion `json:"question"`
        Kind          PollKind     `json:"kind"`
        MaxSelections int          `json:"max_selections"`
        Answers       []PollAnswer `json:"answers"`
        FallbackText  string       `json:"org.matrix.msc1767.text,omitempty"`
}

type PollStartContent struct {
        PollStart PollStart `json:"org.matrix.msc3381.poll.start"`
}

type PollResponse struct {
        Answers []string `json:"answers"`
}

type PollResponseContent struct {
        PollResponse PollResponse     `json:"org.matrix.msc3381.poll.response"`
        RelatesTo    *event.RelatesTo `json:"m.relates_to"`
}

type PollEndContent struct {
        PollEnd      map[string]string `json:"org.matrix.msc3381.poll.end"`
        FallbackText string            `json:"org.matrix.msc1767.text,omitempty"`
        Body         string            `json:"body"`
        RelatesTo    *event.RelatesTo  `json:"m.relates_to"`
        MsgType      event.MessageType `json:"msgtype"`
}

Those still had to be hooked up to the library via the TypeMap:

event.TypeMap[EventPollResponse] = reflect.TypeOf(PollResponseContent{})
event.TypeMap[EventPollStart] = reflect.TypeOf(PollStartContent{})
event.TypeMap[EventPollEnd] = reflect.TypeOf(PollEndContent{})

That finally allowed me to write the code for creating a poll and responding appropriately to the user interacting with the poll.