Exobot
An ES6+ chatbot. Requires Node ^6.2.
Installation
npm install --save @exoplay/exobot
A Brief Example
To start an exobot instance, you need to import the bot itself and initialize it
with plugins and chat service adapters. Here's an example:
import { Exobot, adapters, plugins, LogLevels } from '@exoplay/exobot';
import { Points } from '@exoplay/exobot-plugin-points';
const { Help, Greetings } = plugins;
const BOT_ALIAS = '!e';
const BOT_NAME = 'exobot';
const LOG_LEVEL = LogLevels.INFO;
const shell = adapters.Shell;
const bot = new Exobot(BOT_NAME, {
alias: BOT_ALIAS,
adapters: [
new shell(),
],
plugins: [
new Help(),
new Greetings(),
new Points(),
],
logLevel: LOG_LEVEL,
});
module.exports = bot;
$ npm run build
$ node exobot.js
> Chat: hi
> exobot: hi, shell!
> Chat: exobot++ for being awesome
> exobot: exobot has 1337 points, 1336 of which are for being awesome
What did we do there?
- Created a file named
./src/exobot.js
- Imported the
Exobot
class, service adapters, and plugins - Initialized a new bot, passing in its name, configured service adapters and
plugins
- Built the bot to turn ES7+ into node-compatible ES6
- Started the bot
- Ran
node index.js
and interacted with the bot
Getting started
The easiest way to start is copy the example above - this will get you started
with a chatbot with a shell adapter. The shell adapter will start an
interactive console with which you can chat in a single "room"; Exobot will
respond to messages that trigger plugins.
- Make a directory somewhere that you want to keep your bot's configuration
code.
- Run
git init
(or source control initialization method of choice), then
npm init
to start up an NPM package. (You probably won't publish your bot
as its own package - but this will create a package.json
file that contains
your dependencies.) - Run
npm install --save @exoplay/exobot
to install the chatbot. - Add npm scripts
such as
"build": "exobot-build"
and "watch": "exobot-build --watch"
to
your package.json to get access to the bot building commands - Copy the example above to
./src/exobot.js
. - Build the bot with
npm run build
- Run
node exobot.js
. Chat with yourself for a while, then read on to learn
how to configure your chatbot, or even build your own plugins and adapters.
Configuration
Exobot is configured in its constructor, which takes two arguments - a bot
name (a required string), and an options object.
The bot name is used for commands - if your bot's name is 'exobot'
, it will
respond
to commands beginning with 'exobot'
. You'll want this to match the
name used in your chat service (so if its name is actually 'DEATHBOT_9000'
in
Slack, you should call it that here too, or people may be confused.)
The options object contains all other configuration - such as a list of
plugins and chat service adapters, log levels, and data encryption keys.
alias
- an additional way to trigger exobot commands. '/'
, ';'
, or
'hey bot'
, for example.adapters
- an array of initialized chat adapters, such as
slack,
discord,
or twitch. exobot
also comes with a shell
adapter for playing around in your terminal.plugins
- an array of initialized plugins, such as
giphy or
points. exobot also
comes with help
and greetings
plugins as examples.readFile
and writeFile
- functions called when the in-memory json db
is saved. By default, this writes a json file to cwd/data/botname.json
,
but you could also override the default local file storage to use s3 with
exobot-db-s3.dbPath
- if you're using local file storage, you can set where to save.
Defaults to cwd/data/botname.json
.
Building plugins
Most plugins respond to chat messages - either by listen
ing to all chat
messages, or respond
ing to specific commands. exobot comes with greetings
and help
plugins, but building your own is easy. Some examples:
The ES2017 decorators proposal
is used to hook commands to validation functions or regexes, to assign permission
groups, and to provide help text.
An Example Plugin
import { ChatPlugin, respond, help, permissionGroup } from '@exoplay/exobot';
export default class Ping extends ChatPlugin {
static name = 'ping';
@help('Says "pong" when you send it "ping"');
@permissionGroup('ping');
@respond(/^ping$/);
pong (match, message) {
return 'pong';
}
}
In this plugin, we have extended exobot's ChatPlugin class - this gives it
functionality to respond to chat messages. We've then told it to respond
to
the regex /ping/
by firing a function, called pong
. The return
value of
the function is then sent back to the chat channel.
A Detailed Anatomy of a Chat Plugin
Chat plugins follow the following lifecycle:
First, The constructor
is called with options sent in. As the bot is
initialized with instances of plugins, this is where you would pass in
configuration options, such as:
import { ChatPlugin, respond, help, permissionGroup } from '@exoplay/exobot';
class StatusPlugin extends ChatPlugin {
constructor (options) {
super(options);
this.endpoint = options.endpoint;
}
@help('Gets the status of the configured endpoint.');
@permissionGroup('get');
@respond(m => m.text === 'status');
async getStatus () {
const res = await this.http.get(this.endpoint);
return res.statusCode;
}
}
In the above example, we'd initialize the exobot instance with
plugins: [ new StatsPlugin({ endpoint: 'https://github.com' }) ]
to pass
in the options we need later on.
Next, when the bot instance begins listening, the plugin's register
method is
called, with the bot
instace passed in. Note that the constructor doesn't have
the bot yet - it doesn't exist until register
, fired next.
You'll want to give the plugin a static
name
property - thi is used if you
use the permissions plugin to restrict access to commands.
listen
and respond
are decorators that take a function, and fire the method
when a match is found. listen
and respond
are the most important parts of your chat plugin - these allow the
bot to interact with chat. Each can take either a regex or a function, and if
a match is found (or, if a function, if it is truthy), it will fire the
function passed in. Functions for responding can be promises (or
ES7 async
functions) and will resolve when the promises do. This makes it
easy to write asynchronous code, such as firing http requests.
The responding function gets two arguments: a match
object, which is either
the regex's exec
response or the function return value, and a Message
object, which contains the original message, user, and whether the message is a
whisper.
You can optionally add a help
decorator, which exobot's help
plugin uses to
explain to useres how the plugin works.
You should also add a permissionsGroup
, which you can then use with
exobot's Permissions
plugin to restrict access to certain commands. In the
following case, you can give access to status.get
to groups, and if you deny
access by default in configuration, only users in the group with access to
status.get
can use the command. (The bot will ignore the command from
everyone else.)
Finally, the bot also exposes bot.http
, which is a promise-ified
superagent wrapper, to make http
calls easy to make.
import { ChatPlugin, respond, listen, permissionsGroup, help } from '@exoplay/exobot';
class StatusPlugin extends ChatPlugin {
static name = 'Status';
help = [
'Get the status of an http endpoint. Responds to `status` or listens to',
'status <http://whatever.com>.'
].join('\n');
constructor (options) {
super(options);
this.endpoint = options.endpoint;
}
register (bot) {
super.register(bot);
if (!this.endpoint) {
bot.log.warn('No endpoint passed in to StatusPlugin.');
}
this.respond(/status/, this.getStatus);
this.listen(/^status (http:\/\/\S+)/, this.getStatus);
this.listen(m => m.text === 'status', this.getStatus);
}
@help('use status or status <http> to get http status codes.');
@permissionsGroup('get');
@respond(/^status$/);
@listen(/^status (http:\/\/\S+)/);
@listen(m => m.text === 'status');
async getStatus (match, message) {
let endpoint = this.endpoint;
if (match && match.length) {
endpoint = match[1];
}
const res = await this.http.get(this.endpoint);
return res.statusCode;
}
}
You can also build other types of plugins: EventPlugin
, HTTPPlugin
, or build
your own class of plugin with the Plugin
class. Documentation to come someday.
Exobot exports a handy scripts for testing your plugins: exobot-try
. By either
installing exobot globally (npm install -g @exoplay/exobot
) or adding a script
to your package.json ("try": "exobot-try"
), exobot will fire up a simple bot
with a shell adapter so you can test your plugin.
Building Adapters
Adapters allow your bot to connect to a chat service, such as Slack or Discord.
exobot comes with a shell adapter by default, but you could also build your own
for your chat service of choice. Some examples:
An Example Adapter
import ChatServiceLibrary from '@chatservice/lib';
import { Adapter, User } from '@exoplay/exobot';
export default class ChatServiceAdapter extends Adapter {
constructor ({ token, username }) {
super(...arguments);
this.token = token;
this.username = username;
}
register (bot) {
super.register(bot);
this.service = new ChatServiceLibrary(this.username, this.token);
this.service.on('ready', this.serviceReady.bind(this));
this.service.on('message', this.serviceMessage.bind(this));
}
send (message) {
this.bot.log.debug(`Sending ${message.text} to ${message.channel}`);
this.service.sendMessage({
to: message.channel,
message: message.text,
});
}
serviceReady () {
this.status = Adapter.STATUS.CONNECTED;
this.bot.emitter.emit('connected', this.id);
this.bot.log.notice('Connected to ChatService.');
}
serviceMessage (user, text, channel) {
if (user.name === this.username) { return; }
const user = new User(user.name, user.id);
if (channel.private) {
return super.receiveWhisper({ user, text, channel });
}
return this.receive({ user, text, channel });
}
getUserIdByUserName (name) {
return this.service.getUserByName(name).id;
}
}
A Detailed Anatomy of a Chat Service Adapter
Chat service adapters have a similar lifecycle to plugins:
- Constructor, before the bot is initialized;
- Register, when the bot is initialized, where you first get acccess to the
exobot instance, and where you listen to your chat service;
- Functions called by events fired by the chat service;
- Finally,
send
, called by the bot instance when plugins resolve.
You can listen and fire any arbitrary functions - for example, some chat
services may include presence information, and fire enter
and leave
events.
You can then receive
your own PresenceMessage
similar to how we receive
a
TextMessage
in the serviceMessage
in the example. (Right now, the only
Message
classes are TextMessage
and PresenceMessage
). Many adapters may
also want to make use of the Status
enum, which could be:
- UNINITIALIZED
- CONNECTING
- CONNECTED
- DISCONNECTED
- RECONNECTING
- ERROR
You may also want to use bot.log
to log important events to stdout, such as
connection or configuration errors. bot.log
can fire:
- debug
- info
- notice
- warning
- error
- critical
- alert
- emergency
In order of ascending severity.
Acknowledgements
Exobot is loosely based on hubot, for which
the author has a great deal of admiration. Hubot is more user-friendly in many
ways (autoloading scripts, for example, instead of requiring the user to write
their own imports and configuration). In other ways, this flexibility can be
limiting; it's easier to make a pure-js bot more efficient and testable (and
the author thinks that ES6, rather than Coffeescript, is a more viable choice
of language; plugin-writers can always choose to opt-in to Coffeescript and
export a built file if they want.)
License
LGPL licensed. Copyright 2016 Exoplay, LLC. See LICENSE file for more details.