At 2023, Xene is out-of-date with a lot of outdated dependencies and using old Slack API. Use at own risk
Xene is a framework for building conversational bots with modern JavaScript(or TypeScript). From simple command-based bots to rich natural language bots the framework provides all of the features needed to manage the conversational aspects of a bot.
import { Slackbot } from '@xene/slack'
new Slackbot(/* API token */)
.when(/hi|hello/i).say('Hi there!')
.when(/talk/i).talk(async dialog => {
const user = await dialog.bot.users.info(dialog.user)
const topic = await dialog.ask('What about?', topicParser)
await dialog.say(`Ok ${user.profile.firstName}, let's talk about ${topic}.`)
// ...
})
.listen()
Xene is split into different packages for different services and purposes.
There are 2 main packages that differ from rest: @xene/core
and @xene/test
.
@xene/core
is the place where actual conversation API is implemented and all other packages(like@xene/slack
) derive from it.@xene/test
defines a convenient wrapper to help you test your bots. It works nicely with all other packages (Slack or Telegram).@xene/slack
providesSlackbot
which provides all features to build communication and it also provides all api methods of Slack with promises and in camel case 🙂@xene/telegram
is still in progress but will be as smooth as Slackbot 😉
Xene provides two main ways to talk with users — in response to some users' message and a way to start talking completely programmatically.
To talk with a user when a user says something, first of all, we need to match
user's message. Xene bots provide .when()
method for this.
import { Slackbot } from '@xene/slack'
new Slackbot(/* API token */)
.when(/hi|hello/i).talk(dialog => /* ... */)
Once user says something that matches, callback passed to .talk()
will be
invoked with an instance of Dialog
class. Which provides three main methods
to parse something from most recent users' message(dialog.parse()
), to say
something to user(dialog.say()
) and to ask a question to which user can reply
(dialog.ask()
). Read more about them and Dialog here.
import { Slackbot } from '@xene/slack'
new Slackbot(/* API token */)
.when(/hi|hello/i).talk(async dialog => {
await dialog.say('Hi there!')
const play = await dialog.ask('Wanna play a game?', reply => /yes/i.test(reply))
// start a game...
})
The dialog can also be created proactively when you need them. To do so you can
call bot.dialog()
method. It expects channel id (slack channel would do) and
an array of users' ids. Rest is the same as in the above example.
import { Slackbot } from '@xene/slack'
const bot = new Slackbot(/* API token */)
const getGloriousPurpose = async () => {
const dialog = bot.dialog('#channel-id', ['@user1-id', '@user2-id'])
const purpose = await dialog.ask('Guys, what is my purpose?', reply => reply)
const comment = purpose === 'to pass butter' ? 'Oh my god.' : 'Nice.'
await dialog.say(`"${purpose}"... ${comment}`)
bot.purpose = purpose
dialog.end()
}
getGloriousPurpose()
In the examples above we've been dealing with instances of Dialog
class.
It provides following methods and properties.
Click on ▶ to expand reference.
Dialog.prototype.channel
— the unique id of a channel where the
dialog is happening
Signature:
channel: string
Dialog.prototype.users
— an array of ids of all users to whom
dialog is attached to
Signature:
users: Array<string>
Dialog.prototype.user
— the id of the primary user to whom dialog
is attached to
Signature:
user: string
Dialog.prototype.on()
— add an event listener to life cycle
events of a dialog
Signature:
on(event: string, callback: function)
Example:
dialog.on('end', _ => console.log('Dialog has ended.'))
dialog.on('abort', _ => console.log('Dialog was aborted by user.'))
dialog.on('pause', _ => console.log('Dialog was paused.'))
dialog.on('unpause', _ => console.log('Dialog was unpaused.'))
dialog.on('incomingMessage', m => console.log(`Incoming message ${JSON.stringify(m)}`))
dialog.on('outgoingMessage', m => console.log(`Outgoing message ${JSON.stringify(m)}`))
Dialog.prototype.end()
— abort dialog, use this to stop dialog
Signature:
end()
Example: For example, this method might be used to abort active dialog when users ask to.
dialog.on('abort', _ => dialog.end())
Dialog.prototype.say()
— send a message to channel
Signature:
say(message: Message, [unpause: Boolean = true])
Description:
Type of the message depends on the bot to which dialog
belongs to. For Slackbot message can be either string
or message object
described here.
unpause
is optional it's there to help you control whether dialog should be
unpaused when bot says something or not. By default it's true and the dialog
will be unpaused. Read more about pause.
Example:
dialog.say('Hello world!')
dialog.pause('Paused!')
dialog.say('Hi again', false)
Dialog.prototype.parse()
— parse the most recent message from
the user
Signature:
parse(parser: Function || { parse: Function, isValid: Function } , [onError: Message || Function])
Description: This method accepts one or two arguments.
If an error handler isn't provided, this method will return the result of the first attempt to apply parser even if it's an undefined.
Example:
new Slackbot(/* API token */)
.when(/hi/i).talk(async dialog => {
await dialog.say('Hi!')
const parser = reply => (reply.match(/[A-Z][a-z]+/) || [])[0]
const name = await dialog.parse(parser)
if (!name) await dialog.say("I didn't get your name, but it's OK.")
else await dialog.say(`Nice to meet you ${name}.`)
})
If there is an error handler xene will call it for every failed attempt to parse
user's message. Xene counts all parsing failed if null
or undefined
were
returned from parser function. To fine-tune this behavior you can pass an object
as a parser with two methods — parse
and isValid
. Xene will call isValid
to determine if parsing failed.
new Slackbot(/* API token */)
.when(/invite/i).talk(async dialog => {
const parser = {
parse: reply => reply.match(/[A-Z][a-z]+/),
isValid: parsed => parsed && parsed.length
}
const names = await dialog.parse(parser, 'Whom to invite?')
// ...
})
Dialog.prototype.ask()
— ask a question to user and parse
response to the question
Signature:
ask(question: Message, parser: Function || { parse: Function, isValid: Function }, [onError: Message || Function])
Description:
Ask the question
to a user and parse the response from the user to the question.
If parsing fails and error handler onError
is defined it will be called. If
error handler onError
isn't defined than question will be asked again.
Example:
new Slackbot(/* API token */)
.when(/hi/i).talk(async dialog => {
await dialog.say('Hi!')
const parser = reply => (reply.match(/[A-Z][a-z]+/) || [])[0]
const name = await dialog.ask('How can I call you?', parser, 'Sorry?')
await dialog.say(`Nice to meet you, ${name}.`)
})
Dialog.prototype.pause()
— pause dialog, reply with pause message
until unpaused
Signature:
pause(message: Message)
Description:
Reply to all incoming user's messages with message
until the dialog is
unpaused. Dialog unpauses when a message is sent to user or question is asked
(.say()
and .ask()
methods). This method can help your bot to give status to
a user during some heavy calculation.
Example:
new Slackbot(/* API token */)
.when(/meaning of life/i).talk(async dialog => {
dialog.pause(`Wait human, I'm thinking...`)
await dialog.say('OK, let me think about this.', false)
await new Promise(r => setTimeout(r, 24 * 60 * 60 * 1000)) // wait 24 hours
await dialog.say('The answer is... 42.')
})
Xene provides test module to stub your bot and run assertions.
For example, let's test following bot:
const quizzes = {
math: [ { q: '2 + 2 = x', a: '4' }, { q: '|-10| - x = 12', a: '2' }],
// other quizes
]
const meanbot = new Slackbot(/* API token */)
.when(/hi/i).say('Hi there')
.when(/quiz/i).talk(async dialog => {
const kind = await dialog.ask('Ok, what kind of quizzes do you prefer?')
for (const quiz of quizes[kind]) {
const answer = await dialog.ask(quiz.q, reply => reply)
if (answer === quiz.a) await dialog.say('Not bad for a human.')
else await dialog.say(`Stupid humans... Correct answer is ${quiz.a}.`)
}
await dialog.say(`These are all ${kind} quizes I've got.`)
})
First, to be able to test the bot we need to wrap it in tester object to get
access to assertions methods. The exact same thing as with Sinon but assertions
provided by @xene/test
are dialog specific. Anyhoo, let's write the first
assertion.
import ava from 'ava'
import { wrap } from '@xene/test'
const subject = wrap(meanbot)
test('It does reply to "hi"', async t => {
subject.user.says('Hi')
t.true(subject.bot.said('Hi there'))
// or the same but in one line
t.true(await subject.bot.on('Hi').says('Hi there'))
})
That was simple. But dialogs can be quite complicated and to test them we need to write more stuff.
test('It plays the quiz', async t => {
subject.user.says('quiz')
subject.user.says('math')
t.true(subject.bot.said('2 + 2 = x')
t.true(await subject.bot.on('1').says('Not bad for a human.'))
t.true(await subject.bot.on('1').says('Stupid humans... Correct answer is 2.'))
t.is(subject.bot.lastMessage.message, "These are all math quizes I've got.")
t.is(subject.bot.messages.length, 6)
})
This is it, there are minor things that might be useful in more advanced scenarios. For example, each assertion method can also take user id and channel id. Check out API to learn about them.
@xene/test
module at this moment exposes single function wrap
which wraps
your bot in the Wrapper
class.
Click on ▶ to expand reference.
wrap()
— wrap bot for further testing, stubbing it
Signature:
wrap(bot: Bot)
Description:
Wraps bot under the test exposing assertion methods.
Example:
import { wrap } from '@xene/test'
import { bot } from '../somewhere/from/your/app'
const subject = wrap(bot)
Wrapper.prototype.user.says()
— mock a message from a user
Signature:
wrapper.user.says(text: Message, [channel: String], [user: String])
Description:
Imitate incoming user message from any channel and any user. If channel
or
user
arguments aren't provided wrapper will generate random channel and user
ids.
Example:
import { wrap } from '@xene/test'
import { bot } from '../somewhere/from/your/app'
const subject = wrap(bot)
subject.user.says('Some message')
subject.user.says('Some message', '#some-channel')
subject.user.says('Some message', '#some-channel', '@some-user')
Wrapper.prototype.bot.lastMessage
— retrieve last message bot
have send in the tests
Signature:
wrapper.bot.lastMessage: { channel: String, message: BotMessage }
Wrapper.prototype.bot.messages
— array of all messages bot have
send during the tests
Signature:
wrapper.bot.messages: Array<{ channel: String, message: Message }>
Wrapper.prototype.bot.said()
— assert particular message was
send
Signature:
wrapper.bot.said(message: Message, [channel: String])
Description:
Assert presense of message
in list of all messages bot have send. If channel
isn't provided only message will be compared with existing messages. Otherwise
both message
and channel
will be compared.
Wrapper.prototype.bot.reset()
— reset assertions and messages
Signature:
wrapper.bot.reset()
Description:
Resets all messages and expectations. This method is designed to be used in
beforeEach
like test hooks.
Wrapper.prototype.bot.on()
— register expextation
Signature:
wrapper.bot.on(text: Message, [channel: String], [user: String]) -> { says(message: Message, [channel: String]) }
Description:
Register async assertions which will be ran when bot replyes. channel
and
user
are optional.
Example:
import { wrap } from '@xene/test'
import { bot } from '../somewhere/from/your/app'
test(async t => {
const subject = wrap(bot)
await subject.bot.on('hi').says('hola')
await subject.bot.on('hi', '#channel').says('hola', '#channel')
await subject.bot.on('hi', '#channel', '@user').says('hola', '#channel')
})
Xene is written in TypeScript and npm package already includes all typings.