memo about Botkit v4 (4.10.0)

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

storage (Redis)

Writing the event handlers in Typescript

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

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

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

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…

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