├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── README.md ├── adaptive-card-example.md ├── build.sh ├── example1.md ├── example2.md ├── example3.md ├── header.md ├── installation.md ├── license.md └── overview.md ├── index.js ├── lib ├── bot.js ├── flint.js ├── logs.js ├── process-event.js ├── utils.js ├── webhook.js └── websocket.js ├── package-lock.json ├── package.json ├── quickstart └── README.md ├── storage ├── README.md ├── memory.js ├── redis.js ├── redis_old.js └── template.js ├── templates └── bothub-template │ ├── Procfile │ ├── README.md │ ├── app.js │ ├── config.js │ ├── flint.js │ ├── package.json │ └── test.js └── webhook.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Folder view configuration files 2 | .DS_Store 3 | Desktop.ini 4 | 5 | # Thumbnail cache files 6 | ._* 7 | Thumbs.db 8 | 9 | # Files that might appear on external disks 10 | .Spotlight-V100 11 | .Trashes 12 | 13 | # Compiled Python files 14 | *.pyc 15 | 16 | # Compiled C++ files 17 | *.out 18 | 19 | # Database file 20 | *.db 21 | 22 | # Application specific files 23 | venv 24 | node_modules 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Folder view configuration files 2 | .DS_Store 3 | Desktop.ini 4 | 5 | # Thumbnail cache files 6 | ._* 7 | Thumbs.db 8 | 9 | # Files that might appear on external disks 10 | .Spotlight-V100 11 | .Trashes 12 | 13 | # Compiled Python files 14 | *.pyc 15 | 16 | # Compiled C++ files 17 | *.out 18 | 19 | # Database file 20 | *.db 21 | 22 | # Application specific files 23 | venv 24 | node_modules 25 | docs 26 | templates 27 | quickstart 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **4.6.x Update** 2 | 3 | * `bot.store()`, `bot.recall()`, and `bot.forget` of memory storage module has 4 | been refactored to return resolved/rejected promises. This means that 5 | `bot.recall()` will no longer return a null value for a non-existent key and 6 | will return a promise instead of a value for a existing key. This is to comply 7 | with requirements for other storage modules that are not synchronous and to 8 | make the memory storage module interchangeable with these. 9 | * redis storage module modified so that is interchangeable with the new memory 10 | storage module. This module will be refactored soon to decrease times between 11 | synchronizations rather than syncing the local memory store with redis 12 | periodically. However, the function calls to this module will not change so 13 | code based on this will be safe when this module is refactored. 14 | * The previous redis storage module still exists as redis_old, but is 15 | deprecated / unsupported. If using the redis module, consider migrating to 16 | updated redis storage module. This module will be removed in Flint v5. 17 | 18 | **Breaking Changes in 4.6.x** 19 | 20 | * See update above. `bot.store()`, `bot.recall()`, and `bot.forget` functions have been adjusted. Redis Storage Module refactored. Mem Storage Module refactored. 21 | 22 | **4.5.x Update** 23 | 24 | * Removed some error handling that would cause flint to crash when Spark API 25 | would respond with a 504 error due to API issues. 26 | * Fixed unhandled rejections when despawn happens and bot mem-store is empty. 27 | * Updated handling on of "next" in returned webhook function. 28 | * Changed room being tagged as a "Team" if bot is not member of team. 29 | * Fixed typo in audit process (#15 via @pevandenburie) 30 | 31 | **4.4.x Update** 32 | 33 | * `bot.isDirectTo` property added. This is set to the email of the other 34 | conversant in rooms of type 'direct'. 35 | * `trigger.raw` property added to `flint.hears` trigger callback object. This is 36 | the raw message without any processing to remove multiple spaces, CR/LF, or 37 | leading/trailing spaces. 38 | 39 | **4.3.x Update** 40 | 41 | * `bot.add()` and `bot.remove()` now return an array of successfully 42 | added / removed room membership emails rather than the bot object itself. 43 | * Debug error messages for archived team rooms suppressed. 44 | 45 | **4.2.x Update** 46 | 47 | * Persistent Storage for `bot.store()`, `bot.recall()`, and `bot.forget()` 48 | through new modular storage functionality. 49 | * Added in-memory storage module (default unless storage module is specified) 50 | * Added Redis storage module 51 | * Added boolean property flint.isUserAccount 52 | * Added method `flint.storageDriver()` to define storage backend 53 | * The `flint.hears()` method now can have a weight specified. This allows for 54 | overlapping and default actions. 55 | * Auto detection of Bot accounts 56 | * If Bot account is detected, the behavior of the `trigger.args` property inside 57 | the `flint.hears()` method performs additional parsing. 58 | 59 | **Breaking Changes in 4.2.x** 60 | 61 | * `flint.machine` boolean property renamed to `flint.isBotAccount` 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-flint (v4) 2 | 3 | ### Bot SDK for Node JS 4 | 5 | ## News 6 | 7 | **03/01/2020 Please consider the Webex Node Bot Framework:** 8 | 9 | * This framework is no longer being actively maintained by the original author. Developers who are familiar with flint may consider trying the [webex-node-bot-framework](https://github.com/webex/webex-bot-node-framework). While not 100% directly compatible with flint, this framework is inspired by flint and should be extremely familiar to developers who already use flint. For more information see the [migration from flint guide](https://github.com/webex/webex-bot-node-framework/blob/master/docs/migrate-from-node-flint.md) 10 | 11 | **10/25/19 Support for Adaptive Cards:** 12 | 13 | * Cisco recently introduced support for [Adaptive Cards](https://developer.webex.com/docs/api/guides/cards/) in the Webex Teams. Bots can send cards, using the new `attachment` attribute of the message object. Cards are useful as an alternative to text messages and files in order to display or collect complex bits of information. Cards can be sent by passing an object to the bot.say() method that includes a valid attachment. To process user input to cards, apps must implement a `flint.on('attachmentaction', ..)` function. For more details see the [adaptive-card-example](./docs/adaptive-card-example.md) 14 | 15 | **6/21/19 Deploying behind a firewall:** 16 | 17 | * Cisco has recently introduced support in the Webex Javascript SDK which allows applications to register to receive the message, membership, and room events via a socket instead of via wehbhoks. This allows applications to be deployed behind firewalls and removes the requirement that webex bots and integrations must expose a public IP address to receive events. To take advantage of this in your flint applications simply remove the `webhookUrl` field from the configuration object passed to the flint constructor. If this field is not set, flint will register to listen for these events instead of creating webhooks. 18 | 19 | **6/21/18 IMPORTANT:** 20 | 21 | * On August 31st, 2018 all bots with the sparkbot.io domain name will be 22 | renamed with a webex.bot domain. Today in flint, the code compares the bot's 23 | email with the trigger email to filter out messages from itself. If this code 24 | is running on August 31st the bot will start responding to its own messages. 25 | Please update to Flint v4.7.x as soon as possible to avoid interruption. 26 | 27 | **3/19/18 IMPORTANT:** 28 | 29 | * Note that Flint v4 is still using the node-sparky library version 3.x. 30 | However the repo for node-sparky is now on version 4 which has some major 31 | differences. This misalignment between Flint and Sparky version 32 | will be fixed with the release of Flint v5. In the 33 | short term if you are accessing the spark object directly from Flint via 34 | `flint.spark` be sure to use the documentation for [node-sparky 3.x](https://github.com/flint-bot/sparky/tree/v3). 35 | 36 | **See [CHANGELOG.md](/CHANGELOG.md) for details on changes to versions of Flint.** 37 | 38 | ## Contents 39 | 40 | 41 | 42 | 43 | 44 | - [Installation](#installation) 45 | - [Via Git](#via-git) 46 | - [Via NPM](#via-npm) 47 | - [Example Template Using Express](#example-template-using-express) 48 | - [Overview](#overview) 49 | - [Authentication](#authentication) 50 | - [Storage](#storage) 51 | - [Bot Accounts](#bot-accounts) 52 | 53 | 54 | ## Installation 55 | 56 | #### Via Git 57 | ```bash 58 | mkdir myproj 59 | cd myproj 60 | git clone https://github.com/nmarus/flint 61 | npm install ./flint 62 | ``` 63 | 64 | #### Via NPM 65 | ```bash 66 | mkdir myproj 67 | cd myproj 68 | npm install node-flint 69 | ``` 70 | #### Example Template Using Express 71 | ```js 72 | var Flint = require('node-flint'); 73 | var webhook = require('node-flint/webhook'); 74 | var express = require('express'); 75 | var bodyParser = require('body-parser'); 76 | var app = express(); 77 | app.use(bodyParser.json()); 78 | 79 | // flint options 80 | var config = { 81 | webhookUrl: 'http://myserver.com/flint', 82 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u', 83 | port: 80 84 | }; 85 | 86 | // init flint 87 | var flint = new Flint(config); 88 | flint.start(); 89 | 90 | // say hello 91 | flint.hears('/hello', function(bot, trigger) { 92 | bot.say('Hello %s!', trigger.personDisplayName); 93 | }); 94 | 95 | // define express path for incoming webhooks 96 | // This is not necessary if webhookUrl is not set in the config 97 | app.post('/flint', webhook(flint)); 98 | 99 | // start express server 100 | // This is not necessary if webhookUrl is not set in the config 101 | // unless the bot uses express for other reasons 102 | var server = app.listen(config.port, function () { 103 | flint.debug('Flint listening on port %s', config.port); 104 | }); 105 | 106 | // gracefully shutdown (ctrl-c) 107 | process.on('SIGINT', function() { 108 | flint.debug('stoppping...'); 109 | server.close(); // remove if not using webhooks and express 110 | flint.stop().then(function() { 111 | process.exit(); 112 | }); 113 | }); 114 | ``` 115 | 116 | [**Restify Example**](https://github.com/nmarus/flint/blob/master/docs/example2.md) 117 | ## Overview 118 | 119 | Most of Flint's functionality is based around the flint.hears function. This 120 | defines the phrase or pattern the bot is listening for and what actions to take 121 | when that phrase or pattern is matched. The flint.hears function gets a callback 122 | than includes two objects. The bot object, and the trigger object. 123 | 124 | Flint generates a bot object instance of the Bot class for each room the Spark 125 | account Flint is running under. The bot object instance tracks the specifics 126 | about the room it is running in and is passed to the "hears" command callback 127 | when a phrase is heard. 128 | 129 | Flint also generates a trigger object based on the person and room that the 130 | flint.hears function was triggered. 131 | 132 | A simple example of a flint.hears() function setup: 133 | 134 | ```js 135 | flint.hears(phrase, function(bot, trigger) { 136 | bot. 137 | .then(function(returnedValue) { 138 | // do something with returned value 139 | }) 140 | .catch(function(err) { 141 | // handle errors 142 | }); 143 | }); 144 | ``` 145 | 146 | * `phrase` : This can be either a string or a regex pattern. 147 | If a string, the string is matched against the first word in the room message. 148 | message. 149 | If a regex pattern is used, it is matched against the entire message text. 150 | * `bot` : The bot object that is used to execute commands when the `phrase` is 151 | triggered. 152 | * `bot.` : The Bot method to execute. 153 | * `then` : Node JS Promise keyword that invokes additional logic once the 154 | previous command is executed. 155 | * `catch` : handle errors that happen at either the original command or in any 156 | of the chained 'then' functions. 157 | * `trigger` : The object that describes the details around what triggered the 158 | `phrase`. 159 | * `commands` : The commands that are ran when the `phrase` is heard. 160 | 161 | ## Authentication 162 | The token used to authenticate Flint to the Spark (now Webex) API is passed as part of the 163 | options used when instantiating the Flint class. To change or update the 164 | token, use the Flint#setSparkToken() method. 165 | 166 | **Example:** 167 | 168 | ```js 169 | var newToken = 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u'; 170 | 171 | flint.setSparkToken(newToken) 172 | .then(function(token) { 173 | console.log('token updated to: ' + token); 174 | }); 175 | ``` 176 | 177 | ## Storage 178 | The storage system used in flint is a simple key/value store and resolves around 179 | these 3 methods: 180 | 181 | * `bot.store(key, value)` - Store a value to a bot instance where 'key' is a 182 | string and 'value' is a boolean, number, string, array, or object. *This does 183 | not not support functions or any non serializable data.* Returns the a promise 184 | with the value. 185 | * `bot.recall(key)` - Recall a value by 'key' from a bot instance. Returns a 186 | resolved promise with the value or a rejected promise if not found. 187 | * `bot.forget([key])` - Forget (remove) value(s) from a bot instance where 'key' 188 | is an optional property that when defined, removes the specific key, and when 189 | undefined, removes all keys. Returns a resolved promise if deleted or not found. 190 | 191 | When a bot despawns (removed from room), the key/value store for that bot 192 | instance will automatically be removed from the store. Flint currently has an 193 | in-memory store and a Redis based store. By default, the in-memory store is 194 | used. Other backend stores are possible by replicating any one of the built-in 195 | storage modules and passing it to the `flint.storeageDriver()` method. *See 196 | docs for store, recall, forget for more details.* 197 | 198 | **Example:** 199 | 200 | ```js 201 | var redisDriver = require('node-flint/storage/redis'); 202 | flint.storageDriver(redisDriver('redis://localhost')); 203 | ``` 204 | 205 | ## Bot Accounts 206 | 207 | **When using "Bot Accounts" the major differences are:** 208 | 209 | * Webhooks for message:created only trigger when the Bot is mentioned by name 210 | * Unable to read messages in rooms using the Spark (now Webex) API 211 | 212 | **Differences with trigger.args using Flint with a "Bot Account":** 213 | 214 | The trigger.args array is a shortcut in processing the trigger.text string. It 215 | consists of an array of the words that are in the trigger.message string split 216 | by one or more spaces. Punctation is included if there is no space between the 217 | symbol and the word. With bot accounts, this behaves a bit differently. 218 | 219 | * If defining a `flint.hears()` using a string (not regex), `trigger.args` is a 220 | filtered array of words from the message that begins *after* the first match of 221 | bot mention. 222 | 223 | * If defining a flint.hears() using regex, the trigger.args array is the entire 224 | message. 225 | 226 | # Flint Reference 227 | 228 | 229 | ## Classes 230 | 231 |
232 |
Flint
233 |
234 |
Bot
235 |
236 |
237 | 238 | ## Objects 239 | 240 |
241 |
Message : object
242 |

Message Object

243 |
244 |
File : object
245 |

File Object

246 |
247 |
Trigger : object
248 |

Trigger Object

249 |
250 |
251 | 252 | ## Events 253 | 254 |
255 |
"log"
256 |

Flint log event.

257 |
258 |
"stop"
259 |

Flint stop event.

260 |
261 |
"start"
262 |

Flint start event.

263 |
264 |
"initialized"
265 |

Flint initialized event.

266 |
267 |
"roomLocked"
268 |

Room Locked event.

269 |
270 |
"roomUnocked"
271 |

Room Unocked event.

272 |
273 |
"personEnters"
274 |

Person Enter Room event.

275 |
276 |
"botAddedAsModerator"
277 |

Bot Added as Room Moderator.

278 |
279 |
"botRemovedAsModerator"
280 |

Bot Removed as Room Moderator.

281 |
282 |
"personAddedAsModerator"
283 |

Person Added as Moderator.

284 |
285 |
"personRemovedAsModerator"
286 |

Person Removed as Moderator.

287 |
288 |
"personExits"
289 |

Person Exits Room.

290 |
291 |
"mentioned"
292 |

Bot Mentioned.

293 |
294 |
"message"
295 |

Message Recieved.

296 |
297 |
"files"
298 |

File Recieved.

299 |
300 |
"spawn"
301 |

Bot Spawned.

302 |
303 |
"despawn"
304 |

Bot Despawned.

305 |
306 |
307 | 308 | 309 | 310 | ## Flint 311 | **Kind**: global class 312 | **Properties** 313 | 314 | | Name | Type | Description | 315 | | --- | --- | --- | 316 | | id | string | Flint UUID | 317 | | active | boolean | Flint active state | 318 | | intialized | boolean | Flint fully initialized | 319 | | isBotAccount | boolean | Is Flint attached to Spark using a bot account? | 320 | | isUserAccount | boolean | Is Flint attached to Spark using a user account? | 321 | | person | object | Flint person object | 322 | | email | string | Flint email | 323 | | spark | object | The Spark instance used by flint | 324 | 325 | 326 | * [Flint](#Flint) 327 | * [new Flint(options)](#new_Flint_new) 328 | * [.options](#Flint+options) : object 329 | * [.setSparkToken(token)](#Flint+setSparkToken) ⇒ Promise.<String> 330 | * [.stop()](#Flint+stop) ⇒ Promise.<Boolean> 331 | * [.start()](#Flint+start) ⇒ Promise.<Boolean> 332 | * [.restart()](#Flint+restart) ⇒ Promise.<Boolean> 333 | * [.getMessage(messageId)](#Flint+getMessage) ⇒ [Promise.<Message>](#Message) 334 | * [.getFiles(messageId)](#Flint+getFiles) ⇒ Promise.<Array> 335 | * [.hears(phrase, action, [helpText], [preference])](#Flint+hears) ⇒ String 336 | * [.clearHears(id)](#Flint+clearHears) ⇒ null 337 | * [.showHelp([header], [footer])](#Flint+showHelp) ⇒ String 338 | * [.setAuthorizer(Action)](#Flint+setAuthorizer) ⇒ Boolean 339 | * [.clearAuthorizer()](#Flint+clearAuthorizer) ⇒ null 340 | * [.storageDriver(Driver)](#Flint+storageDriver) ⇒ null 341 | * [.use(path)](#Flint+use) ⇒ Boolean 342 | 343 | 344 | 345 | ### new Flint(options) 346 | Creates an instance of Flint. 347 | 348 | 349 | | Param | Type | Description | 350 | | --- | --- | --- | 351 | | options | Object | Configuration object containing Flint settings. | 352 | 353 | **Example** 354 | ```js 355 | var options = { 356 | webhookUrl: 'http://myserver.com/flint', 357 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u' 358 | }; 359 | var flint = new Flint(options); 360 | ``` 361 | 362 | 363 | ### flint.options : object 364 | Options Object 365 | 366 | **Kind**: instance namespace of [Flint](#Flint) 367 | **Properties** 368 | 369 | | Name | Type | Default | Description | 370 | | --- | --- | --- | --- | 371 | | token | string | | Spark Token. | 372 | | webhookUrl | string | | URL that is used for Spark API to send callbacks. If this field is omitted, flint will use the webex javascript sdk to register to listen for the events via websocket instead.| 373 | | [webhookSecret] | string | | If specified, inbound webhooks are authorized before being processed. This configuration is ignored if `webhookUrl` is not set.| 374 | | [messageFormat] | string | "text" | Default Spark message format to use with bot.say(). | 375 | | [maxPageItems] | number | 50 | Max results that the paginator uses. | 376 | | [maxConcurrent] | number | 3 | Max concurrent sessions to the Spark API | 377 | | [minTime] | number | 600 | Min time between consecutive request starts. | 378 | | [requeueMinTime] | number | minTime*10 | Min time between consecutive request starts of requests that have been re-queued. | 379 | | [requeueMaxRetry] | number | 3 | Msx number of atteempts to make for failed request. | 380 | | [requeueCodes] | array | [429,500,503] | Array of http result codes that should be retried. | 381 | | [requestTimeout] | number | 20000 | Timeout for an individual request recieving a response. | 382 | | [queueSize] | number | 10000 | Size of the buffer that holds outbound requests. | 383 | | [requeueSize] | number | 10000 | Size of the buffer that holds outbound re-queue requests. | 384 | | [id] | string | "random" | The id this instance of flint uses. | 385 | | [webhookRequestJSONLocation] | string | "body" | The property under the Request to find the JSON contents. This configuration is ignored if `webhookUrl` is not set. | 386 | | [removeWebhooksOnStart] | Boolean | true | If you wish to have the bot remove all account webhooks when starting. This configuration is ignored if `webhookUrl` is not set. | 387 | 388 | 389 | 390 | ### flint.setSparkToken(token) ⇒ Promise.<String> 391 | Tests, and then sets a new Spark Token. 392 | 393 | **Kind**: instance method of [Flint](#Flint) 394 | 395 | | Param | Type | Description | 396 | | --- | --- | --- | 397 | | token | String | New Spark Token for Flint to use. | 398 | 399 | **Example** 400 | ```js 401 | flint.setSparkToken('Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u') 402 | .then(function(token) { 403 | console.log('token updated to: ' + token); 404 | }); 405 | ``` 406 | 407 | 408 | ### flint.stop() ⇒ Promise.<Boolean> 409 | Stop Flint. 410 | 411 | **Kind**: instance method of [Flint](#Flint) 412 | **Example** 413 | ```js 414 | flint.stop(); 415 | ``` 416 | 417 | 418 | ### flint.start() ⇒ Promise.<Boolean> 419 | Start Flint. 420 | 421 | **Kind**: instance method of [Flint](#Flint) 422 | **Example** 423 | ```js 424 | flint.start(); 425 | ``` 426 | 427 | 428 | ### flint.restart() ⇒ Promise.<Boolean> 429 | Restart Flint. 430 | 431 | **Kind**: instance method of [Flint](#Flint) 432 | **Example** 433 | ```js 434 | flint.restart(); 435 | ``` 436 | 437 | 438 | ### flint.getMessage(messageId) ⇒ [Promise.<Message>](#Message) 439 | Get Message Object by ID 440 | 441 | **Kind**: instance method of [Flint](#Flint) 442 | 443 | | Param | Type | Description | 444 | | --- | --- | --- | 445 | | messageId | String | Message ID from Spark API. | 446 | 447 | 448 | 449 | ### flint.getFiles(messageId) ⇒ Promise.<Array> 450 | Get Files from Message Object by ID 451 | 452 | **Kind**: instance method of [Flint](#Flint) 453 | 454 | | Param | Type | Description | 455 | | --- | --- | --- | 456 | | messageId | String | Message ID from Spark API. | 457 | 458 | 459 | 460 | ### flint.hears(phrase, action, [helpText], [preference]) ⇒ String 461 | Add action to be performed when bot hears a phrase. 462 | 463 | **Kind**: instance method of [Flint](#Flint) 464 | 465 | | Param | Type | Default | Description | 466 | | --- | --- | --- | --- | 467 | | phrase | Regex \| String | | The phrase as either a regex or string. If regex, matches on entire message.If string, matches on first word. | 468 | | action | function | | The function to execute when phrase is matched. Function is executed with 2 variables. Trigger and Bot. The Trigger Object contains information about the person who entered a message that matched the phrase. The Bot Object is an instance of the Bot Class as it relates to the room the message was heard. | 469 | | [helpText] | String | | The string of text that describes how this command operates. | 470 | | [preference] | Number | 0 | Specifies preference of phrase action when overlapping phrases are matched. On multiple matches with same preference, all matched actions are excuted. On multiple matches with difference preference values, only the lower preferenced matched action(s) are executed. | 471 | 472 | **Example** 473 | ```js 474 | // using a string to match first word and defines help text 475 | flint.hears('/say', function(bot, trigger, id) { 476 | bot.say(trigger.args.slice(1, trigger.arges.length - 1)); 477 | }, '/say - Responds with a greeting'); 478 | ``` 479 | **Example** 480 | ```js 481 | // using regex to match across entire message 482 | flint.hears(/(^| )beer( |.|$)/i, function(bot, trigger, id) { 483 | bot.say('Enjoy a beer, %s! 🍻', trigger.personDisplayName); 484 | }); 485 | ``` 486 | 487 | 488 | ### flint.clearHears(id) ⇒ null 489 | Remove a "flint.hears()" entry. 490 | 491 | **Kind**: instance method of [Flint](#Flint) 492 | 493 | | Param | Type | Description | 494 | | --- | --- | --- | 495 | | id | String | The "hears" ID. | 496 | 497 | **Example** 498 | ```js 499 | // using a string to match first word and defines help text 500 | var hearsHello = flint.hears('/flint', function(bot, trigger, id) { 501 | bot.say('Hello %s!', trigger.personDisplayName); 502 | }); 503 | flint.clearHears(hearsHello); 504 | ``` 505 | 506 | 507 | ### flint.showHelp([header], [footer]) ⇒ String 508 | Display help for registered Flint Commands. 509 | 510 | **Kind**: instance method of [Flint](#Flint) 511 | 512 | | Param | Type | Default | Description | 513 | | --- | --- | --- | --- | 514 | | [header] | String | Usage: | String to use in header before displaying help message. | 515 | | [footer] | String | Powered by Flint - https://github.com/nmarus/flint | String to use in footer before displaying help message. | 516 | 517 | **Example** 518 | ```js 519 | flint.hears('/help', function(bot, trigger, id) { 520 | bot.say(flint.showHelp()); 521 | }); 522 | ``` 523 | 524 | 525 | ### flint.setAuthorizer(Action) ⇒ Boolean 526 | Attaches authorizer function. 527 | 528 | **Kind**: instance method of [Flint](#Flint) 529 | 530 | | Param | Type | Description | 531 | | --- | --- | --- | 532 | | Action | function | The function to execute when phrase is matched to authenticate a user. The function is passed the bot, trigger, and id and expects a return value of true or false. | 533 | 534 | **Example** 535 | ```js 536 | function myAuthorizer(bot, trigger, id) { 537 | if(trigger.personEmail === 'john@test.com') { 538 | return true; 539 | } 540 | else if(trigger.personDomain === 'test.com') { 541 | return true; 542 | } 543 | else { 544 | return false; 545 | } 546 | } 547 | flint.setAuthorizer(myAuthorizer); 548 | ``` 549 | 550 | 551 | ### flint.clearAuthorizer() ⇒ null 552 | Removes authorizer function. 553 | 554 | **Kind**: instance method of [Flint](#Flint) 555 | **Example** 556 | ```js 557 | flint.clearAuthorizer(); 558 | ``` 559 | 560 | 561 | ### flint.storageDriver(Driver) ⇒ null 562 | Defines storage backend. 563 | 564 | **Kind**: instance method of [Flint](#Flint) 565 | 566 | | Param | Type | Description | 567 | | --- | --- | --- | 568 | | Driver | function | The storage driver. | 569 | 570 | **Example** 571 | ```js 572 | // define memory store (default if not specified) 573 | flint.storageDriver(new MemStore()); 574 | ``` 575 | 576 | 577 | ### flint.use(path) ⇒ Boolean 578 | Load a Plugin from a external file. 579 | 580 | **Kind**: instance method of [Flint](#Flint) 581 | 582 | | Param | Type | Description | 583 | | --- | --- | --- | 584 | | path | String | Load a plugin at given path. | 585 | 586 | **Example** 587 | ```js 588 | flint.use('events.js'); 589 | ``` 590 | **Example** 591 | ```js 592 | // events.js 593 | module.exports = function(flint) { 594 | flint.on('spawn', function(bot) { 595 | console.log('new bot spawned in room: %s', bot.myroom.title); 596 | }); 597 | flint.on('despawn', function(bot) { 598 | console.log('bot despawned in room: %s', bot.myroom.title); 599 | }); 600 | flint.on('messageCreated', function(message, bot) { 601 | console.log('"%s" said "%s" in room "%s"', message.personEmail, message.text, bot.myroom.title); 602 | }); 603 | }; 604 | ``` 605 | 606 | 607 | ## Bot 608 | **Kind**: global class 609 | **Properties** 610 | 611 | | Name | Type | Description | 612 | | --- | --- | --- | 613 | | id | string | Bot UUID | 614 | | active | boolean | Bot active state | 615 | | person | object | Bot Person Object | 616 | | email | string | Bot email | 617 | | team | object | Bot team object | 618 | | room | object | Bot room object | 619 | | membership | object | Bot membership object | 620 | | isLocked | boolean | If bot is locked | 621 | | isModerator | boolean | If bot is a moderator | 622 | | isGroup | boolean | If bot is in Group Room | 623 | | isDirect | boolean | If bot is in 1:1/Direct Room | 624 | | isDirectTo | string | Recipient Email if bot is in 1:1/Direct Room | 625 | | isTeam | boolean | If bot is in Team Room | 626 | | lastActivity | date | Last bot activity | 627 | 628 | 629 | * [Bot](#Bot) 630 | * [new Bot(flint)](#new_Bot_new) 631 | * [.exit()](#Bot+exit) ⇒ Promise.<Boolean> 632 | * [.add(email(s), [moderator])](#Bot+add) ⇒ Promise.<Array> 633 | * [.remove(email(s))](#Bot+remove) ⇒ Promise.<Array> 634 | * [.getModerators()](#Bot+getModerators) ⇒ Promise.<Array> 635 | * [.newRoom(name, emails)](#Bot+newRoom) ⇒ [Promise.<Bot>](#Bot) 636 | * [.newTeamRoom(name, emails)](#Bot+newTeamRoom) ⇒ [Promise.<Bot>](#Bot) 637 | * [.moderateRoom()](#Bot+moderateRoom) ⇒ [Promise.<Bot>](#Bot) 638 | * [.unmoderateRoom()](#Bot+unmoderateRoom) ⇒ [Promise.<Bot>](#Bot) 639 | * [.moderatorSet(email(s))](#Bot+moderatorSet) ⇒ [Promise.<Bot>](#Bot) 640 | * [.moderatorClear(email(s))](#Bot+moderatorClear) ⇒ [Promise.<Bot>](#Bot) 641 | * [.implode()](#Bot+implode) ⇒ Promise.<Boolean> 642 | * [.say([format], message)](#Bot+say) ⇒ [Promise.<Message>](#Message) 643 | * [.dm(email, [format], message)](#Bot+dm) ⇒ [Promise.<Message>](#Message) 644 | * [.uploadStream(filename, stream)](#Bot+uploadStream) ⇒ [Promise.<Message>](#Message) 645 | * [.upload(filepath)](#Bot+upload) ⇒ [Promise.<Message>](#Message) 646 | * [.censor(messageId)](#Bot+censor) ⇒ [Promise.<Message>](#Message) 647 | * [.roomRename(title)](#Bot+roomRename) ⇒ Promise.<Room> 648 | * [.getMessages(count)](#Bot+getMessages) ⇒ Promise.<Array> 649 | * [.store(key, value)](#Bot+store) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 650 | * [.recall([key])](#Bot+recall) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 651 | * [.forget([key])](#Bot+forget) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 652 | 653 | 654 | 655 | ### new Bot(flint) 656 | Creates a Bot instance that is then attached to a Spark Room. 657 | 658 | 659 | | Param | Type | Description | 660 | | --- | --- | --- | 661 | | flint | Object | The flint object this Bot spawns under. | 662 | 663 | 664 | 665 | ### bot.exit() ⇒ Promise.<Boolean> 666 | Instructs Bot to exit from room. 667 | 668 | **Kind**: instance method of [Bot](#Bot) 669 | **Example** 670 | ```js 671 | bot.exit(); 672 | ``` 673 | 674 | 675 | ### bot.add(email(s), [moderator]) ⇒ Promise.<Array> 676 | Instructs Bot to add person(s) to room. 677 | 678 | **Kind**: instance method of [Bot](#Bot) 679 | **Returns**: Promise.<Array> - Array of emails added 680 | 681 | | Param | Type | Description | 682 | | --- | --- | --- | 683 | | email(s) | String \| Array | Email Address (or Array of Email Addresses) of Person(s) to add to room. | 684 | | [moderator] | Boolean | Add as moderator. | 685 | 686 | **Example** 687 | ```js 688 | // add one person to room by email 689 | bot.add('john@test.com'); 690 | ``` 691 | **Example** 692 | ```js 693 | // add one person as moderator to room by email 694 | bot.add('john@test.com', true) 695 | .catch(function(err) { 696 | // log error if unsuccessful 697 | console.log(err.message); 698 | }); 699 | ``` 700 | **Example** 701 | ```js 702 | // add 3 people to room by email 703 | bot.add(['john@test.com', 'jane@test.com', 'bill@test.com']); 704 | ``` 705 | 706 | 707 | ### bot.remove(email(s)) ⇒ Promise.<Array> 708 | Instructs Bot to remove person from room. 709 | 710 | **Kind**: instance method of [Bot](#Bot) 711 | **Returns**: Promise.<Array> - Array of emails removed 712 | 713 | | Param | Type | Description | 714 | | --- | --- | --- | 715 | | email(s) | String \| Array | Email Address (or Array of Email Addresses) of Person(s) to remove from room. | 716 | 717 | **Example** 718 | ```js 719 | // remove one person to room by email 720 | bot.remove('john@test.com'); 721 | ``` 722 | **Example** 723 | ```js 724 | // remove 3 people from room by email 725 | bot.remove(['john@test.com', 'jane@test.com', 'bill@test.com']); 726 | ``` 727 | 728 | 729 | ### bot.getModerators() ⇒ Promise.<Array> 730 | Get room moderators. 731 | 732 | **Kind**: instance method of [Bot](#Bot) 733 | **Example** 734 | ```js 735 | bot.getModerators() 736 | .then(function(moderators) { 737 | console.log(moderators); 738 | }); 739 | ``` 740 | 741 | 742 | ### bot.newRoom(name, emails) ⇒ [Promise.<Bot>](#Bot) 743 | Create new room with people by email 744 | 745 | **Kind**: instance method of [Bot](#Bot) 746 | 747 | | Param | Type | Description | 748 | | --- | --- | --- | 749 | | name | String | Name of room. | 750 | | emails | Array | Emails of people to add to room. | 751 | 752 | 753 | 754 | ### bot.newTeamRoom(name, emails) ⇒ [Promise.<Bot>](#Bot) 755 | Create new Team Room 756 | 757 | **Kind**: instance method of [Bot](#Bot) 758 | 759 | | Param | Type | Description | 760 | | --- | --- | --- | 761 | | name | String | Name of room. | 762 | | emails | Array | Emails of people to add to room. | 763 | 764 | 765 | 766 | ### bot.moderateRoom() ⇒ [Promise.<Bot>](#Bot) 767 | Enable Room Moderation.Enable. 768 | 769 | **Kind**: instance method of [Bot](#Bot) 770 | **Example** 771 | ```js 772 | bot.moderateRoom() 773 | .then(function(err) { 774 | console.log(err.message) 775 | }); 776 | ``` 777 | 778 | 779 | ### bot.unmoderateRoom() ⇒ [Promise.<Bot>](#Bot) 780 | Disable Room Moderation. 781 | 782 | **Kind**: instance method of [Bot](#Bot) 783 | **Example** 784 | ```js 785 | bot.unmoderateRoom() 786 | .then(function(err) { 787 | console.log(err.message) 788 | }); 789 | ``` 790 | 791 | 792 | ### bot.moderatorSet(email(s)) ⇒ [Promise.<Bot>](#Bot) 793 | Assign Moderator in Room 794 | 795 | **Kind**: instance method of [Bot](#Bot) 796 | 797 | | Param | Type | Description | 798 | | --- | --- | --- | 799 | | email(s) | String \| Array | Email Address (or Array of Email Addresses) of Person(s) to assign as moderator. | 800 | 801 | **Example** 802 | ```js 803 | bot.moderatorSet('john@test.com') 804 | .then(function(err) { 805 | console.log(err.message) 806 | }); 807 | ``` 808 | 809 | 810 | ### bot.moderatorClear(email(s)) ⇒ [Promise.<Bot>](#Bot) 811 | Unassign Moderator in Room 812 | 813 | **Kind**: instance method of [Bot](#Bot) 814 | 815 | | Param | Type | Description | 816 | | --- | --- | --- | 817 | | email(s) | String \| Array | Email Address (or Array of Email Addresses) of Person(s) to unassign as moderator. | 818 | 819 | **Example** 820 | ```js 821 | bot.moderatorClear('john@test.com') 822 | .then(function(err) { 823 | console.log(err.message) 824 | }); 825 | ``` 826 | 827 | 828 | ### bot.implode() ⇒ Promise.<Boolean> 829 | Remove a room and all memberships. 830 | 831 | **Kind**: instance method of [Bot](#Bot) 832 | **Example** 833 | ```js 834 | flint.hears('/implode', function(bot, trigger) { 835 | bot.implode(); 836 | }); 837 | ``` 838 | 839 | 840 | ### bot.say([format], message) ⇒ [Promise.<Message>](#Message) 841 | Send text with optional file to room. 842 | 843 | **Kind**: instance method of [Bot](#Bot) 844 | 845 | | Param | Type | Default | Description | 846 | | --- | --- | --- | --- | 847 | | [format] | String | text | Set message format. Valid options are 'text' or 'markdown'. | 848 | | message | String \| Object | | Message to send to room. This can be a simple string, or a object for advanced use. | 849 | 850 | **Example** 851 | ```js 852 | // Simple example 853 | flint.hears('/hello', function(bot, trigger) { 854 | bot.say('hello'); 855 | }); 856 | ``` 857 | **Example** 858 | ```js 859 | // Simple example to send message and file 860 | flint.hears('/file', function(bot, trigger) { 861 | bot.say({text: 'Here is your file!', file: 'http://myurl/file.doc'}); 862 | }); 863 | ``` 864 | **Example** 865 | ```js 866 | // Markdown Method 1 - Define markdown as default 867 | flint.messageFormat = 'markdown'; 868 | flint.hears('/hello', function(bot, trigger) { 869 | bot.say('**hello**, How are you today?'); 870 | }); 871 | ``` 872 | **Example** 873 | ```js 874 | // Markdown Method 2 - Define message format as part of argument string 875 | flint.hears('/hello', function(bot, trigger) { 876 | bot.say('markdown', '**hello**, How are you today?'); 877 | }); 878 | ``` 879 | **Example** 880 | ```js 881 | // Mardown Method 3 - Use an object (use this method of bot.say() when needing to send a file in the same message as markdown text. 882 | flint.hears('/hello', function(bot, trigger) { 883 | bot.say({markdown: '*Hello <@personEmail:' + trigger.personEmail + '|' + trigger.personDisplayName + '>*'}); 884 | }); 885 | ``` 886 | 887 | 888 | ### bot.dm(email, [format], message) ⇒ [Promise.<Message>](#Message) 889 | Send text with optional file in a direct message. This sends a message to a 1:1 room with the user (creates 1:1, if one does not already exist) 890 | 891 | **Kind**: instance method of [Bot](#Bot) 892 | 893 | | Param | Type | Default | Description | 894 | | --- | --- | --- | --- | 895 | | email | String | | Email of person to send Direct Message. | 896 | | [format] | String | text | Set message format. Valid options are 'text' or 'markdown'. | 897 | | message | String \| Object | | Message to send to room. This can be a simple string, or a object for advanced use. | 898 | 899 | **Example** 900 | ```js 901 | // Simple example 902 | flint.hears('/dm', function(bot, trigger) { 903 | bot.dm('someone@domain.com', 'hello'); 904 | }); 905 | ``` 906 | **Example** 907 | ```js 908 | // Simple example to send message and file 909 | flint.hears('/dm', function(bot, trigger) { 910 | bot.dm('someone@domain.com', {text: 'Here is your file!', file: 'http://myurl/file.doc'}); 911 | }); 912 | ``` 913 | **Example** 914 | ```js 915 | // Markdown Method 1 - Define markdown as default 916 | flint.messageFormat = 'markdown'; 917 | flint.hears('/dm', function(bot, trigger) { 918 | bot.dm('someone@domain.com', '**hello**, How are you today?'); 919 | }); 920 | ``` 921 | **Example** 922 | ```js 923 | // Markdown Method 2 - Define message format as part of argument string 924 | flint.hears('/dm', function(bot, trigger) { 925 | bot.dm('someone@domain.com', 'markdown', '**hello**, How are you today?'); 926 | }); 927 | ``` 928 | **Example** 929 | ```js 930 | // Mardown Method 3 - Use an object (use this method of bot.dm() when needing to send a file in the same message as markdown text. 931 | flint.hears('/dm', function(bot, trigger) { 932 | bot.dm('someone@domain.com', {markdown: '*Hello <@personEmail:' + trigger.personEmail + '|' + trigger.personDisplayName + '>*'}); 933 | }); 934 | ``` 935 | 936 | 937 | ### bot.uploadStream(filename, stream) ⇒ [Promise.<Message>](#Message) 938 | Upload a file to a room using a Readable Stream 939 | 940 | **Kind**: instance method of [Bot](#Bot) 941 | 942 | | Param | Type | Description | 943 | | --- | --- | --- | 944 | | filename | String | File name used when uploading to room | 945 | | stream | Stream.Readable | Stream Readable | 946 | 947 | **Example** 948 | ```js 949 | flint.hears('/file', function(bot, trigger) { 950 | 951 | // define filename used when uploading to room 952 | var filename = 'test.png'; 953 | 954 | // create readable stream 955 | var stream = fs.createReadStream('/my/file/test.png'); 956 | 957 | bot.uploadStream(filename, stream); 958 | }); 959 | ``` 960 | 961 | 962 | ### bot.upload(filepath) ⇒ [Promise.<Message>](#Message) 963 | Upload a file to room. 964 | 965 | **Kind**: instance method of [Bot](#Bot) 966 | 967 | | Param | Type | Description | 968 | | --- | --- | --- | 969 | | filepath | String | File Path to upload | 970 | 971 | **Example** 972 | ```js 973 | flint.hears('/file', function(bot, trigger) { 974 | bot.upload('test.png'); 975 | }); 976 | ``` 977 | 978 | 979 | ### bot.censor(messageId) ⇒ [Promise.<Message>](#Message) 980 | Remove Message By Id. 981 | 982 | **Kind**: instance method of [Bot](#Bot) 983 | 984 | | Param | Type | 985 | | --- | --- | 986 | | messageId | String | 987 | 988 | 989 | 990 | ### bot.roomRename(title) ⇒ Promise.<Room> 991 | Set Title of Room. 992 | 993 | **Kind**: instance method of [Bot](#Bot) 994 | 995 | | Param | Type | 996 | | --- | --- | 997 | | title | String | 998 | 999 | **Example** 1000 | ```js 1001 | bot.roomRename('My Renamed Room') 1002 | .then(function(err) { 1003 | console.log(err.message) 1004 | }); 1005 | ``` 1006 | 1007 | 1008 | ### bot.getMessages(count) ⇒ Promise.<Array> 1009 | Get messages from room. Returned data has newest message at bottom. 1010 | 1011 | **Kind**: instance method of [Bot](#Bot) 1012 | 1013 | | Param | Type | 1014 | | --- | --- | 1015 | | count | Integer | 1016 | 1017 | **Example** 1018 | ```js 1019 | bot.getMessages(5).then(function(messages) { 1020 | messages.forEach(function(message) { 1021 | // display message text 1022 | if(message.text) { 1023 | console.log(message.text); 1024 | } 1025 | }); 1026 | }); 1027 | ``` 1028 | 1029 | 1030 | ### bot.store(key, value) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 1031 | Store key/value data. 1032 | 1033 | **Kind**: instance method of [Bot](#Bot) 1034 | 1035 | | Param | Type | Description | 1036 | | --- | --- | --- | 1037 | | key | String | Key under id object | 1038 | | value | String \| Number \| Boolean \| Array \| Object | Value of key | 1039 | 1040 | 1041 | 1042 | ### bot.recall([key]) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 1043 | Recall value of data stored by 'key'. 1044 | 1045 | **Kind**: instance method of [Bot](#Bot) 1046 | 1047 | | Param | Type | Description | 1048 | | --- | --- | --- | 1049 | | [key] | String | Key under id object (optional). If key is not passed, all keys for id are returned as an object. | 1050 | 1051 | 1052 | 1053 | ### bot.forget([key]) ⇒ Promise.<String> \| Promise.<Number> \| Promise.<Boolean> \| Promise.<Array> \| Promise.<Object> 1054 | Forget a key or entire store. 1055 | 1056 | **Kind**: instance method of [Bot](#Bot) 1057 | 1058 | | Param | Type | Description | 1059 | | --- | --- | --- | 1060 | | [key] | String | Key under id object (optional). If key is not passed, id and all children are removed. | 1061 | 1062 | 1063 | 1064 | ## Message : object 1065 | Message Object 1066 | 1067 | **Kind**: global namespace 1068 | **Properties** 1069 | 1070 | | Name | Type | Description | 1071 | | --- | --- | --- | 1072 | | id | string | Message ID | 1073 | | personId | string | Person ID | 1074 | | personEmail | string | Person Email | 1075 | | personAvatar | string | PersonAvatar URL | 1076 | | personDomain | string | Person Domain Name | 1077 | | personDisplayName | string | Person Display Name | 1078 | | roomId | string | Room ID | 1079 | | text | string | Message text | 1080 | | files | array | Array of File objects | 1081 | | created | date | Date Message created | 1082 | 1083 | 1084 | 1085 | ## File : object 1086 | File Object 1087 | 1088 | **Kind**: global namespace 1089 | **Properties** 1090 | 1091 | | Name | Type | Description | 1092 | | --- | --- | --- | 1093 | | id | string | Spark API Content ID | 1094 | | name | string | File name | 1095 | | ext | string | File extension | 1096 | | type | string | Header [content-type] for file | 1097 | | binary | buffer | File contents as binary | 1098 | | base64 | string | File contents as base64 encoded string | 1099 | | personId | string | Person ID of who added file | 1100 | | personEmail | string | Person Email of who added file | 1101 | | personAvatar | string | PersonAvatar URL | 1102 | | personDomain | string | Person Domain Name | 1103 | | personDisplayName | string | Person Display Name | 1104 | | created | date | Date file was added to room | 1105 | 1106 | 1107 | 1108 | ## Trigger : object 1109 | Trigger Object 1110 | 1111 | **Kind**: global namespace 1112 | **Properties** 1113 | 1114 | | Name | Type | Description | 1115 | | --- | --- | --- | 1116 | | id | string | Message ID | 1117 | | phrase | string \| regex | Matched lexicon phrase | 1118 | | text | string | Message Text (or false if no text) | 1119 | | raw | string | Unprocessed Message Text (or false if no text) | 1120 | | html | string | Message HTML (or false if no html) | 1121 | | markdown | string | Message Markdown (or false if no markdown) | 1122 | | mentionedPeople | array | Mentioned People (or false if no mentioned) | 1123 | | files | array | Message Files (or false if no files in trigger) | 1124 | | args | array | Filtered array of words in message text. | 1125 | | created | date | Message Created date | 1126 | | roomId | string | Room ID | 1127 | | roomTitle | string | Room Title | 1128 | | roomType | string | Room Type (group or direct) | 1129 | | roomIsLocked | boolean | Room Locked/Moderated status | 1130 | | personId | string | Person ID | 1131 | | personEmail | string | Person Email | 1132 | | personDisplayName | string | Person Display Name | 1133 | | personUsername | string | Person Username | 1134 | | personDomain | string | Person Domain name | 1135 | | personAvatar | string | Person Avatar URL | 1136 | | personMembership | object | Person Membership object for person | 1137 | 1138 | 1139 | 1140 | ## "log" 1141 | Flint log event. 1142 | 1143 | **Kind**: event emitted 1144 | **Properties** 1145 | 1146 | | Name | Type | Description | 1147 | | --- | --- | --- | 1148 | | message | string | Log Message | 1149 | 1150 | 1151 | 1152 | ## "stop" 1153 | Flint stop event. 1154 | 1155 | **Kind**: event emitted 1156 | **Properties** 1157 | 1158 | | Name | Type | Description | 1159 | | --- | --- | --- | 1160 | | id | string | Flint UUID | 1161 | 1162 | 1163 | 1164 | ## "start" 1165 | Flint start event. 1166 | 1167 | **Kind**: event emitted 1168 | **Properties** 1169 | 1170 | | Name | Type | Description | 1171 | | --- | --- | --- | 1172 | | id | string | Flint UUID | 1173 | 1174 | 1175 | 1176 | ## "initialized" 1177 | Flint initialized event. 1178 | 1179 | **Kind**: event emitted 1180 | **Properties** 1181 | 1182 | | Name | Type | Description | 1183 | | --- | --- | --- | 1184 | | id | string | Flint UUID | 1185 | 1186 | 1187 | 1188 | ## "roomLocked" 1189 | Room Locked event. 1190 | 1191 | **Kind**: event emitted 1192 | **Properties** 1193 | 1194 | | Name | Type | Description | 1195 | | --- | --- | --- | 1196 | | bot | object | Bot Object | 1197 | | id | string | Flint UUID | 1198 | 1199 | 1200 | 1201 | ## "roomUnocked" 1202 | Room Unocked event. 1203 | 1204 | **Kind**: event emitted 1205 | **Properties** 1206 | 1207 | | Name | Type | Description | 1208 | | --- | --- | --- | 1209 | | bot | object | Bot Object | 1210 | | id | string | Flint UUID | 1211 | 1212 | 1213 | 1214 | ## "personEnters" 1215 | Person Enter Room event. 1216 | 1217 | **Kind**: event emitted 1218 | **Properties** 1219 | 1220 | | Name | Type | Description | 1221 | | --- | --- | --- | 1222 | | bot | object | Bot Object | 1223 | | person | object | Person Object | 1224 | | id | string | Flint UUID | 1225 | 1226 | 1227 | 1228 | ## "botAddedAsModerator" 1229 | Bot Added as Room Moderator. 1230 | 1231 | **Kind**: event emitted 1232 | **Properties** 1233 | 1234 | | Name | Type | Description | 1235 | | --- | --- | --- | 1236 | | bot | object | Bot Object | 1237 | | id | string | Flint UUID | 1238 | 1239 | 1240 | 1241 | ## "botRemovedAsModerator" 1242 | Bot Removed as Room Moderator. 1243 | 1244 | **Kind**: event emitted 1245 | **Properties** 1246 | 1247 | | Name | Type | Description | 1248 | | --- | --- | --- | 1249 | | bot | object | Bot Object | 1250 | | id | string | Flint UUID | 1251 | 1252 | 1253 | 1254 | ## "personAddedAsModerator" 1255 | Person Added as Moderator. 1256 | 1257 | **Kind**: event emitted 1258 | **Properties** 1259 | 1260 | | Name | Type | Description | 1261 | | --- | --- | --- | 1262 | | bot | object | Bot Object | 1263 | | person | object | Person Object | 1264 | | id | string | Flint UUID | 1265 | 1266 | 1267 | 1268 | ## "personRemovedAsModerator" 1269 | Person Removed as Moderator. 1270 | 1271 | **Kind**: event emitted 1272 | **Properties** 1273 | 1274 | | Name | Type | Description | 1275 | | --- | --- | --- | 1276 | | bot | object | Bot Object | 1277 | | person | object | Person Object | 1278 | | id | string | Flint UUID | 1279 | 1280 | 1281 | 1282 | ## "personExits" 1283 | Person Exits Room. 1284 | 1285 | **Kind**: event emitted 1286 | **Properties** 1287 | 1288 | | Name | Type | Description | 1289 | | --- | --- | --- | 1290 | | bot | object | Bot Object | 1291 | | person | object | Person Object | 1292 | | id | string | Flint UUID | 1293 | 1294 | 1295 | 1296 | ## "mentioned" 1297 | Bot Mentioned. 1298 | 1299 | **Kind**: event emitted 1300 | **Properties** 1301 | 1302 | | Name | Type | Description | 1303 | | --- | --- | --- | 1304 | | bot | object | Bot Object | 1305 | | trigger | object | Trigger Object | 1306 | | id | string | Flint UUID | 1307 | 1308 | 1309 | 1310 | ## "message" 1311 | Message Recieved. 1312 | 1313 | **Kind**: event emitted 1314 | **Properties** 1315 | 1316 | | Name | Type | Description | 1317 | | --- | --- | --- | 1318 | | bot | object | Bot Object | 1319 | | trigger | object | Trigger Object | 1320 | | id | string | Flint UUID | 1321 | 1322 | 1323 | 1324 | ## "files" 1325 | File Recieved. 1326 | 1327 | **Kind**: event emitted 1328 | **Properties** 1329 | 1330 | | Name | Type | Description | 1331 | | --- | --- | --- | 1332 | | bot | object | Bot Object | 1333 | | trigger | trigger | Trigger Object | 1334 | | id | string | Flint UUID | 1335 | 1336 | 1337 | 1338 | ## "spawn" 1339 | Bot Spawned. 1340 | 1341 | **Kind**: event emitted 1342 | **Properties** 1343 | 1344 | | Name | Type | Description | 1345 | | --- | --- | --- | 1346 | | bot | object | Bot Object | 1347 | | id | string | Flint UUID | 1348 | 1349 | 1350 | 1351 | ## "despawn" 1352 | Bot Despawned. 1353 | 1354 | **Kind**: event emitted 1355 | **Properties** 1356 | 1357 | | Name | Type | Description | 1358 | | --- | --- | --- | 1359 | | bot | object | Bot Object | 1360 | | id | string | Flint UUID | 1361 | 1362 | ## License 1363 | 1364 | The MIT License (MIT) 1365 | 1366 | Copyright (c) 2016-2017 1367 | 1368 | Permission is hereby granted, free of charge, to any person obtaining a copy 1369 | of this software and associated documentation files (the "Software"), to deal 1370 | in the Software without restriction, including without limitation the rights 1371 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1372 | copies of the Software, and to permit persons to whom the Software is 1373 | furnished to do so, subject to the following conditions: 1374 | 1375 | The above copyright notice and this permission notice shall be included in 1376 | all copies or substantial portions of the Software. 1377 | 1378 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1379 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1380 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1381 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1382 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1383 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 1384 | THE SOFTWARE. 1385 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Rebuilding Docs 2 | 3 | The `build.sh` script in this folder generates the README.md for the project. 4 | This build script requires that you have installed the dev dependencies of this project. 5 | 6 | ```bash 7 | #!/bin/bash 8 | 9 | JSDOC="$(pwd)/../node_modules/jsdoc-to-markdown/bin/cli.js" 10 | README="$(pwd)/../README.md" 11 | 12 | cat header.md > ${README} 13 | 14 | cat example1.md >> ${README} 15 | cat example2.md >> ${README} 16 | 17 | cat overview.md >> ${README} 18 | cat installation.md >> ${README} 19 | 20 | ${JSDOC} ../lib/flint.js ../lib/bot.js >> ${README} 21 | 22 | cat license.md >> ${README} 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/adaptive-card-example.md: -------------------------------------------------------------------------------- 1 | #### Adaptive Card Template Using Express 2 | ```js 3 | var Flint = require('node-flint'); 4 | var webhook = require('node-flint/webhook'); 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var app = express(); 8 | app.use(bodyParser.json()); 9 | 10 | // flint options 11 | var config = { 12 | webhookUrl: 'http://myserver.com/flint', 13 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u', 14 | port: 80 15 | }; 16 | 17 | // init flint 18 | var flint = new Flint(config); 19 | flint.start(); 20 | 21 | flint.on("initialized", async function () { 22 | flint.debug("Flint initialized successfully! [Press CTRL-C to quit]"); 23 | }); 24 | 25 | 26 | 27 | // send an example card in response to any input 28 | flint.hears(/.*/, function(bot) { 29 | bot.say({ 30 | // Fallback text for clients that don't render cards 31 | markdown: "[Tell us about yourself](https://www.example.com/form/book-vacation). We just need a few more details to get you booked for the trip of a lifetime!", 32 | attachments: cardBody 33 | }); 34 | }); 35 | 36 | // Process a submitted card 37 | flint.on('attachmentAction', function (bot, attachmentAction) { 38 | bot.say(`Got an attachmentAction:\n${JSON.stringify(attachmentAction, null, 2)}`); 39 | }); 40 | 41 | // define express path for incoming webhooks 42 | app.post('/', webhook(flint)); 43 | 44 | // start express server 45 | var server = app.listen(config.port, function () { 46 | flint.debug('Flint listening on port %s', config.port); 47 | }); 48 | 49 | // gracefully shutdown (ctrl-c) 50 | process.on('SIGINT', function() { 51 | flint.debug('stoppping...'); 52 | server.close(); 53 | flint.stop().then(function() { 54 | process.exit(); 55 | }); 56 | }); 57 | 58 | // define the contents of an adaptive card 59 | let cardBody = [ 60 | { 61 | "contentType": "application/vnd.microsoft.card.adaptive", 62 | "content": { 63 | "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", 64 | "type": "AdaptiveCard", 65 | "version": "1.0", 66 | "body": [ 67 | { 68 | "type": "ColumnSet", 69 | "columns": [ 70 | { 71 | "type": "Column", 72 | "width": 2, 73 | "items": [ 74 | { 75 | "type": "TextBlock", 76 | "text": "Tell us about yourself", 77 | "weight": "bolder", 78 | "size": "medium" 79 | }, 80 | { 81 | "type": "TextBlock", 82 | "text": "We just need a few more details to get you booked for the trip of a lifetime!", 83 | "isSubtle": true, 84 | "wrap": true 85 | }, 86 | { 87 | "type": "TextBlock", 88 | "text": "Don't worry, we'll never share or sell your information.", 89 | "isSubtle": true, 90 | "wrap": true, 91 | "size": "small" 92 | }, 93 | { 94 | "type": "TextBlock", 95 | "text": "Your name", 96 | "wrap": true 97 | }, 98 | { 99 | "type": "Input.Text", 100 | "id": "Name", 101 | "placeholder": "John Andersen" 102 | }, 103 | { 104 | "type": "TextBlock", 105 | "text": "Your website", 106 | "wrap": true 107 | }, 108 | { 109 | "type": "Input.Text", 110 | "id" : "Url", 111 | "placeholder": "https://example.com" 112 | }, 113 | { 114 | "type": "TextBlock", 115 | "text": "Your email", 116 | "wrap": true 117 | }, 118 | { 119 | "type": "Input.Text", 120 | "id": "Email", 121 | "placeholder": "john.andersen@example.com", 122 | "style": "email" 123 | }, 124 | { 125 | "type": "TextBlock", 126 | "text": "Phone Number" 127 | }, 128 | { 129 | "type": "Input.Text", 130 | "id": "Tel", 131 | "placeholder": "+1 408 526 7209", 132 | "style": "tel" 133 | } 134 | ] 135 | }, 136 | { 137 | "type": "Column", 138 | "width": 1, 139 | "items": [ 140 | { 141 | "type": "Image", 142 | "url": "https://upload.wikimedia.org/wikipedia/commons/b/b2/Diver_Silhouette%2C_Great_Barrier_Reef.jpg", 143 | "size": "auto" 144 | } 145 | ] 146 | } 147 | ] 148 | } 149 | ], 150 | "actions": [ 151 | { 152 | "type": "Action.Submit", 153 | "title": "Submit" 154 | } 155 | ] 156 | } 157 | } 158 | ]; 159 | ``` 160 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JSDOC="$(pwd)/../node_modules/jsdoc-to-markdown/bin/cli.js" 4 | DOCTOC="$(pwd)/../node_modules/doctoc/doctoc.js" 5 | README="$(pwd)/../README.md" 6 | 7 | cat header.md > ${README} 8 | cat installation.md >> ${README} 9 | cat example1.md >> ${README} 10 | cat overview.md >> ${README} 11 | 12 | ${DOCTOC} --github --notitle --maxlevel 4 ${README} 13 | 14 | echo -e "\n# Flint Reference\n\n" >> ${README} 15 | 16 | ${JSDOC} ../lib/flint.js ../lib/bot.js >> ${README} 17 | 18 | cat license.md >> ${README} 19 | -------------------------------------------------------------------------------- /docs/example1.md: -------------------------------------------------------------------------------- 1 | #### Example Template Using Express 2 | ```js 3 | var Flint = require('node-flint'); 4 | var webhook = require('node-flint/webhook'); 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var app = express(); 8 | app.use(bodyParser.json()); 9 | 10 | // flint options 11 | var config = { 12 | webhookUrl: 'http://myserver.com/flint', 13 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u', 14 | port: 80 15 | }; 16 | 17 | // init flint 18 | var flint = new Flint(config); 19 | flint.start(); 20 | 21 | // say hello 22 | flint.hears('/hello', function(bot, trigger) { 23 | bot.say('Hello %s!', trigger.personDisplayName); 24 | }); 25 | 26 | // define express path for incoming webhooks 27 | app.post('/flint', webhook(flint)); 28 | 29 | // start express server 30 | var server = app.listen(config.port, function () { 31 | flint.debug('Flint listening on port %s', config.port); 32 | }); 33 | 34 | // gracefully shutdown (ctrl-c) 35 | process.on('SIGINT', function() { 36 | flint.debug('stoppping...'); 37 | server.close(); 38 | flint.stop().then(function() { 39 | process.exit(); 40 | }); 41 | }); 42 | ``` 43 | 44 | [**Restify Example**](https://github.com/nmarus/flint/blob/master/docs/example2.md) 45 | -------------------------------------------------------------------------------- /docs/example2.md: -------------------------------------------------------------------------------- 1 | ## Example #2 Using Restify 2 | ```js 3 | var Flint = require('node-flint'); 4 | var webhook = require('node-flint/webhook'); 5 | var Restify = require('restify'); 6 | var server = Restify.createServer(); 7 | server.use(Restify.bodyParser()); 8 | 9 | // flint options 10 | var config = { 11 | webhookUrl: 'http://myserver.com/flint', 12 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u', 13 | port: 80 14 | }; 15 | 16 | // init flint 17 | var flint = new Flint(config); 18 | flint.start(); 19 | 20 | // say hello 21 | flint.hears('/hello', function(bot, trigger) { 22 | bot.say('Hello %s!', trigger.personDisplayName); 23 | }); 24 | 25 | // define restify path for incoming webhooks 26 | server.post('/flint', webhook(flint)); 27 | 28 | // start restify server 29 | server.listen(config.port, function () { 30 | flint.debug('Flint listening on port %s', config.port); 31 | }); 32 | 33 | // gracefully shutdown (ctrl-c) 34 | process.on('SIGINT', function() { 35 | flint.debug('stoppping...'); 36 | server.close(); 37 | flint.stop().then(function() { 38 | process.exit(); 39 | }); 40 | }); 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/example3.md: -------------------------------------------------------------------------------- 1 | ## Example #3 Using Socket2me (experimental and under development) 2 | An inbound, internet reachable port, is required for the Spark API to notify 3 | Flint of webhook events. This is not always easy or possible. 4 | 5 | Flint utilize a remote socket client through a 6 | [socket2me](https://github.com/nmarus/socket2me) server in the event you want to 7 | stand up a bot where forwarding a port is not possible. 8 | 9 | The remote socket2me server allows you to run Flint behind a NAT without adding 10 | a port forward configuration to your firewall. To make use of a socket2me 11 | server, you can either stand up your own socket2me server or make use of a 12 | public/shared socket2me server. A single socket2me server can support many 13 | clients/bots simultaneously. 14 | 15 | ```js 16 | var Flint = require('node-flint'); 17 | var webhook = require('node-flint/webhook'); 18 | var Socket2meClient = require('socket2me-client'); 19 | var server = new Socket2meClient('https://socket.bothub.io'); 20 | 21 | // flint options 22 | var config = { 23 | token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u' 24 | }; 25 | 26 | // get a remote webhook from socket2me server 27 | server.on('connected', function(webhookUrl) { 28 | config.webhookUrl = webhookUrl; 29 | 30 | var flint = new Flint(config); 31 | flint.start(); 32 | 33 | // say hello 34 | flint.hears('/hello', function(bot, trigger) { 35 | bot.say('Hello %s!', trigger.personDisplayName); 36 | }); 37 | 38 | server.requestHandler(function(request, respond) { 39 | webhook(flint)(request); 40 | respond(200, 'OK'); 41 | }); 42 | }); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/header.md: -------------------------------------------------------------------------------- 1 | # node-flint (v4) 2 | 3 | ### Bot SDK for Node JS 4 | 5 | ## News 6 | 7 | **03/01/2020 Please consider the Webex Node Bot Framework:** 8 | 9 | * This framework is no longer being actively maintained by the original author. Developers who are familiar with flint may consider trying the [webex-node-bot-framework](https://github.com/webex/webex-bot-node-framework). While not 100% directly compatible with flint, this framework is inspired by flint and should be extremely familiar to developers who already use flint. For more information see the [migration from flint guide](https://github.com/webex/webex-bot-node-framework/blob/master/docs/migrate-from-node-flint.md) 10 | 11 | **10/25/19 Support for Adaptive Cards:** 12 | 13 | * Cisco recently introduced support for [Adaptive Cards](https://developer.webex.com/docs/api/guides/cards/) in the Webex Teams. Bots can send cards, using the new `attachment` attribute of the message object. Cards are useful as an alternative to text messages and files in order to display or collect complex bits of information. Cards can be sent by passing an object to the bot.say() method that includes a valid attachment. To process user input to cards, apps must implement a `flint.on('attachmentaction', ..)` function. For more details see the [adaptive-card-example](./adaptive-card-example.md) 14 | 15 | **6/21/19 Deploying behind a firewall:** 16 | 17 | * Cisco has recently introduced support in the Webex Javascript SDK which allows applications to register to receive the message, membership, and room events via a socket instead of via wehbhoks. This allows applications to be deployed behind firewalls and removes the requirement that webex bots and integrations must expose a public IP address to receive events. To take advantage of this in your flint applications simply remove the `webhookUrl` field from the configuration object passed to the flint constructor. If this field is not set, flint will register to listen for these events instead of creating webhooks. 18 | 19 | **6/21/18 IMPORTANT:** 20 | 21 | * On August 31st, 2018 all bots with the sparkbot.io domain name will be 22 | renamed with a webex.bot domain. Today in flint, the code compares the bot's 23 | email with the trigger email to filter out messages from itself. If this code 24 | is running on August 31st the bot will start responding to its own messages. 25 | Please update to Flint v4.7.x as soon as possible to avoid interruption. 26 | 27 | **3/19/18 IMPORTANT:** 28 | 29 | * Note that Flint v4 is still using the node-sparky library version 3.x. 30 | However the repo for node-sparky is now on version 4 which has some major 31 | differences. This misalignment between Flint and Sparky version 32 | will be fixed with the release of Flint v5. In the 33 | short term if you are accessing the spark object directly from Flint via 34 | `flint.spark` be sure to use the documentation for [node-sparky 3.x](https://github.com/flint-bot/sparky/tree/v3). 35 | 36 | **See [CHANGELOG.md](/CHANGELOG.md) for details on changes to versions of Flint.** 37 | 38 | ## Contents 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | #### Via Git 4 | ```bash 5 | mkdir myproj 6 | cd myproj 7 | git clone https://github.com/nmarus/flint 8 | npm install ./flint 9 | ``` 10 | 11 | #### Via NPM 12 | ```bash 13 | mkdir myproj 14 | cd myproj 15 | npm install node-flint 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016-2017 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Most of Flint's functionality is based around the flint.hears function. This 4 | defines the phrase or pattern the bot is listening for and what actions to take 5 | when that phrase or pattern is matched. The flint.hears function gets a callback 6 | than includes two objects. The bot object, and the trigger object. 7 | 8 | Flint generates a bot object instance of the Bot class for each room the Spark 9 | account Flint is running under. The bot object instance tracks the specifics 10 | about the room it is running in and is passed to the "hears" command callback 11 | when a phrase is heard. 12 | 13 | Flint also generates a trigger object based on the person and room that the 14 | flint.hears function was triggered. 15 | 16 | A simple example of a flint.hears() function setup: 17 | 18 | ```js 19 | flint.hears(phrase, function(bot, trigger) { 20 | bot. 21 | .then(function(returnedValue) { 22 | // do something with returned value 23 | }) 24 | .catch(function(err) { 25 | // handle errors 26 | }); 27 | }); 28 | ``` 29 | 30 | * `phrase` : This can be either a string or a regex pattern. 31 | If a string, the string is matched against the first word in the room message. 32 | message. 33 | If a regex pattern is used, it is matched against the entire message text. 34 | * `bot` : The bot object that is used to execute commands when the `phrase` is 35 | triggered. 36 | * `bot.` : The Bot method to execute. 37 | * `then` : Node JS Promise keyword that invokes additional logic once the 38 | previous command is executed. 39 | * `catch` : handle errors that happen at either the original command or in any 40 | of the chained 'then' functions. 41 | * `trigger` : The object that describes the details around what triggered the 42 | `phrase`. 43 | * `commands` : The commands that are ran when the `phrase` is heard. 44 | 45 | ## Authentication 46 | The token used to authenticate Flint to the Spark API is passed as part of the 47 | options used when instantiating the Flint class. To change or update the 48 | token, use the Flint#setSparkToken() method. 49 | 50 | **Example:** 51 | 52 | ```js 53 | var newToken = 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u'; 54 | 55 | flint.setSparkToken(newToken) 56 | .then(function(token) { 57 | console.log('token updated to: ' + token); 58 | }); 59 | ``` 60 | 61 | ## Storage 62 | The storage system used in flint is a simple key/value store and resolves around 63 | these 3 methods: 64 | 65 | * `bot.store(key, value)` - Store a value to a bot instance where 'key' is a 66 | string and 'value' is a boolean, number, string, array, or object. *This does 67 | not not support functions or any non serializable data.* Returns the a promise 68 | with the value. 69 | * `bot.recall(key)` - Recall a value by 'key' from a bot instance. Returns a 70 | resolved promise with the value or a rejected promise if not found. 71 | * `bot.forget([key])` - Forget (remove) value(s) from a bot instance where 'key' 72 | is an optional property that when defined, removes the specific key, and when 73 | undefined, removes all keys. Returns a resolved promise if deleted or not found. 74 | 75 | When a bot despawns (removed from room), the key/value store for that bot 76 | instance will automatically be removed from the store. Flint currently has an 77 | in-memory store and a Redis based store. By default, the in-memory store is 78 | used. Other backend stores are possible by replicating any one of the built-in 79 | storage modules and passing it to the `flint.storeageDriver()` method. *See 80 | docs for store, recall, forget for more details.* 81 | 82 | **Example:** 83 | 84 | ```js 85 | var redisDriver = require('node-flint/storage/redis'); 86 | flint.storageDriver(redisDriver('redis://localhost')); 87 | ``` 88 | 89 | ## Bot Accounts 90 | 91 | **When using "Bot Accounts" the major differences are:** 92 | 93 | * Webhooks for message:created only trigger when the Bot is mentioned by name 94 | * Unable to read messages in rooms using the Spark API 95 | 96 | **Differences with trigger.args using Flint with a "Bot Account":** 97 | 98 | The trigger.args array is a shortcut in processing the trigger.text string. It 99 | consists of an array of the words that are in the trigger.message string split 100 | by one or more spaces. Punctation is included if there is no space between the 101 | symbol and the word. With bot accounts, this behaves a bit differently. 102 | 103 | * If defining a `flint.hears()` using a string (not regex), `trigger.args` is a 104 | filtered array of words from the message that begins *after* the first match of 105 | bot mention. 106 | 107 | * If defining a flint.hears() using regex, the trigger.args array is the entire 108 | message. 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/flint'); 2 | -------------------------------------------------------------------------------- /lib/bot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | var validator = require('node-sparky/validator'); 5 | var sequence = require('when/sequence'); 6 | var Stream = require("stream"); 7 | var moment = require('moment'); 8 | var _debug = require('debug')('bot'); 9 | var util = require('util'); 10 | var when = require('when'); 11 | var poll = require('when/poll'); 12 | var _ = require('lodash'); 13 | 14 | var u = require('./utils'); 15 | 16 | // format makrdown type 17 | function markdownFormat(str) { 18 | // if string... 19 | if(str && typeof str === 'string') { 20 | 21 | // process characters that do not render visibly in markdown 22 | str = str.replace(/\<(?!@)/g, '<'); 23 | str = str.split('').reverse().join('').replace(/\>(?!.*@\<)/g, ';tg&').split('').reverse().join(''); 24 | 25 | return str; 26 | } 27 | 28 | // else return empty 29 | else { 30 | return ''; 31 | } 32 | } 33 | 34 | // format html type (place holder for now, does nothing) 35 | function htmlFormat(str) { 36 | return str; 37 | } 38 | 39 | /** 40 | * Creates a Bot instance that is then attached to a Spark Room. 41 | * 42 | * @constructor 43 | * @param {Object} flint - The flint object this Bot spawns under. 44 | * @property {string} id - Bot UUID 45 | * @property {boolean} active - Bot active state 46 | * @property {object} person - Bot Person Object 47 | * @property {string} email - Bot email 48 | * @property {object} team - Bot team object 49 | * @property {object} room - Bot room object 50 | * @property {object} membership - Bot membership object 51 | * @property {boolean} isLocked - If bot is locked 52 | * @property {boolean} isModerator - If bot is a moderator 53 | * @property {boolean} isGroup - If bot is in Group Room 54 | * @property {boolean} isDirect - If bot is in 1:1/Direct Room 55 | * @property {string} isDirectTo - Recipient Email if bot is in 1:1/Direct Room 56 | * @property {boolean} isTeam - If bot is in Team Room 57 | * @property {date} lastActivity - Last bot activity 58 | */ 59 | function Bot(flint) { 60 | EventEmitter.call(this); 61 | 62 | this.id = u.genUUID64(); 63 | 64 | this.flint = flint; 65 | this.options = flint.options; 66 | 67 | this.debug = function(message) { 68 | message = util.format.apply(null, Array.prototype.slice.call(arguments)); 69 | 70 | if(typeof flint.debugger === 'function') { 71 | flint.debugger(message, this.id); 72 | } else { 73 | _debug(message); 74 | } 75 | }; 76 | 77 | //randomize distribution of when audit event should take place for this bot instance... 78 | this.auditTrigger = Math.floor((Math.random() * this.flint.auditDelay)) + 1; 79 | 80 | this.spark = this.flint.spark; 81 | this.batchDelay = this.flint.batchDelay; 82 | this.active = false; 83 | this.room = {}; 84 | this.team = {}; 85 | this.person = this.flint.person; 86 | this.membership = {}; 87 | this.memberships = []; 88 | this.email = this.flint.email; 89 | this.isLocked = false; 90 | this.isModerator = false; 91 | this.isGroup = false; 92 | this.isDirect = false; 93 | this.isTeam = false; 94 | this.lastActivity = moment().utc().toDate(); 95 | 96 | this.on('error', err => { 97 | if(err) { 98 | this.debug(err.stack); 99 | } 100 | }); 101 | } 102 | util.inherits(Bot, EventEmitter); 103 | 104 | /** 105 | * Stop Bot. 106 | * 107 | * @function 108 | * @private 109 | * @returns {Boolean} 110 | * 111 | * @example 112 | * bot.stop(); 113 | */ 114 | Bot.prototype.stop = function() { 115 | // if not stopped... 116 | if(this.active) { 117 | 118 | this.emit('stop', this); 119 | 120 | this.active = false; 121 | return true; 122 | } else { 123 | return false; 124 | } 125 | }; 126 | 127 | /** 128 | * Start Bot. 129 | * 130 | * @function 131 | * @private 132 | * @returns {Boolean} 133 | * 134 | * @example 135 | * bot.start(); 136 | */ 137 | Bot.prototype.start = function() { 138 | // if not started... 139 | if(!this.active) { 140 | 141 | this.emit('start', this); 142 | 143 | this.active = true; 144 | return true; 145 | } else { 146 | return false; 147 | } 148 | }; 149 | 150 | /** 151 | * Instructs Bot to exit from room. 152 | * 153 | * @function 154 | * @returns {Promise.} 155 | * 156 | * @example 157 | * bot.exit(); 158 | */ 159 | Bot.prototype.exit = function() { 160 | if(!this.isGroup) { 161 | return when(false); 162 | } else { 163 | 164 | return this.spark.membershipRemove(this.membership.id) 165 | .then(() => { 166 | return when(true); 167 | }) 168 | .catch(() => { 169 | return when(false); 170 | }); 171 | } 172 | }; 173 | 174 | /** 175 | * Instructs Bot to add person(s) to room. 176 | * 177 | * @function 178 | * @param {(String|Array)} email(s) - Email Address (or Array of Email Addresses) of Person(s) to add to room. 179 | * @param {Boolean} [moderator] 180 | * Add as moderator. 181 | * @returns {Promise.} Array of emails added 182 | * @example 183 | * // add one person to room by email 184 | * bot.add('john@test.com'); 185 | * @example 186 | * // add one person as moderator to room by email 187 | * bot.add('john@test.com', true) 188 | * .catch(function(err) { 189 | * // log error if unsuccessful 190 | * console.log(err.message); 191 | * }); 192 | * @example 193 | * // add 3 people to room by email 194 | * bot.add(['john@test.com', 'jane@test.com', 'bill@test.com']); 195 | */ 196 | Bot.prototype.add = function(email, asModerator) { 197 | 198 | // validate to boolean 199 | asModerator = (typeof asModerator === 'boolean' && asModerator); 200 | 201 | // function to add membership by email address to this room 202 | var add = (e, m) => { 203 | if(validator.isEmail(e)) { 204 | return this.spark.membershipAdd(this.room.id, e, m) 205 | .then(membership => { 206 | this.debug('Added "%s" to room "%s"', e, this.room.title); 207 | return when(e); 208 | }) 209 | .catch(err => when(false)) 210 | .delay(this.batchDelay); 211 | } else { 212 | return when(false); 213 | } 214 | }; 215 | 216 | if(!this.isGroup) { 217 | return when.reject(new Error('can not add person to a 1:1 room')); 218 | } else { 219 | if(this.isLocked && !this.isModerator) { 220 | return when.reject(new Error('room is locked and bot is not moderator')); 221 | } 222 | 223 | if(!this.isLocked && asModerator) { 224 | return when.reject(new Error('can not add moderator to a unlocked room')); 225 | } 226 | 227 | // if passed as array, create batch process 228 | if(email instanceof Array && email.length > 1) { 229 | 230 | // create batch 231 | var batch = _.map(email, e => { 232 | e = _.toLower(e); 233 | return () => add(e, asModerator).catch(err => this.debug(err.stack)); 234 | }); 235 | 236 | // run batch 237 | return sequence(batch).then(batchResult => { 238 | batchResult = _.compact(batchResult); 239 | 240 | // if array of resulting emails is not empty... 241 | if(batchResult instanceof Array && batchResult.length > 0) { 242 | return batchResult; 243 | } else { 244 | return when.reject('invalid email(s) or email not specified'); 245 | } 246 | }); 247 | } 248 | 249 | // else, add using email 250 | else if(typeof email === 'string' || (email instanceof Array && email.length === 1)) { 251 | if(email instanceof Array) { 252 | email = _.toLower(email[0]); 253 | } 254 | 255 | return add(email, asModerator).then(result => { 256 | // if resulting email is not false 257 | if(result) { 258 | return when([result]); 259 | } else { 260 | return when.reject('invalid email(s) or email not specified'); 261 | } 262 | }); 263 | } 264 | 265 | else { 266 | return when.reject(new Error('invalid parameter passed to bot.add()')); 267 | } 268 | } 269 | }; 270 | 271 | /** 272 | * Instructs Bot to remove person from room. 273 | * 274 | * @function 275 | * @param {(String|Array)} email(s) - Email Address (or Array of Email Addresses) of Person(s) to remove from room. 276 | * @returns {Promise.} Array of emails removed 277 | * 278 | * @example 279 | * // remove one person to room by email 280 | * bot.remove('john@test.com'); 281 | * 282 | * @example 283 | * // remove 3 people from room by email 284 | * bot.remove(['john@test.com', 'jane@test.com', 'bill@test.com']); 285 | */ 286 | 287 | // needs to be fixed to pass through errors, or pass through list of users removed. 288 | Bot.prototype.remove = function(email) { 289 | 290 | // remove membership by email address from this room 291 | var remove = e => { 292 | if(validator.isEmail(e) && _.includes(_.map(this.memberships, 'personEmail'), e)) { 293 | return this.spark.membershipByRoomByEmail(this.room.id, e) 294 | .then(membership => this.spark.membershipRemove(membership.id)) 295 | .then(() => { 296 | this.debug('Removed "%s" from room "%s"', e, this.room.title); 297 | return when(e); 298 | }) 299 | .catch(err => when(false)) 300 | .delay(this.batchDelay); 301 | } else { 302 | return when(false); 303 | } 304 | }; 305 | 306 | if(!this.isGroup) { 307 | return when.reject(new Error('can not remove person from a 1:1 room')); 308 | } else { 309 | if(this.isLocked && !this.isModerator) { 310 | return when.reject(new Error('room is locked and bot is not moderator')); 311 | } 312 | 313 | // if passed as array, create batch process 314 | if(email instanceof Array && email.length > 1) { 315 | 316 | // create batch 317 | var batch = _.map(email, e => { 318 | return () => remove(e).catch(err => this.debug(err.stack)); 319 | }); 320 | 321 | // run batch 322 | return sequence(batch).then(batchResult => { 323 | batchResult = _.compact(batchResult); 324 | 325 | // if array of resulting emails is not empty... 326 | if(batchResult instanceof Array && batchResult.length > 0) { 327 | return batchResult; 328 | } else { 329 | return when.reject('invalid email(s) or email not specified'); 330 | } 331 | }); 332 | } 333 | 334 | // else, remove using email 335 | else if(typeof email === 'string' || (email instanceof Array && email.length === 1)) { 336 | if(email instanceof Array) { 337 | email = email[0]; 338 | } 339 | 340 | return remove(email).then(result => { 341 | // if resulting email is not false 342 | if(result) { 343 | return when([result]); 344 | } else { 345 | return when.reject('invalid email(s) or email not specified'); 346 | } 347 | }); 348 | } 349 | 350 | else { 351 | return when.reject(new Error('invalid parameter passed to bot.remove()')); 352 | } 353 | } 354 | }; 355 | 356 | /** 357 | * Get membership object from room using email. 358 | * 359 | * @function 360 | * @private 361 | * @param {String} email - Email of person to retrieve membership object of. 362 | * @returns {Promise.} 363 | * 364 | * @example 365 | * bot.getMembership('john@test.com') 366 | * .then(function(membership) { 367 | * console.log('john@test.com is moderator: %s', membership.isModerator); 368 | * }); 369 | */ 370 | Bot.prototype.getMembership = function(email) { 371 | 372 | // check if person passed as email address 373 | if(validator.isEmail(email)) { 374 | 375 | // check for person in room 376 | var person = _.find(this.memberships, membership => { 377 | return (_.toLower(membership.personEmail) === _.toLower(email)); 378 | }); 379 | 380 | if(person) { 381 | return when(person); 382 | } else { 383 | return when.reject(new Error('Person not found in room')); 384 | } 385 | 386 | } else { 387 | return when.reject(new Error('Not a valid email')); 388 | } 389 | }; 390 | 391 | /** 392 | * Get room moderators. 393 | * 394 | * @function 395 | * @returns {Promise.} 396 | * 397 | * @example 398 | * bot.getModerators() 399 | * .then(function(moderators) { 400 | * console.log(moderators); 401 | * }); 402 | */ 403 | Bot.prototype.getModerators = function() { 404 | return when(_.filter(this.memberships, membership => { 405 | return (membership.isModerator); 406 | })); 407 | }; 408 | 409 | /** 410 | * Create new room with people by email 411 | * 412 | * @function 413 | * @param {String} name - Name of room. 414 | * @param {Array} emails - Emails of people to add to room. 415 | * @returns {Promise.} 416 | */ 417 | Bot.prototype.newRoom = function(name, emails) { 418 | var newRoom = {}; 419 | var newRoomBot = {}; 420 | 421 | // add room 422 | return this.spark.roomAdd(name) 423 | 424 | // create room 425 | .then(room => { 426 | 427 | var count = 0; 428 | 429 | // find bot function 430 | var bot = () => { 431 | // get bot for new room 432 | return _.find(this.flint.bots, bot => { 433 | return (bot.room.id === room.id); 434 | }); 435 | }; 436 | 437 | // validate results of find bot function 438 | var isReady = (result) => { 439 | count++; 440 | // cap wait time at 150 * 100 ms 441 | if(count > 150) { 442 | return true; 443 | } else { 444 | return (typeof result !== 'undefined'); 445 | } 446 | }; 447 | 448 | // poll find bot every 100ms and return fulfilled promise when result function is true 449 | return poll(bot, 100, isReady) 450 | .then(bot => { 451 | if(!bot) { 452 | return when.reject(new Error('Flint timed out when creating a new room')); 453 | } else { 454 | newRoomBot = bot; 455 | newRoom = room; 456 | return when(bot); 457 | } 458 | }); 459 | }) 460 | 461 | // add users to room 462 | .then(bot => { 463 | return bot.add(emails) 464 | .catch(() => { 465 | return when(true); 466 | }); 467 | }) 468 | 469 | // return new Bot 470 | .then(() => when(newRoomBot)) 471 | 472 | // if error, attempt to remove room before rejecting 473 | .catch(err => { 474 | 475 | if(newRoom && newRoom.id) { 476 | this.spark.roomRemove(newRoom.id) 477 | .catch(() => {}); 478 | } 479 | 480 | return when.reject(err); 481 | }); 482 | }; 483 | 484 | /** 485 | * Create new Team Room 486 | * 487 | * @function 488 | * @param {String} name - Name of room. 489 | * @param {Array} emails - Emails of people to add to room. 490 | * @returns {Promise.} 491 | */ 492 | Bot.prototype.newTeamRoom = function(name, emails) { 493 | // new room 494 | var newTeamRoom = {}; 495 | var newTeamRoomBot = {}; 496 | 497 | if(this.isTeam) { 498 | var teamId = this.team.id; 499 | } else { 500 | return when.reject(new Error('This room is not part of a spark team')); 501 | } 502 | 503 | // add room 504 | return this.spark.teamRoomAdd(teamId, name) 505 | 506 | // create room 507 | .then(room => { 508 | 509 | var count = 0; 510 | 511 | // find bot function 512 | var bot = () => { 513 | // get bot for new room 514 | return _.find(this.flint.bots, bot => { 515 | return (bot.room.id === room.id); 516 | }); 517 | }; 518 | 519 | // validate results of find bot function 520 | var isReady = (result) => { 521 | count++; 522 | if(count > 150) { 523 | return true; 524 | } else { 525 | return (typeof result !== 'undefined'); 526 | } 527 | }; 528 | 529 | // poll find bot every 100ms and return fulfilled promise when result function is true 530 | return poll(bot, 100, isReady) 531 | .then(bot => { 532 | if(!bot) { 533 | return when.reject(new Error('Flint timed out when creating a new room')); 534 | } else { 535 | newTeamRoomBot = bot; 536 | newTeamRoom = room; 537 | return when(bot); 538 | } 539 | }); 540 | }) 541 | 542 | // add users to room 543 | .then(bot => { 544 | return bot.add(emails) 545 | .catch(() => { 546 | return when(true); 547 | }); 548 | }) 549 | 550 | // return new Bot 551 | .then(() => when(newTeamRoomBot)) 552 | 553 | // if error, attempt to remove room before rejecting 554 | .catch(err => { 555 | 556 | if(newTeamRoom && newTeamRoom.id) { 557 | this.spark.roomRemove(newTeamRoom.id) 558 | .catch(() => { 559 | // ignore remove room errors 560 | }); 561 | } 562 | 563 | return when.reject(err); 564 | }); 565 | }; 566 | 567 | /** 568 | * Enable Room Moderation.Enable. 569 | * 570 | * @function 571 | * @returns {Promise.} 572 | * 573 | * @example 574 | * bot.moderateRoom() 575 | * .then(function(err) { 576 | * console.log(err.message) 577 | * }); 578 | */ 579 | Bot.prototype.moderateRoom = function() { 580 | // validate flint is not a bot account 581 | if(this.flint.isBotAccount) { 582 | return when.reject(new Error('Bot accounts can not change moderation status in rooms')); 583 | } 584 | 585 | // set moderator 586 | if(!this.isGroup || this.isTeam) { 587 | return when.reject(new Error('Can not change moderation status on 1:1 or Team room')); 588 | } 589 | 590 | else if(this.isLocked) { 591 | return when.reject(new Error('Room is already moderated')); 592 | } 593 | 594 | else { 595 | return this.spark.membershipSetModerator(this.membership.id) 596 | .then(() => when(this)); 597 | } 598 | }; 599 | 600 | /** 601 | * Disable Room Moderation. 602 | * 603 | * @function 604 | * @returns {Promise.} 605 | * 606 | * @example 607 | * bot.unmoderateRoom() 608 | * .then(function(err) { 609 | * console.log(err.message) 610 | * }); 611 | */ 612 | Bot.prototype.unmoderateRoom = function() { 613 | 614 | // validate flint is not a bot account 615 | if(this.flint.isBotAccount) { 616 | return when.reject(new Error('Bot accounts can not change moderator status in rooms')); 617 | } 618 | 619 | if(!this.isGroup || this.isTeam) { 620 | return when.reject(new Error('Can not change moderation status on 1:1 or Team room')); 621 | } 622 | 623 | else if(!this.isLocked) { 624 | return when.reject(new Error('Room is not moderated')); 625 | } 626 | 627 | else if(this.isLocked && !this.isModerator) { 628 | return when.reject(new Error('Flint is not a moderator in this room')); 629 | } 630 | 631 | else { 632 | return this.getModerators() 633 | .then(moderators => { 634 | 635 | // create batch 636 | var batch = _.map(moderators, m => { 637 | return () => this.moderatorClear(m.personEmail).delay(this.batchDelay); 638 | }); 639 | 640 | // run batch 641 | return sequence(batch); 642 | 643 | }) 644 | 645 | // remove bot as moderator 646 | .then(() => this.spark.membershipClearModerator(this.membership.id)) 647 | .then(() => when(this)); 648 | } 649 | }; 650 | 651 | /** 652 | * Assign Moderator in Room 653 | * 654 | * @function 655 | * @param {(String|Array)} email(s) - Email Address (or Array of Email Addresses) of Person(s) to assign as moderator. 656 | * @returns {Promise.} 657 | * 658 | * @example 659 | * bot.moderatorSet('john@test.com') 660 | * .then(function(err) { 661 | * console.log(err.message) 662 | * }); 663 | */ 664 | Bot.prototype.moderatorSet = function(email) { 665 | 666 | // function to set moderator by email address to this room 667 | var set = e => { 668 | return this.getMembership(e) 669 | .then(membership => this.spark.membershipSetModerator(membership.id)) 670 | .then(membership => when(this)); 671 | }; 672 | 673 | // validate bot is not a bot account 674 | if(this.flint.isBotAccount) { 675 | return when.reject(new Error('Bot accounts can not change moderator status in rooms')); 676 | } 677 | 678 | if(!this.isGroup || this.isTeam) { 679 | return when.reject(new Error('Can not change moderation status on 1:1 or Team room')); 680 | } 681 | 682 | else if(!this.isLocked) { 683 | return when.reject(new Error('Room is not moderated')); 684 | } 685 | 686 | else if(this.isLocked && !this.isModerator) { 687 | return when.reject(new Error('Flint is not moderator in this room')); 688 | } 689 | 690 | else { 691 | if(email instanceof Array) { 692 | 693 | // create batch 694 | var batch = _.map(email, e => { 695 | return () => set(e).delay(this.batchDelay); 696 | }); 697 | 698 | // run batch 699 | return sequence(batch).then(() => when(this)); 700 | 701 | } 702 | 703 | else if(typeof email === 'string') { 704 | return set(email).then(() => when(this)); 705 | } 706 | 707 | else { 708 | return when.reject(new Error('Invalid parameter passed to moderatorSet')); 709 | } 710 | } 711 | }; 712 | 713 | /** 714 | * Unassign Moderator in Room 715 | * 716 | * @function 717 | * @param {(String|Array)} email(s) - Email Address (or Array of Email Addresses) of Person(s) to unassign as moderator. 718 | * @returns {Promise.} 719 | * 720 | * @example 721 | * bot.moderatorClear('john@test.com') 722 | * .then(function(err) { 723 | * console.log(err.message) 724 | * }); 725 | */ 726 | Bot.prototype.moderatorClear = function(email) { 727 | 728 | // function to set moderator by email address to this room 729 | var clear = e => { 730 | return this.getMembership(e) 731 | .then(membership => this.spark.membershipClearModerator(membership.id)) 732 | .then(membership => when(this)); 733 | }; 734 | 735 | // validate bot is not a bot account 736 | if(this.flint.isBotAccount) { 737 | return when.reject(new Error('Bot accounts can not change moderator status in rooms')); 738 | } 739 | 740 | if(!this.isGroup || this.isTeam) { 741 | return when.reject(new Error('Can not change moderation status on 1:1 or Team room')); 742 | } 743 | 744 | else if(!this.isLocked) { 745 | return when.reject(new Error('Room is not moderated')); 746 | } 747 | 748 | else if(this.isLocked && !this.isModerator) { 749 | return when.reject(new Error('Flint is not a moderator in this room')); 750 | } 751 | 752 | else { 753 | if(email instanceof Array) { 754 | 755 | // create batch 756 | var batch = _.map(email, e => { 757 | return () => clear(e).delay(this.batchDelay); 758 | }); 759 | 760 | // run batch 761 | return sequence(batch).then(() => when(this)); 762 | 763 | } 764 | 765 | else if(typeof email === 'string') { 766 | return clear(email).then(() => when(this)); 767 | } 768 | 769 | else { 770 | return when.reject(new Error('Invalid parameter passed to moderatorClear')); 771 | } 772 | } 773 | }; 774 | 775 | /** 776 | * Remove a room and all memberships. 777 | * 778 | * @function 779 | * @returns {Promise.} 780 | * 781 | * @example 782 | * flint.hears('/implode', function(bot, trigger) { 783 | * bot.implode(); 784 | * }); 785 | */ 786 | Bot.prototype.implode = function() { 787 | 788 | // validate room is group 789 | if(!this.isGroup || this.isTeam) { 790 | return when.reject(new Error('Can not implode a 1:1 or Team room')); 791 | } 792 | 793 | // validate bot is moderator if room is locked 794 | if(this.isLocked && !this.isModerator) { 795 | return when.reject(new Error('Flint is not moderator in this room')); 796 | } 797 | 798 | return this.spark.roomRemove(this.room.id) 799 | .then(() => when(true)) 800 | .catch(() => when(false)); 801 | }; 802 | 803 | /** 804 | * Send text with optional file to room. 805 | * 806 | * @function 807 | * @param {String} [format=text] - Set message format. Valid options are 'text' or 'markdown'. 808 | * @param {String|Object} message - Message to send to room. This can be a simple string, or a object for advanced use. 809 | * @returns {Promise.} 810 | * 811 | * @example 812 | * // Simple example 813 | * flint.hears('/hello', function(bot, trigger) { 814 | * bot.say('hello'); 815 | * }); 816 | * 817 | * @example 818 | * // Simple example to send message and file 819 | * flint.hears('/file', function(bot, trigger) { 820 | * bot.say({text: 'Here is your file!', file: 'http://myurl/file.doc'}); 821 | * }); 822 | * 823 | * @example 824 | * // Markdown Method 1 - Define markdown as default 825 | * flint.messageFormat = 'markdown'; 826 | * flint.hears('/hello', function(bot, trigger) { 827 | * bot.say('**hello**, How are you today?'); 828 | * }); 829 | * 830 | * @example 831 | * // Markdown Method 2 - Define message format as part of argument string 832 | * flint.hears('/hello', function(bot, trigger) { 833 | * bot.say('markdown', '**hello**, How are you today?'); 834 | * }); 835 | * 836 | * @example 837 | * // Mardown Method 3 - Use an object (use this method of bot.say() when needing to send a file in the same message as markdown text. 838 | * flint.hears('/hello', function(bot, trigger) { 839 | * bot.say({markdown: '*Hello <@personEmail:' + trigger.personEmail + '|' + trigger.personDisplayName + '>*'}); 840 | * }); 841 | */ 842 | Bot.prototype.say = function(format, message) { 843 | 844 | // set default format type 845 | format = this.flint.messageFormat; 846 | 847 | // parse function args 848 | var args = Array.prototype.slice.call(arguments); 849 | 850 | // determine if a format is defined in arguments 851 | // first and second arguments should be string type 852 | // first argument should be one of the valid formats 853 | var formatDefined = (args.length > 1 && typeof args[0] === 'string' && typeof args[1] === 'string' && _.includes(['text', 'markdown', 'html'], _.toLower(args[0]))); 854 | 855 | // if format defined in function arguments, overide default 856 | if(formatDefined) { 857 | format = _.toLower(args.shift()); 858 | } 859 | 860 | // if message is object (raw) 861 | if(typeof args[0] === 'object') { 862 | return this.spark.messageSendRoom(this.room.id, args[0]); 863 | } 864 | 865 | // if message is string 866 | else if(typeof args[0] === 'string') { 867 | // apply string formatters to remaining arguments 868 | message = util.format.apply(null, args); 869 | 870 | // if markdown, apply markdown formatter to contructed message string 871 | message = format === 'markdown' ? markdownFormat(message) : message; 872 | 873 | // if html, apply html formatter to contructed message string 874 | message = format === 'html' ? htmlFormat(message) : message; 875 | 876 | // construct message object 877 | var messageObj = {}; 878 | messageObj[format] = message; 879 | 880 | // send constructed message object to room 881 | return this.spark.messageSendRoom(this.room.id, messageObj); 882 | } 883 | 884 | else { 885 | return when.reject(new Error('Invalid function arguments')); 886 | } 887 | }; 888 | 889 | 890 | /** 891 | * Send text with optional file in a direct message. This sends a message to a 1:1 room with the user (creates 1:1, if one does not already exist) 892 | * 893 | * @function 894 | * @param {String} email - Email of person to send Direct Message. 895 | * @param {String} [format=text] - Set message format. Valid options are 'text' or 'markdown'. 896 | * @param {String|Object} message - Message to send to room. This can be a simple string, or a object for advanced use. 897 | * @returns {Promise.} 898 | * 899 | * @example 900 | * // Simple example 901 | * flint.hears('/dm', function(bot, trigger) { 902 | * bot.dm('someone@domain.com', 'hello'); 903 | * }); 904 | * 905 | * @example 906 | * // Simple example to send message and file 907 | * flint.hears('/dm', function(bot, trigger) { 908 | * bot.dm('someone@domain.com', {text: 'Here is your file!', file: 'http://myurl/file.doc'}); 909 | * }); 910 | * 911 | * @example 912 | * // Markdown Method 1 - Define markdown as default 913 | * flint.messageFormat = 'markdown'; 914 | * flint.hears('/dm', function(bot, trigger) { 915 | * bot.dm('someone@domain.com', '**hello**, How are you today?'); 916 | * }); 917 | * 918 | * @example 919 | * // Markdown Method 2 - Define message format as part of argument string 920 | * flint.hears('/dm', function(bot, trigger) { 921 | * bot.dm('someone@domain.com', 'markdown', '**hello**, How are you today?'); 922 | * }); 923 | * 924 | * @example 925 | * // Mardown Method 3 - Use an object (use this method of bot.dm() when needing to send a file in the same message as markdown text. 926 | * flint.hears('/dm', function(bot, trigger) { 927 | * bot.dm('someone@domain.com', {markdown: '*Hello <@personEmail:' + trigger.personEmail + '|' + trigger.personDisplayName + '>*'}); 928 | * }); 929 | */ 930 | Bot.prototype.dm = function(email, format, message) { 931 | // parse function args 932 | var args = Array.prototype.slice.call(arguments); 933 | 934 | message = args.length > 0 ? args.pop() : false; 935 | email = args.length > 0 ? args.shift() : false; 936 | format = args.length > 0 && _.includes(['markdown', 'html', 'text'], format) ? args.shift() : this.flint.messageFormat || 'text'; 937 | 938 | if(email && validator.isEmail(email) && (typeof message === 'string' || typeof message === 'object')) { 939 | 940 | if(typeof message === 'object') { 941 | return this.spark.messageSendPerson(email, message); 942 | } 943 | 944 | if(typeof message === 'string') { 945 | var msgObj = {}; 946 | 947 | // if markdown, apply markdown formatter to contructed message string 948 | message = format === 'markdown' ? markdownFormat(message) : message; 949 | 950 | // if html, apply html formatter to contructed message string 951 | message = format === 'html' ? htmlFormat(message) : message; 952 | 953 | msgObj[format] = message; 954 | return this.spark.messageSendPerson(email, msgObj); 955 | } 956 | } 957 | 958 | else { 959 | return when.reject(new Error('Invalid function arguments')); 960 | } 961 | }; 962 | 963 | /** 964 | * Upload a file to a room using a Readable Stream 965 | * 966 | * @function 967 | * @param {String} filename - File name used when uploading to room 968 | * @param {Stream.Readable} stream - Stream Readable 969 | * @returns {Promise.} 970 | * 971 | * @example 972 | * flint.hears('/file', function(bot, trigger) { 973 | * 974 | * // define filename used when uploading to room 975 | * var filename = 'test.png'; 976 | * 977 | * // create readable stream 978 | * var stream = fs.createReadStream('/my/file/test.png'); 979 | * 980 | * bot.uploadStream(filename, stream); 981 | * }); 982 | */ 983 | Bot.prototype.uploadStream = function(filename, stream) { 984 | if(typeof filename === 'string' && stream instanceof Stream) { 985 | return this.spark.messageStreamRoom(this.room.id, { filename: filename, stream: stream }); 986 | } else { 987 | return when.reject(new Error('Invalid stream')); 988 | } 989 | }; 990 | 991 | /** 992 | * Upload a file to room. 993 | * 994 | * @function 995 | * @param {String} filepath - File Path to upload 996 | * @returns {Promise.} 997 | * 998 | * @example 999 | * flint.hears('/file', function(bot, trigger) { 1000 | * bot.upload('test.png'); 1001 | * }); 1002 | */ 1003 | Bot.prototype.upload = function(filepath) { 1004 | if(typeof filepath === 'string') { 1005 | return this.spark.upload(this.room.id, filepath); 1006 | } else { 1007 | return when.reject(new Error('Invalid file')); 1008 | } 1009 | }; 1010 | 1011 | /** 1012 | * Remove Message By Id. 1013 | * 1014 | * @function 1015 | * @param {String} messageId 1016 | * @returns {Promise.} 1017 | */ 1018 | Bot.prototype.censor = function(messageId) { 1019 | return this.flint.getMessage(messageId) 1020 | .then(message => { 1021 | 1022 | // if bot can delete a message... 1023 | if((this.isLocked && this.isModerator && !this.flint.isBotAccount) || message.personId === this.person.id) { 1024 | return this.spark.messageRemove(messageId); 1025 | } 1026 | 1027 | else { 1028 | return when.reject(new Error('Can not remove this message')); 1029 | } 1030 | }); 1031 | }; 1032 | 1033 | /** 1034 | * Set Title of Room. 1035 | * 1036 | * @function 1037 | * @param {String} title 1038 | * @returns {Promise.} 1039 | * 1040 | * @example 1041 | * bot.roomRename('My Renamed Room') 1042 | * .then(function(err) { 1043 | * console.log(err.message) 1044 | * }); 1045 | */ 1046 | Bot.prototype.roomRename = function(title) { 1047 | if(!this.isGroup) { 1048 | return when.reject(new Error('Can not set title of 1:1 room')); 1049 | } 1050 | 1051 | else if(this.isLocked && !this.isModerator) { 1052 | return when.reject(new Error('Flint is not moderator in this room')); 1053 | } 1054 | 1055 | else { 1056 | return this.spark.roomRename(this.room.id, title); 1057 | } 1058 | }; 1059 | 1060 | /** 1061 | * Get messages from room. Returned data has newest message at bottom. 1062 | * 1063 | * @function 1064 | * @param {Integer} count 1065 | * @returns {Promise.} 1066 | * 1067 | * @example 1068 | * bot.getMessages(5).then(function(messages) { 1069 | * messages.forEach(function(message) { 1070 | * // display message text 1071 | * if(message.text) { 1072 | * console.log(message.text); 1073 | * } 1074 | * }); 1075 | * }); 1076 | */ 1077 | Bot.prototype.getMessages = function(count) { 1078 | if(this.flint.isBotAccount) { 1079 | return when.reject(new Error('Bot accounts can not read room messages')); 1080 | } else { 1081 | count = typeof count !== 'number' && parseInt(count, 10) ? parseInt(count, 10) : count; 1082 | return this.spark.messagesGet(this.room.id, count) 1083 | .then(messages => when.map(_.reverse(messages), message => this.flint.parseMessage(message))); 1084 | } 1085 | 1086 | }; 1087 | 1088 | /** 1089 | * Store key/value data. 1090 | * 1091 | * @function 1092 | * @param {String} key - Key under id object 1093 | * @param {(String|Number|Boolean|Array|Object)} value - Value of key 1094 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 1095 | */ 1096 | Bot.prototype.store = null; 1097 | 1098 | /** 1099 | * Recall value of data stored by 'key'. 1100 | * 1101 | * @function 1102 | * @param {String} [key] - Key under id object (optional). If key is not passed, all keys for id are returned as an object. 1103 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 1104 | */ 1105 | Bot.prototype.recall = null; 1106 | 1107 | /** 1108 | * Forget a key or entire store. 1109 | * 1110 | * @function 1111 | * @param {String} [key] - Key under id object (optional). If key is not passed, id and all children are removed. 1112 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 1113 | */ 1114 | Bot.prototype.forget = null; 1115 | 1116 | module.exports = Bot; 1117 | -------------------------------------------------------------------------------- /lib/flint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | EventEmitter.prototype._maxListeners = 0; 5 | var sequence = require('when/sequence'); 6 | var moment = require('moment'); 7 | var Spark = require('node-sparky'); 8 | var _debug = require('debug')('flint'); 9 | var util = require('util'); 10 | var when = require('when'); 11 | var path = require('path'); 12 | var _ = require('lodash'); 13 | 14 | var MemStore = require('../storage/memory'); 15 | 16 | var Bot = require('./bot'); 17 | var u = require('./utils'); 18 | 19 | /** 20 | * Creates an instance of Flint. 21 | * 22 | * @constructor Flint 23 | * @param {Object} options - Configuration object containing Flint settings. 24 | * @property {string} id - Flint UUID 25 | * @property {boolean} active - Flint active state 26 | * @property {boolean} intialized - Flint fully initialized 27 | * @property {boolean} isBotAccount - Is Flint attached to Spark using a bot account? 28 | * @property {boolean} isUserAccount - Is Flint attached to Spark using a user account? 29 | * @property {object} person - Flint person object 30 | * @property {string} email - Flint email 31 | * @property {object} spark - The Spark instance used by flint 32 | * 33 | * @example 34 | * var options = { 35 | * webhookUrl: 'http://myserver.com/flint', 36 | * token: 'Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u' 37 | * }; 38 | * var flint = new Flint(options); 39 | */ 40 | function Flint(options) { 41 | EventEmitter.call(this); 42 | 43 | this.id = options.id || u.genUUID64(); 44 | 45 | /** 46 | * Options Object 47 | * 48 | * @memberof Flint 49 | * @instance 50 | * @namespace options 51 | * @property {string} token - Spark Token. 52 | * @property {string} webhookUrl - URL that is used for SPark API to send callbacks. 53 | * @property {string} [webhookSecret] - If specified, inbound webhooks are authorized before being processed. 54 | * @property {string} [messageFormat=text] - Default Spark message format to use with bot.say(). 55 | * @property {number} [maxPageItems=50] - Max results that the paginator uses. 56 | * @property {number} [maxConcurrent=3] - Max concurrent sessions to the Spark API 57 | * @property {number} [minTime=600] - Min time between consecutive request starts. 58 | * @property {number} [requeueMinTime=minTime*10] - Min time between consecutive request starts of requests that have been re-queued. 59 | * @property {number} [requeueMaxRetry=3] - Msx number of atteempts to make for failed request. 60 | * @property {array} [requeueCodes=[429,500,503]] - Array of http result codes that should be retried. 61 | * @property {number} [requestTimeout=20000] - Timeout for an individual request recieving a response. 62 | * @property {number} [queueSize=10000] - Size of the buffer that holds outbound requests. 63 | * @property {number} [requeueSize=10000] - Size of the buffer that holds outbound re-queue requests. 64 | * @property {string} [id=random] - The id this instance of flint uses. 65 | * @property {string} [webhookRequestJSONLocation=body] - The property under the Request to find the JSON contents. 66 | * @property {Boolean} [removeWebhooksOnStart=true] - If you wish to have the bot remove all account webhooks when starting. 67 | */ 68 | this.options = options; 69 | 70 | this.active = false; 71 | this.initialized = false; 72 | this.storageActive = false; 73 | this.isBotAccount = false; 74 | this.isUserAccount = false; 75 | this.person = {}; 76 | this.email; 77 | 78 | // define location in webhook request to find json values of incoming webhook. 79 | // note: this is typically 'request.body' but depending on express/restify configuration, it may be 'request.params' 80 | this.options.webhookRequestJSONLocation = this.options.webhookRequestJSONLocation || 'body'; 81 | 82 | // define if flint remove all webhooks attached to token on start (if not defined, defaults to true) 83 | this.options.removeWebhooksOnStart = typeof this.options.removeWebhooksOnStart === 'boolean' ? this.options.removeWebhooksOnStart : true; 84 | 85 | // define default messageFormat used with bot.say (if not defined, defaults to 'text') 86 | if(typeof this.options.messageFormat === 'string' && _.includes(['text', 'markdown', 'html'], _.toLower(this.options.messageFormat))) { 87 | this.messageFormat = _.toLower(this.options.messageFormat); 88 | } else { 89 | this.messageFormat = 'text'; 90 | } 91 | 92 | this.batchDelay = options.minTime * 2; 93 | this.auditInterval; 94 | this.auditDelay = 300; 95 | this.auditCounter = 0; 96 | this.logs = []; 97 | this.logMax = 1000; 98 | this.lexicon = []; 99 | this.bots = []; 100 | this.spark = {}; 101 | this.webhook = {}; 102 | this.cardsWebhook = {}; 103 | 104 | // register internal events 105 | this.on('error', err => { 106 | if(err) { 107 | console.err(err.stack); 108 | } 109 | }); 110 | this.on('start', () => { 111 | require('./logs')(this); 112 | this.initialize(); 113 | }); 114 | } 115 | util.inherits(Flint, EventEmitter); 116 | 117 | /** 118 | * Internal logger function. 119 | * 120 | * @function 121 | * @memberof Flint 122 | * @private 123 | * @param {String} message - Message to log 124 | * @returns {string} Formatted message 125 | */ 126 | Flint.prototype.log = function(message) { 127 | if(this.log.length > this.logMax) { 128 | this.log = this.log.slice(this.log.length - this.logMax); 129 | } 130 | message = (moment().utc().format('YYYY-MM-DD HH:mm:ss') + ' ' + message); 131 | this.logs.push(message); 132 | 133 | /** 134 | * Flint log event. 135 | * 136 | * @event log 137 | * @property {string} message - Log Message 138 | */ 139 | this.emit('log', message); 140 | return message; 141 | }; 142 | 143 | /** 144 | * Internal debug function. 145 | * 146 | * @function 147 | * @memberof Flint 148 | * @private 149 | * @param {String} message - Message to debug 150 | * @returns {null} 151 | */ 152 | Flint.prototype.debug = function(message) { 153 | message = util.format.apply(null, Array.prototype.slice.call(arguments)); 154 | 155 | if(typeof this.debugger === 'function') { 156 | this.debugger(message, this.id); 157 | } else { 158 | _debug(message); 159 | } 160 | }; 161 | 162 | /** 163 | * Tests, and then sets a new Spark Token. 164 | * 165 | * @function 166 | * @memberof Flint 167 | * @param {String} token - New Spark Token for Flint to use. 168 | * @returns {Promise.} 169 | * 170 | * @example 171 | * flint.setSparkToken('Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u') 172 | * .then(function(token) { 173 | * console.log('token updated to: ' + token); 174 | * }); 175 | */ 176 | Flint.prototype.setSparkToken = function(token) { 177 | return this.testSparkToken(token) 178 | .then(token => { 179 | this.options.token = token; 180 | return when(token); 181 | }) 182 | .catch(() => { 183 | when.reject(new Error('could not change token, token not valid')); 184 | }); 185 | }; 186 | 187 | /** 188 | * Test a new Spark Token. 189 | * 190 | * @function 191 | * @memberof Flint 192 | * @private 193 | * @param {String} token - Test if Token is valid by attempting a simple Spark API Call. 194 | * @returns {Promise.} 195 | * 196 | * @example 197 | * flint.testSparkToken('Tm90aGluZyB0byBzZWUgaGVyZS4uLiBNb3ZlIGFsb25nLi4u') 198 | * .then(function() { 199 | * console.log('token valid'); 200 | * }) 201 | * .catch(function(err) { 202 | * console.log(err.message); 203 | * }); 204 | */ 205 | Flint.prototype.testSparkToken = function(token) { 206 | var testOptions = _.clone(this.options); 207 | testOptions.token = token; 208 | var testSpark = new Spark(testOptions); 209 | 210 | return testSpark.membershipsGet() 211 | .then(memberships => { 212 | testSpark = {}; 213 | return when(token); 214 | }) 215 | .catch(() =>{ 216 | return when.reject(new Error('token not valid')); 217 | }); 218 | }; 219 | 220 | /** 221 | * Stop Flint. 222 | * 223 | * @function 224 | * @memberof Flint 225 | * @returns {Promise.} 226 | * 227 | * @example 228 | * flint.stop(); 229 | */ 230 | Flint.prototype.stop = function() { 231 | 232 | // if not stopped... 233 | if(this.active) { 234 | 235 | return cleanupListeners(this) 236 | .then(() => { 237 | if(this.auditInterval) clearInterval(this.auditInterval); 238 | 239 | /** 240 | * Flint stop event. 241 | * 242 | * @event stop 243 | * @property {string} id - Flint UUID 244 | */ 245 | this.emit('stop', this.id); 246 | 247 | return when.map(this.bots, bot => { 248 | bot.stop(); 249 | return when(true); 250 | }); 251 | }) 252 | 253 | .then(() => { 254 | this.bots = []; 255 | this.spark = {}; 256 | this.webhook = {}; 257 | this.cardsWebhook = {}; 258 | this.active = false; 259 | this.initialized = false; 260 | return when(true); 261 | }); 262 | 263 | } else { 264 | return when(false); 265 | } 266 | }; 267 | 268 | /** 269 | * Start Flint. 270 | * 271 | * @function 272 | * @memberof Flint 273 | * @returns {Promise.} 274 | * 275 | * @example 276 | * flint.start(); 277 | */ 278 | Flint.prototype.start = function() { 279 | 280 | // if not started... 281 | if(!this.active) { 282 | 283 | // init storage default storage driver if start is called before 284 | if(!this.storageActive) { 285 | // define default storage module 286 | this.storageDriver(new MemStore()); 287 | } 288 | 289 | // init spark 290 | this.spark = new Spark(this.options); 291 | 292 | // determine bot identity 293 | return this.spark.personMe() 294 | .then(person => this.getPerson(person.emails[0])) 295 | 296 | // get updqated person object 297 | .then(person => { 298 | this.person = person; 299 | this.email = person.email; 300 | 301 | // check if account is bot or user account 302 | if(this.person.domain === 'sparkbot.io' || this.person.domain === 'webex.bot') { 303 | this.isBotAccount = true; 304 | this.isUserAccount = false; 305 | } else { 306 | this.isBotAccount = false; 307 | this.isUserAccount = true; 308 | } 309 | 310 | return when(person); 311 | }) 312 | 313 | // Configure webhooks or websockets 314 | .then(() => { 315 | if (this.options.webhookUrl) { 316 | // get webhooks 317 | this.getWebhooks() 318 | 319 | // process webhooks 320 | .then(webhooks => { 321 | 322 | // remove only webhooks this app created 323 | if(!this.options.removeWebhooksOnStart) { 324 | 325 | var webhooksToRemove = _.filter(webhooks, webhook => { 326 | return (webhook.name == u.base64encode(this.options.webhookUrl.split('/')[2] + ' ' + this.email)); 327 | }); 328 | 329 | if(webhooksToRemove instanceof Array && webhooksToRemove.length > 0) { 330 | return when.map(webhooksToRemove, webhook => this.spark.webhookRemove(webhook.id)); 331 | } else { 332 | return when(true); 333 | } 334 | } 335 | 336 | // else, remove all webhooks on start 337 | else { 338 | return when.map(webhooks, webhook => this.spark.webhookRemove(webhook.id)); 339 | } 340 | }) 341 | 342 | .then(() => { 343 | if(this.options.webhookUrl) { 344 | this.spark.webhookAdd('all', 'all', u.base64encode(this.options.webhookUrl.split('/')[2] + ' ' + this.email)) 345 | .then(webhook => { 346 | this.webhook = webhook; 347 | return this.spark.webhookAdd('attachmentActions', 'created', u.base64encode(this.options.webhookUrl.split('/')[2] + ' ' + this.email)) 348 | .then(webhook => { 349 | this.cardsWebhook = webhook; 350 | return when(webhook); 351 | }) 352 | .catch(() => { 353 | this.webhook = false; 354 | return when(false); 355 | }); 356 | }) 357 | .catch(() => { 358 | this.webhook = false; 359 | return when(false); 360 | }); 361 | } else { 362 | this.webhook = false; 363 | return when(false); 364 | } 365 | }); 366 | } else { 367 | // There was no webhookUrl specified so we will use websockets instead 368 | var webhook = require('./webhook'); 369 | let Websocket = require('./websocket'); 370 | this.websocket = new Websocket(this, webhook); 371 | return this.websocket.init(); 372 | } 373 | }) 374 | 375 | // start 376 | .then(() => { 377 | /** 378 | * Flint start event. 379 | * 380 | * @event start 381 | * @property {string} id - Flint UUID 382 | */ 383 | this.emit('start', this.id); 384 | this.active = true; 385 | return when(true); 386 | }) 387 | 388 | // setup auditor 389 | .then(() => { 390 | this.auditInterval = setInterval(() => { 391 | this.auditBots(); 392 | }, 1000); 393 | return when(true); 394 | }) 395 | 396 | // handle errors 397 | .catch(err => { 398 | throw err; 399 | }); 400 | } else { 401 | return when(false); 402 | } 403 | }; 404 | 405 | /** 406 | * Initialize Flint. 407 | * 408 | * @function 409 | * @memberof Flint 410 | * @private 411 | * @returns {Promise.} 412 | * 413 | * @example 414 | * flint.initialize(); 415 | */ 416 | Flint.prototype.initialize = function() { 417 | // spawn bots in existing rooms at startup 418 | return this.spark.membershipsGet() 419 | .then(memberships => { 420 | 421 | // create batch 422 | var batch = _.map(memberships, m => { 423 | return () => this.spawn(m.roomId); 424 | }); 425 | 426 | // run batch 427 | return sequence(batch) 428 | .then(() => when(true)) 429 | .catch(err => { 430 | this.debug(err.stack); 431 | return when(true); 432 | }); 433 | }) 434 | 435 | .then(() => { 436 | /** 437 | * Flint initialized event. 438 | * 439 | * @event initialized 440 | * @property {string} id - Flint UUID 441 | */ 442 | this.emit('initialized', this.id); 443 | this.initialized = true; 444 | return when(true); 445 | }); 446 | 447 | }; 448 | 449 | /** 450 | * Restart Flint. 451 | * 452 | * @function 453 | * @memberof Flint 454 | * @returns {Promise.} 455 | * 456 | * @example 457 | * flint.restart(); 458 | */ 459 | Flint.prototype.restart = function() { 460 | return this.stop() 461 | .then(stopped => { 462 | if(stopped) { 463 | return this.start(); 464 | } else { 465 | return when(false); 466 | } 467 | }); 468 | }; 469 | 470 | /** 471 | * Audit bot objects to verify they are in sync with the Spark API. 472 | * 473 | * @function 474 | * @memberof Flint 475 | * @private 476 | * @returns {Promise.} 477 | * 478 | * @example 479 | * flint.auditBots(); 480 | */ 481 | Flint.prototype.auditBots = function() { 482 | // only run if Flint has initialized 483 | if(!this.initialized) { 484 | return when(true); 485 | } 486 | 487 | // increment counter 488 | this.auditCounter++; 489 | 490 | // reset counter when counter exceeds max 491 | if(this.auditCounter > this.auditDelay) { 492 | this.auditCounter = 0; 493 | } 494 | 495 | // update flint.person 496 | if(this.auditCounter === 0) { 497 | this.getPerson(this.person.email) 498 | .then(person => { 499 | this.person = person; 500 | }) 501 | .catch(err => this.debug(err.stack)); 502 | } 503 | 504 | // remove duplicate bots 505 | if(this.auditCounter % 5 === 0) { 506 | var uniqBots = _.uniqBy(this.bots, bot => bot.room.id); 507 | var botsToRemove = _.differenceBy(this.bots, uniqBots, 'id'); 508 | _.forEach(botsToRemove, bot => this.despawn(bot.room.id).catch(() => true)); 509 | } 510 | 511 | // check for zombies 512 | if(this.auditCounter === (this.auditDelay - 1)) { 513 | this.getRooms() 514 | .then(rooms => { 515 | var roomsToAdd = _.differenceBy(rooms, _.map(this.bots, bot => bot.room), 'id'); 516 | _.forEach(roomsToAdd, room => this.spawn(room.id)); 517 | }) 518 | .catch(() => { 519 | return when(true); 520 | }); 521 | } 522 | 523 | // exit rooms where bot is only member 524 | if(this.auditCounter === (this.auditDelay - 1)) { 525 | _.forEach(this.bots, bot => { 526 | if(bot.memberships.length === 0 && bot.isGroup && !bot.isTeam) { 527 | bot.exit(); 528 | } 529 | }); 530 | } 531 | 532 | return when.map(this.bots, bot => { 533 | // if auditDelay < bot auditTrigger, reset bot audit trigger 534 | if(this.auditDelay <= bot.auditTrigger) { 535 | bot.auditTrigger = Math.floor((Math.random() * this.auditDelay)) + 1; 536 | } 537 | 538 | // if bot.auditTrigger matches current count inside auditDelay range 539 | if(this.initialized && bot.auditTrigger === this.auditCounter) { 540 | 541 | // room 542 | var room = () => this.getRoom(bot.room.id) 543 | .then(room => { 544 | // Fix the occassional old room with missing title 545 | if(typeof room.title === 'undefined' || room.title.trim() === '') { 546 | room.title = 'Default title'; 547 | } 548 | return this.onRoomUpdated(room); 549 | }) 550 | .catch(err => { 551 | this.debug(err.stack); 552 | return when(true); 553 | }); 554 | 555 | // membership 556 | var membership = () => this.getMembership(bot.membership.id) 557 | .then(membership => this.onMembershipUpdated(membership)) 558 | .catch(err => { 559 | this.debug(err.stack); 560 | return when(true); 561 | }); 562 | 563 | // memberships 564 | var memberships = () => this.getMemberships(bot.room.id) 565 | .then(memberships => when.map(memberships, membership => this.onMembershipUpdated(membership))) 566 | 567 | .catch(err => { 568 | this.debug(err.stack); 569 | return when(true); 570 | }); 571 | 572 | return sequence([room, membership, memberships]); 573 | } 574 | 575 | else { 576 | return when(true); 577 | } 578 | }); 579 | }; 580 | 581 | /** 582 | * Parse a message object. 583 | * 584 | * @function 585 | * @memberof Flint 586 | * @private 587 | * @param {Object} message - Message Object 588 | * @returns {Promise.} 589 | */ 590 | Flint.prototype.parseMessage = function(message) { 591 | 592 | /** 593 | * Message Object 594 | * 595 | * @namespace Message 596 | * @property {string} id - Message ID 597 | * @property {string} personId - Person ID 598 | * @property {string} personEmail - Person Email 599 | * @property {string} personAvatar - PersonAvatar URL 600 | * @property {string} personDomain - Person Domain Name 601 | * @property {string} personDisplayName - Person Display Name 602 | * @property {string} roomId - Room ID 603 | * @property {string} text - Message text 604 | * @property {array} files - Array of File objects 605 | * @property {date} created - Date Message created 606 | */ 607 | 608 | message.created = moment(message.created).utc().toDate(); 609 | message.personEmail = _.toLower(message.personEmail); 610 | 611 | // parse message text 612 | if(message.text) { 613 | 614 | // capture raw message 615 | message.raw = message.text; 616 | 617 | // trim leading whitespace 618 | message.text = message.text.trim(); 619 | 620 | // replace carriage returns / new lines with a space 621 | message.text = message.text.replace(/[\n\r]+/g, ' '); 622 | 623 | // remove all consecutive white space characters 624 | message.text = message.text.replace(/\s\s+/g, ' '); 625 | } 626 | 627 | return when(true) 628 | .then(() => { 629 | return this.getPerson(message.personEmail) 630 | .then(person => { 631 | message.personDisplayName = person.displayName; 632 | message.personDomain = person.domain; 633 | message.personAvatar = person.avatar || false; 634 | return when(message); 635 | }) 636 | .catch(() => { 637 | message.personDisplayName = message.personEmail; 638 | message.personDomain = 'unknown'; 639 | return when(message); 640 | }); 641 | }) 642 | .catch(() => { 643 | return when(message); 644 | }); 645 | }; 646 | 647 | /** 648 | * Parse a File from Message. 649 | * 650 | * @function 651 | * @memberof Flint 652 | * @private 653 | * @param {Object} message - Previously parsed Message Object 654 | * @returns {Promise.} 655 | */ 656 | Flint.prototype.parseFile = function(message) { 657 | 658 | /** 659 | * File Object 660 | * 661 | * @namespace File 662 | * @property {string} id - Spark API Content ID 663 | * @property {string} name - File name 664 | * @property {string} ext - File extension 665 | * @property {string} type - Header [content-type] for file 666 | * @property {buffer} binary - File contents as binary 667 | * @property {string} base64 - File contents as base64 encoded string 668 | * @property {string} personId - Person ID of who added file 669 | * @property {string} personEmail - Person Email of who added file 670 | * @property {string} personAvatar - PersonAvatar URL 671 | * @property {string} personDomain - Person Domain Name 672 | * @property {string} personDisplayName - Person Display Name 673 | * @property {date} created - Date file was added to room 674 | */ 675 | 676 | // parse message files 677 | if(message.files && message.files instanceof Array) { 678 | var parsedMessage = _.clone(message); 679 | 680 | return when.map(parsedMessage.files, url => this.spark.contentByUrl(url)) 681 | .then(files => { 682 | _.forEach(files, file => { 683 | file.personId = parsedMessage.personId; 684 | file.personEmail = parsedMessage.personEmail; 685 | file.personDisplayName = parsedMessage.personDisplayName; 686 | file.personAvatar = parsedMessage.personAvatar; 687 | file.personDomain = parsedMessage.personDomain; 688 | file.created = parsedMessage.created; 689 | }); 690 | parsedMessage.files = files; 691 | return when(parsedMessage); 692 | }) 693 | .catch(() => { 694 | return when(message); 695 | }); 696 | } else { 697 | return when(message); 698 | } 699 | }; 700 | 701 | /** 702 | * Creates Trigger Object from messageId. 703 | * 704 | * @function 705 | * @memberof Flint 706 | * @private 707 | * @param {Webhook} messageData - Webhook object from message created webhook 708 | * @returns {Promise.} 709 | */ 710 | Flint.prototype.getTrigger = function(messageId) { 711 | 712 | /** 713 | * Trigger Object 714 | * 715 | * @namespace Trigger 716 | * @property {string} id - Message ID 717 | * @property {(string|regex)} phrase - Matched lexicon phrase 718 | * @property {string} text - Message Text (or false if no text) 719 | * @property {string} raw - Unprocessed Message Text (or false if no text) 720 | * @property {string} html - Message HTML (or false if no html) 721 | * @property {string} markdown - Message Markdown (or false if no markdown) 722 | * @property {array} mentionedPeople - Mentioned People (or false if no mentioned) 723 | * @property {array} files - Message Files (or false if no files in trigger) 724 | * @property {array} args - Filtered array of words in message text. 725 | * @property {date} created - Message Created date 726 | * @property {string} roomId - Room ID 727 | * @property {string} roomTitle - Room Title 728 | * @property {string} roomType - Room Type (group or direct) 729 | * @property {boolean} roomIsLocked - Room Locked/Moderated status 730 | * @property {string} personId - Person ID 731 | * @property {string} personEmail - Person Email 732 | * @property {string} personDisplayName - Person Display Name 733 | * @property {string} personUsername - Person Username 734 | * @property {string} personDomain - Person Domain name 735 | * @property {string} personAvatar - Person Avatar URL 736 | * @property {object} personMembership - Person Membership object for person 737 | */ 738 | var trigger = {}; 739 | 740 | return this.getMessage(messageId) 741 | .then(message => { 742 | 743 | trigger.id = message.id; 744 | trigger.text = message.text || false; 745 | trigger.raw = message.raw || false; 746 | trigger.html = message.html || false; 747 | trigger.markdown = message.markdown || false; 748 | trigger.args = trigger.text ? trigger.text.split(' ') : []; 749 | trigger.mentionedPeople = message.mentionedPeople || false; 750 | trigger.created = message.created; 751 | 752 | var room = this.getRoom(message.roomId) 753 | .then(room => { 754 | 755 | trigger.roomId = room.id; 756 | trigger.roomTitle = room.title; 757 | trigger.roomType = room.type; 758 | trigger.roomIsLocked = room.isLocked; 759 | 760 | return when(true); 761 | }); 762 | 763 | var person = this.getPerson(message.personEmail) 764 | .then(person => { 765 | 766 | trigger.personId = person.id; 767 | trigger.personEmail = person.email; 768 | trigger.personUsername = person.username; 769 | trigger.personDomain = person.domain; 770 | trigger.personDisplayName = person.displayName; 771 | trigger.personAvatar = person.avatar; 772 | 773 | return when(true); 774 | }); 775 | 776 | var membership = this.getMemberships(message.roomId) 777 | .then(memberships => _.find(memberships, {'personId': message.personId})) 778 | .then(membership => { 779 | 780 | trigger.personMembership = membership; 781 | 782 | return when(true); 783 | }); 784 | 785 | var files = this.parseFile(message) 786 | .then(message => { 787 | trigger.files = message.files || false; 788 | return when(true); 789 | }); 790 | 791 | return when.all([room, person, membership, files]) 792 | .then(() => when(trigger)); 793 | }); 794 | }; 795 | 796 | /** 797 | * Get Rooms 798 | * 799 | * @function 800 | * @memberof Flint 801 | * @private 802 | * @returns {Promise.} 803 | */ 804 | Flint.prototype.getRooms = function() { 805 | return this.spark.roomsGet() 806 | .then(rooms => { 807 | return when.map(rooms, room => { 808 | room.lastActivity = moment(room.lastActivity).utc().toDate(); 809 | room.created = moment(room.created).utc().toDate(); 810 | room.added = moment().utc().toDate(); 811 | return when(room); 812 | }); 813 | }); 814 | }; 815 | 816 | /** 817 | * Get Room Object By ID 818 | * 819 | * @function 820 | * @memberof Flint 821 | * @private 822 | * @param {String} roomId - Room ID from Spark API. 823 | * @returns {Promise.} 824 | */ 825 | Flint.prototype.getRoom = function(roomId) { 826 | return this.spark.roomGet(roomId) 827 | .then(room => { 828 | room.lastActivity = moment(room.lastActivity).utc().toDate(); 829 | room.created = moment(room.created).utc().toDate(); 830 | room.added = moment().utc().toDate(); 831 | 832 | return when(room); 833 | }); 834 | }; 835 | 836 | /** 837 | * Get Teams 838 | * 839 | * @function 840 | * @memberof Flint 841 | * @private 842 | * @returns {Promise.} 843 | */ 844 | Flint.prototype.getTeams = function() { 845 | return this.spark.teamsGet() 846 | .then(teams => { 847 | return when.map(teams, team => { 848 | team.created = moment(team.created).utc().toDate(); 849 | return when(team); 850 | }); 851 | }); 852 | }; 853 | 854 | /** 855 | * Get Team Object By ID 856 | * 857 | * @function 858 | * @memberof Flint 859 | * @private 860 | * @param {String} teamId - Team ID from Spark API. 861 | * @returns {Promise.} 862 | */ 863 | Flint.prototype.getTeam = function(teamId) { 864 | return this.spark.teamGet(teamId) 865 | .then(team => { 866 | team.created = moment(team.created).utc().toDate(); 867 | return when(team); 868 | }); 869 | }; 870 | 871 | /** 872 | * Get Team Rooms 873 | * 874 | * @function 875 | * @memberof Flint 876 | * @private 877 | * @param {String} teamId - Room ID from Spark API 878 | * @returns {Promise.} 879 | */ 880 | Flint.prototype.getTeamRooms = function(teamId) { 881 | return this.spark.roomsByTeam(teamId) 882 | .then(rooms => { 883 | return when.map(rooms, room => { 884 | room.lastActivity = moment(room.lastActivity).utc().toDate(); 885 | room.created = moment(room.created).utc().toDate(); 886 | return when(room); 887 | }); 888 | }); 889 | }; 890 | 891 | /** 892 | * Get Person Object By Email 893 | * 894 | * @function 895 | * @memberof Flint 896 | * @private 897 | * @param {String} personEmail - Person Email of Spark Account 898 | * @returns {Promise.} 899 | */ 900 | Flint.prototype.getPerson = function(personEmail) { 901 | return this.spark.personByEmail(personEmail) 902 | .then(person => { 903 | person.created = moment(person.created).utc().toDate(); 904 | person.emails = _.forEach(person.emails, email => _.toLower(email)); 905 | person.email = _.toLower(person.emails[0]); 906 | person.username = _.split(person.email, '@', 2)[0]; 907 | person.domain = _.split(person.email, '@', 2)[1]; 908 | person.avatar = person.avatar || ''; 909 | 910 | return when(person); 911 | }); 912 | }; 913 | 914 | /** 915 | * Get Message Object by ID 916 | * 917 | * @function 918 | * @memberof Flint 919 | * @param {String} messageId - Message ID from Spark API. 920 | * @returns {Promise.} 921 | */ 922 | Flint.prototype.getMessage = function(messageId) { 923 | return this.spark.messageGet(messageId) 924 | .then(message => this.parseMessage(message)); 925 | }; 926 | 927 | /** 928 | * Get Files from Message Object by ID 929 | * 930 | * @function 931 | * @memberof Flint 932 | * @param {String} messageId - Message ID from Spark API. 933 | * @returns {Promise.} 934 | */ 935 | Flint.prototype.getFiles = function(messageId) { 936 | return this.spark.messageGet(messageId) 937 | .then(message => this.parseMessage(message)) 938 | .then(message => this.parseFile(message)) 939 | .then(message => { 940 | if(typeof message.files !== undefined && message.files instanceof Array) { 941 | return when(message.files); 942 | } else { 943 | return when.reject(new Error('no files found in message')); 944 | } 945 | }); 946 | }; 947 | 948 | /** 949 | * Get Membership Object by ID 950 | * 951 | * @function 952 | * @memberof Flint 953 | * @private 954 | * @param {String} membershipId - Membership ID from Spark API. 955 | * @returns {Promise.} 956 | */ 957 | Flint.prototype.getMembership = function(membershipId) { 958 | return this.spark.membershipGet(membershipId) 959 | .then(membership => { 960 | membership.created = moment(membership.created).utc().toDate(); 961 | membership.personEmail = _.toLower(membership.personEmail); 962 | membership.email = membership.personEmail; 963 | 964 | return when(membership); 965 | }); 966 | }; 967 | 968 | /** 969 | * Get Memberships by Room ID 970 | * 971 | * @function 972 | * @memberof Flint 973 | * @private 974 | * @param {String} [roomId] - Room ID from Spark API. 975 | * @returns {Promise.} 976 | * Promise fulfilled with Array of updated Membership objects. 977 | */ 978 | Flint.prototype.getMemberships = function(roomId) { 979 | if(!roomId) { 980 | return this.spark.membershipsGet() 981 | .then(memberships => { 982 | return when.map(memberships, membership => { 983 | membership.created = moment(membership.created).utc().toDate(); 984 | membership.personEmail = _.toLower(membership.personEmail); 985 | membership.email = membership.personEmail; 986 | 987 | return when(membership); 988 | }); 989 | }); 990 | } 991 | 992 | else { 993 | return this.spark.membershipsByRoom(roomId) 994 | .then(memberships => { 995 | return when.map(memberships, membership => { 996 | membership.created = moment(membership.created).utc().toDate(); 997 | membership.personEmail = _.toLower(membership.personEmail); 998 | membership.email = membership.personEmail; 999 | 1000 | return when(membership); 1001 | }); 1002 | }); 1003 | } 1004 | }; 1005 | 1006 | /** 1007 | * Get Team Membership Object by ID 1008 | * 1009 | * @function 1010 | * @memberof Flint 1011 | * @private 1012 | * @param {String} teamMembershipId - Team Membership ID from Spark API. 1013 | * @returns {Promise.} 1014 | */ 1015 | Flint.prototype.getTeamMembership = function(teamMembershipId) { 1016 | 1017 | return this.spark.teamMembershipGet(teamMembershipId) 1018 | .then(membership => { 1019 | membership.created = moment(membership.created).utc().toDate(); 1020 | membership.personEmail = _.toLower(membership.personEmail); 1021 | membership.email = membership.personEmail; 1022 | 1023 | return when(membership); 1024 | }); 1025 | }; 1026 | 1027 | /** 1028 | * Get Memberships by Team ID 1029 | * 1030 | * @function 1031 | * @memberof Flint 1032 | * @private 1033 | * @param {String} teamId - Team ID from Spark API. 1034 | * @returns {Promise.} 1035 | */ 1036 | Flint.prototype.getTeamMemberships = function(teamId) { 1037 | if(teamId) { 1038 | return this.spark.teamMembershipsGet(teamId) 1039 | .then(teamMemberships => { 1040 | return when.map(teamMemberships, teamMembership => { 1041 | teamMembership.created = moment(teamMembership.created).utc().toDate(); 1042 | teamMembership.personEmail = _.toLower(teamMembership.personEmail); 1043 | teamMembership.email = teamMembership.personEmail; 1044 | 1045 | return when(teamMembership); 1046 | }); 1047 | }); 1048 | } 1049 | 1050 | else { 1051 | return when.reject(new Error('missing teamId parameter')); 1052 | } 1053 | }; 1054 | 1055 | /** 1056 | * Get Webhook Object by ID 1057 | * 1058 | * @function 1059 | * @memberof Flint 1060 | * @private 1061 | * @param {String} webhookId - Webhook ID from Spark API. 1062 | * @returns {Promise.} 1063 | */ 1064 | Flint.prototype.getWebhook = function(webhookId) { 1065 | return this.spark.webhookGet(webhookId) 1066 | .then(webhook => { 1067 | webhook.created = moment(webhook.created).utc().toDate(); 1068 | if(typeof webhook.filter === 'string') { 1069 | if(webhook.filter.split('=')[0] === 'roomId') { 1070 | webhook.roomId = webhook.filter.split('=')[1]; 1071 | } 1072 | } 1073 | 1074 | return when(webhook); 1075 | }); 1076 | }; 1077 | 1078 | /** 1079 | * Get Webhooks 1080 | * 1081 | * @function 1082 | * @memberof Flint 1083 | * @private 1084 | * @returns {Promise.} 1085 | */ 1086 | Flint.prototype.getWebhooks = function() { 1087 | return this.spark.webhooksGet() 1088 | .then(webhooks => { 1089 | webhooks = _.forEach(webhooks, webhook => { 1090 | webhook.created = moment(webhook.created).utc().toDate(); 1091 | if(typeof webhook.filter === 'string') { 1092 | if(webhook.filter.split('=')[0] === 'roomId') { 1093 | webhook.roomId = webhook.filter.split('=')[1]; 1094 | } 1095 | } 1096 | }); 1097 | return when(webhooks); 1098 | }); 1099 | }; 1100 | 1101 | /** 1102 | * Get Attachement Action by ID 1103 | * 1104 | * @function 1105 | * @memberof Flint 1106 | * @param {String} attachmentActionId - attachmentActionID from Spark API. 1107 | * @returns {Promise.} 1108 | */ 1109 | Flint.prototype.getAttachmentAction = function(attachmentActionId) { 1110 | return this.spark.attachmentActionGet(attachmentActionId); 1111 | }; 1112 | 1113 | 1114 | /** 1115 | * Process a Room create event. 1116 | * 1117 | * @function 1118 | * @memberof Flint 1119 | * @private 1120 | * @returns {Promise} 1121 | */ 1122 | Flint.prototype.onRoomCreated = function(room) { 1123 | var bot = _.find(this.bots, bot => bot.room.id === room.id); 1124 | if(bot) { 1125 | bot.lastActivity = moment().utc().toDate(); 1126 | } 1127 | return when(true); 1128 | }; 1129 | 1130 | /** 1131 | * Process a Room update event. 1132 | * 1133 | * @function 1134 | * @memberof Flint 1135 | * @private 1136 | * @returns {Promise} 1137 | */ 1138 | Flint.prototype.onRoomUpdated = function(room) { 1139 | var bot = _.find(this.bots, bot => bot.room.id === room.id); 1140 | if(bot) bot.lastActivity = moment().utc().toDate(); 1141 | 1142 | // if bot exists in monitored room... 1143 | if(bot) { 1144 | //update bot 1145 | bot.room = room; 1146 | bot.isGroup = (room.type === 'group'); 1147 | bot.isDirect = (room.type === 'direct'); 1148 | 1149 | // if team 1150 | if(typeof room.teamId !== 'undefined') { 1151 | bot.isTeam = true; 1152 | bot.teamId = room.teamId; 1153 | } else { 1154 | bot.isTeam = false; 1155 | bot.teamId = null; 1156 | } 1157 | 1158 | // emit event locked 1159 | if(bot.isLocked != room.isLocked && room.isLocked) { 1160 | bot.isLocked = room.isLocked; 1161 | 1162 | /** 1163 | * Room Locked event. 1164 | * 1165 | * @event roomLocked 1166 | * @property {object} bot - Bot Object 1167 | * @property {string} id - Flint UUID 1168 | */ 1169 | this.emit('roomLocked', bot, this.id); 1170 | bot.emit('roomLocked', bot, bot.id); 1171 | 1172 | return when(true); 1173 | } 1174 | 1175 | // emit event unLocked 1176 | else if(bot.isLocked != room.isLocked && !room.isLocked) { 1177 | bot.isLocked = room.isLocked; 1178 | /** 1179 | * Room Unocked event. 1180 | * 1181 | * @event roomUnocked 1182 | * @property {object} bot - Bot Object 1183 | * @property {string} id - Flint UUID 1184 | */ 1185 | this.emit('roomUnlocked', bot, this.id); 1186 | bot.emit('roomUnlocked', bot, bot.id); 1187 | return when(true); 1188 | } 1189 | 1190 | else { 1191 | return when(true); 1192 | } 1193 | } 1194 | 1195 | // else bot does not exist in monitored room 1196 | else { 1197 | return when(true); 1198 | } 1199 | }; 1200 | 1201 | /** 1202 | * Process a new Membership event. 1203 | * 1204 | * @function 1205 | * @memberof Flint 1206 | * @private 1207 | * @param {Object} membership - Spark Team Membership Object 1208 | * @returns {Promise} 1209 | */ 1210 | Flint.prototype.onMembershipCreated = function(membership) { 1211 | var bot = _.find(this.bots, bot => bot.room.id === membership.roomId); 1212 | if(bot) bot.lastActivity = moment().utc().toDate(); 1213 | 1214 | // if bot membership added to un-monitored room... 1215 | if(!bot && this.initialized && membership.personEmail === this.person.email) { 1216 | // spawn bot 1217 | return this.spawn(membership.roomId); 1218 | } 1219 | 1220 | // else if other membership added to monitored room... 1221 | else if(bot) { 1222 | 1223 | // add new membership to bot.memberships 1224 | bot.memberships.push(membership); 1225 | 1226 | return this.getPerson(membership.personEmail) 1227 | .then(person => { 1228 | 1229 | /** 1230 | * Person Enter Room event. 1231 | * 1232 | * @event personEnters 1233 | * @property {object} bot - Bot Object 1234 | * @property {object} person - Person Object 1235 | * @property {string} id - Flint UUID 1236 | */ 1237 | this.emit('personEnters', bot, person, this.id); 1238 | bot.emit('personEnters', bot, person, bot.id); 1239 | return when(true); 1240 | }); 1241 | } 1242 | 1243 | // else, bot not found and membership added for other user 1244 | else { 1245 | return when(true); 1246 | } 1247 | }; 1248 | 1249 | /** 1250 | * Process a updated Membership event. 1251 | * 1252 | * @function 1253 | * @memberof Flint 1254 | * @private 1255 | * @param {Object} membership - Spark Membership Object 1256 | * @returns {Promise} 1257 | */ 1258 | Flint.prototype.onMembershipUpdated = function(membership) { 1259 | var bot = _.find(this.bots, bot => bot.room.id === membership.roomId); 1260 | if(bot) bot.lastActivity = moment().utc().toDate(); 1261 | 1262 | // if membership updated in monitored room 1263 | if(bot && membership.personEmail === this.person.email) { 1264 | // update bot membership 1265 | bot.membership = membership; 1266 | 1267 | // emit event Moderator 1268 | if(bot.isModerator != membership.isModerator && membership.isModerator) { 1269 | bot.isModerator = membership.isModerator; 1270 | 1271 | /** 1272 | * Bot Added as Room Moderator. 1273 | * 1274 | * @event botAddedAsModerator 1275 | * @property {object} bot - Bot Object 1276 | * @property {string} id - Flint UUID 1277 | */ 1278 | this.emit('botAddedAsModerator', bot, this.id); 1279 | bot.emit('botAddedAsModerator', bot, bot.id); 1280 | 1281 | return when(true); 1282 | } 1283 | 1284 | // emit event not Moderator 1285 | else if(bot.isModerator != membership.isModerator && !membership.isModerator) { 1286 | bot.isModerator = membership.isModerator; 1287 | 1288 | /** 1289 | * Bot Removed as Room Moderator. 1290 | * 1291 | * @event botRemovedAsModerator 1292 | * @property {object} bot - Bot Object 1293 | * @property {string} id - Flint UUID 1294 | */ 1295 | this.emit('botRemovedAsModerator', bot, this.id); 1296 | bot.emit('botRemovedAsModerator', bot, bot.id); 1297 | 1298 | return when(true); 1299 | } 1300 | 1301 | else { 1302 | return when(true); 1303 | } 1304 | } 1305 | 1306 | // else if other membership updated in monitored room 1307 | else if(bot && this.initialized) { 1308 | // update bot room membership 1309 | bot.memberships = _.map(bot.memberships, m => { 1310 | // if membership ... 1311 | if(m.id === membership.id) { 1312 | 1313 | // get person 1314 | if(m.isModerator != membership.isModerator) { 1315 | this.getPerson(membership.personEmail) 1316 | .then(person => { 1317 | // emit event added Moderator 1318 | if(membership.isModerator) { 1319 | 1320 | /** 1321 | * Person Added as Moderator. 1322 | * 1323 | * @event personAddedAsModerator 1324 | * @property {object} bot - Bot Object 1325 | * @property {object} person - Person Object 1326 | * @property {string} id - Flint UUID 1327 | */ 1328 | this.emit('personAddedAsModerator', bot, person, this.id); 1329 | bot.emit('personAddedAsModerator', bot, person, bot.id); 1330 | } 1331 | 1332 | // emit event removed Moderator 1333 | if(!membership.isModerator) { 1334 | 1335 | /** 1336 | * Person Removed as Moderator. 1337 | * 1338 | * @event personRemovedAsModerator 1339 | * @property {object} bot - Bot Object 1340 | * @property {object} person - Person Object 1341 | * @property {string} id - Flint UUID 1342 | */ 1343 | this.emit('personRemovedAsModerator', bot, person, this.id); 1344 | bot.emit('personRemovedAsModerator', bot, person, bot.id); 1345 | } 1346 | 1347 | }); 1348 | } 1349 | 1350 | //update membership; 1351 | return membership; 1352 | } 1353 | 1354 | // if not membership... 1355 | else { 1356 | // do not update membership 1357 | return m; 1358 | } 1359 | }); 1360 | 1361 | return when(true); 1362 | } 1363 | 1364 | // else, bot not found and membership updated for other user 1365 | else { 1366 | return when(true); 1367 | } 1368 | }; 1369 | 1370 | /** 1371 | * Process a deleted Membership event. 1372 | * 1373 | * @function 1374 | * @memberof Flint 1375 | * @private 1376 | * 1377 | * @param {Object} membership - Spark Membership Object 1378 | * @returns {Promise} 1379 | */ 1380 | Flint.prototype.onMembershipDeleted = function(membership) { 1381 | var bot = _.find(this.bots, bot => bot.room.id === membership.roomId); 1382 | 1383 | // if bot membership deleted in monitored room 1384 | if(bot && membership.personEmail === this.person.email) { 1385 | // despawn bot 1386 | return this.despawn(bot.room.id) 1387 | .then(() => when(true)) 1388 | .catch(() => when(false)); 1389 | } 1390 | 1391 | // else if other membership deleted in monitored room... 1392 | else if(bot) { 1393 | // remove bot room membership 1394 | bot.memberships = _.reject(bot.memberships, {'id': membership.id}); 1395 | 1396 | return this.getPerson(membership.personEmail) 1397 | .then(person => { 1398 | 1399 | /** 1400 | * Person Exits Room. 1401 | * 1402 | * @event personExits 1403 | * @property {object} bot - Bot Object 1404 | * @property {object} person - Person Object 1405 | * @property {string} id - Flint UUID 1406 | */ 1407 | this.emit('personExits', bot, person, this.id); 1408 | bot.emit('personExits', bot, person, bot.id); 1409 | 1410 | return when(true); 1411 | }); 1412 | } 1413 | 1414 | // else, bot not found and membership deleted for other user 1415 | else { 1416 | return when(true); 1417 | } 1418 | }; 1419 | 1420 | /** 1421 | * Process a new Message event. 1422 | * 1423 | * @function 1424 | * @memberof Flint 1425 | * @private 1426 | * @param {Object} tembership - Spark Team Membership Object 1427 | * @returns {Promise} 1428 | */ 1429 | Flint.prototype.onMessageCreated = function(message) { 1430 | var bot = _.find(this.bots, bot => bot.room.id === message.roomId); 1431 | if(bot) bot.lastActivity = moment().utc().toDate(); 1432 | 1433 | // if bot found... 1434 | if(bot) { 1435 | return this.getTrigger(message.id) 1436 | .then(trigger => { 1437 | 1438 | // function to run the action 1439 | function runActions(matched, bot, trigger, id) { 1440 | // process preference logic 1441 | if(matched.length > 1) { 1442 | matched = _.sortBy(matched, match => match.preference); 1443 | var prefLow = matched[0].preference; 1444 | var prefHigh = matched[matched.length - 1].preference; 1445 | 1446 | if(prefLow !== prefHigh) { 1447 | matched = _.filter(matched, match => (match.preference === prefLow)); 1448 | } 1449 | } 1450 | 1451 | _.forEach(matched, lex => { 1452 | // for regex 1453 | if(lex.phrase instanceof RegExp && typeof lex.action === 'function') { 1454 | // define trigger.args, trigger.phrase 1455 | trigger.args = trigger.text.split(' '); 1456 | trigger.phrase = lex.phrase; 1457 | 1458 | // run action 1459 | lex.action(bot, trigger, id); 1460 | return true; 1461 | } 1462 | 1463 | // for string 1464 | else if (typeof lex.phrase === 'string' && typeof lex.action === 'function') { 1465 | // find index of match 1466 | var args = _.toLower(trigger.text).split(' '); 1467 | var indexOfMatch = args.indexOf(lex.phrase) !== -1 ? args.indexOf(lex.phrase) : 0; 1468 | 1469 | // define trigger.args, trigger.phrase 1470 | trigger.args = trigger.text.split(' '); 1471 | trigger.args = trigger.args.slice(indexOfMatch, trigger.args.length); 1472 | trigger.phrase = lex.phrase; 1473 | 1474 | // run action 1475 | lex.action(bot, trigger, id); 1476 | return true; 1477 | } 1478 | 1479 | // for nothing... 1480 | else { 1481 | return false; 1482 | } 1483 | }); 1484 | } 1485 | 1486 | // if mentioned 1487 | if(trigger.mentionedPeople && _.includes(trigger.mentionedPeople, this.person.id)) { 1488 | 1489 | trigger.args = trigger.text.split(' '); 1490 | 1491 | /** 1492 | * Bot Mentioned. 1493 | * 1494 | * @event mentioned 1495 | * @property {object} bot - Bot Object 1496 | * @property {object} trigger - Trigger Object 1497 | * @property {string} id - Flint UUID 1498 | */ 1499 | this.emit('mentioned', bot, trigger, this.id); 1500 | bot.emit('mentioned', bot, trigger, bot.id); 1501 | } 1502 | 1503 | // emit message event 1504 | if(trigger.text) { 1505 | 1506 | /** 1507 | * Message Recieved. 1508 | * 1509 | * @event message 1510 | * @property {object} bot - Bot Object 1511 | * @property {object} trigger - Trigger Object 1512 | * @property {string} id - Flint UUID 1513 | */ 1514 | this.emit('message', bot, trigger, this.id); 1515 | bot.emit('message', bot, trigger, bot.id); 1516 | } 1517 | 1518 | // emit file event 1519 | if(trigger.files) { 1520 | 1521 | /** 1522 | * File Recieved. 1523 | * 1524 | * @event files 1525 | * @property {object} bot - Bot Object 1526 | * @property {trigger} trigger - Trigger Object 1527 | * @property {string} id - Flint UUID 1528 | */ 1529 | this.emit('files', bot, trigger, this.id); 1530 | bot.emit('files', bot, trigger, bot.id); 1531 | } 1532 | 1533 | // check if message is from bot... 1534 | // using the bot's ID instead of the email guarantees this will work 1535 | // even if the bot's name changes (eg: mybot@sparkbot.io -> mybot@webex.bot) 1536 | if(trigger.personId === bot.person.id) { 1537 | // ignore messages from bot 1538 | return when(false); 1539 | } 1540 | 1541 | // if trigger text present... 1542 | if(trigger.text) { 1543 | 1544 | // return matched lexicon entry 1545 | var matched = _.filter(this.lexicon, lex => { 1546 | 1547 | // if lex.phrase is regex 1548 | if(lex.phrase && lex.phrase instanceof RegExp && lex.phrase.test(trigger.text)) { 1549 | return true; 1550 | } 1551 | 1552 | // if lex.phrase is string and this is NOT a bot account 1553 | else if(!this.isBotAccount && lex.phrase && typeof lex.phrase === 'string' && lex.phrase === _.toLower(trigger.text).split(' ')[0]) { 1554 | return true; 1555 | } 1556 | 1557 | // if lex.phrase is string and this is a bot account 1558 | else if(this.isBotAccount && lex.phrase && typeof lex.phrase === 'string') { 1559 | var regexPhrase = new RegExp('(^| )' + lex.phrase.replace(/([\.\^\$\*\+\?\(\)\[\{\\\|])/g, '\\$1') + '($| )','i'); 1560 | return (regexPhrase.test(trigger.text)); 1561 | } 1562 | 1563 | // else, no valid match 1564 | else return false; 1565 | }); 1566 | } 1567 | 1568 | // else trigger.text not present... 1569 | else { 1570 | return when(false); 1571 | } 1572 | 1573 | // if matched 1574 | if(matched && typeof this.authorize === 'function') { 1575 | // if authorization function exists... 1576 | return when(this.authorize(bot, trigger, this.id)) 1577 | .then(authorized => { 1578 | 1579 | //if authorized 1580 | if(authorized) { 1581 | runActions(matched, bot, trigger, this.id); 1582 | return when(trigger); 1583 | } else { 1584 | this.debug('"%s" was denied running command in room "%s" for account "%s"', trigger.personEmail, trigger.roomTitle, this.email); 1585 | return when(false); 1586 | } 1587 | }); 1588 | } 1589 | 1590 | // else, if matched and no authorization configured, run command 1591 | else if(matched) { 1592 | runActions(matched, bot, trigger, this.id); 1593 | return when(trigger); 1594 | } 1595 | 1596 | // else, do nothing... 1597 | else { 1598 | return when(false); 1599 | } 1600 | }); 1601 | } 1602 | 1603 | // else, bot not found... 1604 | else { 1605 | return when(false); 1606 | } 1607 | }; 1608 | 1609 | /** 1610 | * Process a new attachment action event. 1611 | * 1612 | * @function 1613 | * @memberof Flint 1614 | * @private 1615 | * @param {Object} attachmentAction - Spark attachentAction Object 1616 | * @returns {Promise} 1617 | */ 1618 | Flint.prototype.onAttachmentActions = function(attachmentAction) { 1619 | var bot = _.find(this.bots, bot => bot.room.id === attachmentAction.roomId); 1620 | if(bot) bot.lastActivity = moment().utc().toDate(); 1621 | 1622 | // if bot found... 1623 | if(bot) { 1624 | this.emit('attachmentAction', bot, attachmentAction, this.id); 1625 | // else, bot not found... 1626 | } else { 1627 | return when(false); 1628 | } 1629 | }; 1630 | 1631 | /** 1632 | * Spawns a bot in a Spark Room. 1633 | * 1634 | * @function 1635 | * @memberof Flint 1636 | * @private 1637 | * @param {String} Room ID - The ID for a Spark Room. 1638 | * @returns {Promise.} 1639 | */ 1640 | Flint.prototype.spawn = function(roomId) { 1641 | 1642 | // if active... 1643 | if(!this.active) { 1644 | return when(false); 1645 | } 1646 | 1647 | // validate params 1648 | if(typeof roomId !== 'string') { 1649 | this.debug('A bot for acount "%s" could not spawn as room id not valid', this.email); 1650 | return when(false); 1651 | } 1652 | 1653 | // validate bot is not already assigned to room 1654 | var foundBot = _.find(this.bots, bot => (bot.room.id === roomId)); 1655 | if(foundBot) { 1656 | this.debug('A bot for acount "%s" could not spawn as bot already exists in room', this.email); 1657 | return when(false); 1658 | } 1659 | 1660 | // create new bot 1661 | var newBot = new Bot(this); 1662 | 1663 | // get room that bot is spawning in 1664 | return this.getRoom(roomId) 1665 | .then(room => { 1666 | if(room.title == '') { 1667 | room.title = 'Default title'; 1668 | } 1669 | 1670 | newBot.room = room; 1671 | newBot.isDirect = (room.type === 'direct'); 1672 | newBot.isGroup = (room.type === 'group'); 1673 | newBot.isLocked = room.isLocked; 1674 | 1675 | return when(room); 1676 | }) 1677 | 1678 | // get team 1679 | .then(room => { 1680 | // if team 1681 | if(typeof room.teamId !== 'undefined') { 1682 | return this.getTeam(room.teamId) 1683 | .then(team => { 1684 | newBot.team = team; 1685 | newBot.isTeam = true; 1686 | return when(room); 1687 | }) 1688 | .catch(err => { 1689 | newBot.team = {}; 1690 | newBot.isTeam = false; 1691 | return when(room); 1692 | }); 1693 | } else { 1694 | newBot.isTeam = false; 1695 | newBot.team = {}; 1696 | return when(room); 1697 | } 1698 | }) 1699 | 1700 | // get memberships of room 1701 | .then(room => this.getMemberships(room.id)) 1702 | .then(memberships => { 1703 | 1704 | // get bot membership from room memberships 1705 | var botMembership = _.find(memberships, { 'personEmail': this.person.email }); 1706 | 1707 | // remove bot membership from room memberships 1708 | memberships = _.reject(memberships, { 'personId': this.person.id }); 1709 | 1710 | // assign room memberships to bot 1711 | newBot.memberships = memberships; 1712 | 1713 | // assign membership properties to bot object 1714 | newBot.membership = botMembership; 1715 | newBot.isModerator = botMembership.isModerator; 1716 | newBot.isMonitor = botMembership.isMonitor; 1717 | 1718 | // if direct, set recipient 1719 | if(newBot.isDirect) { 1720 | newBot.isDirectTo = memberships[0].personEmail; 1721 | } 1722 | 1723 | return when(memberships); 1724 | }) 1725 | 1726 | // register and start bot 1727 | .then(() => { 1728 | 1729 | // start bot 1730 | newBot.start(); 1731 | 1732 | // add bot to array of bots 1733 | this.bots.push(newBot); 1734 | 1735 | /** 1736 | * Bot Spawned. 1737 | * 1738 | * @event spawn 1739 | * @property {object} bot - Bot Object 1740 | * @property {string} id - Flint UUID 1741 | */ 1742 | this.emit('spawn', newBot, this.id); 1743 | 1744 | return when(true); 1745 | }) 1746 | 1747 | // insert delay 1748 | .delay(this.spark.minTime) 1749 | 1750 | // catch errors with spawn 1751 | .catch(err => { 1752 | 1753 | // remove reference 1754 | newBot = {}; 1755 | 1756 | return when(false); 1757 | }); 1758 | }; 1759 | 1760 | /** 1761 | * Despawns a bot in a Spark Room. 1762 | * 1763 | * @function 1764 | * @memberof Flint 1765 | * @private 1766 | * @param {String} Room ID - The ID for a Spark Room. 1767 | * @returns {Promise.} 1768 | */ 1769 | Flint.prototype.despawn = function(roomId) { 1770 | var bot = _.find(this.bots, bot => (bot.room.id === roomId)); 1771 | 1772 | if(bot) { 1773 | // shutdown bot 1774 | bot.stop(); 1775 | 1776 | // remove objects assigned to memory store for this bot 1777 | return this.forgetByRoomId(bot.room.id) 1778 | .then(() => { 1779 | /** 1780 | * Bot Despawned. 1781 | * 1782 | * @event despawn 1783 | * @property {object} bot - Bot Object 1784 | * @property {string} id - Flint UUID 1785 | */ 1786 | this.emit('despawn', bot, this.id); 1787 | 1788 | // remove bot from flint 1789 | this.bots = _.reject(this.bots, { 'id': bot.id }); 1790 | 1791 | return when(true); 1792 | }); 1793 | } else { 1794 | return when.reject(new Error('despawn failed to find bot in room')); 1795 | } 1796 | }; 1797 | 1798 | /** 1799 | * Add action to be performed when bot hears a phrase. 1800 | * 1801 | * @function 1802 | * @memberof Flint 1803 | * @param {Regex|String} phrase - The phrase as either a regex or string. If 1804 | * regex, matches on entire message.If string, matches on first word. 1805 | * @param {Function} action - The function to execute when phrase is matched. 1806 | * Function is executed with 2 variables. Trigger and Bot. The Trigger Object 1807 | * contains information about the person who entered a message that matched the 1808 | * phrase. The Bot Object is an instance of the Bot Class as it relates to the 1809 | * room the message was heard. 1810 | * @param {String} [helpText] - The string of text that describes how this 1811 | * command operates. 1812 | * @param {Number} [preference=0] - Specifies preference of phrase action when 1813 | * overlapping phrases are matched. On multiple matches with same preference, 1814 | * all matched actions are excuted. On multiple matches with difference 1815 | * preference values, only the lower preferenced matched action(s) are executed. 1816 | * @returns {String} 1817 | * 1818 | * @example 1819 | * // using a string to match first word and defines help text 1820 | * flint.hears('/say', function(bot, trigger, id) { 1821 | * bot.say(trigger.args.slice(1, trigger.arges.length - 1)); 1822 | * }, '/say - Responds with a greeting'); 1823 | * 1824 | * @example 1825 | * // using regex to match across entire message 1826 | * flint.hears(/(^| )beer( |.|$)/i, function(bot, trigger, id) { 1827 | * bot.say('Enjoy a beer, %s! 🍻', trigger.personDisplayName); 1828 | * }); 1829 | */ 1830 | Flint.prototype.hears = function(phrase, action, helpText, preference) { 1831 | var id = u.genUUID64(); 1832 | 1833 | // parse function args 1834 | var args = Array.prototype.slice.call(arguments); 1835 | phrase = args.length > 0 && (typeof args[0] === 'string' || args[0] instanceof RegExp) ? args.shift() : null; 1836 | action = args.length > 0 && typeof args[0] === 'function' ? args.shift() : null; 1837 | helpText = args.length > 0 && typeof args[0] === 'string' ? args.shift() : null; 1838 | preference = args.length > 0 && typeof args[0] === 'number' ? args.shift() : 0; 1839 | 1840 | if(typeof phrase === 'string' && action) { 1841 | phrase = _.toLower(phrase); 1842 | this.lexicon.push({ 'id': id, 'phrase': phrase, 'action': action, 'helpText': helpText, 'preference': preference }); 1843 | return id; 1844 | } 1845 | 1846 | else if(phrase instanceof RegExp && action) { 1847 | this.lexicon.push({ 'id': id, 'phrase': phrase, 'action': action, 'helpText': helpText, 'preference': preference }); 1848 | return id; 1849 | } 1850 | 1851 | else { 1852 | throw new Error('Invalid flint.hears() syntax'); 1853 | } 1854 | }; 1855 | 1856 | /** 1857 | * Remove a "flint.hears()" entry. 1858 | * 1859 | * @function 1860 | * @memberof Flint 1861 | * @param {String} id - The "hears" ID. 1862 | * @returns {null} 1863 | * 1864 | * @example 1865 | * // using a string to match first word and defines help text 1866 | * var hearsHello = flint.hears('/flint', function(bot, trigger, id) { 1867 | * bot.say('Hello %s!', trigger.personDisplayName); 1868 | * }); 1869 | * flint.clearHears(hearsHello); 1870 | */ 1871 | Flint.prototype.clearHears = function(hearsId) { 1872 | this.lexicon = _.reject(this.lexicon, lex => (lex.id === hearsId)); 1873 | }; 1874 | 1875 | /** 1876 | * Display help for registered Flint Commands. 1877 | * 1878 | * @function 1879 | * @param {String} [header=Usage:] - String to use in header before displaying help message. 1880 | * @param {String} [footer=Powered by Flint - https://github.com/nmarus/flint] - String to use in footer before displaying help message. 1881 | * @returns {String} 1882 | * 1883 | * @example 1884 | * flint.hears('/help', function(bot, trigger, id) { 1885 | * bot.say(flint.showHelp()); 1886 | * }); 1887 | */ 1888 | Flint.prototype.showHelp = function(header, footer) { 1889 | header = header ? header : 'Usage:'; 1890 | footer = footer ? footer : 'Powered by Flint - https://github.com/nmarus/flint'; 1891 | 1892 | var helpText = ''; 1893 | 1894 | _.forEach(this.lexicon, lex => { 1895 | if(lex.helpText) { 1896 | helpText = helpText + '* ' + lex.helpText + '\n'; 1897 | } 1898 | }); 1899 | 1900 | helpText = header + '\n\n' + helpText + '\n' + footer + '\n\n'; 1901 | 1902 | return helpText; 1903 | }; 1904 | 1905 | /** 1906 | * Attaches authorizer function. 1907 | * 1908 | * @function 1909 | * @memberof Flint 1910 | * @param {Function} Action - The function to execute when phrase is matched 1911 | * to authenticate a user. The function is passed the bot, trigger, and id and 1912 | * expects a return value of true or false. 1913 | * @returns {Boolean} 1914 | * 1915 | * @example 1916 | * function myAuthorizer(bot, trigger, id) { 1917 | * if(trigger.personEmail === 'john@test.com') { 1918 | * return true; 1919 | * } 1920 | * else if(trigger.personDomain === 'test.com') { 1921 | * return true; 1922 | * } 1923 | * else { 1924 | * return false; 1925 | * } 1926 | * } 1927 | * flint.setAuthorizer(myAuthorizer); 1928 | */ 1929 | Flint.prototype.setAuthorizer = function(fn) { 1930 | if(typeof fn === 'function') { 1931 | this.authorize = when.lift(fn); 1932 | return true; 1933 | } else { 1934 | this.authorize = null; 1935 | return false; 1936 | } 1937 | }; 1938 | Flint.prototype.authorize = null; 1939 | 1940 | /** 1941 | * Removes authorizer function. 1942 | * 1943 | * @function 1944 | * @memberof Flint 1945 | * @returns {null} 1946 | * 1947 | * @example 1948 | * flint.clearAuthorizer(); 1949 | */ 1950 | Flint.prototype.clearAuthorizer = function() { 1951 | this.authorize = null; 1952 | }; 1953 | 1954 | /** 1955 | * Defines storage backend. 1956 | * 1957 | * @function 1958 | * @memberof Flint 1959 | * @param {Function} Driver - The storage driver. 1960 | * @returns {null} 1961 | * 1962 | * @example 1963 | * // define memory store (default if not specified) 1964 | * flint.storageDriver(new MemStore()); 1965 | */ 1966 | Flint.prototype.storageDriver = function(driver) { 1967 | 1968 | // validate storage module store() method 1969 | if(typeof driver.store === 'function') { 1970 | Bot.prototype.store = function(key, value) { 1971 | if (this.active) { 1972 | var id = this.room.id; 1973 | return driver.store.call(driver, id, key, value); 1974 | } else { 1975 | return when(value); 1976 | } 1977 | }; 1978 | } else { 1979 | throw new Error('storage module missing store() function'); 1980 | } 1981 | 1982 | // validate storage module recall() method 1983 | if(typeof driver.recall === 'function') { 1984 | Bot.prototype.recall = function(key) { 1985 | if (this.active) { 1986 | var id = this.room.id; 1987 | return driver.recall.call(driver, id, key); 1988 | } else { 1989 | return when(value); 1990 | } 1991 | }; 1992 | } else { 1993 | throw new Error('storage module missing recall() function'); 1994 | } 1995 | 1996 | // validate storage module forget() method 1997 | if(typeof driver.forget === 'function') { 1998 | Bot.prototype.forget = function(key) { 1999 | if (this.active) { 2000 | var id = this.room.id; 2001 | return driver.forget.call(driver, id, key); 2002 | } else { 2003 | return when(value); 2004 | } 2005 | }; 2006 | 2007 | Flint.prototype.forgetByRoomId = function(roomId) { 2008 | return driver.forget.call(driver, roomId) 2009 | .catch(err => { 2010 | // ignore errors when called by forgetByRoomId 2011 | return when(true); 2012 | }); 2013 | }; 2014 | } else { 2015 | throw new Error('storage module missing forget() function'); 2016 | } 2017 | 2018 | // storage defined 2019 | this.storageActive = true; 2020 | }; 2021 | 2022 | /** 2023 | * Remove objects from memory store associated to a roomId. 2024 | * 2025 | * @function 2026 | * @private 2027 | * @param {String} roomId 2028 | * @returns {Boolean} 2029 | */ 2030 | Flint.prototype.forgetByRoomId = null; 2031 | 2032 | /** 2033 | * Load a Plugin from a external file. 2034 | * @function 2035 | * @memberof Flint 2036 | * @param {String} path - Load a plugin at given path. 2037 | * @returns {Boolean} 2038 | * 2039 | * @example 2040 | * flint.use('events.js'); 2041 | * 2042 | * @example 2043 | * // events.js 2044 | * module.exports = function(flint) { 2045 | * flint.on('spawn', function(bot) { 2046 | * console.log('new bot spawned in room: %s', bot.myroom.title); 2047 | * }); 2048 | * flint.on('despawn', function(bot) { 2049 | * console.log('bot despawned in room: %s', bot.myroom.title); 2050 | * }); 2051 | * flint.on('messageCreated', function(message, bot) { 2052 | * console.log('"%s" said "%s" in room "%s"', message.personEmail, message.text, bot.myroom.title); 2053 | * }); 2054 | * }; 2055 | */ 2056 | Flint.prototype.use = function(pluginPath) { 2057 | if(path.parse(pluginPath).ext === '.js') { 2058 | try { 2059 | require(pluginPath)(this); 2060 | this.debug('Loading flint plugin at "%s"', pluginPath); 2061 | return true; 2062 | } 2063 | 2064 | catch(err) { 2065 | this.debug('Could not load flint plugin at "%s"', pluginPath); 2066 | return false; 2067 | } 2068 | } 2069 | }; 2070 | 2071 | module.exports = Flint; 2072 | 2073 | function cleanupListeners(flint) { 2074 | // Cleanup webhooks or websockets 2075 | if (flint.options.webhookUrl) { 2076 | return flint.getWebhooks() 2077 | // get webhooks 2078 | .then(webhooks => { 2079 | 2080 | // remove all webhooks on stop 2081 | if(!flint.options.removeWebhooksOnStart) { 2082 | var webhooksToRemove = _.filter(webhooks, webhook => { 2083 | return (webhook.name == u.base64encode(flint.options.webhookUrl.split('/')[2] + ' ' + flint.email)); 2084 | }); 2085 | 2086 | if(webhooksToRemove instanceof Array && webhooksToRemove.length > 0) { 2087 | return when.map(webhooksToRemove, webhook => flint.spark.webhookRemove(webhook.id)) 2088 | .then(() => when(true)) 2089 | .catch(() => when(true)); 2090 | } else { 2091 | return when(true); 2092 | } 2093 | } 2094 | 2095 | // else, only remove webhooks this app created 2096 | else { 2097 | return when.map(webhooks, webhook => flint.spark.webhookRemove(webhook.id)) 2098 | .then(() => when(true)) 2099 | .catch(() => when(true)); 2100 | } 2101 | 2102 | }); 2103 | } else { 2104 | return flint.websocket.cleanup() 2105 | .then(() => { 2106 | delete flint.websocket; 2107 | return when(true); 2108 | }); 2109 | } 2110 | } -------------------------------------------------------------------------------- /lib/logs.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = function(flint) { 5 | 6 | flint.on('initialized', function(id) { 7 | var msg = util.format('(Flint Initialized) %s rooms', flint.bots.length); 8 | flint.log(msg); 9 | }); 10 | 11 | flint.on('start', function(id) { 12 | var msg = util.format('(Flint Started) "%s"', flint.email); 13 | flint.log(msg); 14 | }); 15 | 16 | flint.on('stop', function(id) { 17 | var msg = util.format('(Flint Stopped) "%s"', flint.email); 18 | flint.log(msg); 19 | }); 20 | 21 | flint.on('spawn', (bot, id) => { 22 | var msg = util.format('(Room Discovered) "%s"', bot.room.title); 23 | flint.log(msg); 24 | }); 25 | 26 | flint.on('despawn', (bot, id) => { 27 | var msg = util.format('(Room Removed) "%s"', bot.room.title); 28 | flint.log(msg); 29 | }); 30 | 31 | flint.on('message', (bot, trigger, id) => { 32 | var msg = util.format('(Messsage Received) "%s" "%s" "%s"', bot.room.title, trigger.personEmail, trigger.text); 33 | flint.log(msg); 34 | }); 35 | 36 | flint.on('files', (bot, trigger, id) => { 37 | _.forEach(trigger.files, file => { 38 | var msg = util.format('(File Uploaded) "%s" "%s" "%s"', bot.room.title, trigger.personEmail, file.name); 39 | flint.log(msg); 40 | }); 41 | }); 42 | 43 | flint.on('roomLocked', (bot, id) => { 44 | var msg = util.format('(Room moderated) "%s"', bot.room.title); 45 | flint.log(msg); 46 | }); 47 | 48 | flint.on('roomUnlocked', (bot, id) => { 49 | var msg = util.format('(Room unmoderated) "%s"', bot.room.title); 50 | flint.log(msg); 51 | }); 52 | 53 | flint.on('botAddedAsModerator', (bot, id) => { 54 | var msg = util.format('(Added as Room Moderator) "%s" "%s"', bot.room.title, bot.email); 55 | flint.log(msg); 56 | }); 57 | 58 | flint.on('botRemovedAsModerator', (bot, id) => { 59 | var msg = util.format('(Removed as Room Moderator) "%s" "%s"', bot.room.title, bot.email); 60 | flint.log(msg); 61 | }); 62 | 63 | flint.on('personAddedAsModerator', (bot, person, id) => { 64 | var msg = util.format('(Added as Room Moderator) "%s" "%s"', bot.room.title, person.email); 65 | flint.log(msg); 66 | }); 67 | 68 | flint.on('personRemovedAsModerator', (bot, person, id) => { 69 | var msg = util.format('(Removed as Room Moderator) "%s" "%s"', bot.room.title, person.email); 70 | flint.log(msg); 71 | }); 72 | 73 | flint.on('personEnters', function(bot, person, id) { 74 | var msg = util.format('(Room Entered) "%s" "%s"', bot.room.title, person.email); 75 | flint.log(msg); 76 | }); 77 | 78 | flint.on('personExits', function(bot, person, id) { 79 | var msg = util.format('(Room Exited) "%s" "%s"', bot.room.title, person.email); 80 | flint.log(msg); 81 | }); 82 | 83 | }; 84 | -------------------------------------------------------------------------------- /lib/process-event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var when = require('when'); 4 | 5 | /** 6 | * Processes an inbound Webex event. 7 | * This can be called by either the webhook or websocket handler. 8 | * @function 9 | * @private 10 | * @param {Object} flint - The flint object this function applies to. 11 | * @param {Object} body - The body of the event being processed 12 | * @param {String} name - The name of the webhook, if a webhook is being processed 13 | */ 14 | function processEvent(flint, body, name = '') { 15 | if(!flint.active) { 16 | return when(true); 17 | } 18 | 19 | // get event content 20 | var name = name ? name : body.name; 21 | var resource = body.resource; 22 | var event = body.event; 23 | var data = body.data; 24 | var roomId = body.filter ? body.filter.split('=')[1] : null; 25 | 26 | // validate event is bound for this instance of flint 27 | if(name !== flint.webhook.name || (typeof flint.webhook.roomId !== 'undefined' && flint.webhook.roomId !== roomId)) { 28 | return when(true); 29 | } 30 | 31 | if(typeof resource !== 'string' || typeof event !== 'string') { 32 | flint.debug('Can not determine webhook type'); 33 | return when(true); 34 | } 35 | 36 | // rooms 37 | if(resource === 'rooms') { 38 | return flint.getRoom(data.id) 39 | .then(room => { 40 | 41 | // set room title for rooms with none set (api bug?) 42 | if(room.title == '') { 43 | room.title = 'Default title'; 44 | } 45 | 46 | // room created 47 | if(event === 'created') { 48 | flint.emit('roomCreated', room, flint.id); 49 | 50 | return flint.onRoomCreated(room) 51 | .catch(err => { 52 | flint.debug(err.stack); 53 | return when(true); 54 | }); 55 | } 56 | 57 | // room updated 58 | if(event === 'updated') { 59 | flint.emit('roomUpdated', room, flint.id); 60 | 61 | return flint.onRoomUpdated(room) 62 | .catch(err => { 63 | flint.debug(err.stack); 64 | return when(true); 65 | }); 66 | } 67 | 68 | }) 69 | .catch(() => { 70 | return when(true); 71 | }); 72 | } 73 | 74 | // memberships 75 | if(resource === 'memberships') { 76 | 77 | // membership created 78 | if(event === 'created') { 79 | return flint.getMembership(data.id) 80 | .then(membership => { 81 | flint.emit('membershipCreated', membership, flint.id); 82 | 83 | return flint.onMembershipCreated(membership) 84 | .catch(err => { 85 | flint.debug(err.stack); 86 | return when(true); 87 | }); 88 | }) 89 | .catch(() => { 90 | return when(true); 91 | }); 92 | } 93 | 94 | // membership updated 95 | if(event === 'updated') { 96 | return flint.getMembership(data.id) 97 | .then(membership => { 98 | flint.emit('membershipUpdated', membership, flint.id); 99 | 100 | return flint.onMembershipUpdated(membership) 101 | .catch(err => { 102 | flint.debug(err.stack); 103 | return when(true); 104 | }); 105 | }) 106 | .catch(() => { 107 | return when(true); 108 | }); 109 | } 110 | 111 | // membership deleted 112 | if(event === 'deleted') { 113 | flint.emit('membershipDeleted', data, flint.id); 114 | 115 | return flint.onMembershipDeleted(data) 116 | .catch(err => { 117 | flint.debug(err.stack); 118 | return when(true); 119 | }); 120 | } 121 | 122 | } 123 | 124 | // messages 125 | if(resource === 'messages') { 126 | // membership created 127 | if(event === 'created') { 128 | return flint.getMessage(data.id) 129 | .then(message => { 130 | flint.emit('messageCreated', message, flint.id); 131 | 132 | return flint.onMessageCreated(message) 133 | .catch(err => { 134 | flint.debug(err.stack); 135 | return when(true); 136 | }); 137 | }) 138 | .catch(() => { 139 | return when(true); 140 | }); 141 | } 142 | 143 | // message deleted 144 | if(event === 'deleted') { 145 | flint.emit('messageDeleted', data, flint.id); 146 | return when(true); 147 | } 148 | } 149 | 150 | // Buttons & Cards Attachment Actions 151 | if(resource === 'attachmentActions') { 152 | // action created 153 | if(event === 'created') { 154 | return flint.getAttachmentAction(data.id) 155 | .then(attachmentAction => { 156 | // Not really sure what this does 157 | //flint.emit('messageCreated', message, flint.id); 158 | 159 | return flint.onAttachmentActions(attachmentAction) 160 | .catch(err => { 161 | flint.debug(err.stack); 162 | return when(true); 163 | }); 164 | }) 165 | .catch(() => { 166 | return when(true); 167 | }); 168 | } 169 | } 170 | 171 | } 172 | 173 | module.exports = processEvent; 174 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var uuid = require('uuid'); 4 | 5 | var Utils = {}; 6 | 7 | /** 8 | * Base64 encode a string 9 | * @function 10 | * @private 11 | * @param {String} string 12 | * @returns {String} Base64 encoded string. 13 | */ 14 | Utils.base64encode = function(string) { 15 | return new Buffer(string).toString('base64'); 16 | }; 17 | 18 | /** 19 | * Generate UUID string 20 | * @function 21 | * @private 22 | * @returns {String} UUID string. 23 | */ 24 | Utils.genUUID = function() { 25 | return uuid.v4(); 26 | }; 27 | 28 | /** 29 | * Generate a Base64 encoded UUID 30 | * @function 31 | * @private 32 | * @returns {String} Base64 encoded UUID. 33 | */ 34 | Utils.genUUID64 = function() { 35 | return Utils.base64encode(Utils.genUUID()); 36 | }; 37 | 38 | module.exports = Utils; 39 | -------------------------------------------------------------------------------- /lib/webhook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var when = require('when'); 4 | var processEvent = require('./process-event'); 5 | 6 | /** 7 | * Processes a inbound Spark API webhook. 8 | * @function 9 | * @private 10 | * @param {Object} flint - The flint object this function applies to. 11 | * @returns {Function} 12 | * Function that can be used for Express and Express-like webserver routes. 13 | * 14 | */ 15 | function Webhook(flint) { 16 | 17 | return function(req, res) { 18 | 19 | // emit webhook event (mostly here for debugging...) 20 | flint.emit('webhook', req[flint.options.webhookRequestJSONLocation]); 21 | 22 | // if "res" is passed to function... 23 | if(typeof res !== 'undefined') { 24 | res.status(200); 25 | res.send('OK'); 26 | } 27 | 28 | // get webhook header to determine if security is enabled 29 | var sig = req.headers['x-spark-signature'] || false; 30 | var body = req[flint.options.webhookRequestJSONLocation] || false; 31 | 32 | if(!body){ 33 | return when(true); 34 | } 35 | 36 | if(flint.spark.webhookSecret && !(sig && flint.spark.webhookAuth(sig, body))) { 37 | // invalid signature, ignore processing webhook 38 | flint.debug('invalid signature in webhook callback, ignoring...'); 39 | return when(true); 40 | } 41 | 42 | return processEvent(flint, body); 43 | 44 | }; // end of return function... 45 | } 46 | 47 | module.exports = Webhook; 48 | -------------------------------------------------------------------------------- /lib/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var when = require('when'); 4 | var Webex = require('webex'); 5 | var processEvent = require('./process-event'); 6 | 7 | /** 8 | * A class to register for webex teams messaging events to be delivered 9 | * via socket using the webex SDK 10 | * 11 | * This class will register to listen to the events. When an event 12 | * is received it will call the webhook handler with the event payload 13 | * 14 | * This approach allows bot developers to deploy bots behind a firewall 15 | * without requiring a public IP address in order to receive webhooks 16 | * 17 | * @function 18 | * @private 19 | * @param {Object} flint - The flint object this function applies to. 20 | * @param {Object} webhook - The webhook handler object for this instance 21 | * @returns {Object} 22 | * 23 | */ 24 | function Websocket(flint, webhook) { 25 | this.flint = flint; 26 | this.webhook = webhook; 27 | // Todo make this more like the traditional flint "name" 28 | // B64 encoding of URL and bot name... 29 | this.name = 'webex sdk socket event'; 30 | flint.webhook.name = this.name; 31 | } 32 | 33 | Websocket.prototype.init = function() { 34 | this.flint.webex = Webex.init({ 35 | credentials: { 36 | access_token: this.flint.options.token 37 | } 38 | }); 39 | 40 | if (!((this.flint.webex) && (this.flint.webex.canAuthorize))) { 41 | console.error('Unable to intiatize Webex SDK for events'); 42 | return when(false); 43 | } 44 | 45 | // register for message, membership and room events 46 | let messagesPromise = this.flint.webex.messages.listen(); 47 | let membershipsPromise = this.flint.webex.memberships.listen(); 48 | let roomsPromise = this.flint.webex.rooms.listen(); 49 | 50 | return Promise.all([messagesPromise, membershipsPromise, roomsPromise]) 51 | .then(() => { 52 | this.flint.webex.messages.on('created', (event) => processEvent(this.flint, event, this.name)); 53 | this.flint.webex.messages.on('deleted', (event) => processEvent(this.flint, event, this.name)); 54 | this.flint.webex.memberships.on('created', (event) => processEvent(this.flint, event, this.name)); 55 | this.flint.webex.memberships.on('deleted', (event) => processEvent(this.flint, event, this.name)); 56 | this.flint.webex.memberships.on('updated', (event) => processEvent(this.flint, event, this.name)); 57 | this.flint.webex.rooms.on('created', (event) => processEvent(this.flint, event, this.name)); 58 | this.flint.webex.rooms.on('updated', (event) => processEvent(this.flint, event, this.name)); 59 | console.log('Listening for webex teams events...'); 60 | return when(true); 61 | }) 62 | .catch((err) => { 63 | console.error(`error listening for webex teams events: ${err}`); 64 | return Promise.reject(err); 65 | }); 66 | }; 67 | 68 | Websocket.prototype.cleanup = function() { 69 | // register for message, membership and room events 70 | this.flint.webex.messages.stopListening(); 71 | this.flint.webex.memberships.stopListening(); 72 | this.flint.webex.rooms.stopListening(); 73 | 74 | this.flint.webex.messages.off('created'); 75 | this.flint.webex.messages.off('deleted'); 76 | this.flint.webex.memberships.off('created'); 77 | this.flint.webex.memberships.off('deleted'); 78 | this.flint.webex.memberships.off('updated'); 79 | this.flint.webex.rooms.off('created'); 80 | this.flint.webex.rooms.off('updated'); 81 | console.log('Stopped istening for webex teams events...'); 82 | return when(true); 83 | }; 84 | 85 | module.exports = Websocket; 86 | 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-flint", 3 | "version": "4.8.1", 4 | "description": "Bot SDK for Node JS", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/flint-bot/flint.git" 12 | }, 13 | "keywords": [ 14 | "bot", 15 | "sdk", 16 | "spark" 17 | ], 18 | "bugs": { 19 | "url": "https://github.com/flint-bot/flint/issues" 20 | }, 21 | "homepage": "https://github.com/flint-bot/flint#readme", 22 | "author": "Nicholas Marus ", 23 | "license": "MIT", 24 | "dependencies": { 25 | "debug": "2.6.8", 26 | "lodash": "4.17.4", 27 | "mime-types": "2.1.15", 28 | "moment": "2.19.x", 29 | "node-sparky": "3.1.x", 30 | "redis": "2.8.x", 31 | "uuid": "3.1.0", 32 | "webex": "^1.61.1", 33 | "when": "3.7.8" 34 | }, 35 | "devDependencies": { 36 | "doctoc": "^1.3.0", 37 | "jsdoc-to-markdown": "^3.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /quickstart/README.md: -------------------------------------------------------------------------------- 1 | # Flint Quick Start on Cloud 9 2 | The following is a quick demo of building a bot using the new FLint version 4 framework. The example code used in the video is included below. 3 | 4 | 5 | #### Video 6 | [![Flint version 4 Howto](https://img.youtube.com/vi/nx3kvs-gB_I/0.jpg)](https://www.youtube.com/watch?v=nx3kvs-gB_I) 7 | 8 | #### package.json 9 | 10 | ```json 11 | { 12 | "name": "workspace", 13 | "version": "1.0.0", 14 | "description": "", 15 | "main": "mybot.js", 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "dependencies": { 22 | "body-parser": "^1.15.2", 23 | "express": "^4.14.0", 24 | "node-flint": "^4.0.1" 25 | } 26 | } 27 | ``` 28 | 29 | 30 | #### mybot.js 31 | 32 | ```js 33 | var Flint = require('node-flint'); 34 | var webhook = require('node-flint/webhook'); 35 | var express = require('express'); 36 | var bodyParser = require('body-parser'); 37 | var app = express(); 38 | app.use(bodyParser.json()); 39 | 40 | // flint options 41 | var config = { 42 | webhookUrl: 'https:///flint', 43 | token: '', 44 | port: 8080, 45 | removeWebhooksOnStart: false, 46 | maxConcurrent: 5, 47 | minTime: 50 48 | }; 49 | 50 | // init flint 51 | var flint = new Flint(config); 52 | flint.start(); 53 | 54 | // say hello 55 | flint.hears('/hello', function(bot, trigger) { 56 | bot.say('Hello %s!', trigger.personDisplayName); 57 | }); 58 | 59 | // add flint event listeners 60 | flint.on('message', function(bot, trigger, id) { 61 | flint.debug('"%s" said "%s" in room "%s"', trigger.personEmail, trigger.text, trigger.roomTitle); 62 | }); 63 | 64 | flint.on('initialized', function() { 65 | flint.debug('initialized %s rooms', flint.bots.length); 66 | }); 67 | 68 | // define express path for incoming webhooks 69 | app.post('/flint', webhook(flint)); 70 | 71 | // start express server 72 | var server = app.listen(config.port, function () { 73 | flint.debug('Flint listening on port %s', config.port); 74 | }); 75 | 76 | // gracefully shutdown (ctrl-c) 77 | process.on('SIGINT', function() { 78 | flint.debug('stoppping...'); 79 | server.close(); 80 | flint.stop().then(function() { 81 | process.exit(); 82 | }); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /storage/README.md: -------------------------------------------------------------------------------- 1 | # Flint Storage Modules 2 | 3 | This folder contains the storage modules that can optionally be used in Flint for the `bot.store()`, `bot.recall()` and `bot.forget()` methods. 4 | 5 | If not specified, Flint will default to the "memory" module. 6 | 7 | See the 'memory.js' module for an example, and 'template.js' as a starting point in defining your own Storage module. 8 | -------------------------------------------------------------------------------- /storage/memory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const when = require('when'); 4 | const _ = require('lodash'); 5 | 6 | module.exports = exports = function() { 7 | // define memstore object 8 | let memStore = {}; 9 | 10 | return { 11 | /** 12 | * Store key/value data. 13 | * 14 | * This method is exposed as bot.store(key, value); 15 | * 16 | * @function 17 | * @param {String} id - Room/Conversation/Context ID 18 | * @param {String} key - Key under id object 19 | * @param {(String|Number|Boolean|Array|Object)} value - Value of key 20 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 21 | */ 22 | store: function(id, key, value) { 23 | if(typeof id === 'string') { 24 | // if id does not exist, create 25 | if(!memStore[id]) { 26 | // create id object in memStore 27 | memStore[id] = {}; 28 | } 29 | 30 | if(typeof key === 'string' && typeof value !== 'undefined') { 31 | memStore[id][key] = value; 32 | return when(memStore[id][key]); 33 | } else { 34 | return when.reject(new Error('bot.store() must include a "key" argument of type "string"')); 35 | } 36 | 37 | } else { 38 | return when.reject(new Error('bot.store() Storage module must include a "id" argument of type "string"')); 39 | } 40 | }, 41 | 42 | /** 43 | * Recall value of data stored by 'key'. 44 | * 45 | * This method is exposed as bot.recall(key); 46 | * 47 | * @function 48 | * @param {String} id - Room/Conversation/Context ID 49 | * @param {String} [key] - Key under id object (optional). If key is not passed, all keys for id are returned as an object. 50 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 51 | */ 52 | recall: function(id, key) { 53 | if(typeof id === 'string') { 54 | // if key is defined and of type string.... 55 | if(typeof key === 'string') { 56 | // if id/key exists... 57 | if(memStore[id] && memStore[id][key]) { 58 | return when(memStore[id][key]); 59 | } else { 60 | return when.reject(new Error('bot.recall() could not find the value referenced by id/key')); 61 | } 62 | } 63 | 64 | // else if key is not defined 65 | else if(typeof key === 'undefined') { 66 | // if id exists... 67 | if(memStore[id]) { 68 | return when(memStore[id]); 69 | } else { 70 | return when.reject(new Error('bot.recall() has no key/values defined')); 71 | } 72 | } 73 | 74 | // else key is defined, but of wrong type 75 | else { 76 | return when.reject(new Error('bot.recall() key must be of type "string"')); 77 | } 78 | } else { 79 | return when.reject(new Error('bot.recall() Storage module must include a "id" argument of type "string"')); 80 | } 81 | }, 82 | 83 | /** 84 | * Forget a key or entire store. 85 | * 86 | * This method is exposed as bot.forget(key); 87 | * 88 | * @function 89 | * @param {String} id - Room/Conversation/Context ID 90 | * @param {String} [key] - Key under id object (optional). If key is not passed, id and all children are removed. 91 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 92 | */ 93 | forget: function(id, key) { 94 | if(typeof id === 'string') { 95 | // if key is defined and of type string.... 96 | if(typeof key === 'string') { 97 | // if id/key exists... 98 | if(memStore[id] && memStore[id][key]) { 99 | let deletedKey = _.cloneDeep(memStore[id][key]); 100 | delete memStore[id][key]; 101 | return when(deletedKey); 102 | } else { 103 | return when.reject(new Error('bot.forget() could not find the value referenced by id/key')); 104 | } 105 | } 106 | 107 | // else if key is not defined 108 | else if(typeof key === 'undefined') { 109 | // if id exists... 110 | if(memStore[id]) { 111 | let deletedId = _.cloneDeep(memStore[id]); 112 | delete memStore[id]; 113 | return when(deletedId); 114 | } else { 115 | return when.reject(new Error('bot.forget() has no key/values defined')); 116 | } 117 | } 118 | 119 | // else key is defined, but of wrong type 120 | else { 121 | return when.reject(new Error('bot.forget() key must be of type "string"')); 122 | } 123 | } else { 124 | return when.reject(new Error('bot.forget() Storage module must include a "id" argument of type "string"')); 125 | } 126 | } 127 | }; 128 | 129 | }; 130 | -------------------------------------------------------------------------------- /storage/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Redis = require('redis'); 4 | const when = require('when'); 5 | const _ = require('lodash'); 6 | 7 | // promisfy JSON.parse and JSON.stringify 8 | const jsonParse = when.lift(JSON.parse); 9 | const jsonStringify = when.lift(JSON.stringify); 10 | 11 | module.exports = exports = function(connectionUrl) { 12 | const redis = Redis.createClient({ url: connectionUrl }); 13 | 14 | return { 15 | 16 | /** 17 | * Store key/value data. 18 | * 19 | * This method is exposed as bot.store(key, value); 20 | * 21 | * @function 22 | * @param {String} id - Room/Conversation/Context ID 23 | * @param {String} key - Key under id object 24 | * @param {(String|Number|Boolean|Array|Object)} value - Value of key 25 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 26 | */ 27 | store: function(id, key, value) { 28 | if (id && key) { 29 | if (value) { 30 | return jsonStringify(value) 31 | .then(stringVal => when.promise((resolve, reject) => redis.hset(id, key, stringVal, (err, result) => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(result); 36 | } 37 | }))); 38 | } 39 | return when.promise((resolve, reject) => redis.hset(id, key, '', (err, result) => { 40 | if (err) { 41 | reject(err); 42 | } else { 43 | resolve(result); 44 | } 45 | })); 46 | } 47 | return when.reject(new Error('invalid args')); 48 | }, 49 | 50 | /** 51 | * Recall value of data stored by 'key'. 52 | * 53 | * This method is exposed as bot.recall(key); 54 | * 55 | * @function 56 | * @param {String} id - Room/Conversation/Context ID 57 | * @param {String} [key] - Key under id object (optional). If key is not passed, all keys for id are returned as an object. 58 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 59 | */ 60 | recall: function(id, key) { 61 | if (id) { 62 | if (key) { 63 | return when.promise((resolve, reject) => redis.hget(id, key, (err, result) => { 64 | if (err) { 65 | reject(err); 66 | } else { 67 | resolve(result); 68 | } 69 | })).then((res) => { 70 | const parsedRes = jsonParse(res) 71 | .catch(() => when(res)); 72 | return parsedRes; 73 | }); 74 | } 75 | return when.promise((resolve, reject) => redis.hgetall(id, (err, result) => { 76 | if (err) { 77 | reject(err); 78 | } else { 79 | resolve(result); 80 | } 81 | })).then((res) => { 82 | const resKeys = _.keys(res); 83 | return when.map(resKeys, (resKey) => { 84 | const parsedRes = jsonParse(res[resKey]) 85 | .catch(() => when(res[resKey])); 86 | return parsedRes; 87 | }); 88 | }); 89 | } 90 | return when.reject(new Error('invalid args')); 91 | }, 92 | 93 | /** 94 | * Forget a key or entire store. 95 | * 96 | * This method is exposed as bot.forget(key); 97 | * 98 | * @function 99 | * @param {String} id - Room/Conversation/Context ID 100 | * @param {String} [key] - Key under id object (optional). If key is not passed, id and all children are removed. 101 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 102 | */ 103 | forget: function(id, key) { 104 | if (id) { 105 | if (key) { 106 | return when.promise((resolve, reject) => redis.hdel(id, key, (err, result) => { 107 | if (err) { 108 | reject(err); 109 | } else { 110 | resolve(result); 111 | } 112 | })); 113 | } 114 | return when.promise((resolve, reject) => redis.del(id, (err, result) => { 115 | if (err) { 116 | resolve(true); 117 | } else { 118 | resolve(true); 119 | } 120 | })); 121 | } 122 | return when.reject(new Error('invalid args')); 123 | } 124 | }; 125 | 126 | }; 127 | -------------------------------------------------------------------------------- /storage/redis_old.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Redis = require('redis'); 4 | var when = require('when'); 5 | var _ = require('lodash'); 6 | 7 | function Storage(connectionUrl, name) { 8 | name = typeof name === 'string' ? name : 'flint'; 9 | var redis = Redis.createClient({ url: connectionUrl }); 10 | 11 | var memStore = {}; 12 | var memCache = {}; 13 | var active = false; 14 | var syncInterval = 1000; 15 | 16 | // load memStore state from redis 17 | function initRedis() { 18 | return when.promise((resolve, reject) => { 19 | redis.get(name, (err, res) => { 20 | if(err) { 21 | memStore = {}; 22 | } else if(res) { 23 | memStore = JSON.parse(res); 24 | } else { 25 | memStore = {}; 26 | } 27 | resolve(true); 28 | }); 29 | }); 30 | } 31 | 32 | // start periodicly sync of memStore state to redis 33 | function syncRedis() { 34 | // if memStore has changed... 35 | if(JSON.stringify(memCache) !== JSON.stringify(memStore)) { 36 | return when.promise((resolve, reject) => { 37 | var serializedStore = JSON.stringify(memStore); 38 | redis.set(name, serializedStore, err => { 39 | if(err) { 40 | reject(err); 41 | } else { 42 | resolve(true); 43 | } 44 | }); 45 | }) 46 | .delay(syncInterval) 47 | .catch(err => { 48 | console.log(err.stack); 49 | return when(true); 50 | }) 51 | .finally(() => { 52 | memCache = _.cloneDeep(memStore); 53 | if(active) syncRedis(memStore); 54 | return when(true); 55 | }); 56 | } 57 | 58 | // else memStore has not changed... 59 | else { 60 | return when(true) 61 | .delay(syncInterval) 62 | .then(() => { 63 | if(active) syncRedis(memStore); 64 | return when(true); 65 | }); 66 | } 67 | } 68 | 69 | // init redis and begin memStore sync 70 | initRedis() 71 | .then(() => { 72 | active = true; 73 | syncRedis(memStore); 74 | }); 75 | 76 | return { 77 | 78 | /** 79 | * Store key/value data. 80 | * 81 | * @function 82 | * @param {String} id 83 | * @param {String} key 84 | * @param {(String|Number|Boolean|Array|Object)} value 85 | * @returns {(String|Number|Boolean|Array|Object)} 86 | */ 87 | store: function(id, key, value) { 88 | if(!memStore[id]) { 89 | memStore[id] = {}; 90 | } 91 | 92 | if(typeof key === 'string' && typeof value !== 'undefined') { 93 | memStore[id][key] = value; 94 | return memStore[id][key]; 95 | } else { 96 | throw new Error('invalid data type or missing value'); 97 | return false; 98 | } 99 | }, 100 | 101 | /** 102 | * Recall value of data stored by 'key'. 103 | * 104 | * @function 105 | * @param {String} id 106 | * @param {String} key 107 | * @returns {(String|Number|Boolean|Array|Object|undefined)} 108 | */ 109 | recall: function(id, key) { 110 | if(memStore[id] && typeof key === 'string' && memStore[id][key]) { 111 | return memStore[id][key]; 112 | } else { 113 | return undefined; 114 | } 115 | }, 116 | 117 | /** 118 | * Forget a key or entire store. 119 | * 120 | * @function 121 | * @param {String} id 122 | * @param {String} [key] - Optional key value to forget. If key is not passed, id is removed. 123 | * @returns {Boolean} 124 | */ 125 | forget: function(id, key) { 126 | // if key is defined and of type string.... 127 | if(typeof key !== 'undefined' && typeof key === 'string') { 128 | 129 | // if id/key exists... 130 | if(memStore[id] && memStore[id][key]) { 131 | delete memStore[id][key]; 132 | } 133 | 134 | return true; 135 | } 136 | 137 | // else if key is not defined... 138 | else if(typeof key === 'undefined') { 139 | delete memStore[id]; 140 | return true; 141 | } 142 | 143 | // else key defined, but not of type string... 144 | else { 145 | throw new Error('invalid data type or missing value'); 146 | return false; 147 | } 148 | } 149 | 150 | }; 151 | } 152 | module.exports = Storage; 153 | -------------------------------------------------------------------------------- /storage/template.js: -------------------------------------------------------------------------------- 1 | // template for creating custom storage modules 2 | 3 | 'use strict'; 4 | 5 | module.exports = exports = function() { 6 | 7 | return { 8 | /** 9 | * Store key/value data. 10 | * 11 | * This method is exposed as bot.store(key, value); 12 | * 13 | * @function 14 | * @param {String} id - Room/Conversation/Context ID 15 | * @param {String} key - Key under id object 16 | * @param {(String|Number|Boolean|Array|Object)} value - Value of key 17 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 18 | */ 19 | store: function(id, key, value) { 20 | // if id does not exist, create 21 | // if success, return promise that resolves to value 22 | // if failure, returns a rejected promise 23 | }, 24 | 25 | /** 26 | * Recall value of data stored by 'key'. 27 | * 28 | * This method is exposed as bot.recall(key); 29 | * 30 | * @function 31 | * @param {String} id - Room/Conversation/Context ID 32 | * @param {String} [key] - Key under id object (optional). If key is not passed, all keys for id are returned as an object. 33 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 34 | */ 35 | recall: function(id, key) { 36 | // if exists, returns promise that resolves to value of id/key referenced 37 | // if does not exist, or a failure, returns a rejected promise 38 | }, 39 | 40 | /** 41 | * Forget a key or entire store. 42 | * 43 | * This method is exposed as bot.forget(key); 44 | * 45 | * @function 46 | * @param {String} id - Room/Conversation/Context ID 47 | * @param {String} [key] - Key under id object (optional). If key is not passed, id and all children are removed. 48 | * @returns {(Promise.|Promise.|Promise.|Promise.|Promise.)} 49 | */ 50 | forget: function(id, key) { 51 | // if exists, returns promise that resolves to value of deleted value 52 | // if does not exist, or a failure, returns a rejected promise 53 | } 54 | }; 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /templates/bothub-template/Procfile: -------------------------------------------------------------------------------- 1 | web: DEBUG=flint,bot node app.js 2 | -------------------------------------------------------------------------------- /templates/bothub-template/README.md: -------------------------------------------------------------------------------- 1 | #bothub-template 2 | (work in progress...) 3 | -------------------------------------------------------------------------------- /templates/bothub-template/app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Flint = require('node-flint'); 4 | var webhook = require('node-flint/webhook'); 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var path = require('path'); 8 | 9 | var config = require(path.join(__dirname, 'config.js')); 10 | 11 | // var RedisStore = require('node-flint/storage/redis'); 12 | 13 | var app = express(); 14 | app.use(bodyParser.json()); 15 | 16 | // init flint 17 | var flint = new Flint(config); 18 | 19 | // use redis storage 20 | // flint.storageDriver(new RedisStore(process.env.REDIS_URL)); 21 | 22 | //start flint, load plugin(s) 23 | flint.start() 24 | .then(() => { 25 | flint.use(path.join(__dirname, 'flint.js')); 26 | }) 27 | .then(() => { 28 | flint.debug('Flint has started'); 29 | }); 30 | 31 | // define express path for incoming webhooks 32 | app.post('/flint', webhook(flint)); 33 | 34 | // start express server 35 | var server = app.listen(process.env.PORT, function () { 36 | flint.debug('Flint listening on port %s', process.env.PORT); 37 | }); 38 | 39 | // gracefully shutdown (ctrl-c) 40 | process.on('SIGINT', function() { 41 | flint.debug('stoppping...'); 42 | server.close(); 43 | flint.stop().then(function() { 44 | process.exit(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /templates/bothub-template/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | token: process.env.TOKEN, 3 | maxPageItems: 100, 4 | maxConcurrent: 5, 5 | minTime: 50, 6 | requeueCodes: [ 429, 500, 501, 502, 503 ], 7 | requeueMinTime: 500, 8 | removeWebhooksOnStart: true, 9 | webhookSecret: process.env.FLYNN_APP_ID, 10 | webhookUrl: 'http://' + process.env.FLYNN_APP_NAME + '.engine.bothub.io/flint' 11 | }; 12 | -------------------------------------------------------------------------------- /templates/bothub-template/flint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(flint) { 4 | flint.hears('hello', function(bot, trigger) { 5 | bot.say('Hello %s!', trigger.personDisplayName); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /templates/bothub-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "app.js", 3 | "dependencies": { 4 | "body-parser": "^1.15.2", 5 | "express": "^4.14.0", 6 | "node-flint": "^4.2.0", 7 | "socket2me-client": "^1.2.0" 8 | }, 9 | "engines": { 10 | "node": ">4.4.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /templates/bothub-template/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Flint = require('node-flint'); 4 | var webhook = require('node-flint/webhook'); 5 | var Socket2meClient = require('socket2me-client'); 6 | var path = require('path'); 7 | 8 | var server = new Socket2meClient('https://socket.bothub.io'); 9 | 10 | // var RedisStore = require('node-flint/storage/redis'); 11 | 12 | // flint options 13 | var config = require(path.join(__dirname, 'config.js')); 14 | 15 | // get a remote webhook from socket2me server 16 | server.on('connected', function(webhookUrl) { 17 | config.webhookUrl = webhookUrl; 18 | 19 | var flint = new Flint(config); 20 | 21 | // use redis storage 22 | // flint.storageDriver(new RedisStore('redis://127.0.0.1')); 23 | 24 | //start flint, load plugin(s) 25 | flint.start() 26 | .then(() => { 27 | flint.use(path.join(__dirname, 'flint.js')); 28 | }) 29 | .then(() => { 30 | flint.debug('Flint has started'); 31 | }); 32 | 33 | server.requestHandler(function(request, respond) { 34 | webhook(flint)(request); 35 | respond(200, 'OK'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /webhook.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/webhook'); 2 | --------------------------------------------------------------------------------