@exoplay/exobot
v5.0.0-rc.6
Published
a chatbox
Downloads
22
Readme
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), thennpm init
to start up an NPM package. (You probably won't publish your bot as its own package - but this will create apackage.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 ashell
adapter for playing around in your terminal.plugins
- an array of initialized plugins, such as giphy or points. exobot also comes withhelp
andgreetings
plugins as examples.readFile
andwriteFile
- functions called when the in-memory json db is saved. By default, this writes a json file tocwd/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 tocwd/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 the regex succeeded, match[1] should be an http 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
// An example: import an API lib for your chat service, or do it with raw
// sockets or http, or whatever.
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);
// Initialize the chat service lib we pulled in earlier
this.service = new ChatServiceLibrary(this.username, this.token);
// listen to some events. bind the functions to `this` to make sure we can
// access our class instance, bot, and `super`.
this.service.on('ready', this.serviceReady.bind(this));
this.service.on('message', this.serviceMessage.bind(this));
}
// the `send` funciton is defined by Adapter and called by plugins when they
// resolve (if they resolve.)
send (message) {
this.bot.log.debug(`Sending ${message.text} to ${message.channel}`);
// Send the message data to the chat service client.
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.');
}
// We'll pretend our fake chat service lib takes a function which is called
// with message, user, and channel. We'll take these arguments and "receive"
// them, which will fire off all of the plugins so they can respond where
// necessary.
serviceMessage (user, text, channel) {
// We don't want to listen to messages from ourself.
if (user.name === this.username) { return; }
// Create a new User instance to pass along in the Message.
const user = new User(user.name, user.id);
// Check if our fake chat service lib says the channel is "private". If it
// is a private message between the user and bot, we'll make it act like a
// "respond" command instead of just a "listen".
if (channel.private) {
return super.receiveWhisper({ user, text, channel });
}
return this.receive({ user, text, channel });
}
// This is useful for chat services where names aren't unique, for use by
// plugins where uniqueness matters (like Permissions.)
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.