memo about Botkit v4 (4.10.0)

Daisuke Maki
3 min readSep 7, 2021

So I inherited a bot that used run on an ancient version of Botkit. There were problems and we needed to upgrade the whole thing to Botkit v4. I found the Botkit documentation to be utterly lacking and confusing, so I decided to leave my notes here so I don’t have to look them up again.

(Note: this is a work in progress, as I’m still in the middle of work)

Botkit Mock

It only works with newer versions of Botkit. Upgrade if you were in the same boat as I was.

storage (Redis)

At least as of this writing, there is no Redis storage backend for v4. Write your own. Use MemoryStorage until you are ready.

Writing the event handlers in Typescript

If you are using Typescript to write the event handlers, you are going to need to somehow give the handler object parameters some type information. That is, this won’t work:

controller.hears("foo", "message", async(bot, message) => {
...
});

You need to give bot and message some type. This information is not immediately obvious from the Botkit docs. Upon further inspection, you will find the definition for these in botkit/src/core.ts , then you can write:

import { Botkit, BotWorker, BotkitMessage } from 'botkit';controller.hears("foo", "message", async(bot :BotWorker, message :BotkitMessage) => {
...
});

The asymmetry in naming between the various BotkitXXXXX types and BotWorker is just so-o-o-o-o deliciously dangerous.

controller.on and controller.hears

controller.on responds to event type names. controller.hears responds to the message text that was received.

The doc is lying when they say controller.on('event', ...) because this does not match any event. Instead of 'event' you need to specify the event name. So if you want to match a message, you need to say controller.on('message', ...) .

Also, if you have controllers that match the same event, the first one wins.

controller.hears('^hello', 'message', ....);
controller.on('message', ...);

In the above case, any message starting with the text “hello” will be intercepted by the controller.hears handler, and everything else will fallthrough to controller.on .

Matching against ‘app_mention’ events

You can’t. Typing @bot foo in Slack generates two events, one for an app_mention event type and one for message . The message type can be matched using controller.hears('foo', 'message', ...) but notice that the first string (the string that matches against the message text) is not anchored to the beginning of the string. This is because the message payload actually contains the bot’s user ID in the form of "<@U********> foo" and therefore you cannot safely anchor the pattern to the beginning of the line.

On the other hand, using the type app_mention in either controller.hears() or controller.on() does absolutely nothing, because the message.type field is populated with the value "event" and not "app_mention" .

The only way to safely match against events of type "app_mention" is to use controller.on('event', ...) then filter out for the "app_metion" type, but this needs more introspection, because the fields in the message object just aren’t populated correctly.

You are going to have to write something that resembles the following:

controller.on('event', async(bot :BotWorker, message :BotkitMessage) => {
// message.incoming_message.channelData contains the raw
// data from the incoming message
let chData = message.incoming_message.channelData;
if (chData.type == 'app_mention') {
// Inspecting the message object reveals that when
// "@bot foo" is issued in Slack, this hook receives a
// message with message.text == null
//
// So we extract the raw text from and remove the
// bot's user ID from the beginning of the text so
// it's easier to match against the text pattern
let botID = message.incoming_message.recipient.id;
let text = chData.text;
if (text.startsWith('<@' + botID + '> ')) {
text = text.slice(botID.length + 4);
}
message.text = text; // Overwrite
// now match against message.text, do whatever
}
});

Starting Slack Thread Conversations

As of this writing, no document properly explains this.

First, do not believe the documentation that you can find by searching for startConversationInThread() because they are all lies. Well, they are about older versions of the library.

The way to do this as of this writing is by importing the SlackBotWorker type, and casting the bot parameter to it.

import { Botkit, BotWorker, BotkitMessage } from 'botkit';
import { SlackBotWorker } from 'botbuilder-adapter-slack';
controller.on(..., (bot :BotWorker, message :BotkitMessage) => {
let sbot = bot as SlackBotWorker;
await sbot.startConversationInThread(...);
await sbot.reply(...)
});

To be continued…

--

--

Daisuke Maki

Go/perl hacker; author of peco; works @ Mercari; ex-mastermind of builderscon; Proud father of three boys;