Sunday, September 6, 2020

Mattermost Chatbot using Golang

Anyone who uses Slack may ever heard about Mattermost, or even managed to migrate into it as internal / work messaging platform. If you aren't one of them, you can thank me later. Yes, Mattermost is an open source messaging platform that i can say (with confident) as a fairly good replacement of your beloved Slack app. It has private and public channels, multiple running workspaces, mention ability, and one thing you may miss in other subscription based messaging app: it's admin panel. You can pretty much do anything with it since you are the 'root' there. Did i mention it has enterprise version? It is indeed an open source platform with MIT license. But as we all know, not all IT departments or organizations have enough 'geeks' to dig dive into the code and adding feature needed for their own purpose, that's why enterprise support existed.

Like many other messaging apps, Mattermost supports bot creation. It utilizes websocket client to listen to messaging events. Language binding also exists for several programming language like python and golang, but this time i use golang as a language of choice.
Mattermost uses channel to exchange messages. So every conversation happened in mattermost is executed via channel, whether it's direct, private channel or public channel's messages. Let me explain how it works with the code. Go ahead to this link for full source code, i'm about explaining only what's important part of the implementation, so here we go...
First thing first, obviously is the client object to access mattermost functionality as a bot (i'm not sure if it satisfies the language design principle to call it an 'object' in golang, anyway i give you a hint: it's just a struct!). Here's the snippet:
client = model.NewAPIv4Client(URL)
Then we should set access token, we use token to do auth instead of username and password since we use 'bot' account instead of actual user account who-becoming-bot. I know what you're thinking, mattermost has 2 types of user: 'actual' user and bot. 'Actual' user can be you or anyone else that's breathing this second who use mattermost, it's human. Then bot is,.. well.. bot. Bot is a kind of special type user who can't login, hence they don't have password. So i guess you won't be surprised that the only way for bot to get into mattermost is using token. This one line code should do it:
client.SetToken(BOT_TOKEN_SECRET)
Next important thing is creating websocket client to listen to chat event.
webSocketClienterr := model.NewWebSocketClient4("ws://localhost:8065", client.AuthToken)
if err != nil {
    println("We failed to connect to the web socket")
    PrintError(err)
}
We need to pass an websocket url and Auth Token from the client object we set in previous step: SetToken function. This code returns an error object if something wrong happened, in which an error is a type of  mattermost's model.AppError. To give you better picture of it, model.AppError has several useful properties such as error id, message, detail, http status code, and soon. After the websocket client created, don't forget to make it listen to event:
webSocketClient.Listen()
That line for sure is a self explained code, so let's get further to the core of our journey: responding to a websocket event:
go func() {
    for {
        select {
        case resp := <-webSocketClient.EventChannel:
            HandleWebSocketResponse(resp)
        }
    }
}()
There a goroutine launched to make infinite process to catch websocket's channel value, in which comes the most interesting part of this topic: responding to an incoming chat. Here's the snippet you're looking for:
if post.ParentId == "" {
    if matched_ := regexp.MatchString(`(?:^|\W)leave day(?:$|\W)`, post.Message); matched {
        SendMsgToChannel(dm.Id, "You have 16 leave days left.", post.Id)
        return
    } else {
        SendMsgToChannel(dm.Id, "I did not understand you!", post.Id)
    }
else {
    form := url.Values{}
    form.Add("post_root", post.ParentId)
    form.Add("user_mm_id", post.UserId)
    form.Add("comment", post.Message)
    endpoint := "timesheet/broadcast_task_comment_except_user"
    response_ := http.PostForm(API_URL+endpoint, form)

    defer response.Body.Close()
}
In this case as i use in my workplace, my bot can differentiate between a reply chat or new chat using post.ParentID variable. I programmed my bot to tell how much current annual leave a user (employee) has and to broadcast message in a thread of a bug discussion that comes from our internal system. As you can see, some lines stay as a dummy code since the completed one sure has been moved to our internal private repository. Nonetheless, all these should already gives you an idea how to create a bot from initiating an API client to responding a chat event. The sample still uses rule based mechanism of bot, sure you can use more advanced technique like machine learning, it actually depends on your needs. Until then, happy hacking! See you...   

No comments:

Post a Comment