{"id":20789811,"url":"https://github.com/toptal/xene","last_synced_at":"2025-08-25T01:30:55.854Z","repository":{"id":57401246,"uuid":"73180531","full_name":"toptal/xene","owner":"toptal","description":"🤖 Modern library with simple API to build great conversational bots.","archived":false,"fork":false,"pushed_at":"2024-06-25T09:18:50.000Z","size":1948,"stargazers_count":67,"open_issues_count":1,"forks_count":7,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-12-15T14:05:40.045Z","etag":null,"topics":["bot","conversational-bots","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/toptal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-11-08T11:41:44.000Z","updated_at":"2024-11-07T06:42:03.000Z","dependencies_parsed_at":"2023-09-11T08:06:42.988Z","dependency_job_id":"31dc7b2a-92ff-4cd4-b26c-eef6c53bbdfe","html_url":"https://github.com/toptal/xene","commit_stats":{"total_commits":195,"total_committers":13,"mean_commits":15.0,"dds":0.2205128205128205,"last_synced_commit":"805cd4eecb16a611493e2e20798feaf1acd49c1b"},"previous_names":["toptal/xene","dempfi/xene"],"tags_count":148,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fxene","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fxene/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fxene/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/toptal%2Fxene/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/toptal","download_url":"https://codeload.github.com/toptal/xene/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230859414,"owners_count":18291154,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bot","conversational-bots","typescript"],"created_at":"2024-11-17T15:29:18.275Z","updated_at":"2024-12-22T17:13:40.469Z","avatar_url":"https://github.com/toptal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/hero.png\" width=\"445\"/\u003e\u003c/div\u003e\n\n[![Travis](https://img.shields.io/travis/dempfi/xene.svg?style=flat-square\u0026label=tests)](https://travis-ci.org/dempfi/xene) [![npm](https://img.shields.io/npm/dm/@xene/core.svg?style=flat-square)](https://www.npmjs.com/package/@xene/core) [![first timers only](http://img.shields.io/badge/first--timers--only-friendly-blue.svg?style=flat-square)](http://www.firsttimersonly.com)\n\n**At 2023, Xene is out-of-date with a lot of outdated dependencies and using old Slack API. Use at own risk**\n\nXene is a framework for building conversational bots with modern JavaScript(or\nTypeScript). From simple command-based bots to rich natural language bots the\nframework provides all of the features needed to manage the conversational\naspects of a bot.\n\n```js\nimport { Slackbot } from '@xene/slack'\n\nnew Slackbot(/* API token */)\n  .when(/hi|hello/i).say('Hi there!')\n  .when(/talk/i).talk(async dialog =\u003e {\n    const user = await dialog.bot.users.info(dialog.user)\n    const topic = await dialog.ask('What about?', topicParser)\n    await dialog.say(`Ok ${user.profile.firstName}, let's talk about ${topic}.`)\n    // ...\n  })\n  .listen()\n```\n\n\u003cimg src=\"assets/blank.png\" width=\"1\" height=\"30\"/\u003e\n\n## 📦 Packages\nXene is split into different packages for different services and purposes.\nThere are 2 main packages that differ from rest: `@xene/core` and `@xene/test`.\n\n- [`@xene/core`](https://www.npmjs.com/package/@xene/core) is the place where\n  actual conversation API is implemented and all other packages(like\n`@xene/slack`) derive from it.\n- [`@xene/test`](https://www.npmjs.com/package/@xene/test) defines a convenient\n  wrapper to help you test your bots. It works nicely with all other packages\n  (Slack or Telegram).\n- [`@xene/slack`](https://www.npmjs.com/package/@xene/slack) provides\n  `Slackbot` which provides all features to build communication and it also\n  provides all [api methods of Slack](https://api.slack.com/methods) with\n  promises and in camel case 🙂\n- `@xene/telegram` is still in progress but will be as smooth as Slackbot 😉\n\n\u003cimg src=\"assets/blank.png\" width=\"1\" height=\"30\"/\u003e\n\n## 💬 Talking\n\nXene provides two main ways to talk with users — in response to some users'\nmessage and a way to start talking completely programmatically.\n\n### 📥 Starting conversations in response to user's message\nTo talk with a user when a user says something, first of all, we need to match\nuser's message. Xene bots provide `.when()` method for this.\n\n```js\nimport { Slackbot } from '@xene/slack'\n\nnew Slackbot(/* API token */)\n  .when(/hi|hello/i).talk(dialog =\u003e /* ... */)\n```\n\nOnce user says something that matches, callback passed to `.talk()` will be\ninvoked with an instance of `Dialog` class. Which provides three main methods\nto parse something from most recent users' message(`dialog.parse()`), to say\nsomething to user(`dialog.say()`) and to ask a question to which user can reply\n(`dialog.ask()`). Read more about them and Dialog [here](#dialog).\n\n```js\nimport { Slackbot } from '@xene/slack'\n\nnew Slackbot(/* API token */)\n  .when(/hi|hello/i).talk(async dialog =\u003e {\n    await dialog.say('Hi there!')\n    const play = await dialog.ask('Wanna play a game?', reply =\u003e /yes/i.test(reply))\n    // start a game...\n  })\n```\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-1.png\" width=\"400\"/\u003e\u003c/div\u003e\n\n### 📤 Starting conversations proactively\n\nThe dialog can also be created proactively when you need them. To do so you can\ncall `bot.dialog()` method. It expects channel id (slack channel would do) and\nan array of users' ids. Rest is the same as in the above example.\n\n```js\nimport { Slackbot } from '@xene/slack'\n\nconst bot = new Slackbot(/* API token */)\n\nconst getGloriousPurpose = async () =\u003e {\n  const dialog = bot.dialog('#channel-id', ['@user1-id', '@user2-id'])\n  const purpose = await dialog.ask('Guys, what is my purpose?', reply =\u003e reply)\n  const comment = purpose === 'to pass butter' ? 'Oh my god.' : 'Nice.'\n  await dialog.say(`\"${purpose}\"... ${comment}`)\n  bot.purpose = purpose\n  dialog.end()\n}\n\ngetGloriousPurpose()\n```\n\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-2.png\" width=\"400\"/\u003e\u003c/div\u003e\n\n### ⚙️ Dialog API\nIn the examples above we've been dealing with instances of `Dialog` class.\nIt provides following methods and properties.\n\n**Click on ▶ to expand reference.**\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.bot\u003c/code\u003e — access an instance of the Bot to which\n  dialog belongs to\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nbot: Bot\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.channel\u003c/code\u003e — the unique id of a channel where the\n  dialog is happening\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nchannel: string\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.users\u003c/code\u003e — an array of ids of all users to whom\n  dialog is attached to\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nusers: Array\u003cstring\u003e\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.user\u003c/code\u003e — the id of the primary user to whom dialog\n  is attached to\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nuser: string\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.on()\u003c/code\u003e — add an event listener to life cycle\n  events of a dialog\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\non(event: string, callback: function)\n```\n\n**Example:**\n\n```js\ndialog.on('end', _ =\u003e console.log('Dialog has ended.'))\ndialog.on('abort', _ =\u003e console.log('Dialog was aborted by user.'))\ndialog.on('pause', _ =\u003e console.log('Dialog was paused.'))\ndialog.on('unpause', _ =\u003e console.log('Dialog was unpaused.'))\ndialog.on('incomingMessage', m =\u003e console.log(`Incoming message ${JSON.stringify(m)}`))\ndialog.on('outgoingMessage', m =\u003e console.log(`Outgoing message ${JSON.stringify(m)}`))\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.end()\u003c/code\u003e — abort dialog, use this to stop dialog\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nend()\n```\n\n**Example:**\nFor example, this method might be used to abort active dialog when users ask to.\n\n```js\ndialog.on('abort', _ =\u003e dialog.end())\n```\n\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.say()\u003c/code\u003e — send a message to channel\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n```js\nsay(message: Message, [unpause: Boolean = true])\n```\n\n**Description:**\n\nType of the message depends on the bot to which dialog\nbelongs to. For Slackbot message can be either `string` or message object\ndescribed [here](https://api.slack.com/methods/channel.postMessage).\n\n`unpause` is optional it's there to help you control whether dialog should be\nunpaused when bot says something or not. By default it's true and the dialog\nwill be unpaused. [Read more about pause.](#pause)\n\n**Example:**\n\n```js\ndialog.say('Hello world!')\ndialog.pause('Paused!')\ndialog.say('Hi again', false)\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.parse()\u003c/code\u003e — parse the most recent message from\n  the user\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nparse(parser: Function || { parse: Function, isValid: Function } , [onError: Message || Function])\n```\n\n**Description:**\nThis method accepts one or two arguments.\n\nIf an error handler isn't provided, this method will return the result of the\nfirst attempt to apply parser even if it's an undefined.\n\n**Example:**\n\n```js\nnew Slackbot(/* API token */)\n  .when(/hi/i).talk(async dialog =\u003e {\n    await dialog.say('Hi!')\n    const parser = reply =\u003e (reply.match(/[A-Z][a-z]+/) || [])[0]\n    const name = await dialog.parse(parser)\n    if (!name) await dialog.say(\"I didn't get your name, but it's OK.\")\n    else await dialog.say(`Nice to meet you ${name}.`)\n  })\n```\n\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-3.png\" width=\"400\"/\u003e\u003c/div\u003e\n\nIf there is an error handler xene will call it for every failed attempt to parse\nuser's message. Xene counts all parsing failed if `null` or `undefined` were\nreturned from parser function. To fine-tune this behavior you can pass an object\nas a parser with two methods — `parse` and `isValid`. Xene will call `isValid`\nto determine if parsing failed.\n\n```js\nnew Slackbot(/* API token */)\n  .when(/invite/i).talk(async dialog =\u003e {\n    const parser = {\n      parse: reply =\u003e reply.match(/[A-Z][a-z]+/),\n      isValid: parsed =\u003e parsed \u0026\u0026 parsed.length\n    }\n    const names = await dialog.parse(parser, 'Whom to invite?')\n    // ...\n  })\n```\n\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-4.png\" width=\"400\"/\u003e\u003c/div\u003e\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.ask()\u003c/code\u003e — ask a question to user and parse\n  response to the question\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nask(question: Message, parser: Function || { parse: Function, isValid: Function }, [onError: Message || Function])\n```\n\n**Description:**\n\nAsk the `question` to a user and parse the response from the user to the question.\nIf parsing fails and error handler `onError` is defined it will be called. If\nerror handler `onError` isn't defined than question will be asked again.\n\n**Example:**\n\n```js\nnew Slackbot(/* API token */)\n  .when(/hi/i).talk(async dialog =\u003e {\n    await dialog.say('Hi!')\n    const parser = reply =\u003e (reply.match(/[A-Z][a-z]+/) || [])[0]\n    const name = await dialog.ask('How can I call you?', parser, 'Sorry?')\n    await dialog.say(`Nice to meet you, ${name}.`)\n  })\n```\n\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-5.png\" width=\"400\"/\u003e\u003c/div\u003e\nThis example also shows us importance of better parser then one based on capital\nletter in front of the words 😅.\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eDialog.prototype.pause()\u003c/code\u003e — pause dialog, reply with pause message\n  until unpaused\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\npause(message: Message)\n```\n\n**Description:**\n\nReply to all incoming user's messages with `message` until the dialog is\nunpaused. Dialog unpauses when a message is sent to user or question is asked\n(`.say()` and `.ask()` methods). This method can help your bot to give status to\na user during some heavy calculation.\n\n**Example:**\n```js\nnew Slackbot(/* API token */)\n  .when(/meaning of life/i).talk(async dialog =\u003e {\n    dialog.pause(`Wait human, I'm thinking...`)\n    await dialog.say('OK, let me think about this.', false)\n    await new Promise(r =\u003e setTimeout(r, 24 * 60 * 60 * 1000)) // wait 24 hours\n    await dialog.say('The answer is... 42.')\n  })\n```\n\u003cdiv align=\"center\"\u003e\u003cimg src=\"assets/ex-6.png\" width=\"400\"/\u003e\u003c/div\u003e\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cimg src=\"assets/blank.png\" width=\"1\" height=\"30\"/\u003e\n\n## ✅ Testing bots\n\nXene provides [test](https://www.npmjs.com/package/@xene/test) module to stub\nyour bot and run assertions.\n\nFor example, let's test following bot:\n\n```js\nconst quizzes = {\n  math: [ { q: '2 + 2 = x', a: '4' }, { q: '|-10| - x = 12', a: '2' }],\n  // other quizes\n]\n\nconst meanbot = new Slackbot(/* API token */)\n  .when(/hi/i).say('Hi there')\n  .when(/quiz/i).talk(async dialog =\u003e {\n    const kind = await dialog.ask('Ok, what kind of quizzes do you prefer?')\n    for (const quiz of quizes[kind]) {\n      const answer = await dialog.ask(quiz.q, reply =\u003e reply)\n      if (answer === quiz.a) await dialog.say('Not bad for a human.')\n      else await dialog.say(`Stupid humans... Correct answer is ${quiz.a}.`)\n    }\n    await dialog.say(`These are all ${kind} quizes I've got.`)\n  })\n```\n\nFirst, to be able to test the bot we need to wrap it in tester object to get\naccess to assertions methods. The exact same thing as with Sinon but assertions\nprovided by `@xene/test` are dialog specific. Anyhoo, let's write the first\nassertion.\n\n```js\nimport ava from 'ava'\nimport { wrap } from '@xene/test'\n\nconst subject = wrap(meanbot)\n\ntest('It does reply to \"hi\"', async t =\u003e {\n  subject.user.says('Hi')\n  t.true(subject.bot.said('Hi there'))\n  // or the same but in one line\n  t.true(await subject.bot.on('Hi').says('Hi there'))\n})\n```\n\nThat was simple. But dialogs can be quite complicated and to test them we need\nto write more stuff.\n\n```js\ntest('It plays the quiz', async t =\u003e {\n  subject.user.says('quiz')\n  subject.user.says('math')\n  t.true(subject.bot.said('2 + 2 = x')\n  t.true(await subject.bot.on('1').says('Not bad for a human.'))\n  t.true(await subject.bot.on('1').says('Stupid humans... Correct answer is 2.'))\n  t.is(subject.bot.lastMessage.message, \"These are all math quizes I've got.\")\n  t.is(subject.bot.messages.length, 6)\n})\n```\n\nThis is it, there are minor things that might be useful in more advanced\nscenarios. For example, each assertion method can also take user id and channel\nid. Check out API to learn about them.\n\n\u003cimg src=\"assets/blank.png\" width=\"1\" height=\"30\"/\u003e\n\n### ⚙️ Tester API\n\n`@xene/test` module at this moment exposes single function `wrap` which wraps\nyour bot in the `Wrapper` class.\n\n**Click on ▶ to expand reference.**\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003ewrap()\u003c/code\u003e — wrap bot for further testing, stubbing it\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrap(bot: Bot)\n```\n\n**Description:**\n\nWraps bot under the test exposing assertion methods.\n\n**Example:**\n\n```js\nimport { wrap } from '@xene/test'\nimport { bot } from '../somewhere/from/your/app'\n\nconst subject = wrap(bot)\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.user.says()\u003c/code\u003e — mock a message from a user\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.user.says(text: Message, [channel: String], [user: String])\n```\n\n\n**Description:**\n\nImitate incoming user message from any channel and any user. If `channel` or\n`user` arguments aren't provided wrapper will generate random channel and user\nids.\n\n**Example:**\n\n```js\nimport { wrap } from '@xene/test'\nimport { bot } from '../somewhere/from/your/app'\n\nconst subject = wrap(bot)\nsubject.user.says('Some message')\nsubject.user.says('Some message', '#some-channel')\nsubject.user.says('Some message', '#some-channel', '@some-user')\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.bot.lastMessage\u003c/code\u003e — retrieve last message bot\n  have send in the tests\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.bot.lastMessage: { channel: String, message: BotMessage }\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.bot.messages\u003c/code\u003e — array of all messages bot have\n  send during the tests\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.bot.messages: Array\u003c{ channel: String, message: Message }\u003e\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.bot.said()\u003c/code\u003e — assert particular message was\n  send\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.bot.said(message: Message, [channel: String])\n```\n\n**Description:**\n\nAssert presense of `message` in list of all messages bot have send. If `channel`\nisn't provided only message will be compared with existing messages. Otherwise\nboth `message` and `channel` will be compared.\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.bot.reset()\u003c/code\u003e — reset assertions and messages\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.bot.reset()\n```\n\n**Description:**\n\nResets all messages and expectations. This method is designed to be used in\n`beforeEach` like test hooks.\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\n  \u003ccode\u003eWrapper.prototype.bot.on()\u003c/code\u003e — register expextation\n  \u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/summary\u003e\n\u003cp\u003e\n\n**Signature:**\n\n```js\nwrapper.bot.on(text: Message, [channel: String], [user: String]) -\u003e { says(message: Message, [channel: String]) }\n```\n\n**Description:**\n\nRegister async assertions which will be ran when bot replyes. `channel` and\n`user` are optional.\n\n**Example:**\n\n```js\nimport { wrap } from '@xene/test'\nimport { bot } from '../somewhere/from/your/app'\n\ntest(async t =\u003e {\n  const subject = wrap(bot)\n  await subject.bot.on('hi').says('hola')\n  await subject.bot.on('hi', '#channel').says('hola', '#channel')\n  await subject.bot.on('hi', '#channel', '@user').says('hola', '#channel')\n})\n```\n\n\u003cimg src=\"assets/divider.png\" width=\"100%\" height=\"18\"/\u003e\n\u003c/p\u003e\n\u003c/details\u003e\n\n#### TypeScript\nXene is written in TypeScript and npm package already includes all typings.\n\n\u003cdiv align=\"right\"\u003e\u003csup\u003e\n  made with ❤️ by \u003ca href=\"https://github.com/dempfi\"\u003e@dempfi\u003c/a\u003e\n\u003c/sup\u003e\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Fxene","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftoptal%2Fxene","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftoptal%2Fxene/lists"}