
Developing bots is a snap with Botkit. I’ve created a Rock Paper Scissors referee to illustrate a variety of interactions. Here’s how it’ll work:
Disclaimer, I’m using node 6 to take advantage of ES6!
Before we get into the code, go make a Slack bot and grab its API token.
Got it? Now make a new folder, npm init and:
npm i -S botkit.
Copy and paste this Botkit starter
into a new file called bot.js and run:
token=YOUR_SLACK_API_TOKEN node bot.js.
The script will fire up a server and put your bot online. Now, invite your Slack bot into your channel with “/invite @YOUR_BOT_NAME”. Try saying “@YOUR_BOT_NAME hello”, you’ll should get a “YO!”. You’re bot is happy to hear from you.
I want to challenge someone by saying “@gamebotplay @michell”, so instead of listening for “hello” let’s keep our bot tuned to “play”.
Botkit will hand the phrase back as “play <@*USER_ID**>*“. Using a little regex we can parse out the ID of player two. If no user ID is found, don’t start the game.
hears(['play'], 'direct_message,direct_mention,mention', (bot, message) => {
const { user, channel, text } = message; // destructure message variable
const userData = text.match(/<@([A-Z0–9]{9})>/); // parse the text for user's 9 character id
if (userData) {
// if there is a user challenged, start the game
} else {
bot.reply(message, 'You didn\'t challenge anyone…');
}
});Let’s structure info about the new game. We’ll need the channel ID, where the game started, and data on both players. Player data can be accessed in the “players” object using their respective IDs as the key.
if (userData) {
const playerTwo = userData[1]; // player two's id is at the first index of match results
const gameData = {
id: channel,
players: {
[user]: {
accepted: true,
played: '',
},
[playerTwo]: {
accepted: false,
played: '',
},
},
};
} else {
}Botkit has built in storage handy for small apps like this. Back up top where we destructure our controller, grab the channels storage.
const { hears, storage: { channels } } = controller;Botkit’s storage system provides a “save” method where we’ll pass the game data and a callback. In the callback, let player two know they’ve been challenged and how they can join.
if (userData) {
...
channels.save(gameData, (err) => {
if (err) throw err;
bot.say({
text: `<@${playerTwo}> you've been challenged to a game of ROCK PAPER SCISSORS by <@${user}>, say \`accept\` unless you're too scared.`,
channel,
});
});
} else {
}Our bot will need to listen to any mention of “accept”. Once our bot detects the word, we’ll query our storage to see if we already have game data.
// a bot can listen for text it's not mentioned in
// by passing 'ambient' as the second parameter
hears(['accept'], 'ambient', (bot, message) => {
const { channel } = message;
channels.get(channel, (err, data) => {
if (err) throw err;
if (data && 'players' in data) {
// game has started !
}
});
});Confirm that it is player two responding…
if (data && 'players' in data) {
const { user } = message;
const { players } = data;
if (user in players && !players[user].accepted) {
// player two accepted
bot.reply(message, 'GREAT, LET THE BATTLE BEGIN!!!');
} else {
const player = Object.keys(players).find((p) => !players[p].accepted);
bot.reply(message, `Not you <@${user}>, waiting for <@${player}>.`);
}
}In the real world Rock Paper Scissors players have to “shoot” at the same time. That won’t work on Slack, so instead use “startPrivateConversation” to get each player’s response in a private channel. Once we have both we’ll reveal the results.
bot.startPrivateConversation(message, callback);A conversation can only be in response to a message initiated by the user. So each player must engage the bot for us to start a private conversation. In our case, player one engages when they say “play” and player two when they say “accept”. Those two moments allow us to pass the message object to start the private conversation.
player one:
hears([‘play’], ‘direct_message,direct_mention,mention’, (bot, message) => {
...
channels.save(gameData, (err) => {
if (err) throw err;
bot.say({
text: `<@${playerTwo}> you've been challenged to a game of ROCK PAPER SCISSORS by <@${user}>, say \`accept\` unless you're too scared.`,
channel,
});
bot.startPrivateConversation(message, callback);
});
} else {
...
});player two:
hears(['accept'], 'ambient', (bot, message) => {
...
if (user in players && !players[user].accepted) {
bot.reply(message, 'GREAT, LET THE BATTLE BEGIN!!!');
bot.startPrivateConversation(message, callback);
} else {
...
}
});Inside the callback function we need to do a few things. First, capture each player’s response (rock/paper/scissors), then add the response to the game data.
To accomplish this, the callback will need access to the message data and bot, but the callback’s only parameters are “error” and “conversation”. We can remedy this by making a higher order function that returns our callback with context.
function privateConvo(bot, message) {
return (err, convo) => {};
}
...
bot.startPrivateConversation(message, privateConvo(bot, message));A conversation starts with a bot asking a question and listening for a response. As arguments, the ask ** method takes a question, an array of callbacks, and capture options. Each item in the array of callbacks has a regex pattern to check the user’s response against. If there’s a match, the bot calls the pattern’s callback. In our case, we’re listening for “rock”, “paper”, or “scissors”.
return (err, convo) => {
convo.ask('Do you want to play `paper`, `rock`, or `scissors`?', [
{
pattern: 'paper|rock|scissors',
callback(response, convo) {
// since no further messages are queued after this,
// the conversation will end naturally with status === ‘completed’
convo.next();
},
}, {
default: true,
callback(response, convo) {
convo.repeat();
convo.next();
},
},
]);
};If the bot can’t find a match, the last callback repeats the question.
The third parameter lets you to capture the user’s response. By passing an object with “key” set to a string of our choice, we’ll be able to retrieve the user’s response.
convo.ask('...', [...], {key: 'rockPaperScissors'});After the bot captures the response, there are no other messages queued so the conversation status is set to “completed”.
Botkit’s conversation has an “on” method we’ll use to detect when it ends. Upon completion, update the game data and broadcast the results.
function privateConvo(bot, message) {
const { user, channel } = message;
return (err, convo) => {
if (err) throw err;
convo.ask(...);
// on end, update game data and broadcast info
convo.on('end', callback);
};
}Check to make sure things ended correctly. If so, extract the rockPaperScissors data.
convo.on('end', (convo) => {
if (convo.status === 'completed') {
// conversation completed correctly
const prc = convo.extractResponse('rockPaperScissors');
} else {
// this happens if the conversation ended prematurely for some reason
bot.reply(message, 'OK, nevermind!');
}
});Grab the channel data from our store and check if there are any players who haven’t gone yet.
if (convo.status === 'completed') {
const prc = convo.extractResponse('rockPaperScissors');
channels.get(channel, (err, data) => {
if (err) throw err;
const updateData = data;
updateData.players[user].played = prc;
const { players } = updateData;
const playerIDs = Object.keys(players);
// check if only one player has played
const onlyOnePlayed = playerIDs.find((id) => players[id].played === '');
});
} else {
...
}If only one person has responded, save the info. In the original channel let the other player know their opponent has played.
channels.get(channel, (err, data) => {
...
if (onlyOnePlayed) {
channels.save(updateData, (err) => {
if (err) throw err;
bot.reply(message, `<@${user}> has played!`);
});
}
});Otherwise, if both players have played broadcast the results! Then clear them so they can play again. (“how about best out of three??”)
} else {
const gameResults = playerIDs.map((id) => `<@${id}> played ${players[id].played}`);
bot.reply(message, gameResults.join(' & '));
// reset the game data
channels.save({ id: updateData.id }, (err) => {
if (err) throw err;
});
}(Bonus points, you can take this tutorial one step further and actually tell the players who won ;)
Here’s the final code, give it a whirl.