@xene/test v0.3.3
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()📦 Packages
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/coreis the place where actual conversation API is implemented and all other packages(like@xene/slack) derive from it.@xene/testdefines a convenient wrapper to help you test your bots. It works nicely with all other packages (Slack or Telegram).@xene/slackprovidesSlackbotwhich provides all features to build communication and it also provides all api methods of Slack with promises and in camel case 🙂@xene/telegramis still in progress but will be as smooth as Slackbot 😉
💬 Talking
Xene provides two main ways to talk with users — in response to some users' message and a way to start talking completely programmatically.
📥 Starting conversations in response to user's message
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...
})📤 Starting conversations proactively
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()⚙️ Dialog API
In the examples above we've been dealing with instances of Dialog class.
It provides following methods and properties.
Click on ▶ to expand reference.
Signature:
bot: BotSignature:
channel: stringSignature:
users: Array<string>Signature:
user: stringSignature:
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)}`))Signature:
end()Example: For example, this method might be used to abort active dialog when users ask to.
dialog.on('abort', _ => dialog.end())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)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?')
// ...
})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}.`)
})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.')
})✅ Testing bots
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.
⚙️ Tester API
@xene/test module at this moment exposes single function wrap which wraps
your bot in the Wrapper class.
Click on ▶ to expand reference.
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)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')Signature:
wrapper.bot.lastMessage: { channel: String, message: BotMessage }Signature:
wrapper.bot.messages: Array<{ channel: String, message: Message }>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.
Signature:
wrapper.bot.reset()Description:
Resets all messages and expectations. This method is designed to be used in
beforeEach like test hooks.
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')
})TypeScript
Xene is written in TypeScript and npm package already includes all typings.
2 years ago
2 years ago
2 years ago
2 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago