From 5c3c0e66d4704d4b6e8ccd5944ea79abc429629b Mon Sep 17 00:00:00 2001 From: lot Date: Thu, 11 Jan 2024 21:58:38 +0800 Subject: [PATCH] init --- .eslintrc.json | 23 + .gitignore | 8 + .nycrc | 20 + .vscode/launch.json | 14 + .vscode/settings.json | 19 + LICENSE | 21 + README.md | 634 ++++++ coverage/lcov.info | 787 +++++++ dist/config/asteroidInterfaces.d.ts | 110 + dist/config/asteroidInterfaces.js | 3 + dist/config/asteroidInterfaces.js.map | 1 + dist/config/driverInterfaces.d.ts | 42 + dist/config/driverInterfaces.js | 3 + dist/config/driverInterfaces.js.map | 1 + dist/config/messageInterfaces.d.ts | 70 + dist/config/messageInterfaces.js | 4 + dist/config/messageInterfaces.js.map | 1 + dist/index.d.ts | 5 + dist/index.js | 18 + dist/index.js.map | 1 + dist/lib/api.d.ts | 87 + dist/lib/api.js | 218 ++ dist/lib/api.js.map | 1 + dist/lib/driver.d.ts | 201 ++ dist/lib/driver.js | 666 ++++++ dist/lib/driver.js.map | 1 + dist/lib/log.d.ts | 5 + dist/lib/log.js | 37 + dist/lib/log.js.map | 1 + dist/lib/message.d.ts | 13 + dist/lib/message.js | 23 + dist/lib/message.js.map | 1 + dist/lib/methodCache.d.ts | 46 + dist/lib/methodCache.js | 97 + dist/lib/methodCache.js.map | 1 + dist/lib/settings.d.ts | 16 + dist/lib/settings.js | 28 + dist/lib/settings.js.map | 1 + dist/utils/config.d.ts | 4 + dist/utils/config.js | 34 + dist/utils/config.js.map | 1 + dist/utils/interfaces.d.ts | 154 ++ dist/utils/interfaces.js | 3 + dist/utils/interfaces.js.map | 1 + dist/utils/setup.d.ts | 1 + dist/utils/setup.js | 8 + dist/utils/setup.js.map | 1 + dist/utils/start.d.ts | 1 + dist/utils/start.js | 65 + dist/utils/start.js.map | 1 + dist/utils/testing.d.ts | 49 + dist/utils/testing.js | 238 ++ dist/utils/testing.js.map | 1 + dist/utils/users.d.ts | 1 + dist/utils/users.js | 50 + dist/utils/users.js.map | 1 + docma.json | 79 + favicon.ico | Bin 0 -> 15086 bytes package.json | 79 + src/config/asteroidInterfaces.ts | 124 ++ src/config/driverInterfaces.ts | 45 + src/config/messageInterfaces.ts | 75 + src/index.spec.ts | 13 + src/index.ts | 10 + src/lib/api.spec.ts | 123 ++ src/lib/api.ts | 219 ++ src/lib/driver.spec.ts | 458 ++++ src/lib/driver.ts | 568 +++++ src/lib/log.ts | 42 + src/lib/message.spec.ts | 47 + src/lib/message.ts | 23 + src/lib/methodCache.spec.ts | 150 ++ src/lib/methodCache.ts | 90 + src/lib/settings.spec.ts | 76 + src/lib/settings.ts | 30 + src/types/Asteroid.d.ts | 1 + src/types/Client.d.ts | 1 + src/types/immutableCollectionMixin.d.ts | 1 + src/utils/.DS_Store | Bin 0 -> 6148 bytes src/utils/config.ts | 35 + src/utils/interfaces.ts | 168 ++ src/utils/setup.ts | 5 + src/utils/start.ts | 50 + src/utils/testing.ts | 214 ++ src/utils/users.ts | 32 + test/mocha.opts | 7 + tsconfig.json | 54 + tslint.json | 14 + typedoc.json | 10 + wallaby.js | 23 + yarn.lock | 2653 +++++++++++++++++++++++ 91 files changed, 9361 insertions(+) create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .nycrc create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 coverage/lcov.info create mode 100644 dist/config/asteroidInterfaces.d.ts create mode 100644 dist/config/asteroidInterfaces.js create mode 100644 dist/config/asteroidInterfaces.js.map create mode 100644 dist/config/driverInterfaces.d.ts create mode 100644 dist/config/driverInterfaces.js create mode 100644 dist/config/driverInterfaces.js.map create mode 100644 dist/config/messageInterfaces.d.ts create mode 100644 dist/config/messageInterfaces.js create mode 100644 dist/config/messageInterfaces.js.map create mode 100644 dist/index.d.ts create mode 100644 dist/index.js create mode 100644 dist/index.js.map create mode 100644 dist/lib/api.d.ts create mode 100644 dist/lib/api.js create mode 100644 dist/lib/api.js.map create mode 100644 dist/lib/driver.d.ts create mode 100644 dist/lib/driver.js create mode 100644 dist/lib/driver.js.map create mode 100644 dist/lib/log.d.ts create mode 100644 dist/lib/log.js create mode 100644 dist/lib/log.js.map create mode 100644 dist/lib/message.d.ts create mode 100644 dist/lib/message.js create mode 100644 dist/lib/message.js.map create mode 100644 dist/lib/methodCache.d.ts create mode 100644 dist/lib/methodCache.js create mode 100644 dist/lib/methodCache.js.map create mode 100644 dist/lib/settings.d.ts create mode 100644 dist/lib/settings.js create mode 100644 dist/lib/settings.js.map create mode 100644 dist/utils/config.d.ts create mode 100644 dist/utils/config.js create mode 100644 dist/utils/config.js.map create mode 100644 dist/utils/interfaces.d.ts create mode 100644 dist/utils/interfaces.js create mode 100644 dist/utils/interfaces.js.map create mode 100644 dist/utils/setup.d.ts create mode 100644 dist/utils/setup.js create mode 100644 dist/utils/setup.js.map create mode 100644 dist/utils/start.d.ts create mode 100644 dist/utils/start.js create mode 100644 dist/utils/start.js.map create mode 100644 dist/utils/testing.d.ts create mode 100644 dist/utils/testing.js create mode 100644 dist/utils/testing.js.map create mode 100644 dist/utils/users.d.ts create mode 100644 dist/utils/users.js create mode 100644 dist/utils/users.js.map create mode 100644 docma.json create mode 100644 favicon.ico create mode 100644 package.json create mode 100644 src/config/asteroidInterfaces.ts create mode 100644 src/config/driverInterfaces.ts create mode 100644 src/config/messageInterfaces.ts create mode 100644 src/index.spec.ts create mode 100644 src/index.ts create mode 100644 src/lib/api.spec.ts create mode 100644 src/lib/api.ts create mode 100644 src/lib/driver.spec.ts create mode 100644 src/lib/driver.ts create mode 100644 src/lib/log.ts create mode 100644 src/lib/message.spec.ts create mode 100644 src/lib/message.ts create mode 100644 src/lib/methodCache.spec.ts create mode 100644 src/lib/methodCache.ts create mode 100644 src/lib/settings.spec.ts create mode 100644 src/lib/settings.ts create mode 100644 src/types/Asteroid.d.ts create mode 100644 src/types/Client.d.ts create mode 100644 src/types/immutableCollectionMixin.d.ts create mode 100644 src/utils/.DS_Store create mode 100644 src/utils/config.ts create mode 100644 src/utils/interfaces.ts create mode 100644 src/utils/setup.ts create mode 100644 src/utils/start.ts create mode 100644 src/utils/testing.ts create mode 100644 src/utils/users.ts create mode 100644 test/mocha.opts create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 typedoc.json create mode 100644 wallaby.js create mode 100644 yarn.lock diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7db5754 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,23 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es6": true, + "node": true + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "sourceType": "module" + }, + "rules": { + "no-const-assign": "warn", + "no-this-before-super": "warn", + "no-undef": "warn", + "no-unreachable": "warn", + "no-unused-vars": "warn", + "constructor-super": "warn", + "valid-typeof": "warn" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab6b7b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/node_modules +/yarn-error.log +/.nyc_output +/.env +/shrinkwrap.yaml +/package-lock.json +/docs/ +.DS_Store \ No newline at end of file diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..90662d1 --- /dev/null +++ b/.nycrc @@ -0,0 +1,20 @@ +{ + "extension": [ + ".ts" + ], + "require": [ + "ts-node/register" + ], + "include": [ + "src/lib/**/*.ts" + ], + "exclude": [ + "**/*.d.ts", + "**/*.spec.ts" + ], + "reporter": [ + "lcovonly", + "text" + ], + "all": true +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..00fabf6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach", + "port": 9229 + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b7a36cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "tslint.autoFixOnSave": true, + "tslint.jsEnable": false, + "mocha-snippets.semicolon": false, + "mocha-snippets.glob": "src/**/*.spec.ts", + "mocha.files.glob": "src/**/*.spec.ts", + "mocha.requires": [ + "dotenv/config", + "ts-node/register" + ], + "mocha.options": { + "reporter": "list" + }, + "coverage-gutters.showGutterCoverage": true, + "eslint.packageManager": "yarn", + "search.exclude": { + "**/dist": true + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c13d939 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Rocket.Chat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c1b975d --- /dev/null +++ b/README.md @@ -0,0 +1,634 @@ +[asteroid]: https://www.npmjs.com/package/asteroid +[lru]: https://www.npmjs.com/package/lru +[rest]: https://rocket.chat/docs/developer-guides/rest-api/ +[start]: https://github.com/RocketChat/Rocket.Chat.js.SDK/blob/master/src/utils/start.ts + +# Rocket.Chat Node.js SDK + +Application interface for server methods and message stream subscriptions. + +## Super Quick Start (30 seconds) + +Create your own working BOT for Rocket.Chat, in seconds, at [glitch.com](https://glitch.com/~rocketchat-bot). + +## Quick Start + +Add your own Rocket.Chat BOT, running on your favorite Linux, MacOS or Windows system. + +First, make sure you have the latest version of [nodeJS](https://nodejs.org/) (nodeJS 8.x or higher). + +``` +node -v +v8.9.3 +``` + +In a project directory, add Rocket.Chat.js.SDK as dependency: + +``` +npm install @rocket.chat/sdk --save +``` + +Next, create _easybot.js_ with the following: + +```js +const { driver } = require('@rocket.chat/sdk'); +// customize the following with your server and BOT account information +const HOST = 'myserver.com'; +const USER = 'mysuer'; +const PASS = 'mypassword'; +const BOTNAME = 'easybot'; // name bot response to +const SSL = true; // server uses https ? +const ROOMS = ['GENERAL', 'myroom1']; + +var myuserid; +// this simple bot does not handle errors, different message types, server resets +// and other production situations + +const runbot = async () => { + const conn = await driver.connect( { host: HOST, useSsl: SSL}) + myuserid = await driver.login({username: USER, password: PASS}); + const roomsJoined = await driver.joinRooms(ROOMS); + console.log('joined rooms'); + + // set up subscriptions - rooms we are interested in listening to + const subscribed = await driver.subscribeToMessages(); + console.log('subscribed'); + + // connect the processMessages callback + const msgloop = await driver.reactToMessages( processMessages ); + console.log('connected and waiting for messages'); + + // when a message is created in one of the ROOMS, we + // receive it in the processMesssages callback + + // greets from the first room in ROOMS + const sent = await driver.sendToRoom( BOTNAME + ' is listening ...',ROOMS[0]); + console.log('Greeting message sent'); +} + +// callback for incoming messages filter and processing +const processMessages = async(err, message, messageOptions) => { + if (!err) { + // filter our own message + if (message.u._id === myuserid) return; + // can filter further based on message.rid + const roomname = await driver.getRoomName(message.rid); + if (message.msg.toLowerCase().startsWith(BOTNAME)) { + const response = message.u.username + + ', how can ' + BOTNAME + ' help you with ' + + message.msg.substr(BOTNAME.length + 1); + const sentmsg = await driver.sendToRoom(response, roomname); + } + } +} + +runbot() +``` + +The above code uses async calls to login, join rooms, subscribe to +message streams and respond to messages (with a callback) using provided +options to filter the types of messages to respond to. + +Make sure you customize the constants to your Rocket.Chat server account. + +Finally, run the bot: + +``` +node easybot.js +``` + +_TBD: insert screenshot of bot working on a server_ + +### Demo + +There's a simple listener script provided to demonstrate functionality locally. +[See the source here][start] and/or run it with `yarn start`. + +The start script will log to console any message events that appear in its +stream. It will respond to a couple specific commands demonstrating usage of +the API helpers. Try messaging the bot directly one of the following: + +- `tell everyone ` - It will send that "something" to everyone +- `who's online` - It will tell you who's online + +## Overview + +Using this package third party apps can control and query a Rocket.Chat server +instance, via Asteroid login and method calls as well as DDP for subscribing +to stream events. + +Designed especially for chat automation, this SDK makes it easy for bot and +integration developers to provide the best solutions and experience for their +community. + +For example, the Hubot Rocketchat adapter uses this package to enable chat-ops +workflows and multi-channel, multi-user, public and private interactions. +We have more bot features and adapters on the roadmap and encourage the +community to implement this SDK to provide adapters for their bot framework +or platform of choice. + +## Docs + +Full documentation can be generated locally using `yarn docs`. +This isn't in a format we can publish yet, but can be useful for development. + +Below is just a summary: + +--- + +The following modules are exported by the SDK: +- `driver` - Handles connection, method calls, room subscriptions (via Asteroid) +- `methodCache` - Manages results cache for calls to server (via LRU cache) +- `api` - Provides a client for making requests with Rocket.Chat's REST API + +Access these modules by importing them from SDK, e.g: + +For Node 8 / ES5 + +```js +const { driver, methodCache, api } = require('@rocket.chat/sdk') +``` + +For ES6 supporting platforms + +```js +import { driver, methodCache, api } from '@rocket.chat/sdk' +``` + +Any Rocket.Chat server method can be called via `driver.callMethod`, +`driver.cacheCall` or `driver.asyncCall`. Server methods are not fully +documented, most require searching the Rocket.Chat codebase. + +Driver methods use an [Asteroid][asteroid] DDP connection. See its own docs for +more advanced methods that can be called from the `driver.asteroid` interface. + +Rocket.Chat REST API calls can be made via `api.get` or `api.post`, with +parameters defining the endpoint, payload and if authorization is required +(respectively). See the [REST API docs][rest] for details. + +Some common requests for user queries are made available as simple helpers under +`api.users`, such as `api.users.onlineIds()` which returns the user IDs of all +online users. Run `ts-node src/utils/users.ts` for a demo of user query outputs. + +## MESSAGE OBJECTS + +--- + +The Rocket.Chat message schema can be found here: +https://rocket.chat/docs/developer-guides/schema-definition/ + +The structure for messages in this package matches that schema, with a +TypeScript interface defined here: https://github.com/RocketChat/Rocket.Chat.js.SDK/blob/master/src/config/messageInterfaces.ts + +The `driver.prepareMessage` method (documented below) provides a helper for +simple message creation and the `message` module can also be imported to create +new `Message` class instances directly if detailed attributes are required. + +## DRIVER METHODS + +--- + +### `driver.connect(options[, cb])` + +Connects to a Rocket.Chat server +- Options accepts `host` and `timeout` attributes +- Can return a promise, or use error-first callback pattern +- Resolves with an [Asteroid][asteroid] instance + +### `driver.disconnect()` + +Unsubscribe, logout, disconnect from Rocket.Chat +- Returns promise + +### `driver.login([credentials])` + +Login to Rocket.Chat via Asteroid +- Accepts object with `username` and/or `email` and `password` +- Uses defaults from env `ROCKETCHAT_USER` and `ROCKETCHAT_PASSWORD` +- Returns promise +- Resolves with logged in user ID + +### `driver.logout()` + +Logout current user via Asteroid +- Returns promise + +### `driver.subscribe(topic, roomId)` + +Subscribe to Meteor subscription +- Accepts parameters for Rocket.Chat streamer +- Returns promise +- Resolves with subscription instance (with ID) + +### `driver.unsubscribe(subscription)` + +Cancel a subscription +- Accepts a subscription instance +- Returns promise + +### `driver.unsubscribeAll()` + +Cancel all current subscriptions +- Returns promise + +### `driver.subscribeToMessages()` + +Shortcut to subscribe to user's message stream +- Uses `.subscribe` arguments with defaults + - topic: `stream-room-messages` + - roomId: `__my_messages__` +- Returns a subscription instance + +### `driver.reactToMessages(callback)` + +Once a subscription is created, using `driver.subscribeToMessages()` this method +can be used to attach a callback to changes in the message stream. + +Fires callback with every change in subscriptions. +- Uses error-first callback pattern +- Second argument is the changed item +- Third argument is additional attributes, such as `roomType` + +For example usage, see the Rocket.Chat Hubot adapter's receive function, which +is bound as a callback to this method: +https://github.com/RocketChat/hubot-rocketchat/blob/convert-es6/index.js#L97-L193 + +### `driver.respondToMessages(callback, options)` + +Proxy for `reactToMessages` with some filtering of messages based on config. +This is a more user-friendly method for bots to subscribe to a message stream. + +Fires callback after filters run on subscription events. +- Uses error-first callback pattern +- Second argument is the changed item +- Third argument is additional attributes, such as `roomType` + +Accepts options object, that parallels respond filter env variables: +- options.rooms : respond to messages in joined rooms +- options.allPublic : respond to messages on all channels +- options.dm : respond to messages in DMs with the SDK user +- options.livechat : respond to messages in Livechat rooms +- options.edited : respond to edited messages + +If rooms are given as option or set in the environment with `ROCKETCHAT_ROOM` +but have not been joined yet this method will join to those rooms automatically. + +If `allPublic` is true, the `rooms` option will be ignored. + +### `driver.asyncCall(method, params)` + +Wraps server method calls to always be async +- Accepts a method name and params (array or single param) +- Returns a Promise + +### `driver.cacheCall(method, key)` + +Call server method with `methodCache` +- Accepts a method name and single param (used as cache key) +- Returns a promise +- Resolves with server results or cached if still valid + +### `driver.callMethod(method, params)` + +Implements either `asyncCall` or `cacheCall` if cache exists +- Accepts a method name and params (array or single param) +- Outcome depends on if `methodCache.create` was done for the method + +### `driver.useLog(logger)` + +Replace the default log, e.g. with one from a bot framework +- Accepts class or object with `debug`, `info`, `warn`, `error` methods. +- Returns nothing + +### `driver.getRoomId(name)` + +Get ID for a room by name +- Accepts name or ID string +- Is cached +- Returns a promise +- Resolves with room ID + +### `driver.getRoomName(id)` + +Get name for a room by ID +- Accepts ID string +- Is cached +- Returns a promise +- Resolves with room name + +### `driver.getDirectMessageRoomId(username)` + +Get ID for a DM room by its recipient's name +- Accepts string username +- Returns a promise +- Resolves with room ID + +### `driver.joinRoom(room)` + +Join the logged in user into a room +- Accepts room name or ID string +- Returns a promise + +### `driver.joinRooms(rooms)` + +As above, with array of room names/IDs + +### `driver.prepareMessage(content[, roomId])` + +Structure message content for sending +- Accepts a message object or message text string +- Optionally addressing to room ID with second param +- Returns a message object + +### `driver.sendMessage(message)` + +Send a prepared message object (with pre-defined room ID) +- Accepts a message object +- Returns a promise that resolves to sent message object + +### `driver.sendToRoomId(content, roomId)` + +Prepare and send string/s to specified room ID +- Accepts message text string or array of strings +- Returns a promise or array of promises that resolve to sent message object/s + +### `driver.sendToRoom(content, room)` + +As above, with room name instead of ID + +### `driver.sendDirectToUser(content, username)` + +As above, with username for DM instead of ID +- Creates DM room if it doesn't exist + +--- + +## METHOD CACHE + +[LRU][lru] is used to cache results from the server, to reduce unnecessary calls +for data that is unlikely to change, such as room IDs. Utility methods and env +vars allow configuring, creating and resetting caches for specific methods. + +--- + +### `methodCache.use(instance)` + +Set the instance to call methods on, with cached results +- Accepts an Asteroid instance (or possibly other classes) +- Returns nothing + +### `methodCache.create(method[, options])` + +Setup a cache for a method call +- Accepts method name and cache options object, such as: + - `max` Maximum size of cache + - `maxAge` Maximum age of cache + +### `methodCache.call(method, key)` + +Get results of a prior method call or call and cache +- Accepts method name to call and key as single param +- Only methods with a single string argument can be cached (currently) due to +the usage of this argument as the index for the cached results. + +### `methodCache.has(method)` + +Checking if method has been cached +- Accepts method name +- Returns bool + +### `methodCache.get(method, key)` + +Get results of a prior method call +- Accepts method name and key (argument method called with) +- Returns results at key + +### `methodCache.reset(method[, key])` + +Reset a cached method call's results +- Accepts a method name, optional key +- If key given, clears only that result set +- Returns bool + +### `methodCache.resetAll()` + + Reset cached results for all methods + - Returns nothing + +--- + +### API CLIENT + +[node-rest]: https://www.npmjs.com/package/node-rest-client +[rest-api]: https://rocket.chat/docs/developer-guides/rest-api/ +We've included an [API client][node-rest] to make it super simple for bots and +apps consuming the SDK to call the [Rocket.Chat REST API][rest-api] endpoints. + +By default, it will attempt to login with the same defaults or env config as +the driver, but the `.login` method could be used manually prior to requests to +use different credentials. + +If a request is made to an endpoint requiring authentication, before login is +called, it will attempt to login first and keep the response token for later. + +Bots and apps should manually call the API `.logout` method on shutdown if they +have used the API. + +--- + +### `api.loggedIn()` + +Returns boolean status of existing login + +### `api.post(endpoint, data[, auth, ignore])` + +Make a POST request to the REST API +- `endpoint` - The API resource ID, e.g. `channels.info` +- `data` - Request payload object to send, e.g. { roomName: 'general' } +- `auth` - If authorisation is required (defaults to true) +- Returns promise + +### `api.get(endpoint, data[, auth, ignore])` + +Make a GET request to the REST API +- `endpoint` - The API endpoint resource ID, e.g. `users.list` +- `data` - Params (converted to query string), e.g. { fields: { 'username': 1 } } +- `auth` - If authorisation is required (defaults to true) +- Returns promise + +### `api.login([user])` + +Perform login with default or given credentials +- `user` object with `.username` and `.password` properties. +- Returns promise, resolves with login result + +### `api.logout()` + +Logout the current user. Returns promise + +### `api.currentLogin` + +Exported property with details of the current API session +- `.result` - The login request result +- `.username` - The logged in user's username +- `.userId` - The logged in user's ID +- `.authToken` - The current auth token + +### `api.userFields` + +Exported property for user query helper default fields +- Defaults to `{ name: 1, username: 1, status: 1, type: 1 }` +- See https://rocket.chat/docs/developer-guides/rest-api/query-and-fields-info/ + +### `api.users.all([fields])` + +Helper for querying all users +- Optional fields object (see fields docs link above) +- Returns promise, resolves with array of user objects + +### `api.users.allNames()` + +Helper for querying all usernames +- Returns promise, resolves with array of usernames + +### `api.users.allIDs()` + +Helper for querying all user IDs +- Returns promise, resolves with array of IDs + +### `api.users.online([fields])` + +Helper for querying online users +- Optional fields object (see fields docs link above) +- Returns promise, resolves with array of user objects + +### `api.users.onlineNames()` + +Helper for querying online usernames +- Returns promise, resolves with array of usernames + +### `api.users.onlineIds()` + +Helper for querying online user IDs +- Returns promise, resolves with array of IDs + +--- + +## Development + +A local instance of Rocket.Chat is required for unit tests to confirm connection +and subscription methods are functional. And it helps to manually run your SDK +interactions (i.e. bots) locally while in development. + +## Use as Dependency + +``` +yarn add @rocket.chat/sdk +``` +or + +``` +npm install --save @rocket.chat/sdk +``` + +ES6 module, using async + +```js +import * as rocketchat from '@rocket.chat/sdk' + +const asteroid = await rocketchat.driver.connect({ host: 'localhost:3000' }) +console.log('connected', asteroid) +``` + +ES5 module, using callback + +```js +const rocketchat = require('@rocket.chat/sdk') + +rocketchat.driver.connect({ host: 'localhost:3000' }, function (err, asteroid) { + if (err) console.error(err) + else console.log('connected', asteroid) +}) +``` + +### Settings + +| Env var | Description | +| ---------------------- | ----------------------------------------------------- | +| `ROCKETCHAT_URL`* | URL of the Rocket.Chat to connect to | +| `ROCKETCHAT_USER`* | Username for bot account login | +| `ROCKETCHAT_PASSWORD`* | Password for bot account login | +| `ROCKETCHAT_AUTH` | Set to 'ldap' to enable LDAP login | +| `ROCKETCHAT_USE_SSL` | Force bot to connect with SSL | +| `ROCKETCHAT_ROOM` | Respond listens in the named channel/s (can be csv) | +| `LISTEN_ON_ALL_PUBLIC` | true/false, respond listens in all public channels | +| `RESPOND_TO_LIVECHAT` | true/false, respond listens in livechat | +| `RESPOND_TO_DM` | true/false, respond listens to DMs with bot | +| `RESPOND_TO_EDITED` | true/false, respond listens to edited messages | +| `INTEGRATION_ID` | ID applied to message object to integration source | +| **Advanced configs** | | +| `ROOM_CACHE_SIZE` | Size of cache (LRU) for room (ID or name) lookups | +| `ROOM_CACHE_MAX_AGE` | Max age of cache for room lookups | +| `DM_ROOM_CACHE_SIZE` | Size of cache for Direct Message room lookups | +| `DM_ROOM_CACHE_MAX_AGE`| Max age of cache for DM lookups | +| **Test configs** | | +| `ADMIN_USERNAME` | Admin user password for API | +| `ADMIN_PASS` | Admin user password for API | + +These are only required in test and development, assuming in production they +will be passed from the adapter implementing this package. + +`ROCKETCHAT_ROOM` is ignored when using `LISTEN_ON_ALL_PUBLIC`. This option also +allows the bot to listen and respond to messages _from all private groups_ where +the bot's user has been added as a member. + +### Installing Rocket.Chat + +Clone and run a new instance of Rocket.Chat locally, using either the internal +mongo or a dedicated local mongo for testing, so you won't affect any other +Rocket.Chat development you might do locally. + +The following will provision a default admin user on build, so it can be used to +access the API, allowing SDK utils to prepare for and clean up tests. + +``` +git clone https://github.com/RocketChat/Rocket.Chat.git rc-sdk-test +cd rc-sdk-test +meteor npm install +export ADMIN_PASS=pass; export ADMIN_USERNAME=sdk; export MONGO_URL='mongodb://localhost:27017/rc-sdk-test'; meteor +``` + +Using `yarn` to run local tests and build scripts is recommended. + +Do `npm install -g yarn` if you don't have it. Then setup the project: + +``` +git clone https://github.com/RocketChat/Rocket.Chat.js.SDK.git +cd Rocket.Chat.js.SDK +yarn +``` + +### Test and Build Scripts + +- `yarn test` runs tests and coverage locally (pretest does lint) +- `yarn test:debug` runs tests without coverage, breaking for debug attach +- `yarn start` run locally from source, to allow manual testing of streams +- `yarn docs` generates API docs locally, then `open docs/index.html` +- `yarn build` runs tests, coverage, compiles, and tests package for publishing +- `yarn test:package` uses package-preview to make sure the published node +package can be required and run only with defined dependencies, to avoid errors +that might pass locally due to existing global dependencies or symlinks. + +`yarn:hook` is run on git push hooks to prevent publishing with failing tests, +but won't change coverage to avoid making any working copy changes after commit. + +### Integration Tests + +The node scripts in `utils` are used to prepare for and clean up after test +interactions. They use the Rocket.Chat API to create a bot user and a mock human +user for the bot to interact with. It is always advised to only run tests with +a connection to a clean local or re-usable container instance of Rocket.Chat. + +### Debugging + +Configs are included in source for VS Code using Wallaby or Mocha Sidebar. diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..c237f62 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,787 @@ +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/api.ts +FN:25,loggedIn +FN:42,getQueryString +FN:44,(anonymous_9) +FN:57,setAuth +FN:63,getHeaders +FN:76,clearHeaders +FN:82,success +FN:104,post +FN:114,(anonymous_16) +FN:115,(anonymous_17) +FN:119,(anonymous_18) +FN:136,get +FN:147,(anonymous_21) +FN:148,(anonymous_22) +FN:152,(anonymous_23) +FN:166,login +FN:196,logout +FN:202,(anonymous_27) +FN:213,(anonymous_28) +FN:213,(anonymous_29) +FN:214,(anonymous_30) +FN:214,(anonymous_31) +FN:214,(anonymous_32) +FN:215,(anonymous_33) +FN:215,(anonymous_34) +FN:215,(anonymous_35) +FN:216,(anonymous_36) +FN:216,(anonymous_37) +FN:217,(anonymous_38) +FN:217,(anonymous_39) +FN:217,(anonymous_40) +FN:218,(anonymous_41) +FN:218,(anonymous_42) +FN:218,(anonymous_43) +FNF:34 +FNH:16 +FNDA:65,loggedIn +FNDA:50,getQueryString +FNDA:66,(anonymous_9) +FNDA:12,setAuth +FNDA:81,getHeaders +FNDA:15,clearHeaders +FNDA:83,success +FNDA:31,post +FNDA:31,(anonymous_16) +FNDA:31,(anonymous_17) +FNDA:0,(anonymous_18) +FNDA:47,get +FNDA:47,(anonymous_21) +FNDA:47,(anonymous_22) +FNDA:0,(anonymous_23) +FNDA:26,login +FNDA:25,logout +FNDA:12,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:0,(anonymous_29) +FNDA:0,(anonymous_30) +FNDA:0,(anonymous_31) +FNDA:0,(anonymous_32) +FNDA:0,(anonymous_33) +FNDA:0,(anonymous_34) +FNDA:0,(anonymous_35) +FNDA:0,(anonymous_36) +FNDA:0,(anonymous_37) +FNDA:0,(anonymous_38) +FNDA:0,(anonymous_39) +FNDA:0,(anonymous_40) +FNDA:0,(anonymous_41) +FNDA:0,(anonymous_42) +FNDA:0,(anonymous_43) +DA:1,1 +DA:2,1 +DA:3,1 +DA:17,1 +DA:25,1 +DA:26,65 +DA:30,1 +DA:31,1 +DA:37,1 +DA:42,1 +DA:43,50 +DA:44,36 +DA:45,66 +DA:48,66 +DA:53,1 +DA:54,1 +DA:57,1 +DA:58,12 +DA:59,12 +DA:63,1 +DA:64,81 +DA:65,67 +DA:70,1 +DA:72,66 +DA:76,1 +DA:77,15 +DA:78,15 +DA:82,1 +DA:83,83 +DA:104,1 +DA:110,31 +DA:111,31 +DA:112,31 +DA:113,31 +DA:114,31 +DA:115,31 +DA:116,31 +DA:117,31 +DA:118,31 +DA:119,0 +DA:121,31 +DA:122,31 +DA:124,0 +DA:125,0 +DA:136,1 +DA:142,47 +DA:143,47 +DA:144,47 +DA:145,47 +DA:146,47 +DA:147,47 +DA:148,47 +DA:149,47 +DA:150,47 +DA:151,47 +DA:152,0 +DA:154,47 +DA:155,47 +DA:157,0 +DA:166,1 +DA:170,26 +DA:171,26 +DA:172,19 +DA:173,19 +DA:174,14 +DA:176,5 +DA:179,12 +DA:180,12 +DA:181,12 +DA:187,12 +DA:188,12 +DA:189,12 +DA:191,0 +DA:196,1 +DA:197,25 +DA:198,13 +DA:199,13 +DA:201,12 +DA:202,12 +DA:203,12 +DA:204,12 +DA:209,1 +DA:212,1 +DA:213,0 +DA:214,0 +DA:215,0 +DA:216,0 +DA:217,0 +DA:218,0 +LF:89 +LH:77 +BRDA:38,0,0,1 +BRDA:38,0,1,0 +BRDA:43,1,0,14 +BRDA:43,1,1,36 +BRDA:43,2,0,50 +BRDA:43,2,1,38 +BRDA:43,2,2,38 +BRDA:46,3,0,3 +BRDA:46,3,1,63 +BRDA:63,4,0,1 +BRDA:64,5,0,14 +BRDA:64,5,1,67 +BRDA:65,6,0,1 +BRDA:65,6,1,66 +BRDA:66,7,0,67 +BRDA:66,7,1,66 +BRDA:66,7,2,66 +BRDA:66,7,3,66 +BRDA:92,8,0,81 +BRDA:92,8,1,2 +BRDA:85,9,0,83 +BRDA:85,9,1,83 +BRDA:85,9,2,57 +BRDA:85,9,3,82 +BRDA:85,9,4,26 +BRDA:85,9,5,57 +BRDA:85,9,6,55 +BRDA:85,9,7,2 +BRDA:85,9,8,0 +BRDA:85,9,9,0 +BRDA:107,10,0,4 +BRDA:112,11,0,0 +BRDA:112,11,1,31 +BRDA:112,12,0,31 +BRDA:112,12,1,19 +BRDA:116,13,0,0 +BRDA:116,13,1,31 +BRDA:117,14,0,0 +BRDA:117,14,1,31 +BRDA:139,15,0,17 +BRDA:144,16,0,1 +BRDA:144,16,1,46 +BRDA:144,17,0,47 +BRDA:144,17,1,46 +BRDA:149,18,0,0 +BRDA:149,18,1,47 +BRDA:150,19,0,0 +BRDA:150,19,1,47 +BRDA:166,20,0,6 +BRDA:171,21,0,19 +BRDA:171,21,1,7 +BRDA:173,22,0,14 +BRDA:173,22,1,5 +BRDA:180,23,0,12 +BRDA:180,23,1,0 +BRDA:180,24,0,12 +BRDA:180,24,1,12 +BRDA:180,24,2,12 +BRDA:197,25,0,13 +BRDA:197,25,1,12 +BRDA:213,26,0,0 +BRDA:216,27,0,0 +BRF:62 +BRH:51 +end_of_record +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/driver.ts +FN:78,useLog +FN:99,connect +FN:103,(anonymous_10) +FN:110,(anonymous_11) +FN:111,(anonymous_12) +FN:116,(anonymous_13) +FN:118,(anonymous_14) +FN:129,(anonymous_15) +FN:141,disconnect +FN:145,(anonymous_17) +FN:155,setupMethodCache +FN:176,asyncCall +FN:180,(anonymous_20) +FN:184,(anonymous_21) +FN:199,callMethod +FN:210,cacheCall +FN:212,(anonymous_24) +FN:216,(anonymous_25) +FN:228,login +FN:249,(anonymous_27) +FN:253,(anonymous_28) +FN:260,logout +FN:262,(anonymous_30) +FN:273,subscribe +FN:277,(anonymous_32) +FN:282,(anonymous_33) +FN:290,unsubscribe +FN:300,unsubscribeAll +FN:301,(anonymous_36) +FN:308,subscribeToMessages +FN:310,(anonymous_38) +FN:339,reactToMessages +FN:342,(anonymous_40) +FN:367,respondToMessages +FN:384,(anonymous_42) +FN:390,(anonymous_43) +FN:390,(anonymous_44) +FN:436,getRoomId +FN:441,getRoomName +FN:450,getDirectMessageRoomId +FN:452,(anonymous_48) +FN:456,joinRoom +FN:468,leaveRoom +FN:480,joinRooms +FN:481,(anonymous_54) +FN:488,prepareMessage +FN:501,sendMessage +FN:514,sendToRoomId +FN:521,(anonymous_58) +FN:532,sendToRoom +FN:537,(anonymous_60) +FN:545,sendDirectToUser +FN:550,(anonymous_62) +FN:557,editMessage +FN:566,setReaction +FNF:55 +FNH:47 +FNDA:0,useLog +FNDA:37,connect +FNDA:37,(anonymous_10) +FNDA:33,(anonymous_11) +FNDA:1,(anonymous_12) +FNDA:0,(anonymous_13) +FNDA:4,(anonymous_14) +FNDA:33,(anonymous_15) +FNDA:1,disconnect +FNDA:1,(anonymous_17) +FNDA:37,setupMethodCache +FNDA:23,asyncCall +FNDA:0,(anonymous_20) +FNDA:23,(anonymous_21) +FNDA:1,callMethod +FNDA:13,cacheCall +FNDA:0,(anonymous_24) +FNDA:13,(anonymous_25) +FNDA:28,login +FNDA:28,(anonymous_27) +FNDA:0,(anonymous_28) +FNDA:2,logout +FNDA:0,(anonymous_30) +FNDA:15,subscribe +FNDA:15,(anonymous_32) +FNDA:15,(anonymous_33) +FNDA:8,unsubscribe +FNDA:1,unsubscribeAll +FNDA:8,(anonymous_36) +FNDA:15,subscribeToMessages +FNDA:15,(anonymous_38) +FNDA:12,reactToMessages +FNDA:151,(anonymous_40) +FNDA:10,respondToMessages +FNDA:0,(anonymous_42) +FNDA:40,(anonymous_43) +FNDA:40,(anonymous_44) +FNDA:8,getRoomId +FNDA:3,getRoomName +FNDA:2,getDirectMessageRoomId +FNDA:2,(anonymous_48) +FNDA:4,joinRoom +FNDA:0,leaveRoom +FNDA:2,joinRooms +FNDA:4,(anonymous_54) +FNDA:14,prepareMessage +FNDA:15,sendMessage +FNDA:8,sendToRoomId +FNDA:6,(anonymous_58) +FNDA:2,sendToRoom +FNDA:2,(anonymous_60) +FNDA:2,sendDirectToUser +FNDA:2,(anonymous_62) +FNDA:1,editMessage +FNDA:2,setReaction +DA:1,1 +DA:2,1 +DA:3,1 +DA:4,1 +DA:5,1 +DA:19,1 +DA:23,1 +DA:24,1 +DA:37,1 +DA:46,1 +DA:58,1 +DA:68,1 +DA:78,1 +DA:79,0 +DA:99,1 +DA:103,37 +DA:104,37 +DA:105,37 +DA:106,37 +DA:107,37 +DA:109,37 +DA:110,37 +DA:111,33 +DA:114,33 +DA:116,37 +DA:117,37 +DA:118,37 +DA:119,4 +DA:120,4 +DA:121,4 +DA:122,4 +DA:123,4 +DA:128,37 +DA:129,37 +DA:130,33 +DA:132,33 +DA:133,33 +DA:134,33 +DA:141,1 +DA:142,1 +DA:143,1 +DA:144,1 +DA:145,1 +DA:156,37 +DA:157,37 +DA:165,37 +DA:176,1 +DA:177,23 +DA:178,23 +DA:179,23 +DA:181,0 +DA:182,0 +DA:185,23 +DA:188,23 +DA:199,1 +DA:200,1 +DA:210,1 +DA:211,13 +DA:213,0 +DA:214,0 +DA:217,13 +DA:220,13 +DA:228,1 +DA:242,28 +DA:243,28 +DA:248,28 +DA:250,28 +DA:251,28 +DA:254,0 +DA:255,0 +DA:260,1 +DA:261,2 +DA:263,0 +DA:264,0 +DA:273,1 +DA:277,15 +DA:278,15 +DA:279,15 +DA:280,15 +DA:281,15 +DA:283,15 +DA:284,15 +DA:290,1 +DA:291,8 +DA:292,8 +DA:293,8 +DA:295,8 +DA:296,8 +DA:300,1 +DA:301,8 +DA:308,1 +DA:309,15 +DA:311,15 +DA:312,15 +DA:339,1 +DA:340,12 +DA:342,12 +DA:343,151 +DA:344,151 +DA:345,151 +DA:346,151 +DA:347,81 +DA:348,81 +DA:350,70 +DA:353,0 +DA:367,1 +DA:371,10 +DA:373,10 +DA:377,10 +DA:383,1 +DA:385,0 +DA:389,10 +DA:390,40 +DA:391,40 +DA:392,0 +DA:393,0 +DA:397,40 +DA:400,39 +DA:401,39 +DA:404,38 +DA:405,38 +DA:408,38 +DA:411,38 +DA:414,38 +DA:417,36 +DA:420,36 +DA:423,7 +DA:424,7 +DA:427,7 +DA:429,10 +DA:436,1 +DA:437,8 +DA:441,1 +DA:442,3 +DA:450,1 +DA:451,2 +DA:452,2 +DA:456,1 +DA:457,4 +DA:458,4 +DA:459,4 +DA:460,0 +DA:462,4 +DA:463,4 +DA:468,1 +DA:469,0 +DA:470,0 +DA:471,0 +DA:472,0 +DA:474,0 +DA:475,0 +DA:480,1 +DA:481,4 +DA:488,1 +DA:492,14 +DA:493,14 +DA:494,14 +DA:501,1 +DA:502,15 +DA:514,1 +DA:518,8 +DA:519,5 +DA:521,3 +DA:522,6 +DA:532,1 +DA:536,2 +DA:537,2 +DA:545,1 +DA:549,2 +DA:550,2 +DA:557,1 +DA:558,1 +DA:566,1 +DA:567,2 +LF:174 +LH:154 +BRDA:100,0,0,30 +BRDA:123,1,0,3 +BRDA:123,1,1,1 +BRDA:128,2,0,37 +BRDA:128,2,1,0 +BRDA:133,3,0,2 +BRDA:133,3,1,31 +BRDA:177,4,0,21 +BRDA:177,4,1,2 +BRDA:186,5,0,16 +BRDA:186,5,1,7 +BRDA:201,6,0,1 +BRDA:201,6,1,0 +BRDA:200,7,0,1 +BRDA:200,7,1,1 +BRDA:218,8,0,12 +BRDA:218,8,1,1 +BRDA:228,9,0,28 +BRDA:244,10,0,28 +BRDA:244,10,1,28 +BRDA:292,11,0,0 +BRDA:292,11,1,8 +BRDA:344,12,0,151 +BRDA:344,12,1,0 +BRDA:344,13,0,151 +BRDA:344,13,1,151 +BRDA:346,14,0,81 +BRDA:346,14,1,70 +BRDA:369,15,0,1 +BRDA:377,16,0,1 +BRDA:377,16,1,9 +BRDA:378,17,0,10 +BRDA:378,17,1,10 +BRDA:378,17,2,1 +BRDA:378,17,3,1 +BRDA:391,18,0,0 +BRDA:391,18,1,40 +BRDA:397,19,0,1 +BRDA:397,19,1,39 +BRDA:401,20,0,1 +BRDA:401,20,1,38 +BRDA:401,21,0,39 +BRDA:401,21,1,2 +BRDA:405,22,0,0 +BRDA:405,22,1,38 +BRDA:405,23,0,38 +BRDA:405,23,1,0 +BRDA:408,24,0,0 +BRDA:408,24,1,38 +BRDA:408,25,0,38 +BRDA:408,25,1,38 +BRDA:408,25,2,37 +BRDA:414,26,0,2 +BRDA:414,26,1,36 +BRDA:414,27,0,38 +BRDA:414,27,1,35 +BRDA:417,28,0,1 +BRDA:417,28,1,35 +BRDA:420,29,0,29 +BRDA:420,29,1,7 +BRDA:459,30,0,0 +BRDA:459,30,1,4 +BRDA:471,31,0,0 +BRDA:471,31,1,0 +BRDA:493,32,0,11 +BRDA:493,32,1,3 +BRDA:518,33,0,5 +BRDA:518,33,1,3 +BRF:68 +BRH:57 +end_of_record +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/log.ts +FN:5,(anonymous_0) +FN:8,(anonymous_1) +FN:11,(anonymous_2) +FN:14,(anonymous_3) +FN:17,(anonymous_4) +FN:24,replaceLog +FN:28,silence +FN:30,(anonymous_7) +FN:31,(anonymous_8) +FN:32,(anonymous_9) +FN:33,(anonymous_10) +FN:34,(anonymous_11) +FNF:12 +FNH:4 +FNDA:0,(anonymous_0) +FNDA:0,(anonymous_1) +FNDA:0,(anonymous_2) +FNDA:0,(anonymous_3) +FNDA:0,(anonymous_4) +FNDA:2,replaceLog +FNDA:2,silence +FNDA:330,(anonymous_7) +FNDA:314,(anonymous_8) +FNDA:0,(anonymous_9) +FNDA:0,(anonymous_10) +FNDA:0,(anonymous_11) +DA:6,0 +DA:9,0 +DA:12,0 +DA:15,0 +DA:18,0 +DA:22,1 +DA:25,2 +DA:29,2 +DA:30,330 +DA:31,314 +DA:32,0 +DA:33,0 +DA:34,0 +DA:39,1 +DA:40,1 +DA:41,1 +LF:16 +LH:8 +BRF:0 +BRH:0 +end_of_record +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/message.ts +FN:14,(anonymous_0) +FN:19,(anonymous_1) +FNF:2 +FNH:2 +FNDA:20,(anonymous_0) +FNDA:14,(anonymous_1) +DA:13,1 +DA:15,20 +DA:16,5 +DA:17,20 +DA:20,14 +DA:21,14 +LF:6 +LH:6 +BRDA:15,0,0,15 +BRDA:15,0,1,5 +BRF:2 +BRH:2 +end_of_record +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/methodCache.ts +FN:16,use +FN:26,create +FN:37,call +FN:60,has +FN:69,get +FN:78,reset +FN:88,resetAll +FN:89,(anonymous_8) +FNF:8 +FNH:8 +FNDA:53,use +FNDA:118,create +FNDA:36,call +FNDA:4,has +FNDA:4,get +FNDA:3,reset +FNDA:20,resetAll +FNDA:126,(anonymous_8) +DA:1,1 +DA:2,1 +DA:6,1 +DA:7,1 +DA:16,1 +DA:17,53 +DA:26,1 +DA:27,118 +DA:28,118 +DA:29,118 +DA:37,1 +DA:38,36 +DA:39,36 +DA:42,36 +DA:43,3 +DA:45,3 +DA:48,33 +DA:49,33 +DA:50,31 +DA:52,34 +DA:60,1 +DA:61,4 +DA:69,1 +DA:70,4 +DA:78,1 +DA:79,3 +DA:80,3 +DA:81,1 +DA:88,1 +DA:89,126 +LF:30 +LH:30 +BRDA:26,0,0,5 +BRDA:38,1,0,3 +BRDA:38,1,1,33 +BRDA:42,2,0,3 +BRDA:42,2,1,33 +BRDA:70,3,0,4 +BRDA:70,3,1,0 +BRDA:79,4,0,3 +BRDA:79,4,1,0 +BRDA:80,5,0,2 +BRDA:80,5,1,1 +BRF:11 +BRH:9 +end_of_record +TN: +SF:/Volumes/x/code/rocketchat/Rocket.Chat.js.SDK/src/lib/settings.ts +FN:16,(anonymous_0) +FNF:1 +FNH:1 +FNDA:3,(anonymous_0) +DA:3,9 +DA:4,9 +DA:5,9 +DA:8,9 +DA:9,9 +DA:12,9 +DA:15,9 +DA:16,3 +DA:18,9 +DA:19,9 +DA:20,9 +DA:21,9 +DA:24,9 +DA:27,9 +DA:28,9 +DA:29,9 +DA:30,9 +LF:17 +LH:17 +BRDA:3,0,0,9 +BRDA:3,0,1,9 +BRDA:4,1,0,9 +BRDA:4,1,1,9 +BRDA:8,2,0,9 +BRDA:8,2,1,6 +BRDA:10,3,0,2 +BRDA:10,3,1,7 +BRDA:10,4,0,2 +BRDA:10,4,1,0 +BRDA:11,5,0,7 +BRDA:11,5,1,5 +BRDA:16,6,0,2 +BRDA:16,6,1,7 +BRDA:16,7,0,2 +BRDA:16,7,1,0 +BRDA:18,8,0,9 +BRDA:18,8,1,8 +BRDA:19,9,0,9 +BRDA:19,9,1,8 +BRDA:20,10,0,9 +BRDA:20,10,1,8 +BRDA:21,11,0,9 +BRDA:21,11,1,8 +BRDA:24,12,0,9 +BRDA:24,12,1,9 +BRDA:27,13,0,9 +BRDA:27,13,1,9 +BRDA:28,14,0,9 +BRDA:28,14,1,9 +BRDA:29,15,0,9 +BRDA:29,15,1,9 +BRDA:30,16,0,9 +BRDA:30,16,1,9 +BRF:34 +BRH:32 +end_of_record diff --git a/dist/config/asteroidInterfaces.d.ts b/dist/config/asteroidInterfaces.d.ts new file mode 100644 index 0000000..b1ee396 --- /dev/null +++ b/dist/config/asteroidInterfaces.d.ts @@ -0,0 +1,110 @@ +/// +import { EventEmitter } from 'events'; +/** + * Asteroid DDP - add known properties to avoid TS lint errors + */ +export interface IAsteroidDDP extends EventEmitter { + readyState: 1 | 0; +} +/** + * Asteroid type + * @todo Update with typing from definitely typed (when available) + */ +export interface IAsteroid extends EventEmitter { + connect: () => Promise; + disconnect: () => Promise; + createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise; + loginWithLDAP: (...params: any[]) => Promise; + loginWithFacebook: (...params: any[]) => Promise; + loginWithGoogle: (...params: any[]) => Promise; + loginWithTwitter: (...params: any[]) => Promise; + loginWithGithub: (...params: any[]) => Promise; + loginWithPassword: (usernameOrEmail: string, password: string) => Promise; + logout: () => Promise; + subscribe: (name: string, ...params: any[]) => ISubscription; + subscriptions: ISubscription[]; + call: (method: string, ...params: any[]) => IMethodResult; + apply: (method: string, params: any[]) => IMethodResult; + getCollection: (name: string) => ICollection; + resumeLoginPromise: Promise; + ddp: IAsteroidDDP; +} +/** + * Asteroid user options type + * @todo Update with typing from definitely typed (when available) + */ +export interface IUserOptions { + username?: string; + email?: string; + password: string; +} +/** + * Asteroid subscription type. + * ID is populated when ready promise resolves. + * @todo Update with typing from definitely typed (when available) + */ +export interface ISubscription { + stop: () => void; + ready: Promise; + id?: string; +} +export interface IReady { + state: string; + value: string; +} +/** + * If the method is successful, the `result` promise will be resolved with the + * return value passed by the server. The `updated` promise will be resolved + * with nothing once the server emits the updated message, that tells the client + * that any side-effect that the method execution caused on the database has + * been reflected on the client (for example, if the method caused the insertion + * of an item into a collection, the client has been notified of said + * insertion). + * + * If the method fails, the `result` promise will be rejected with the error + * returned by the server. The `updated` promise will be rejected as well + * (with nothing). + */ +export interface IMethodResult { + result: Promise; + updated: Promise; +} +/** + * + */ +export interface ICollection { + name: string; + insert: (item: any) => ICollectionResult; + update: (id: string, item: any) => ICollectionResult; + remove: (id: string) => ICollectionResult; + reactiveQuery: (selector: object | Function) => IReactiveQuery; +} +/** + * The `local` promise is immediately resolved with the `_id` of the updated + * item. That is, unless an error occurred. In that case, an exception will be + * raised. + * The `remote` promise is resolved with the `_id` of the updated item if the + * remote update is successful. Otherwise it's rejected with the reason of the + * failure. + */ +export interface ICollectionResult { + local: Promise; + remote: Promise; +} +/** + * A reactive subset of a collection. Possible events are: + * `change`: emitted whenever the result of the query changes. The id of the + * item that changed is passed to the handler. + */ +export interface IReactiveQuery { + on: (event: string, handler: Function) => void; + result: any[]; +} +/** Credentials for Asteroid login method */ +export interface ICredentials { + password: string; + username?: string; + email?: string; + ldap?: boolean; + ldapOptions?: object; +} diff --git a/dist/config/asteroidInterfaces.js b/dist/config/asteroidInterfaces.js new file mode 100644 index 0000000..80bf5dc --- /dev/null +++ b/dist/config/asteroidInterfaces.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=asteroidInterfaces.js.map \ No newline at end of file diff --git a/dist/config/asteroidInterfaces.js.map b/dist/config/asteroidInterfaces.js.map new file mode 100644 index 0000000..1c8555f --- /dev/null +++ b/dist/config/asteroidInterfaces.js.map @@ -0,0 +1 @@ +{"version":3,"file":"asteroidInterfaces.js","sourceRoot":"","sources":["../../src/config/asteroidInterfaces.ts"],"names":[],"mappings":"","sourcesContent":["import { EventEmitter } from 'events'\n// import { Map } from 'immutable'\n\n/**\n * Asteroid DDP - add known properties to avoid TS lint errors\n */\nexport interface IAsteroidDDP extends EventEmitter {\n readyState: 1 | 0\n}\n\n/**\n * Asteroid type\n * @todo Update with typing from definitely typed (when available)\n */\nexport interface IAsteroid extends EventEmitter {\n connect: () => Promise,\n disconnect: () => Promise,\n createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise\n loginWithLDAP: (...params: any[]) => Promise\n loginWithFacebook: (...params: any[]) => Promise\n loginWithGoogle: (...params: any[]) => Promise\n loginWithTwitter: (...params: any[]) => Promise\n loginWithGithub: (...params: any[]) => Promise\n loginWithPassword: (usernameOrEmail: string, password: string) => Promise\n logout: () => Promise\n subscribe: (name: string, ...params: any[]) => ISubscription\n subscriptions: ISubscription[],\n call: (method: string, ...params: any[]) => IMethodResult\n apply: (method: string, params: any[]) => IMethodResult\n getCollection: (name: string) => ICollection\n resumeLoginPromise: Promise\n ddp: IAsteroidDDP\n}\n\n/**\n * Asteroid user options type\n * @todo Update with typing from definitely typed (when available)\n */\nexport interface IUserOptions {\n username?: string,\n email?: string,\n password: string\n}\n\n/**\n * Asteroid subscription type.\n * ID is populated when ready promise resolves.\n * @todo Update with typing from definitely typed (when available)\n */\nexport interface ISubscription {\n stop: () => void,\n ready: Promise,\n id?: string\n}\n\n// Asteroid v1 only\nexport interface IReady { state: string, value: string }\n\n/* // v2\nexport interface ISubscription extends EventEmitter {\n id: string\n}\n*/\n\n/**\n * If the method is successful, the `result` promise will be resolved with the\n * return value passed by the server. The `updated` promise will be resolved\n * with nothing once the server emits the updated message, that tells the client\n * that any side-effect that the method execution caused on the database has\n * been reflected on the client (for example, if the method caused the insertion\n * of an item into a collection, the client has been notified of said\n * insertion).\n *\n * If the method fails, the `result` promise will be rejected with the error\n * returned by the server. The `updated` promise will be rejected as well\n * (with nothing).\n */\nexport interface IMethodResult {\n result: Promise,\n updated: Promise\n}\n\n/**\n *\n */\nexport interface ICollection {\n name: string,\n insert: (item: any) => ICollectionResult,\n update: (id: string, item: any) => ICollectionResult,\n remove: (id: string) => ICollectionResult,\n reactiveQuery: (selector: object | Function) => IReactiveQuery\n}\n\n/**\n * The `local` promise is immediately resolved with the `_id` of the updated\n * item. That is, unless an error occurred. In that case, an exception will be\n * raised.\n * The `remote` promise is resolved with the `_id` of the updated item if the\n * remote update is successful. Otherwise it's rejected with the reason of the\n * failure.\n */\nexport interface ICollectionResult {\n local: Promise,\n remote: Promise\n}\n\n/**\n * A reactive subset of a collection. Possible events are:\n * `change`: emitted whenever the result of the query changes. The id of the\n * item that changed is passed to the handler.\n */\nexport interface IReactiveQuery {\n on: (event: string, handler: Function) => void,\n result: any[]\n}\n\n/** Credentials for Asteroid login method */\nexport interface ICredentials {\n password: string,\n username?: string,\n email?: string,\n ldap?: boolean,\n ldapOptions?: object\n}\n"]} \ No newline at end of file diff --git a/dist/config/driverInterfaces.d.ts b/dist/config/driverInterfaces.d.ts new file mode 100644 index 0000000..d915b7a --- /dev/null +++ b/dist/config/driverInterfaces.d.ts @@ -0,0 +1,42 @@ +/** + * Connection options type + * @param host Rocket.Chat instance Host URL:PORT (without protocol) + * @param timeout How long to wait (ms) before abandoning connection + */ +export interface IConnectOptions { + host?: string; + useSsl?: boolean; + timeout?: number; + integration?: string; +} +/** + * Message respond options + * @param rooms Respond to only selected room/s (names or IDs) + * @param allPublic Respond on all public channels (ignores rooms if true) + * @param dm Respond to messages in DM / private chats + * @param livechat Respond to messages in livechat + * @param edited Respond to edited messages + */ +export interface IRespondOptions { + rooms?: string[]; + allPublic?: boolean; + dm?: boolean; + livechat?: boolean; + edited?: boolean; +} +/** + * Loggers need to provide the same set of methods + */ +export interface ILogger { + debug: (...args: any[]) => void; + info: (...args: any[]) => void; + warning: (...args: any[]) => void; + warn: (...args: any[]) => void; + error: (...args: any[]) => void; +} +/** + * Error-first callback param type + */ +export interface ICallback { + (error: Error | null, ...args: any[]): void; +} diff --git a/dist/config/driverInterfaces.js b/dist/config/driverInterfaces.js new file mode 100644 index 0000000..77736db --- /dev/null +++ b/dist/config/driverInterfaces.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=driverInterfaces.js.map \ No newline at end of file diff --git a/dist/config/driverInterfaces.js.map b/dist/config/driverInterfaces.js.map new file mode 100644 index 0000000..73cb290 --- /dev/null +++ b/dist/config/driverInterfaces.js.map @@ -0,0 +1 @@ +{"version":3,"file":"driverInterfaces.js","sourceRoot":"","sources":["../../src/config/driverInterfaces.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Connection options type\n * @param host Rocket.Chat instance Host URL:PORT (without protocol)\n * @param timeout How long to wait (ms) before abandoning connection\n */\nexport interface IConnectOptions {\n host?: string,\n useSsl?: boolean,\n timeout?: number,\n integration?: string\n}\n\n/**\n * Message respond options\n * @param rooms Respond to only selected room/s (names or IDs)\n * @param allPublic Respond on all public channels (ignores rooms if true)\n * @param dm Respond to messages in DM / private chats\n * @param livechat Respond to messages in livechat\n * @param edited Respond to edited messages\n */\nexport interface IRespondOptions {\n rooms?: string[],\n allPublic?: boolean,\n dm?: boolean,\n livechat?: boolean,\n edited?: boolean\n}\n\n/**\n * Loggers need to provide the same set of methods\n */\nexport interface ILogger {\n debug: (...args: any[]) => void\n info: (...args: any[]) => void\n warning: (...args: any[]) => void\n warn: (...args: any[]) => void\n error: (...args: any[]) => void\n}\n\n/**\n * Error-first callback param type\n */\nexport interface ICallback {\n (error: Error | null, ...args: any[]): void\n}\n"]} \ No newline at end of file diff --git a/dist/config/messageInterfaces.d.ts b/dist/config/messageInterfaces.d.ts new file mode 100644 index 0000000..525a346 --- /dev/null +++ b/dist/config/messageInterfaces.d.ts @@ -0,0 +1,70 @@ +/** @todo contribute these to @types/rocketchat and require */ +export interface IMessage { + rid: string | null; + _id?: string; + t?: string; + msg?: string; + alias?: string; + emoji?: string; + avatar?: string; + groupable?: boolean; + bot?: any; + urls?: string[]; + mentions?: string[]; + attachments?: IMessageAttachment[]; + reactions?: IMessageReaction; + location?: IMessageLocation; + u?: IUser; + editedBy?: IUser; + editedAt?: Date; +} +export interface IUser { + _id: string; + username: string; + name?: string; +} +export interface IMessageAttachment { + fields?: IAttachmentField[]; + actions?: IMessageAction[]; + color?: string; + text?: string; + ts?: string; + thumb_url?: string; + message_link?: string; + collapsed?: boolean; + author_name?: string; + author_link?: string; + author_icon?: string; + title?: string; + title_link?: string; + title_link_download?: string; + image_url?: string; + audio_url?: string; + video_url?: string; +} +export interface IAttachmentField { + short?: boolean; + title?: string; + value?: string; +} +export interface IMessageAction { + type?: string; + text?: string; + url?: string; + image_url?: string; + is_webview?: boolean; + webview_height_ratio?: 'compact' | 'tall' | 'full'; + msg?: string; + msg_in_chat_window?: boolean; + button_alignment?: 'vertical' | 'horizontal'; + temporary_buttons?: boolean; +} +export interface IMessageLocation { + type: string; + coordinates: string[]; +} +export interface IMessageReaction { + [emoji: string]: { + usernames: string[]; + }; +} diff --git a/dist/config/messageInterfaces.js b/dist/config/messageInterfaces.js new file mode 100644 index 0000000..9d96ba0 --- /dev/null +++ b/dist/config/messageInterfaces.js @@ -0,0 +1,4 @@ +"use strict"; +/** @todo contribute these to @types/rocketchat and require */ +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=messageInterfaces.js.map \ No newline at end of file diff --git a/dist/config/messageInterfaces.js.map b/dist/config/messageInterfaces.js.map new file mode 100644 index 0000000..75792e7 --- /dev/null +++ b/dist/config/messageInterfaces.js.map @@ -0,0 +1 @@ +{"version":3,"file":"messageInterfaces.js","sourceRoot":"","sources":["../../src/config/messageInterfaces.ts"],"names":[],"mappings":";AAAA,8DAA8D","sourcesContent":["/** @todo contribute these to @types/rocketchat and require */\n\nexport interface IMessage {\n rid: string | null // room ID\n _id?: string // generated by Random.id()\n t?: string // type e.g. rm\n msg?: string // text content\n alias?: string // ??\n emoji?: string // emoji to use as avatar\n avatar?: string // url\n groupable?: boolean // ?\n bot?: any // integration details\n urls?: string[] // ?\n mentions?: string[] // ?\n attachments?: IMessageAttachment[]\n reactions?: IMessageReaction\n location ?: IMessageLocation\n u?: IUser // User that sent the message\n editedBy?: IUser // User that edited the message\n editedAt?: Date // When the message was edited\n}\n\nexport interface IUser {\n _id: string\n username: string\n name?: string\n}\n\nexport interface IMessageAttachment {\n fields?: IAttachmentField[]\n actions?: IMessageAction[]\n color?: string\n text?: string\n ts?: string\n thumb_url?: string\n message_link?: string\n collapsed?: boolean\n author_name?: string\n author_link?: string\n author_icon?: string\n title?: string\n title_link?: string\n title_link_download?: string\n image_url?: string\n audio_url?: string\n video_url?: string\n}\n\nexport interface IAttachmentField {\n short?: boolean\n title?: string\n value?: string\n}\n\nexport interface IMessageAction {\n type?: string\n text?: string\n url?: string\n image_url?: string\n is_webview?: boolean\n webview_height_ratio?: 'compact' | 'tall' | 'full'\n msg?: string\n msg_in_chat_window?: boolean\n button_alignment?: 'vertical' | 'horizontal'\n temporary_buttons?: boolean\n}\n\nexport interface IMessageLocation {\n type: string // e.g. Point\n coordinates: string[] // longitude latitude\n}\n\nexport interface IMessageReaction {\n [emoji: string]: { usernames: string[] } // emoji: [usernames that reacted]\n}\n"]} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..852d5fe --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,5 @@ +import * as driver from './lib/driver'; +import * as methodCache from './lib/methodCache'; +import * as api from './lib/api'; +import * as settings from './lib/settings'; +export { driver, methodCache, api, settings }; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..636f632 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,18 @@ +"use strict"; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +} +Object.defineProperty(exports, "__esModule", { value: true }); +const driver = __importStar(require("./lib/driver")); +exports.driver = driver; +const methodCache = __importStar(require("./lib/methodCache")); +exports.methodCache = methodCache; +const api = __importStar(require("./lib/api")); +exports.api = api; +const settings = __importStar(require("./lib/settings")); +exports.settings = settings; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..62c43ee --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;AAAA,qDAAsC;AAKpC,wBAAM;AAJR,+DAAgD;AAK9C,kCAAW;AAJb,+CAAgC;AAK9B,kBAAG;AAJL,yDAA0C;AAKxC,4BAAQ","sourcesContent":["import * as driver from './lib/driver'\nimport * as methodCache from './lib/methodCache'\nimport * as api from './lib/api'\nimport * as settings from './lib/settings'\nexport {\n driver,\n methodCache,\n api,\n settings\n}\n"]} \ No newline at end of file diff --git a/dist/lib/api.d.ts b/dist/lib/api.d.ts new file mode 100644 index 0000000..d2c386b --- /dev/null +++ b/dist/lib/api.d.ts @@ -0,0 +1,87 @@ +/** Result object from an API login */ +export interface ILoginResultAPI { + status: string; + data: { + authToken: string; + userId: string; + }; +} +/** Structure for passing and keeping login credentials */ +export interface ILoginCredentials { + username: string; + password: string; +} +export declare let currentLogin: { + username: string; + userId: string; + authToken: string; + result: ILoginResultAPI; +} | null; +/** Check for existing login */ +export declare function loggedIn(): boolean; +/** Initialise client and configs */ +export declare const client: any; +export declare const host: string; +/** + * Prepend protocol (or put back if removed from env settings for driver) + * Hard code endpoint prefix, because all syntax depends on this version + */ +export declare const url: string; +/** Convert payload data to query string for GET requests */ +export declare function getQueryString(data: any): string; +/** Setup default headers with empty auth for now */ +export declare const basicHeaders: { + 'Content-Type': string; +}; +export declare const authHeaders: { + 'X-Auth-Token': string; + 'X-User-Id': string; +}; +/** Populate auth headers (from response data on login) */ +export declare function setAuth(authData: { + authToken: string; + userId: string; +}): void; +/** Join basic headers with auth headers if required */ +export declare function getHeaders(authRequired?: boolean): { + 'Content-Type': string; +}; +/** Clear headers so they can't be used without logging in again */ +export declare function clearHeaders(): void; +/** Check result data for success, allowing override to ignore some errors */ +export declare function success(result: any, ignore?: RegExp): boolean; +/** + * Do a POST request to an API endpoint. + * If it needs a token, login first (with defaults) to set auth headers. + * @todo Look at why some errors return HTML (caught as buffer) instead of JSON + * @param endpoint The API endpoint (including version) e.g. `chat.update` + * @param data Payload for POST request to endpoint + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +export declare function post(endpoint: string, data: any, auth?: boolean, ignore?: RegExp): Promise; +/** + * Do a GET request to an API endpoint + * @param endpoint The API endpoint (including version) e.g. `users.info` + * @param data Object to serialise for GET request query string + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +export declare function get(endpoint: string, data?: any, auth?: boolean, ignore?: RegExp): Promise; +/** + * Login a user for further API calls + * Result should come back with a token, to authorise following requests. + * Use env default credentials, unless overridden by login arguments. + */ +export declare function login(user?: ILoginCredentials): Promise; +/** Logout a user at end of API calls */ +export declare function logout(): Promise; +/** Defaults for user queries */ +export declare const userFields: { + name: number; + username: number; + status: number; + type: number; +}; +/** Query helpers for user collection requests */ +export declare const users: any; diff --git a/dist/lib/api.js b/dist/lib/api.js new file mode 100644 index 0000000..245f3f9 --- /dev/null +++ b/dist/lib/api.js @@ -0,0 +1,218 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +} +Object.defineProperty(exports, "__esModule", { value: true }); +const node_rest_client_1 = require("node-rest-client"); +const settings = __importStar(require("./settings")); +const log_1 = require("./log"); +exports.currentLogin = null; +/** Check for existing login */ +function loggedIn() { + return (exports.currentLogin !== null); +} +exports.loggedIn = loggedIn; +/** Initialise client and configs */ +exports.client = new node_rest_client_1.Client(); +exports.host = settings.host; +/** + * Prepend protocol (or put back if removed from env settings for driver) + * Hard code endpoint prefix, because all syntax depends on this version + */ +exports.url = ((exports.host.indexOf('http') === -1) + ? exports.host.replace(/^(\/\/)?/, 'http://') + : exports.host) + '/api/v1/'; +/** Convert payload data to query string for GET requests */ +function getQueryString(data) { + if (!data || typeof data !== 'object' || !Object.keys(data).length) + return ''; + return '?' + Object.keys(data).map((k) => { + const value = (typeof data[k] === 'object') + ? JSON.stringify(data[k]) + : encodeURIComponent(data[k]); + return `${encodeURIComponent(k)}=${value}`; + }).join('&'); +} +exports.getQueryString = getQueryString; +/** Setup default headers with empty auth for now */ +exports.basicHeaders = { 'Content-Type': 'application/json' }; +exports.authHeaders = { 'X-Auth-Token': '', 'X-User-Id': '' }; +/** Populate auth headers (from response data on login) */ +function setAuth(authData) { + exports.authHeaders['X-Auth-Token'] = authData.authToken; + exports.authHeaders['X-User-Id'] = authData.userId; +} +exports.setAuth = setAuth; +/** Join basic headers with auth headers if required */ +function getHeaders(authRequired = false) { + if (!authRequired) + return exports.basicHeaders; + if ((!('X-Auth-Token' in exports.authHeaders) || !('X-User-Id' in exports.authHeaders)) || + exports.authHeaders['X-Auth-Token'] === '' || + exports.authHeaders['X-User-Id'] === '') { + throw new Error('Auth required endpoint cannot be called before login'); + } + return Object.assign({}, exports.basicHeaders, exports.authHeaders); +} +exports.getHeaders = getHeaders; +/** Clear headers so they can't be used without logging in again */ +function clearHeaders() { + delete exports.authHeaders['X-Auth-Token']; + delete exports.authHeaders['X-User-Id']; +} +exports.clearHeaders = clearHeaders; +/** Check result data for success, allowing override to ignore some errors */ +function success(result, ignore) { + return ((typeof result.error === 'undefined' && + typeof result.status === 'undefined' && + typeof result.success === 'undefined') || + (result.status && result.status === 'success') || + (result.success && result.success === true) || + (ignore && result.error && !ignore.test(result.error))) ? true : false; +} +exports.success = success; +/** + * Do a POST request to an API endpoint. + * If it needs a token, login first (with defaults) to set auth headers. + * @todo Look at why some errors return HTML (caught as buffer) instead of JSON + * @param endpoint The API endpoint (including version) e.g. `chat.update` + * @param data Payload for POST request to endpoint + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +function post(endpoint, data, auth = true, ignore) { + return __awaiter(this, void 0, void 0, function* () { + try { + log_1.logger.debug(`[API] POST: ${endpoint}`, JSON.stringify(data)); + if (auth && !loggedIn()) + yield login(); + let headers = getHeaders(auth); + const result = yield new Promise((resolve, reject) => { + exports.client.post(exports.url + endpoint, { headers, data }, (result) => { + if (Buffer.isBuffer(result)) + reject('Result was buffer (HTML, not JSON)'); + else if (!success(result, ignore)) + reject(result); + else + resolve(result); + }).on('error', (err) => reject(err)); + }); + log_1.logger.debug('[API] POST result:', result); + return result; + } + catch (err) { + console.error(err); + log_1.logger.error(`[API] POST error (${endpoint}):`, err); + } + }); +} +exports.post = post; +/** + * Do a GET request to an API endpoint + * @param endpoint The API endpoint (including version) e.g. `users.info` + * @param data Object to serialise for GET request query string + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +function get(endpoint, data, auth = true, ignore) { + return __awaiter(this, void 0, void 0, function* () { + try { + log_1.logger.debug(`[API] GET: ${endpoint}`, data); + if (auth && !loggedIn()) + yield login(); + let headers = getHeaders(auth); + const query = getQueryString(data); + const result = yield new Promise((resolve, reject) => { + exports.client.get(exports.url + endpoint + query, { headers }, (result) => { + if (Buffer.isBuffer(result)) + reject('Result was buffer (HTML, not JSON)'); + else if (!success(result, ignore)) + reject(result); + else + resolve(result); + }).on('error', (err) => reject(err)); + }); + log_1.logger.debug('[API] GET result:', result); + return result; + } + catch (err) { + log_1.logger.error(`[API] GET error (${endpoint}):`, err); + } + }); +} +exports.get = get; +/** + * Login a user for further API calls + * Result should come back with a token, to authorise following requests. + * Use env default credentials, unless overridden by login arguments. + */ +function login(user = { + username: settings.username, + password: settings.password +}) { + return __awaiter(this, void 0, void 0, function* () { + log_1.logger.info(`[API] Logging in ${user.username}`); + if (exports.currentLogin !== null) { + log_1.logger.debug(`[API] Already logged in`); + if (exports.currentLogin.username === user.username) { + return exports.currentLogin.result; + } + else { + yield logout(); + } + } + const result = yield post('login', user, false); + if (result && result.data && result.data.authToken) { + exports.currentLogin = { + result: result, + username: user.username, + authToken: result.data.authToken, + userId: result.data.userId + }; + setAuth(exports.currentLogin); + log_1.logger.info(`[API] Logged in ID ${exports.currentLogin.userId}`); + return result; + } + else { + throw new Error(`[API] Login failed for ${user.username}`); + } + }); +} +exports.login = login; +/** Logout a user at end of API calls */ +function logout() { + if (exports.currentLogin === null) { + log_1.logger.debug(`[API] Already logged out`); + return Promise.resolve(); + } + log_1.logger.info(`[API] Logging out ${exports.currentLogin.username}`); + return get('logout', null, true).then(() => { + clearHeaders(); + exports.currentLogin = null; + }); +} +exports.logout = logout; +/** Defaults for user queries */ +exports.userFields = { name: 1, username: 1, status: 1, type: 1 }; +/** Query helpers for user collection requests */ +exports.users = { + all: (fields = exports.userFields) => get('users.list', { fields }).then((r) => r.users), + allNames: () => get('users.list', { fields: { 'username': 1 } }).then((r) => r.users.map((u) => u.username)), + allIDs: () => get('users.list', { fields: { '_id': 1 } }).then((r) => r.users.map((u) => u._id)), + online: (fields = exports.userFields) => get('users.list', { fields, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users), + onlineNames: () => get('users.list', { fields: { 'username': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u) => u.username)), + onlineIds: () => get('users.list', { fields: { '_id': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u) => u._id)) +}; +//# sourceMappingURL=api.js.map \ No newline at end of file diff --git a/dist/lib/api.js.map b/dist/lib/api.js.map new file mode 100644 index 0000000..a28a7bc --- /dev/null +++ b/dist/lib/api.js.map @@ -0,0 +1 @@ +{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/lib/api.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client } from 'node-rest-client'\nimport * as settings from './settings'\nimport { logger } from './log'\nimport { IUserAPI } from '../utils/interfaces'\n\n/** Result object from an API login */\nexport interface ILoginResultAPI {\n status: string // e.g. 'success'\n data: { authToken: string, userId: string }\n}\n\n/** Structure for passing and keeping login credentials */\nexport interface ILoginCredentials {\n username: string,\n password: string\n}\nexport let currentLogin: {\n username: string,\n userId: string,\n authToken: string,\n result: ILoginResultAPI\n} | null = null\n\n/** Check for existing login */\nexport function loggedIn (): boolean {\n return (currentLogin !== null)\n}\n\n/** Initialise client and configs */\nexport const client = new Client()\nexport const host = settings.host\n\n/**\n * Prepend protocol (or put back if removed from env settings for driver)\n * Hard code endpoint prefix, because all syntax depends on this version\n */\nexport const url = ((host.indexOf('http') === -1)\n ? host.replace(/^(\\/\\/)?/, 'http://')\n : host) + '/api/v1/'\n\n/** Convert payload data to query string for GET requests */\nexport function getQueryString (data: any) {\n if (!data || typeof data !== 'object' || !Object.keys(data).length) return ''\n return '?' + Object.keys(data).map((k) => {\n const value = (typeof data[k] === 'object')\n ? JSON.stringify(data[k])\n : encodeURIComponent(data[k])\n return `${encodeURIComponent(k)}=${value}`\n }).join('&')\n}\n\n/** Setup default headers with empty auth for now */\nexport const basicHeaders = { 'Content-Type': 'application/json' }\nexport const authHeaders = { 'X-Auth-Token': '', 'X-User-Id': '' }\n\n/** Populate auth headers (from response data on login) */\nexport function setAuth (authData: {authToken: string, userId: string}) {\n authHeaders['X-Auth-Token'] = authData.authToken\n authHeaders['X-User-Id'] = authData.userId\n}\n\n/** Join basic headers with auth headers if required */\nexport function getHeaders (authRequired = false) {\n if (!authRequired) return basicHeaders\n if (\n (!('X-Auth-Token' in authHeaders) || !('X-User-Id' in authHeaders)) ||\n authHeaders['X-Auth-Token'] === '' ||\n authHeaders['X-User-Id'] === ''\n ) {\n throw new Error('Auth required endpoint cannot be called before login')\n }\n return Object.assign({}, basicHeaders, authHeaders)\n}\n\n/** Clear headers so they can't be used without logging in again */\nexport function clearHeaders () {\n delete authHeaders['X-Auth-Token']\n delete authHeaders['X-User-Id']\n}\n\n/** Check result data for success, allowing override to ignore some errors */\nexport function success (result: any, ignore?: RegExp) {\n return (\n (\n typeof result.error === 'undefined' &&\n typeof result.status === 'undefined' &&\n typeof result.success === 'undefined'\n ) ||\n (result.status && result.status === 'success') ||\n (result.success && result.success === true) ||\n (ignore && result.error && !ignore.test(result.error))\n ) ? true : false\n}\n\n/**\n * Do a POST request to an API endpoint.\n * If it needs a token, login first (with defaults) to set auth headers.\n * @todo Look at why some errors return HTML (caught as buffer) instead of JSON\n * @param endpoint The API endpoint (including version) e.g. `chat.update`\n * @param data Payload for POST request to endpoint\n * @param auth Require auth headers for endpoint, default true\n * @param ignore Allows certain matching error messages to not count as errors\n */\nexport async function post (\n endpoint: string,\n data: any,\n auth: boolean = true,\n ignore?: RegExp\n): Promise {\n try {\n logger.debug(`[API] POST: ${endpoint}`, JSON.stringify(data))\n if (auth && !loggedIn()) await login()\n let headers = getHeaders(auth)\n const result = await new Promise((resolve, reject) => {\n client.post(url + endpoint, { headers, data }, (result: any) => {\n if (Buffer.isBuffer(result)) reject('Result was buffer (HTML, not JSON)')\n else if (!success(result, ignore)) reject(result)\n else resolve(result)\n }).on('error', (err: Error) => reject(err))\n })\n logger.debug('[API] POST result:', result)\n return result\n } catch (err) {\n console.error(err)\n logger.error(`[API] POST error (${endpoint}):`, err)\n }\n}\n\n/**\n * Do a GET request to an API endpoint\n * @param endpoint The API endpoint (including version) e.g. `users.info`\n * @param data Object to serialise for GET request query string\n * @param auth Require auth headers for endpoint, default true\n * @param ignore Allows certain matching error messages to not count as errors\n */\nexport async function get (\n endpoint: string,\n data?: any,\n auth: boolean = true,\n ignore?: RegExp\n): Promise {\n try {\n logger.debug(`[API] GET: ${endpoint}`, data)\n if (auth && !loggedIn()) await login()\n let headers = getHeaders(auth)\n const query = getQueryString(data)\n const result = await new Promise((resolve, reject) => {\n client.get(url + endpoint + query, { headers }, (result: any) => {\n if (Buffer.isBuffer(result)) reject('Result was buffer (HTML, not JSON)')\n else if (!success(result, ignore)) reject(result)\n else resolve(result)\n }).on('error', (err: Error) => reject(err))\n })\n logger.debug('[API] GET result:', result)\n return result\n } catch (err) {\n logger.error(`[API] GET error (${endpoint}):`, err)\n }\n}\n\n/**\n * Login a user for further API calls\n * Result should come back with a token, to authorise following requests.\n * Use env default credentials, unless overridden by login arguments.\n */\nexport async function login (user: ILoginCredentials = {\n username: settings.username,\n password: settings.password\n}): Promise {\n logger.info(`[API] Logging in ${user.username}`)\n if (currentLogin !== null) {\n logger.debug(`[API] Already logged in`)\n if (currentLogin.username === user.username) {\n return currentLogin.result\n } else {\n await logout()\n }\n }\n const result = await post('login', user, false)\n if (result && result.data && result.data.authToken) {\n currentLogin = {\n result: result, // keep to return if login requested again for same user\n username: user.username, // keep to compare with following login attempt\n authToken: result.data.authToken,\n userId: result.data.userId\n }\n setAuth(currentLogin)\n logger.info(`[API] Logged in ID ${ currentLogin.userId }`)\n return result\n } else {\n throw new Error(`[API] Login failed for ${user.username}`)\n }\n}\n\n/** Logout a user at end of API calls */\nexport function logout () {\n if (currentLogin === null) {\n logger.debug(`[API] Already logged out`)\n return Promise.resolve()\n }\n logger.info(`[API] Logging out ${ currentLogin.username }`)\n return get('logout', null, true).then(() => {\n clearHeaders()\n currentLogin = null\n })\n}\n\n/** Defaults for user queries */\nexport const userFields = { name: 1, username: 1, status: 1, type: 1 }\n\n/** Query helpers for user collection requests */\nexport const users: any = {\n all: (fields: any = userFields) => get('users.list', { fields }).then((r) => r.users),\n allNames: () => get('users.list', { fields: { 'username': 1 } }).then((r) => r.users.map((u: IUserAPI) => u.username)),\n allIDs: () => get('users.list', { fields: { '_id': 1 } }).then((r) => r.users.map((u: IUserAPI) => u._id)),\n online: (fields: any = userFields) => get('users.list', { fields, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users),\n onlineNames: () => get('users.list', { fields: { 'username': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u: IUserAPI) => u.username)),\n onlineIds: () => get('users.list', { fields: { '_id': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u: IUserAPI) => u._id))\n}\n"]} \ No newline at end of file diff --git a/dist/lib/driver.d.ts b/dist/lib/driver.d.ts new file mode 100644 index 0000000..e1d6589 --- /dev/null +++ b/dist/lib/driver.d.ts @@ -0,0 +1,201 @@ +/// +import { EventEmitter } from 'events'; +import { Message } from './message'; +import { IConnectOptions, IRespondOptions, ICallback, ILogger } from '../config/driverInterfaces'; +import { IAsteroid, ICredentials, ISubscription, ICollection } from '../config/asteroidInterfaces'; +import { IMessage } from '../config/messageInterfaces'; +import { IMessageReceiptAPI } from '../utils/interfaces'; +/** Internal for comparing message update timestamps */ +export declare let lastReadTime: Date; +/** + * The integration property is applied as an ID on sent messages `bot.i` param + * Should be replaced when connection is invoked by a package using the SDK + * e.g. The Hubot adapter would pass its integration ID with credentials, like: + */ +export declare const integrationId: string; +/** + * Event Emitter for listening to connection. + * @example + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * driver.events.on('connected', () => console.log('driver connected')) + */ +export declare const events: EventEmitter; +/** + * An Asteroid instance for interacting with Rocket.Chat. + * Variable not initialised until `connect` called. + */ +export declare let asteroid: IAsteroid; +/** + * Asteroid subscriptions, exported for direct polling by adapters + * Variable not initialised until `prepMeteorSubscriptions` called. + */ +export declare let subscriptions: ISubscription[]; +/** + * Current user object populated from resolved login + */ +export declare let userId: string; +/** + * Array of joined room IDs (for reactive queries) + */ +export declare let joinedIds: string[]; +/** + * Array of messages received from reactive collection + */ +export declare let messages: ICollection; +/** + * Allow override of default logging with adapter's log instance + */ +export declare function useLog(externalLog: ILogger): void; +/** + * Initialise asteroid instance with given options or defaults. + * Returns promise, resolved with Asteroid instance. Callback follows + * error-first-pattern. Error returned or promise rejected on timeout. + * Removes http/s protocol to get connection hostname if taken from URL. + * @example Use with callback + * import { driver } from '@rocket.chat/sdk' + * driver.connect({}, (err) => { + * if (err) throw err + * else console.log('connected') + * }) + * @example Using promise + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * .then(() => console.log('connected')) + * .catch((err) => console.error(err)) + */ +export declare function connect(options?: IConnectOptions, callback?: ICallback): Promise; +/** Remove all active subscriptions, logout and disconnect from Rocket.Chat */ +export declare function disconnect(): Promise; +/** + * Wraps method calls to ensure they return a Promise with caught exceptions. + * @param method The Rocket.Chat server method, to call through Asteroid + * @param params Single or array of parameters of the method to call + */ +export declare function asyncCall(method: string, params: any | any[]): Promise; +/** + * Call a method as async via Asteroid, or through cache if one is created. + * If the method doesn't have or need parameters, it can't use them for caching + * so it will always call asynchronously. + * @param name The Rocket.Chat server method to call + * @param params Single or array of parameters of the method to call + */ +export declare function callMethod(name: string, params?: any | any[]): Promise; +/** + * Wraps Asteroid method calls, passed through method cache if cache is valid. + * @param method The Rocket.Chat server method, to call through Asteroid + * @param key Single string parameters only, required to use as cache key + */ +export declare function cacheCall(method: string, key: string): Promise; +/** Login to Rocket.Chat via Asteroid */ +export declare function login(credentials?: ICredentials): Promise; +/** Logout of Rocket.Chat via Asteroid */ +export declare function logout(): Promise; +/** + * Subscribe to Meteor subscription + * Resolves with subscription (added to array), with ID property + * @todo - 3rd param of asteroid.subscribe is deprecated in Rocket.Chat? + */ +export declare function subscribe(topic: string, roomId: string): Promise; +/** Unsubscribe from Meteor subscription */ +export declare function unsubscribe(subscription: ISubscription): void; +/** Unsubscribe from all subscriptions in collection */ +export declare function unsubscribeAll(): void; +/** + * Begin subscription to room events for user. + * Older adapters used an option for this method but it was always the default. + */ +export declare function subscribeToMessages(): Promise; +/** + * Once a subscription is created, using `subscribeToMessages` this method + * can be used to attach a callback to changes in the message stream. + * This can be called directly for custom extensions, but for most usage (e.g. + * for bots) the respondToMessages is more useful to only receive messages + * matching configuration. + * + * If the bot hasn't been joined to any rooms at this point, it will attempt to + * join now based on environment config, otherwise it might not receive any + * messages. It doesn't matter that this happens asynchronously because the + * bot's joined rooms can change after the reactive query is set up. + * + * @todo `reactToMessages` should call `subscribeToMessages` if not already + * done, so it's not required as an arbitrary step for simpler adapters. + * Also make `login` call `connect` for the same reason, the way + * `respondToMessages` calls `respondToMessages`, so all that's really + * required is: + * `driver.login(credentials).then(() => driver.respondToMessages(callback))` + * @param callback Function called with every change in subscriptions. + * - Uses error-first callback pattern + * - Second argument is the changed item + * - Third argument is additional attributes, such as `roomType` + */ +export declare function reactToMessages(callback: ICallback): void; +/** + * Proxy for `reactToMessages` with some filtering of messages based on config. + * + * @param callback Function called after filters run on subscription events. + * - Uses error-first callback pattern + * - Second argument is the changed item + * - Third argument is additional attributes, such as `roomType` + * @param options Sets filters for different event/message types. + */ +export declare function respondToMessages(callback: ICallback, options?: IRespondOptions): Promise; +/** Get ID for a room by name (or ID). */ +export declare function getRoomId(name: string): Promise; +/** Get name for a room by ID. */ +export declare function getRoomName(id: string): Promise; +/** + * Get ID for a DM room by its recipient's name. + * Will create a DM (with the bot) if it doesn't exist already. + * @todo test why create resolves with object instead of simply ID + */ +export declare function getDirectMessageRoomId(username: string): Promise; +/** Join the bot into a room by its name or ID */ +export declare function joinRoom(room: string): Promise; +/** Exit a room the bot has joined */ +export declare function leaveRoom(room: string): Promise; +/** Join a set of rooms by array of names or IDs */ +export declare function joinRooms(rooms: string[]): Promise; +/** + * Structure message content, optionally addressing to room ID. + * Accepts message text string or a structured message object. + */ +export declare function prepareMessage(content: string | IMessage, roomId?: string): Message; +/** + * Send a prepared message object (with pre-defined room ID). + * Usually prepared and called by sendMessageByRoomId or sendMessageByRoom. + */ +export declare function sendMessage(message: IMessage): Promise; +/** + * Prepare and send string/s to specified room ID. + * @param content Accepts message text string or array of strings. + * @param roomId ID of the target room to use in send. + * @todo Returning one or many gets complicated with type checking not allowing + * use of a property because result may be array, when you know it's not. + * Solution would probably be to always return an array, even for single + * send. This would be a breaking change, should hold until major version. + */ +export declare function sendToRoomId(content: string | string[] | IMessage, roomId: string): Promise; +/** + * Prepare and send string/s to specified room name (or ID). + * @param content Accepts message text string or array of strings. + * @param room A name (or ID) to resolve as ID to use in send. + */ +export declare function sendToRoom(content: string | string[] | IMessage, room: string): Promise; +/** + * Prepare and send string/s to a user in a DM. + * @param content Accepts message text string or array of strings. + * @param username Name to create (or get) DM for room ID to use in send. + */ +export declare function sendDirectToUser(content: string | string[] | IMessage, username: string): Promise; +/** + * Edit an existing message, replacing any attributes with those provided. + * The given message object should have the ID of an existing message. + */ +export declare function editMessage(message: IMessage): Promise; +/** + * Send a reaction to an existing message. Simple proxy for method call. + * @param emoji Accepts string like `:thumbsup:` to add 👍 reaction + * @param messageId ID for a previously sent message + */ +export declare function setReaction(emoji: string, messageId: string): Promise; diff --git a/dist/lib/driver.js b/dist/lib/driver.js new file mode 100644 index 0000000..357ea51 --- /dev/null +++ b/dist/lib/driver.js @@ -0,0 +1,666 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +} +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +} +Object.defineProperty(exports, "__esModule", { value: true }); +const events_1 = require("events"); +const asteroid_1 = __importDefault(require("asteroid")); +const settings = __importStar(require("./settings")); +const methodCache = __importStar(require("./methodCache")); +const message_1 = require("./message"); +const log_1 = require("./log"); + +const _messageCollectionName = 'stream-room-messages'; +const _messageStreamName = '__my_messages__'; +/** + * The integration property is applied as an ID on sent messages `bot.i` param + * Should be replaced when connection is invoked by a package using the SDK + * e.g. The Hubot adapter would pass its integration ID with credentials, like: + * @ignore + */ +exports.integrationId = settings.integrationId; +/** + * Event Emitter for listening to connection. + * @example + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * driver.events.on('connected', () => console.log('driver connected')) + * @ignore + */ +exports.events = new events_1.EventEmitter(); +/** + * Asteroid subscriptions, exported for direct polling by adapters + * Variable not initialised until `prepMeteorSubscriptions` called. + * @ignore + */ +exports.subscriptions = []; +/** + * Array of joined room IDs (for reactive queries) + * @ignore + */ +exports.joinedIds = []; +/** + * @memberof module:driver + * @instance + * @description Replaces the default logger with one from a bot framework + * @param {Class|Object} externalLog - Class or object with `debug`, `info`, `warn`, `error` methods + * @returns {void} + */ +function useLog(externalLog) { + log_1.replaceLog(externalLog); +} +exports.useLog = useLog; + +/** + * @memberof module:driver + * @instance + * @description Initialize asteroid instance with given options or defaults. + * Callback follows error-first-pattern. + * Error returned or promise rejected on timeout. + * @param {Object} [options={}] - an object containing options to initialize Asteroid instance with + * @param {string} [options.host=''] - hostname to connect to. Removes http/s protocol if taken from URL + * @param {boolean} [options.useSsl=false] - whether the SSL is enabled for the host + * @param {number} [options.timeout] - timeframe after which the connection attempt will be considered as failed + * @returns {Promise} + * @throws {Error} Asteroid connection timeout + * @example Usage with callback + * import { driver } from '@rocket.chat/sdk' + * driver.connect({}, (err) => { + * if (err) throw err + * else console.log('connected') + * }) + * @example Usage with promise + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * .then(() => console.log('connected')) + * .catch((err) => console.error(err)) + */ +function connect(options = {}, callback) { + return new Promise((resolve, reject) => { + const config = Object.assign({}, settings, options); // override defaults + config.host = config.host.replace(/(^\w+:|^)\/\//, ''); + log_1.logger.info('[connect] Connecting', config); + exports.asteroid = new asteroid_1.default(config.host, config.useSsl); + setupMethodCache(exports.asteroid); // init instance for later caching method calls + exports.asteroid.on('connected', () => { + exports.asteroid.resumeLoginPromise.catch(function () { + // pass + }); + exports.events.emit('connected'); + }); + exports.asteroid.on('reconnected', () => exports.events.emit('reconnected')); + let cancelled = false; + const rejectionTimeout = setTimeout(function () { + log_1.logger.info(`[connect] Timeout (${config.timeout})`); + const err = new Error('Asteroid connection timeout'); + cancelled = true; + exports.events.removeAllListeners('connected'); + callback ? callback(err, exports.asteroid) : reject(err); + }, config.timeout); + // if to avoid condition where timeout happens before listener to 'connected' is added + // and this listener is not removed (because it was added after the removal) + if (!cancelled) { + exports.events.once('connected', () => { + log_1.logger.info('[connect] Connected'); + // if (cancelled) return asteroid.ddp.disconnect() // cancel if already rejected + clearTimeout(rejectionTimeout); + if (callback) + callback(null, exports.asteroid); + resolve(exports.asteroid); + }); + } + }); +} +exports.connect = connect; +/** + * @memberof module:driver + * @instance + * @description Remove all active subscriptions, logout and disconnect from Rocket.Chat + * @returns {Promise} + */ +function disconnect() { + log_1.logger.info('Unsubscribing, logging out, disconnecting'); + unsubscribeAll(); + return logout() + .then(() => Promise.resolve()); +} +exports.disconnect = disconnect; +// ASYNC AND CACHE METHOD UTILS +// ----------------------------------------------------------------------------- +/** + * Setup method cache configs from env or defaults, before they are called. + * @param asteroid The asteroid instance to cache method calls + * @ignore + */ +function setupMethodCache(asteroid) { + methodCache.use(asteroid); + methodCache.create('getRoomIdByNameOrId', { + max: settings.roomCacheMaxSize, + maxAge: settings.roomCacheMaxAge + }), + methodCache.create('getRoomNameById', { + max: settings.roomCacheMaxSize, + maxAge: settings.roomCacheMaxAge + }); + methodCache.create('createDirectMessage', { + max: settings.dmCacheMaxSize, + maxAge: settings.dmCacheMaxAge + }); +} +/** + * @memberof module:driver + * @instance + * @description Wrap server method calls to always be async (return a Promise with caught exceptions) + * @param {any} method - the Rocket.Chat server method, to call through Asteroid + * @param {string|string[]} params - single or array of parameters of the method to call + * @returns {Promise} + */ +function asyncCall(method, params) { + if (!Array.isArray(params)) + params = [params]; // cast to array for apply + log_1.logger.info(`[${method}] Calling (async): ${JSON.stringify(params)}`); + return Promise.resolve(exports.asteroid.apply(method, params).result) + .catch((err) => { + log_1.logger.error(`[${method}] Error:`, err); + throw err; // throw after log to stop async chain + }) + .then((result) => { + (result) + ? log_1.logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) + : log_1.logger.debug(`[${method}] Success`); + return result; + }); +} +exports.asyncCall = asyncCall; +/** + * @memberof module:driver + * @instance + * @description Call a method as async ({@link module:driver#asyncCall|asyncCall}) via Asteroid, + * or through cache ({@link module:driver#cacheCall|cacheCall}) if one is created. + * + * If the method was called without parameters, they cannot be cached. + * As the result, the method will always be called asynchronously. + * @param {any} name - the Rocket.Chat server method to call through Asteroid + * @param {string|string[]} params - single or array of parameters of the method to call + * @returns {Promise} + */ +function callMethod(name, params) { + return (methodCache.has(name) || typeof params === 'undefined') + ? asyncCall(name, params) + : cacheCall(name, params); +} +exports.callMethod = callMethod; +/** + * @memberof module:driver + * @instance + * @description Call Asteroid method calls with `methodCache`, if cache is valid + * @param {any} method - the Rocket.Chat server method, to call through Asteroid + * @param {string} key - single string parameters only, used as a cache key + * @returns {Promise} - Server results or cached results, if valid + */ +function cacheCall(method, key) { + return methodCache.call(method, key) + .catch((err) => { + log_1.logger.error(`[${method}] Error:`, err); + throw err; // throw after log to stop async chain + }) + .then((result) => { + (result) + ? log_1.logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) + : log_1.logger.debug(`[${method}] Success`); + return result; + }); +} +exports.cacheCall = cacheCall; +// LOGIN AND SUBSCRIBE TO ROOMS +// ----------------------------------------------------------------------------- +/** + * @memberof module:driver + * @instance + * @description Login to Rocket.Chat via Asteroid + * @param {Object} [credentials={}] - an object containing credentials to log in to Rocket.Chat + * @param {string} [credentials.username = ROCKETCHAT_USER] - username of the Rocket.Chat user + * @param {string} [credentials.email = ROCKETCHAT_USER] - email of the Rocket.Chat user. + * @param {string} [credentials.password=ROCKETCHAT_PASSWORD] - password of the Rocket.Chat user + * @param {boolean} [credentials.ldap=false] - whether LDAP is enabled for login + * @returns {Promise} + */ +function login(credentials = { + username: settings.username, + password: settings.password, + ldap: settings.ldap +}) { + let login; + // if (credentials.ldap) { + // logger.info(`[login] Logging in ${credentials.username} with LDAP`) + // login = asteroid.loginWithLDAP( + // credentials.email || credentials.username, + // credentials.password, + // { ldap: true, ldapOptions: credentials.ldapOptions || {} } + // ) + // } else { + log_1.logger.info(`[login] Logging in ${credentials.username}`); + login = exports.asteroid.loginWithPassword(credentials.email || credentials.username, credentials.password); + // } + return login + .then((loggedInUserId) => { + exports.userId = loggedInUserId; + return loggedInUserId; + }) + .catch((err) => { + log_1.logger.info('[login] Error:', err); + throw err; // throw after log to stop async chain + }); +} +exports.login = login; +/** + * @memberof module:driver + * @instance + * @description Logout Rocket.Chat via Asteroid + * @returns {Promise} + * */ +function logout() { + return exports.asteroid.logout() + .catch((err) => { + log_1.logger.error('[Logout] Error:', err); + throw err; // throw after log to stop async chain + }); +} +exports.logout = logout; +/** + * @memberof module:driver + * @instance + * @description Subscribe to Meteor subscription + * @param {string} topic - subscription topic + * @param {number} roomId - unique ID of the room to subscribe to + * @todo - 3rd param of asteroid.subscribe is deprecated in Rocket.Chat? + * @returns {Promise} - Subscription instance (added to array), with ID property + */ +function subscribe(topic, roomId) { + return new Promise((resolve, reject) => { + log_1.logger.info(`[subscribe] Preparing subscription: ${topic}: ${roomId}`); + const subscription = exports.asteroid.subscribe(topic, roomId, true); + exports.subscriptions.push(subscription); + return subscription.ready + .then((id) => { + log_1.logger.info(`[subscribe] Stream ready: ${id}`); + resolve(subscription); + }); + }); +} +exports.subscribe = subscribe; +/** + * @memberof module:driver + * @instance + * @description Unsubscribe from Meteor subscription + * @param {any} subscription - Subscription instance to unsbscribe from + * @returns {Promise} + */ +function unsubscribe(subscription) { + const index = exports.subscriptions.indexOf(subscription); + if (index === -1) + return; + subscription.stop(); + // asteroid.unsubscribe(subscription.id) // v2 + exports.subscriptions.splice(index, 1); // remove from collection + log_1.logger.info(`[${subscription.id}] Unsubscribed`); +} +exports.unsubscribe = unsubscribe; +/** + * @memberof module:driver + * @instance + * @description Unsubscribe from all subscriptions in collection + * @returns {Promise} + */ +function unsubscribeAll() { + exports.subscriptions.map((s) => unsubscribe(s)); +} +exports.unsubscribeAll = unsubscribeAll; +/** + * @memberof module:driver + * @instance + * @description Begin subscription to room events for user + * + * > NOTE: Older adapters used an option for this method but it was always the default. + * @param {string} [topic=stream-room-messages] - subscription topic + * @param {number} [roomId=__my_messages__] - unique ID of the room to subscribe to + * @returns {Promise} - Subscription instance + */ +function subscribeToMessages() { + return subscribe(_messageCollectionName, _messageStreamName) + .then((subscription) => { + exports.messages = exports.asteroid.getCollection(_messageCollectionName); + return subscription; + }); +} +exports.subscribeToMessages = subscribeToMessages; +/** + * @memberof module:driver + * @instance + * @description Attach a callback to changes in the message stream. + * + * This method should be used only after a subscription was created using + * {@link module:driver#subscribeToMessages|subscribeToMessages}. + * Fires callback with every change in subscriptions. + * + * > NOTE: This method can be called directly for custom extensions, but for most usage + * (e.g. for bots) the + * {@link module:driver#respondToMessages|respondToMessages} is more useful to only receive messages + * matching configuration. + * + * If the bot hasn't been joined to any rooms at this point, it will attempt to + * join now based on environment config, otherwise it might not receive any + * messages. It doesn't matter that this happens asynchronously because the rooms + * the bot joined to can change after the reactive query is set up. + * + * @todo `reactToMessages` should call `subscribeToMessages` if not already + * done, so it's not required as an arbitrary step for simpler adapters. + * Also make `login` call `connect` for the same reason, the way + * `respondToMessages` calls `respondToMessages`, so all that's really + * required is: + * `driver.login(credentials).then(() => driver.respondToMessages(callback))` + * @param {Function} callback - function called with every change in subscriptions. + * - It uses error-first callback pattern + * - The second argument is the changed item + * - The third argument is additional attributes, such as `roomType` + */ +function reactToMessages(callback) { + log_1.logger.info(`[reactive] Listening for change events in collection ${exports.messages.name}`); + exports.messages.reactiveQuery({}).on('change', (_id) => { + const changedMessageQuery = exports.messages.reactiveQuery({ _id }); + if (changedMessageQuery.result && changedMessageQuery.result.length > 0) { + const changedMessage = changedMessageQuery.result[0]; + if (Array.isArray(changedMessage.args)) { + log_1.logger.info(`[received] Message in room ${changedMessage.args[0].rid}`); + callback(null, changedMessage.args[0], changedMessage.args[1]); + } + else { + log_1.logger.debug('[received] Update without message args'); + } + } + else { + log_1.logger.debug('[received] Reactive query at ID ${ _id } without results'); + } + }); +} +exports.reactToMessages = reactToMessages; +/** + * @memberof module:driver + * @instance + * @description Proxy for {@link module:driver#reactToMessages|reactToMessages} + * with some filtering of messages based on config. This is a more user-friendly method + * for bots to subscribe to a message stream. + * @param {Function} callback - function called after filters run on subscription events. + * - It uses error-first callback pattern + * - The second argument is the changed item + * - The third argument is additional attributes, such as `roomType` + * @param {Object} options - an object that sets filters for different event/message types + * @param {string[]} options.rooms - respond to only selected room/s (names or IDs). Ignored if `options.allPublic=true` + * If rooms are given as option or set in the environment with `ROCKETCHAT_ROOM` but have not been joined yet, + * this method will join to those rooms automatically. + * @param {boolean} options.allPublic - respond on all public channels. Ignored if `options.rooms=true` + * @param {boolean} options.dm - respond to messages in direct messages / private chats with the SDK user + * @param {boolean} options.livechat - respond to messages in Livechat rooms + * @param {boolean} options.edited - respond to edited messages + */ +function respondToMessages(callback, options = {}) { + const config = Object.assign({}, settings, options); + // return value, may be replaced by async ops + let promise = Promise.resolve(); + // Join configured rooms if they haven't been already, unless listening to all + // public rooms, in which case it doesn't matter + if (!config.allPublic && + exports.joinedIds.length === 0 && + config.rooms && + config.rooms.length > 0) { + promise = joinRooms(config.rooms) + .catch((err) => { + log_1.logger.error(`[joinRooms] Failed to join configured rooms (${config.rooms.join(', ')}): ${err.message}`); + }); + } + exports.lastReadTime = new Date(); // init before any message read + reactToMessages((err, message, meta) => __awaiter(this, void 0, void 0, function* () { + if (err) { + log_1.logger.error(`[received] Unable to receive: ${err.message}`); + callback(err); // bubble errors back to adapter + } + // Ignore bot's own messages + if (message.u._id === exports.userId) + return; + // Ignore DMs unless configured not to + const isDM = meta.roomType === 'd'; + if (isDM && !config.dm) + return; + // Ignore Livechat unless configured not to + const isLC = meta.roomType === 'l'; + if (isLC && !config.livechat) + return; + // Ignore messages in un-joined public rooms unless configured not to + if (!config.allPublic && !isDM && !meta.roomParticipant) + return; + // Set current time for comparison to incoming + let currentReadTime = new Date(message.ts.$date); + // Ignore edited messages if configured to + if (!config.edited && message.editedAt) + return; + // Set read time as time of edit, if message is edited + if (message.editedAt) + currentReadTime = new Date(message.editedAt.$date); + // Ignore messages in stream that aren't new + if (currentReadTime <= exports.lastReadTime) + return; + // At this point, message has passed checks and can be responded to + log_1.logger.info(`[received] Message ${message._id} from ${message.u.username}`); + exports.lastReadTime = currentReadTime; + // Processing completed, call callback to respond to message + callback(null, message, meta); + })); + return promise; +} +exports.respondToMessages = respondToMessages; +// PREPARE AND SEND MESSAGES +// ----------------------------------------------------------------------------- +/** + * @memberof module:driver + * @instance + * @description Get room's ID by its name + * @param {string} name - room's name or ID + * @returns {Promise} + */ +function getRoomId(name) { + return cacheCall('getRoomIdByNameOrId', name); +} +exports.getRoomId = getRoomId; +/** + * @memberof module:driver + * @instance + * @description Get room's name by its ID + * @param {string} id - room's ID + * @returns {Promise} +*/ +function getRoomName(id) { + return cacheCall('getRoomNameById', id); +} +exports.getRoomName = getRoomName; +/** + * @memberof module:driver + * @instance + * @description Get ID for a DM room by its recipient's name. + * + * The call will create a DM (with the bot) if it doesn't exist yet. + * @param {string} username - recipient's username + * @returns {Promise} + * @todo test why create resolves with object instead of simply ID + */ +function getDirectMessageRoomId(username) { + return cacheCall('createDirectMessage', username) + .then((DM) => DM.rid); +} +exports.getDirectMessageRoomId = getDirectMessageRoomId; +/** + * @memberof module:driver + * @instance + * @description Join the bot into a room using room's name or ID + * @param {string} room - room's name or ID + * @returns {Promise} + * */ +function joinRoom(room) { + return __awaiter(this, void 0, void 0, function* () { + let roomId = yield getRoomId(room); + let joinedIndex = exports.joinedIds.indexOf(room); + if (joinedIndex !== -1) { + log_1.logger.error(`[joinRoom] room was already joined`); + } + else { + yield asyncCall('joinRoom', roomId); + exports.joinedIds.push(roomId); + } + }); +} +exports.joinRoom = joinRoom; +/** + * Exit a room the bot has joined + * @ignore + * */ +function leaveRoom(room) { + return __awaiter(this, void 0, void 0, function* () { + let roomId = yield getRoomId(room); + let joinedIndex = exports.joinedIds.indexOf(room); + if (joinedIndex === -1) { + log_1.logger.error(`[leaveRoom] failed because bot has not joined ${room}`); + } + else { + yield asyncCall('leaveRoom', roomId); + delete exports.joinedIds[joinedIndex]; + } + }); +} +exports.leaveRoom = leaveRoom; +/** + * @memberof module:driver + * @instance + * @description Join a set of rooms by array of room names or IDs + * @param {string[]} rooms - array of room names or IDs + * @returns {Promise} + * */ +function joinRooms(rooms) { + return Promise.all(rooms.map((room) => joinRoom(room))); +} +exports.joinRooms = joinRooms; +/** + * @memberof module:driver + * @instance + * @description Structure message content, optionally sending it to a specific room ID + * @param {string|Object} content - message text string or a structured message object + * @param {string} [roomId] - room's ID to send message content to + * @returns {Object} + */ +function prepareMessage(content, roomId) { + const message = new message_1.Message(content, exports.integrationId); + if (roomId) + message.setRoomId(roomId); + return message; +} +exports.prepareMessage = prepareMessage; +/** + * @memberof module:driver + * @instance + * @description Send a prepared message object (with a pre-defined room ID). + * Usually prepared and called by `sendMessageByRoomId` or `sendMessageByRoom`. + * @param {Object} message - structured message object + * @returns {Promise} + */ +function sendMessage(message) { + return asyncCall('sendMessage', message); +} +exports.sendMessage = sendMessage; +/** + * @memberof module:driver + * @instance + * @description Prepare and send string(s) to the specified room ID + * @param {string|string[]} content - message text string or array of strings + * @param {string} roomId - ID of the target room to use in send + * @returns {Promise|Promise[]} + * @todo Returning one or many gets complicated with type checking not allowing + * use of a property because result may be array, when you know it's not. + * Solution would probably be to always return an array, even for single + * send. This would be a breaking change, should hold until major version. + */ +function sendToRoomId(content, roomId) { + if (!Array.isArray(content)) { + return sendMessage(prepareMessage(content, roomId)); + } + else { + return Promise.all(content.map((text) => { + return sendMessage(prepareMessage(text, roomId)); + })); + } +} +exports.sendToRoomId = sendToRoomId; +/** + * @memberof module:driver + * @instance + * @description Prepare and send string(s) to the specified room name (or ID). + * @param {string|string[]} content - message text string or array of strings + * @param {string} room - name or ID of the target room to use in send + * @returns {Promise} + */ +function sendToRoom(content, room) { + return getRoomId(room) + .then((roomId) => sendToRoomId(content, roomId)); +} +exports.sendToRoom = sendToRoom; +/** + * @memberof module:driver + * @instance + * @description Prepare and send string(s) to a user in a DM + * @param {string|string[]} content - message text string or array of strings + * @param {string} username - name or ID of the target room to use in send. + * Creates DM room if it does not exist + * @returns {Promise} + */ +function sendDirectToUser(content, username) { + return getDirectMessageRoomId(username) + .then((rid) => sendToRoomId(content, rid)); +} +exports.sendDirectToUser = sendDirectToUser; +/** + * @memberof module:driver + * @instance + * @description Edit an existing message, replacing any attributes with the provided ones + * @param {Object} message - structured message object. + * The given message object should have the ID of an existing message. + * @returns {Object} + */ +function editMessage(message) { + return asyncCall('updateMessage', message); +} +exports.editMessage = editMessage; +/** + * @memberof module:driver + * @instance + * @description Send a reaction to an existing message. Simple proxy for method call. + * @param {string} emoji - reaction emoji to add. For example, `:thumbsup:` to add 👍. + * @param {string} messageId - ID of the previously sent message + * @returns {Object} + */ +function setReaction(emoji, messageId) { + return asyncCall('setReaction', [emoji, messageId]); +} +exports.setReaction = setReaction; +//# sourceMappingURL=driver.js.map \ No newline at end of file diff --git a/dist/lib/driver.js.map b/dist/lib/driver.js.map new file mode 100644 index 0000000..738d03c --- /dev/null +++ b/dist/lib/driver.js.map @@ -0,0 +1 @@ +{"version":3,"file":"driver.js","sourceRoot":"","sources":["../../src/lib/driver.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from 'events'\nimport Asteroid from 'asteroid'\nimport * as settings from './settings'\nimport * as methodCache from './methodCache'\nimport { Message } from './message'\nimport {\n IConnectOptions,\n IRespondOptions,\n ICallback,\n ILogger\n} from '../config/driverInterfaces'\nimport {\n IAsteroid,\n ICredentials,\n ISubscription,\n ICollection\n} from '../config/asteroidInterfaces'\nimport { IMessage } from '../config/messageInterfaces'\nimport { logger, replaceLog } from './log'\nimport { IMessageReceiptAPI } from '../utils/interfaces'\n\n/** Collection names */\nconst _messageCollectionName = 'stream-room-messages'\nconst _messageStreamName = '__my_messages__'\n\n// CONNECTION SETUP AND CONFIGURE\n// -----------------------------------------------------------------------------\n\n/** Internal for comparing message update timestamps */\nexport let lastReadTime: Date\n\n/**\n * The integration property is applied as an ID on sent messages `bot.i` param\n * Should be replaced when connection is invoked by a package using the SDK\n * e.g. The Hubot adapter would pass its integration ID with credentials, like:\n */\nexport const integrationId = settings.integrationId\n\n/**\n * Event Emitter for listening to connection.\n * @example\n * import { driver } from '@rocket.chat/sdk'\n * driver.connect()\n * driver.events.on('connected', () => console.log('driver connected'))\n */\nexport const events = new EventEmitter()\n\n/**\n * An Asteroid instance for interacting with Rocket.Chat.\n * Variable not initialised until `connect` called.\n */\nexport let asteroid: IAsteroid\n\n/**\n * Asteroid subscriptions, exported for direct polling by adapters\n * Variable not initialised until `prepMeteorSubscriptions` called.\n */\nexport let subscriptions: ISubscription[] = []\n\n/**\n * Current user object populated from resolved login\n */\nexport let userId: string\n\n/**\n * Array of joined room IDs (for reactive queries)\n */\nexport let joinedIds: string[] = []\n\n/**\n * Array of messages received from reactive collection\n */\nexport let messages: ICollection\n\n/**\n * Allow override of default logging with adapter's log instance\n */\nexport function useLog (externalLog: ILogger) {\n replaceLog(externalLog)\n}\n\n/**\n * Initialise asteroid instance with given options or defaults.\n * Returns promise, resolved with Asteroid instance. Callback follows\n * error-first-pattern. Error returned or promise rejected on timeout.\n * Removes http/s protocol to get connection hostname if taken from URL.\n * @example Use with callback\n * import { driver } from '@rocket.chat/sdk'\n * driver.connect({}, (err) => {\n * if (err) throw err\n * else console.log('connected')\n * })\n * @example Using promise\n * import { driver } from '@rocket.chat/sdk'\n * driver.connect()\n * .then(() => console.log('connected'))\n * .catch((err) => console.error(err))\n */\nexport function connect (\n options: IConnectOptions = {},\n callback?: ICallback\n): Promise {\n return new Promise((resolve, reject) => {\n const config = Object.assign({}, settings, options) // override defaults\n config.host = config.host.replace(/(^\\w+:|^)\\/\\//, '')\n logger.info('[connect] Connecting', config)\n asteroid = new Asteroid(config.host, config.useSsl)\n\n setupMethodCache(asteroid) // init instance for later caching method calls\n asteroid.on('connected', () => {\n asteroid.resumeLoginPromise.catch(function () {\n // pass\n })\n events.emit('connected')\n })\n asteroid.on('reconnected', () => events.emit('reconnected'))\n let cancelled = false\n const rejectionTimeout = setTimeout(function () {\n logger.info(`[connect] Timeout (${config.timeout})`)\n const err = new Error('Asteroid connection timeout')\n cancelled = true\n events.removeAllListeners('connected')\n callback ? callback(err, asteroid) : reject(err)\n }, config.timeout)\n\n // if to avoid condition where timeout happens before listener to 'connected' is added\n // and this listener is not removed (because it was added after the removal)\n if (!cancelled) {\n events.once('connected', () => {\n logger.info('[connect] Connected')\n // if (cancelled) return asteroid.ddp.disconnect() // cancel if already rejected\n clearTimeout(rejectionTimeout)\n if (callback) callback(null, asteroid)\n resolve(asteroid)\n })\n }\n })\n}\n\n/** Remove all active subscriptions, logout and disconnect from Rocket.Chat */\nexport function disconnect (): Promise {\n logger.info('Unsubscribing, logging out, disconnecting')\n unsubscribeAll()\n return logout()\n .then(() => Promise.resolve())\n}\n\n// ASYNC AND CACHE METHOD UTILS\n// -----------------------------------------------------------------------------\n\n/**\n * Setup method cache configs from env or defaults, before they are called.\n * @param asteroid The asteroid instance to cache method calls\n */\nfunction setupMethodCache (asteroid: IAsteroid): void {\n methodCache.use(asteroid)\n methodCache.create('getRoomIdByNameOrId', {\n max: settings.roomCacheMaxSize,\n maxAge: settings.roomCacheMaxAge\n }),\n methodCache.create('getRoomNameById', {\n max: settings.roomCacheMaxSize,\n maxAge: settings.roomCacheMaxAge\n })\n methodCache.create('createDirectMessage', {\n max: settings.dmCacheMaxSize,\n maxAge: settings.dmCacheMaxAge\n })\n}\n\n/**\n * Wraps method calls to ensure they return a Promise with caught exceptions.\n * @param method The Rocket.Chat server method, to call through Asteroid\n * @param params Single or array of parameters of the method to call\n */\nexport function asyncCall (method: string, params: any | any[]): Promise {\n if (!Array.isArray(params)) params = [params] // cast to array for apply\n logger.info(`[${method}] Calling (async): ${JSON.stringify(params)}`)\n return Promise.resolve(asteroid.apply(method, params).result)\n .catch((err: Error) => {\n logger.error(`[${method}] Error:`, err)\n throw err // throw after log to stop async chain\n })\n .then((result: any) => {\n (result)\n ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`)\n : logger.debug(`[${method}] Success`)\n return result\n })\n}\n\n/**\n * Call a method as async via Asteroid, or through cache if one is created.\n * If the method doesn't have or need parameters, it can't use them for caching\n * so it will always call asynchronously.\n * @param name The Rocket.Chat server method to call\n * @param params Single or array of parameters of the method to call\n */\nexport function callMethod (name: string, params?: any | any[]): Promise {\n return (methodCache.has(name) || typeof params === 'undefined')\n ? asyncCall(name, params)\n : cacheCall(name, params)\n}\n\n/**\n * Wraps Asteroid method calls, passed through method cache if cache is valid.\n * @param method The Rocket.Chat server method, to call through Asteroid\n * @param key Single string parameters only, required to use as cache key\n */\nexport function cacheCall (method: string, key: string): Promise {\n return methodCache.call(method, key)\n .catch((err: Error) => {\n logger.error(`[${method}] Error:`, err)\n throw err // throw after log to stop async chain\n })\n .then((result: any) => {\n (result)\n ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`)\n : logger.debug(`[${method}] Success`)\n return result\n })\n}\n\n// LOGIN AND SUBSCRIBE TO ROOMS\n// -----------------------------------------------------------------------------\n\n/** Login to Rocket.Chat via Asteroid */\nexport function login (credentials: ICredentials = {\n username: settings.username,\n password: settings.password,\n ldap: settings.ldap\n}): Promise {\n let login: Promise\n // if (credentials.ldap) {\n // logger.info(`[login] Logging in ${credentials.username} with LDAP`)\n // login = asteroid.loginWithLDAP(\n // credentials.email || credentials.username,\n // credentials.password,\n // { ldap: true, ldapOptions: credentials.ldapOptions || {} }\n // )\n // } else {\n logger.info(`[login] Logging in ${credentials.username}`)\n login = asteroid.loginWithPassword(\n credentials.email || credentials.username!,\n credentials.password\n )\n // }\n return login\n .then((loggedInUserId) => {\n userId = loggedInUserId\n return loggedInUserId\n })\n .catch((err: Error) => {\n logger.info('[login] Error:', err)\n throw err // throw after log to stop async chain\n })\n}\n\n/** Logout of Rocket.Chat via Asteroid */\nexport function logout (): Promise {\n return asteroid.logout()\n .catch((err: Error) => {\n logger.error('[Logout] Error:', err)\n throw err // throw after log to stop async chain\n })\n}\n\n/**\n * Subscribe to Meteor subscription\n * Resolves with subscription (added to array), with ID property\n * @todo - 3rd param of asteroid.subscribe is deprecated in Rocket.Chat?\n */\nexport function subscribe (\n topic: string,\n roomId: string\n): Promise {\n return new Promise((resolve, reject) => {\n logger.info(`[subscribe] Preparing subscription: ${topic}: ${roomId}`)\n const subscription = asteroid.subscribe(topic, roomId, true)\n subscriptions.push(subscription)\n return subscription.ready\n .then((id) => {\n logger.info(`[subscribe] Stream ready: ${id}`)\n resolve(subscription)\n })\n })\n}\n\n/** Unsubscribe from Meteor subscription */\nexport function unsubscribe (subscription: ISubscription): void {\n const index = subscriptions.indexOf(subscription)\n if (index === -1) return\n subscription.stop()\n // asteroid.unsubscribe(subscription.id) // v2\n subscriptions.splice(index, 1) // remove from collection\n logger.info(`[${subscription.id}] Unsubscribed`)\n}\n\n/** Unsubscribe from all subscriptions in collection */\nexport function unsubscribeAll (): void {\n subscriptions.map((s: ISubscription) => unsubscribe(s))\n}\n\n/**\n * Begin subscription to room events for user.\n * Older adapters used an option for this method but it was always the default.\n */\nexport function subscribeToMessages (): Promise {\n return subscribe(_messageCollectionName, _messageStreamName)\n .then((subscription) => {\n messages = asteroid.getCollection(_messageCollectionName)\n return subscription\n })\n}\n\n/**\n * Once a subscription is created, using `subscribeToMessages` this method\n * can be used to attach a callback to changes in the message stream.\n * This can be called directly for custom extensions, but for most usage (e.g.\n * for bots) the respondToMessages is more useful to only receive messages\n * matching configuration.\n *\n * If the bot hasn't been joined to any rooms at this point, it will attempt to\n * join now based on environment config, otherwise it might not receive any\n * messages. It doesn't matter that this happens asynchronously because the\n * bot's joined rooms can change after the reactive query is set up.\n *\n * @todo `reactToMessages` should call `subscribeToMessages` if not already\n * done, so it's not required as an arbitrary step for simpler adapters.\n * Also make `login` call `connect` for the same reason, the way\n * `respondToMessages` calls `respondToMessages`, so all that's really\n * required is:\n * `driver.login(credentials).then(() => driver.respondToMessages(callback))`\n * @param callback Function called with every change in subscriptions.\n * - Uses error-first callback pattern\n * - Second argument is the changed item\n * - Third argument is additional attributes, such as `roomType`\n */\nexport function reactToMessages (callback: ICallback): void {\n logger.info(`[reactive] Listening for change events in collection ${messages.name}`)\n\n messages.reactiveQuery({}).on('change', (_id: string) => {\n const changedMessageQuery = messages.reactiveQuery({ _id })\n if (changedMessageQuery.result && changedMessageQuery.result.length > 0) {\n const changedMessage = changedMessageQuery.result[0]\n if (Array.isArray(changedMessage.args)) {\n logger.info(`[received] Message in room ${ changedMessage.args[0].rid }`)\n callback(null, changedMessage.args[0], changedMessage.args[1])\n } else {\n logger.debug('[received] Update without message args')\n }\n } else {\n logger.debug('[received] Reactive query at ID ${ _id } without results')\n }\n })\n}\n\n/**\n * Proxy for `reactToMessages` with some filtering of messages based on config.\n *\n * @param callback Function called after filters run on subscription events.\n * - Uses error-first callback pattern\n * - Second argument is the changed item\n * - Third argument is additional attributes, such as `roomType`\n * @param options Sets filters for different event/message types.\n */\nexport function respondToMessages (\n callback: ICallback,\n options: IRespondOptions = {}\n): Promise {\n const config = Object.assign({}, settings, options)\n // return value, may be replaced by async ops\n let promise: Promise = Promise.resolve()\n\n // Join configured rooms if they haven't been already, unless listening to all\n // public rooms, in which case it doesn't matter\n if (\n !config.allPublic &&\n joinedIds.length === 0 &&\n config.rooms &&\n config.rooms.length > 0\n ) {\n promise = joinRooms(config.rooms)\n .catch((err) => {\n logger.error(`[joinRooms] Failed to join configured rooms (${config.rooms.join(', ')}): ${err.message}`)\n })\n }\n\n lastReadTime = new Date() // init before any message read\n reactToMessages(async (err, message, meta) => {\n if (err) {\n logger.error(`[received] Unable to receive: ${err.message}`)\n callback(err) // bubble errors back to adapter\n }\n\n // Ignore bot's own messages\n if (message.u._id === userId) return\n\n // Ignore DMs unless configured not to\n const isDM = meta.roomType === 'd'\n if (isDM && !config.dm) return\n\n // Ignore Livechat unless configured not to\n const isLC = meta.roomType === 'l'\n if (isLC && !config.livechat) return\n\n // Ignore messages in un-joined public rooms unless configured not to\n if (!config.allPublic && !isDM && !meta.roomParticipant) return\n\n // Set current time for comparison to incoming\n let currentReadTime = new Date(message.ts.$date)\n\n // Ignore edited messages if configured to\n if (!config.edited && message.editedAt) return\n\n // Set read time as time of edit, if message is edited\n if (message.editedAt) currentReadTime = new Date(message.editedAt.$date)\n\n // Ignore messages in stream that aren't new\n if (currentReadTime <= lastReadTime) return\n\n // At this point, message has passed checks and can be responded to\n logger.info(`[received] Message ${message._id} from ${message.u.username}`)\n lastReadTime = currentReadTime\n\n // Processing completed, call callback to respond to message\n callback(null, message, meta)\n })\n return promise\n}\n\n// PREPARE AND SEND MESSAGES\n// -----------------------------------------------------------------------------\n\n/** Get ID for a room by name (or ID). */\nexport function getRoomId (name: string): Promise {\n return cacheCall('getRoomIdByNameOrId', name)\n}\n\n/** Get name for a room by ID. */\nexport function getRoomName (id: string): Promise {\n return cacheCall('getRoomNameById', id)\n}\n\n/**\n * Get ID for a DM room by its recipient's name.\n * Will create a DM (with the bot) if it doesn't exist already.\n * @todo test why create resolves with object instead of simply ID\n */\nexport function getDirectMessageRoomId (username: string): Promise {\n return cacheCall('createDirectMessage', username)\n .then((DM) => DM.rid)\n}\n\n/** Join the bot into a room by its name or ID */\nexport async function joinRoom (room: string): Promise {\n let roomId = await getRoomId(room)\n let joinedIndex = joinedIds.indexOf(room)\n if (joinedIndex !== -1) {\n logger.error(`[joinRoom] room was already joined`)\n } else {\n await asyncCall('joinRoom', roomId)\n joinedIds.push(roomId)\n }\n}\n\n/** Exit a room the bot has joined */\nexport async function leaveRoom (room: string): Promise {\n let roomId = await getRoomId(room)\n let joinedIndex = joinedIds.indexOf(room)\n if (joinedIndex === -1) {\n logger.error(`[leaveRoom] failed because bot has not joined ${room}`)\n } else {\n await asyncCall('leaveRoom', roomId)\n delete joinedIds[joinedIndex]\n }\n}\n\n/** Join a set of rooms by array of names or IDs */\nexport function joinRooms (rooms: string[]): Promise {\n return Promise.all(rooms.map((room) => joinRoom(room)))\n}\n\n/**\n * Structure message content, optionally addressing to room ID.\n * Accepts message text string or a structured message object.\n */\nexport function prepareMessage (\n content: string | IMessage,\n roomId?: string\n): Message {\n const message = new Message(content, integrationId)\n if (roomId) message.setRoomId(roomId)\n return message\n}\n\n/**\n * Send a prepared message object (with pre-defined room ID).\n * Usually prepared and called by sendMessageByRoomId or sendMessageByRoom.\n */\nexport function sendMessage (message: IMessage): Promise {\n return asyncCall('sendMessage', message)\n}\n\n/**\n * Prepare and send string/s to specified room ID.\n * @param content Accepts message text string or array of strings.\n * @param roomId ID of the target room to use in send.\n * @todo Returning one or many gets complicated with type checking not allowing\n * use of a property because result may be array, when you know it's not.\n * Solution would probably be to always return an array, even for single\n * send. This would be a breaking change, should hold until major version.\n */\nexport function sendToRoomId (\n content: string | string[] | IMessage,\n roomId: string\n): Promise {\n if (!Array.isArray(content)) {\n return sendMessage(prepareMessage(content, roomId))\n } else {\n return Promise.all(content.map((text) => {\n return sendMessage(prepareMessage(text, roomId))\n }))\n }\n}\n\n/**\n * Prepare and send string/s to specified room name (or ID).\n * @param content Accepts message text string or array of strings.\n * @param room A name (or ID) to resolve as ID to use in send.\n */\nexport function sendToRoom (\n content: string | string[] | IMessage,\n room: string\n): Promise {\n return getRoomId(room)\n .then((roomId) => sendToRoomId(content, roomId))\n}\n\n/**\n * Prepare and send string/s to a user in a DM.\n * @param content Accepts message text string or array of strings.\n * @param username Name to create (or get) DM for room ID to use in send.\n */\nexport function sendDirectToUser (\n content: string | string[] | IMessage,\n username: string\n): Promise {\n return getDirectMessageRoomId(username)\n .then((rid) => sendToRoomId(content, rid))\n}\n\n/**\n * Edit an existing message, replacing any attributes with those provided.\n * The given message object should have the ID of an existing message.\n */\nexport function editMessage (message: IMessage): Promise {\n return asyncCall('updateMessage', message)\n}\n\n/**\n * Send a reaction to an existing message. Simple proxy for method call.\n * @param emoji Accepts string like `:thumbsup:` to add 👍 reaction\n * @param messageId ID for a previously sent message\n */\nexport function setReaction (emoji: string, messageId: string) {\n return asyncCall('setReaction', [emoji, messageId])\n}\n"]} \ No newline at end of file diff --git a/dist/lib/log.d.ts b/dist/lib/log.d.ts new file mode 100644 index 0000000..8362643 --- /dev/null +++ b/dist/lib/log.d.ts @@ -0,0 +1,5 @@ +import { ILogger } from '../config/driverInterfaces'; +declare let logger: ILogger; +declare function replaceLog(externalLog: ILogger): void; +declare function silence(): void; +export { logger, replaceLog, silence }; diff --git a/dist/lib/log.js b/dist/lib/log.js new file mode 100644 index 0000000..2035214 --- /dev/null +++ b/dist/lib/log.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** Temp logging, should override form adapter's log */ +class InternalLog { + debug(...args) { + console.log(...args); + } + info(...args) { + console.log(...args); + } + warning(...args) { + console.warn(...args); + } + warn(...args) { + return this.warning(...args); + } + error(...args) { + console.error(...args); + } +} +let logger = new InternalLog(); +exports.logger = logger; +function replaceLog(externalLog) { + exports.logger = logger = externalLog; +} +exports.replaceLog = replaceLog; +function silence() { + replaceLog({ + debug: () => null, + info: () => null, + warn: () => null, + warning: () => null, + error: () => null + }); +} +exports.silence = silence; +//# sourceMappingURL=log.js.map \ No newline at end of file diff --git a/dist/lib/log.js.map b/dist/lib/log.js.map new file mode 100644 index 0000000..6425a35 --- /dev/null +++ b/dist/lib/log.js.map @@ -0,0 +1 @@ +{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/lib/log.ts"],"names":[],"mappings":";;AAEA,uDAAuD;AACvD;IACE,KAAK,CAAE,GAAG,IAAW;QACnB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACtB,CAAC;IACD,IAAI,CAAE,GAAG,IAAW;QAClB,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;IACtB,CAAC;IACD,OAAO,CAAE,GAAG,IAAW;QACrB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAA;IACvB,CAAC;IACD,IAAI,CAAE,GAAG,IAAW;QAClB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAA;IAC9B,CAAC;IACD,KAAK,CAAE,GAAG,IAAW;QACnB,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAA;IACxB,CAAC;CACF;AAED,IAAI,MAAM,GAAY,IAAI,WAAW,EAAE,CAAA;AAiBrC,wBAAM;AAfR,oBAAqB,WAAoB;IACvC,iBAAA,MAAM,GAAG,WAAW,CAAA;AACtB,CAAC;AAcC,gCAAU;AAZZ;IACE,UAAU,CAAC;QACT,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI;QACjB,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI;QAChB,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI;QAChB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;QACnB,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI;KAClB,CAAC,CAAA;AACJ,CAAC;AAKC,0BAAO","sourcesContent":["import { ILogger } from '../config/driverInterfaces'\n\n/** Temp logging, should override form adapter's log */\nclass InternalLog implements ILogger {\n debug (...args: any[]) {\n console.log(...args)\n }\n info (...args: any[]) {\n console.log(...args)\n }\n warning (...args: any[]) {\n console.warn(...args)\n }\n warn (...args: any[]) { // legacy method\n return this.warning(...args)\n }\n error (...args: any[]) {\n console.error(...args)\n }\n}\n\nlet logger: ILogger = new InternalLog()\n\nfunction replaceLog (externalLog: ILogger) {\n logger = externalLog\n}\n\nfunction silence () {\n replaceLog({\n debug: () => null,\n info: () => null,\n warn: () => null,\n warning: () => null,\n error: () => null\n })\n}\n\nexport {\n logger,\n replaceLog,\n silence\n}\n"]} \ No newline at end of file diff --git a/dist/lib/message.d.ts b/dist/lib/message.d.ts new file mode 100644 index 0000000..fc21774 --- /dev/null +++ b/dist/lib/message.d.ts @@ -0,0 +1,13 @@ +import { IMessage } from '../config/messageInterfaces'; +export interface Message extends IMessage { +} +/** + * Rocket.Chat message class. + * Sets integration param to allow tracing source of automated sends. + * @param content Accepts message text or a preformed message object + * @todo Potential for SDK usage that isn't bots, bot prop should be optional? + */ +export declare class Message { + constructor(content: string | IMessage, integrationId: string); + setRoomId(roomId: string): Message; +} diff --git a/dist/lib/message.js b/dist/lib/message.js new file mode 100644 index 0000000..1561fe1 --- /dev/null +++ b/dist/lib/message.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Rocket.Chat message class. + * Sets integration param to allow tracing source of automated sends. + * @param content Accepts message text or a preformed message object + * @todo Potential for SDK usage that isn't bots, bot prop should be optional? + */ +class Message { + constructor(content, integrationId) { + if (typeof content === 'string') + this.msg = content; + else + Object.assign(this, content); + this.bot = { i: integrationId }; + } + setRoomId(roomId) { + this.rid = roomId; + return this; + } +} +exports.Message = Message; +//# sourceMappingURL=message.js.map \ No newline at end of file diff --git a/dist/lib/message.js.map b/dist/lib/message.js.map new file mode 100644 index 0000000..a7f852a --- /dev/null +++ b/dist/lib/message.js.map @@ -0,0 +1 @@ +{"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/lib/message.ts"],"names":[],"mappings":";;AAMA;;;;;GAKG;AACH;IACE,YAAa,OAA0B,EAAE,aAAqB;QAC5D,EAAE,CAAC,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC;YAAC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAA;QACnD,IAAI;YAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QACjC,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,aAAa,EAAE,CAAA;IACjC,CAAC;IACD,SAAS,CAAE,MAAc;QACvB,IAAI,CAAC,GAAG,GAAG,MAAM,CAAA;QACjB,MAAM,CAAC,IAAI,CAAA;IACb,CAAC;CACF;AAVD,0BAUC","sourcesContent":["import { IMessage } from '../config/messageInterfaces'\n\n// Message class declaration implicitly implements interface\n// https://github.com/Microsoft/TypeScript/issues/340\nexport interface Message extends IMessage {}\n\n/**\n * Rocket.Chat message class.\n * Sets integration param to allow tracing source of automated sends.\n * @param content Accepts message text or a preformed message object\n * @todo Potential for SDK usage that isn't bots, bot prop should be optional?\n */\nexport class Message {\n constructor (content: string | IMessage, integrationId: string) {\n if (typeof content === 'string') this.msg = content\n else Object.assign(this, content)\n this.bot = { i: integrationId }\n }\n setRoomId (roomId: string): Message {\n this.rid = roomId\n return this\n }\n}\n"]} \ No newline at end of file diff --git a/dist/lib/methodCache.d.ts b/dist/lib/methodCache.d.ts new file mode 100644 index 0000000..0dde4a2 --- /dev/null +++ b/dist/lib/methodCache.d.ts @@ -0,0 +1,46 @@ +/// +import LRU from 'lru-cache'; +/** @TODO: Remove ! post-fix expression when TypeScript #9619 resolved */ +export declare let instance: any; +export declare const results: Map>; +export declare const defaults: LRU.Options; +/** + * Set the instance to call methods on, with cached results. + * @param instanceToUse Instance of a class + */ +export declare function use(instanceToUse: object): void; +/** + * Setup a cache for a method call. + * @param method Method name, for index of cached results + * @param options.max Maximum size of cache + * @param options.maxAge Maximum age of cache + */ +export declare function create(method: string, options?: LRU.Options): LRU.Cache | undefined; +/** + * Get results of a prior method call or call and cache. + * @param method Method name, to call on instance in use + * @param key Key to pass to method call and save results against + */ +export declare function call(method: string, key: string): Promise; +/** + * Proxy for checking if method has been cached. + * Cache may exist from manual creation, or prior call. + * @param method Method name for cache to get + */ +export declare function has(method: string): boolean; +/** + * Get results of a prior method call. + * @param method Method name for cache to get + * @param key Key for method result set to return + */ +export declare function get(method: string, key: string): LRU.Cache | undefined; +/** + * Reset a cached method call's results (all or only for given key). + * @param method Method name for cache to clear + * @param key Key for method result set to clear + */ +export declare function reset(method: string, key?: string): void; +/** + * Reset cached results for all methods. + */ +export declare function resetAll(): void; diff --git a/dist/lib/methodCache.js b/dist/lib/methodCache.js new file mode 100644 index 0000000..340a288 --- /dev/null +++ b/dist/lib/methodCache.js @@ -0,0 +1,97 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +} +Object.defineProperty(exports, "__esModule", { value: true }); +const lru_cache_1 = __importDefault(require("lru-cache")); +const log_1 = require("./log"); +exports.results = new Map(); +exports.defaults = { + max: 100, + maxAge: 300 * 1000 +}; +/** + * Set the instance to call methods on, with cached results. + * @param instanceToUse Instance of a class + */ +function use(instanceToUse) { + exports.instance = instanceToUse; +} +exports.use = use; +/** + * Setup a cache for a method call. + * @param method Method name, for index of cached results + * @param options.max Maximum size of cache + * @param options.maxAge Maximum age of cache + */ +function create(method, options = {}) { + options = Object.assign(exports.defaults, options); + exports.results.set(method, new lru_cache_1.default(options)); + return exports.results.get(method); +} +exports.create = create; +/** + * Get results of a prior method call or call and cache. + * @param method Method name, to call on instance in use + * @param key Key to pass to method call and save results against + */ +function call(method, key) { + if (!exports.results.has(method)) + create(method); // create as needed + const methodCache = exports.results.get(method); + let callResults; + if (methodCache.has(key)) { + log_1.logger.debug(`[${method}] Calling (cached): ${key}`); + // return from cache if key has been used on method before + callResults = methodCache.get(key); + } + else { + // call and cache for next time, returning results + log_1.logger.debug(`[${method}] Calling (caching): ${key}`); + callResults = exports.instance.call(method, key).result; + methodCache.set(key, callResults); + } + return Promise.resolve(callResults); +} +exports.call = call; +/** + * Proxy for checking if method has been cached. + * Cache may exist from manual creation, or prior call. + * @param method Method name for cache to get + */ +function has(method) { + return exports.results.has(method); +} +exports.has = has; +/** + * Get results of a prior method call. + * @param method Method name for cache to get + * @param key Key for method result set to return + */ +function get(method, key) { + if (exports.results.has(method)) + return exports.results.get(method).get(key); +} +exports.get = get; +/** + * Reset a cached method call's results (all or only for given key). + * @param method Method name for cache to clear + * @param key Key for method result set to clear + */ +function reset(method, key) { + if (exports.results.has(method)) { + if (key) + return exports.results.get(method).del(key); + else + return exports.results.get(method).reset(); + } +} +exports.reset = reset; +/** + * Reset cached results for all methods. + */ +function resetAll() { + exports.results.forEach((cache) => cache.reset()); +} +exports.resetAll = resetAll; +//# sourceMappingURL=methodCache.js.map \ No newline at end of file diff --git a/dist/lib/methodCache.js.map b/dist/lib/methodCache.js.map new file mode 100644 index 0000000..3ec5fda --- /dev/null +++ b/dist/lib/methodCache.js.map @@ -0,0 +1 @@ +{"version":3,"file":"methodCache.js","sourceRoot":"","sources":["../../src/lib/methodCache.ts"],"names":[],"mappings":";;;;;AAAA,0DAA2B;AAC3B,+BAA8B;AAIjB,QAAA,OAAO,GAAwC,IAAI,GAAG,EAAE,CAAA;AACxD,QAAA,QAAQ,GAAgB;IACnC,GAAG,EAAE,GAAG;IACR,MAAM,EAAE,GAAG,GAAG,IAAI;CACnB,CAAA;AAED;;;GAGG;AACH,aAAqB,aAAqB;IACxC,gBAAQ,GAAG,aAAa,CAAA;AAC1B,CAAC;AAFD,kBAEC;AAED;;;;;GAKG;AACH,gBAAwB,MAAc,EAAE,UAAuB,EAAE;IAC/D,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAQ,EAAE,OAAO,CAAC,CAAA;IAC1C,eAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,mBAAG,CAAC,OAAO,CAAC,CAAC,CAAA;IACrC,MAAM,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AAC5B,CAAC;AAJD,wBAIC;AAED;;;;GAIG;AACH,cAAsB,MAAc,EAAE,GAAW;IAC/C,EAAE,CAAC,CAAC,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,MAAM,CAAC,CAAA,CAAC,mBAAmB;IAC5D,MAAM,WAAW,GAAG,eAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAA;IACxC,IAAI,WAAW,CAAA;IAEf,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACzB,YAAM,CAAC,KAAK,CAAC,IAAI,MAAM,uBAAuB,GAAG,EAAE,CAAC,CAAA;QACpD,0DAA0D;QAC1D,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,CAAC;IAAC,IAAI,CAAC,CAAC;QACN,kDAAkD;QAClD,YAAM,CAAC,KAAK,CAAC,IAAI,MAAM,wBAAwB,GAAG,EAAE,CAAC,CAAA;QACrD,WAAW,GAAG,gBAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,MAAM,CAAA;QAC/C,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAA;IACnC,CAAC;IACD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;AACrC,CAAC;AAhBD,oBAgBC;AAED;;;;GAIG;AACH,aAAqB,MAAc;IACjC,MAAM,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AAC5B,CAAC;AAFD,kBAEC;AAED;;;;GAIG;AACH,aAAqB,MAAc,EAAE,GAAW;IAC9C,EAAE,CAAC,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAAC,MAAM,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAC/D,CAAC;AAFD,kBAEC;AAED;;;;GAIG;AACH,eAAuB,MAAc,EAAE,GAAY;IACjD,EAAE,CAAC,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,EAAE,CAAC,CAAC,GAAG,CAAC;YAAC,MAAM,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7C,IAAI;YAAC,MAAM,CAAC,eAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,KAAK,EAAE,CAAA;IAC1C,CAAC;AACH,CAAC;AALD,sBAKC;AAED;;GAEG;AACH;IACE,eAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;AAC3C,CAAC;AAFD,4BAEC","sourcesContent":["import LRU from 'lru-cache'\nimport { logger } from './log'\n\n/** @TODO: Remove ! post-fix expression when TypeScript #9619 resolved */\nexport let instance: any\nexport const results: Map> = new Map()\nexport const defaults: LRU.Options = {\n max: 100,\n maxAge: 300 * 1000\n}\n\n/**\n * Set the instance to call methods on, with cached results.\n * @param instanceToUse Instance of a class\n */\nexport function use (instanceToUse: object): void {\n instance = instanceToUse\n}\n\n/**\n * Setup a cache for a method call.\n * @param method Method name, for index of cached results\n * @param options.max Maximum size of cache\n * @param options.maxAge Maximum age of cache\n */\nexport function create (method: string, options: LRU.Options = {}): LRU.Cache | undefined {\n options = Object.assign(defaults, options)\n results.set(method, new LRU(options))\n return results.get(method)\n}\n\n/**\n * Get results of a prior method call or call and cache.\n * @param method Method name, to call on instance in use\n * @param key Key to pass to method call and save results against\n */\nexport function call (method: string, key: string): Promise {\n if (!results.has(method)) create(method) // create as needed\n const methodCache = results.get(method)!\n let callResults\n\n if (methodCache.has(key)) {\n logger.debug(`[${method}] Calling (cached): ${key}`)\n // return from cache if key has been used on method before\n callResults = methodCache.get(key)\n } else {\n // call and cache for next time, returning results\n logger.debug(`[${method}] Calling (caching): ${key}`)\n callResults = instance.call(method, key).result\n methodCache.set(key, callResults)\n }\n return Promise.resolve(callResults)\n}\n\n/**\n * Proxy for checking if method has been cached.\n * Cache may exist from manual creation, or prior call.\n * @param method Method name for cache to get\n */\nexport function has (method: string): boolean {\n return results.has(method)\n}\n\n/**\n * Get results of a prior method call.\n * @param method Method name for cache to get\n * @param key Key for method result set to return\n */\nexport function get (method: string, key: string): LRU.Cache | undefined {\n if (results.has(method)) return results.get(method)!.get(key)\n}\n\n/**\n * Reset a cached method call's results (all or only for given key).\n * @param method Method name for cache to clear\n * @param key Key for method result set to clear\n */\nexport function reset (method: string, key?: string): void {\n if (results.has(method)) {\n if (key) return results.get(method)!.del(key)\n else return results.get(method)!.reset()\n }\n}\n\n/**\n * Reset cached results for all methods.\n */\nexport function resetAll (): void {\n results.forEach((cache) => cache.reset())\n}\n"]} \ No newline at end of file diff --git a/dist/lib/settings.d.ts b/dist/lib/settings.d.ts new file mode 100644 index 0000000..2696663 --- /dev/null +++ b/dist/lib/settings.d.ts @@ -0,0 +1,16 @@ +export declare let username: string; +export declare let password: string; +export declare let ldap: boolean; +export declare let host: string; +export declare let useSsl: boolean; +export declare let timeout: number; +export declare let rooms: string[]; +export declare let allPublic: boolean; +export declare let dm: boolean; +export declare let livechat: boolean; +export declare let edited: boolean; +export declare let integrationId: string; +export declare let roomCacheMaxSize: number; +export declare let roomCacheMaxAge: number; +export declare let dmCacheMaxSize: number; +export declare let dmCacheMaxAge: number; diff --git a/dist/lib/settings.js b/dist/lib/settings.js new file mode 100644 index 0000000..3825afa --- /dev/null +++ b/dist/lib/settings.js @@ -0,0 +1,28 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// Login settings - LDAP needs to be explicitly enabled +exports.username = process.env.ROCKETCHAT_USER || 'bot'; +exports.password = process.env.ROCKETCHAT_PASSWORD || 'pass'; +exports.ldap = (process.env.ROCKETCHAT_AUTH === 'ldap'); +// Connection settings - Enable SSL by default if Rocket.Chat URL contains https +exports.host = process.env.ROCKETCHAT_URL || 'localhost:3000'; +exports.useSsl = (process.env.ROCKETCHAT_USE_SSL) + ? ((process.env.ROCKETCHAT_USE_SSL || '').toString().toLowerCase() === 'true') + : ((process.env.ROCKETCHAT_URL || '').toString().toLowerCase().startsWith('https')); +exports.timeout = 20 * 1000; // 20 seconds +// Respond settings - reactive callback filters for .respondToMessages +exports.rooms = (process.env.ROCKETCHAT_ROOM) + ? (process.env.ROCKETCHAT_ROOM || '').split(',').map((room) => room.trim()) + : []; +exports.allPublic = (process.env.LISTEN_ON_ALL_PUBLIC || 'false').toLowerCase() === 'true'; +exports.dm = (process.env.RESPOND_TO_DM || 'false').toLowerCase() === 'true'; +exports.livechat = (process.env.RESPOND_TO_LIVECHAT || 'false').toLowerCase() === 'true'; +exports.edited = (process.env.RESPOND_TO_EDITED || 'false').toLowerCase() === 'true'; +// Message attribute settings +exports.integrationId = process.env.INTEGRATION_ID || 'js.SDK'; +// Cache settings +exports.roomCacheMaxSize = parseInt(process.env.ROOM_CACHE_SIZE || '10', 10); +exports.roomCacheMaxAge = 1000 * parseInt(process.env.ROOM_CACHE_MAX_AGE || '300', 10); +exports.dmCacheMaxSize = parseInt(process.env.DM_ROOM_CACHE_SIZE || '10', 10); +exports.dmCacheMaxAge = 1000 * parseInt(process.env.DM_ROOM_CACHE_MAX_AGE || '100', 10); +//# sourceMappingURL=settings.js.map \ No newline at end of file diff --git a/dist/lib/settings.js.map b/dist/lib/settings.js.map new file mode 100644 index 0000000..c7f8afc --- /dev/null +++ b/dist/lib/settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/lib/settings.ts"],"names":[],"mappings":";;AACA,uDAAuD;AAC5C,QAAA,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,KAAK,CAAA;AAC/C,QAAA,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,MAAM,CAAA;AACpD,QAAA,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,MAAM,CAAC,CAAA;AAE1D,gFAAgF;AACrE,QAAA,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,gBAAgB,CAAA;AACrD,QAAA,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAClD,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;IAC9E,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;AAC1E,QAAA,OAAO,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,aAAa;AAE5C,sEAAsE;AAC3D,QAAA,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;IAC9C,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3E,CAAC,CAAC,EAAE,CAAA;AACK,QAAA,SAAS,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;AAClF,QAAA,EAAE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;AACpE,QAAA,QAAQ,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;AAChF,QAAA,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAA;AAEvF,6BAA6B;AAClB,QAAA,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,QAAQ,CAAA;AAEjE,iBAAiB;AACN,QAAA,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;AACpE,QAAA,eAAe,GAAG,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,KAAK,EAAE,EAAE,CAAC,CAAA;AAC9E,QAAA,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,EAAE,EAAE,CAAC,CAAA;AACrE,QAAA,aAAa,GAAG,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,KAAK,EAAE,EAAE,CAAC,CAAA","sourcesContent":["\n// Login settings - LDAP needs to be explicitly enabled\nexport let username = process.env.ROCKETCHAT_USER || 'bot'\nexport let password = process.env.ROCKETCHAT_PASSWORD || 'pass'\nexport let ldap = (process.env.ROCKETCHAT_AUTH === 'ldap')\n\n// Connection settings - Enable SSL by default if Rocket.Chat URL contains https\nexport let host = process.env.ROCKETCHAT_URL || 'localhost:3000'\nexport let useSsl = (process.env.ROCKETCHAT_USE_SSL)\n ? ((process.env.ROCKETCHAT_USE_SSL || '').toString().toLowerCase() === 'true')\n : ((process.env.ROCKETCHAT_URL || '').toString().toLowerCase().startsWith('https'))\nexport let timeout = 20 * 1000 // 20 seconds\n\n// Respond settings - reactive callback filters for .respondToMessages\nexport let rooms = (process.env.ROCKETCHAT_ROOM)\n ? (process.env.ROCKETCHAT_ROOM || '').split(',').map((room) => room.trim())\n : []\nexport let allPublic = (process.env.LISTEN_ON_ALL_PUBLIC || 'false').toLowerCase() === 'true'\nexport let dm = (process.env.RESPOND_TO_DM || 'false').toLowerCase() === 'true'\nexport let livechat = (process.env.RESPOND_TO_LIVECHAT || 'false').toLowerCase() === 'true'\nexport let edited = (process.env.RESPOND_TO_EDITED || 'false').toLowerCase() === 'true'\n\n// Message attribute settings\nexport let integrationId = process.env.INTEGRATION_ID || 'js.SDK'\n\n// Cache settings\nexport let roomCacheMaxSize = parseInt(process.env.ROOM_CACHE_SIZE || '10', 10)\nexport let roomCacheMaxAge = 1000 * parseInt(process.env.ROOM_CACHE_MAX_AGE || '300', 10)\nexport let dmCacheMaxSize = parseInt(process.env.DM_ROOM_CACHE_SIZE || '10', 10)\nexport let dmCacheMaxAge = 1000 * parseInt(process.env.DM_ROOM_CACHE_MAX_AGE || '100', 10)\n"]} \ No newline at end of file diff --git a/dist/utils/config.d.ts b/dist/utils/config.d.ts new file mode 100644 index 0000000..1897a4d --- /dev/null +++ b/dist/utils/config.d.ts @@ -0,0 +1,4 @@ +import { INewUserAPI } from './interfaces'; +export declare const apiUser: INewUserAPI; +export declare const botUser: INewUserAPI; +export declare const mockUser: INewUserAPI; diff --git a/dist/utils/config.js b/dist/utils/config.js new file mode 100644 index 0000000..27267da --- /dev/null +++ b/dist/utils/config.js @@ -0,0 +1,34 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +// The API user, should be provisioned on build with local Rocket.Chat +exports.apiUser = { + username: process.env.ADMIN_USERNAME || 'admin', + password: process.env.ADMIN_PASS || 'pass' +}; +// The Bot user, will attempt to login and run methods in tests +exports.botUser = { + email: 'bot@localhost', + name: 'Bot', + password: process.env.ROCKETCHAT_PASSWORD || 'pass', + username: process.env.ROCKETCHAT_USER || 'bot', + active: true, + roles: ['bot'], + joinDefaultChannels: true, + requirePasswordChange: false, + sendWelcomeEmail: false, + verified: true +}; +// The Mock user, will send messages via API for the bot to respond to +exports.mockUser = { + email: 'mock@localhost', + name: 'Mock User', + password: 'mock', + username: 'mock', + active: true, + roles: ['user'], + joinDefaultChannels: true, + requirePasswordChange: false, + sendWelcomeEmail: false, + verified: true +}; +//# sourceMappingURL=config.js.map \ No newline at end of file diff --git a/dist/utils/config.js.map b/dist/utils/config.js.map new file mode 100644 index 0000000..aa5019d --- /dev/null +++ b/dist/utils/config.js.map @@ -0,0 +1 @@ +{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":";;AAEA,sEAAsE;AACzD,QAAA,OAAO,GAAgB;IAClC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO;IAC/C,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM;CAC3C,CAAA;AAED,+DAA+D;AAClD,QAAA,OAAO,GAAgB;IAClC,KAAK,EAAE,eAAe;IACtB,IAAI,EAAE,KAAK;IACX,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,MAAM;IACnD,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,KAAK;IAC9C,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,CAAC,KAAK,CAAC;IACd,mBAAmB,EAAE,IAAI;IACzB,qBAAqB,EAAE,KAAK;IAC5B,gBAAgB,EAAE,KAAK;IACvB,QAAQ,EAAE,IAAI;CACf,CAAA;AAED,sEAAsE;AACzD,QAAA,QAAQ,GAAgB;IACnC,KAAK,EAAE,gBAAgB;IACvB,IAAI,EAAE,WAAW;IACjB,QAAQ,EAAE,MAAM;IAChB,QAAQ,EAAE,MAAM;IAChB,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,mBAAmB,EAAE,IAAI;IACzB,qBAAqB,EAAE,KAAK;IAC5B,gBAAgB,EAAE,KAAK;IACvB,QAAQ,EAAE,IAAI;CACf,CAAA","sourcesContent":["import { INewUserAPI } from './interfaces'\n\n// The API user, should be provisioned on build with local Rocket.Chat\nexport const apiUser: INewUserAPI = {\n username: process.env.ADMIN_USERNAME || 'admin',\n password: process.env.ADMIN_PASS || 'pass'\n}\n\n// The Bot user, will attempt to login and run methods in tests\nexport const botUser: INewUserAPI = {\n email: 'bot@localhost',\n name: 'Bot',\n password: process.env.ROCKETCHAT_PASSWORD || 'pass',\n username: process.env.ROCKETCHAT_USER || 'bot',\n active: true,\n roles: ['bot'],\n joinDefaultChannels: true,\n requirePasswordChange: false,\n sendWelcomeEmail: false,\n verified: true\n}\n\n// The Mock user, will send messages via API for the bot to respond to\nexport const mockUser: INewUserAPI = {\n email: 'mock@localhost',\n name: 'Mock User',\n password: 'mock',\n username: 'mock',\n active: true,\n roles: ['user'],\n joinDefaultChannels: true,\n requirePasswordChange: false,\n sendWelcomeEmail: false,\n verified: true\n}\n"]} \ No newline at end of file diff --git a/dist/utils/interfaces.d.ts b/dist/utils/interfaces.d.ts new file mode 100644 index 0000000..c7fe4a7 --- /dev/null +++ b/dist/utils/interfaces.d.ts @@ -0,0 +1,154 @@ +/** Payload structure for `chat.postMessage` endpoint */ +export interface IMessageAPI { + roomId: string; + channel?: string; + text?: string; + alias?: string; + emoji?: string; + avatar?: string; + attachments?: IAttachmentAPI[]; +} +/** Payload structure for `chat.update` endpoint */ +export interface IMessageUpdateAPI { + roomId: string; + msgId: string; + text: string; +} +/** Message receipt returned after send (not the same as sent object) */ +export interface IMessageReceiptAPI { + _id: string; + rid: string; + alias: string; + msg: string; + parseUrls: boolean; + groupable: boolean; + ts: string; + u: { + _id: string; + username: string; + }; + _updatedAt: string; + editedAt?: string; + editedBy?: { + _id: string; + username: string; + }; +} +/** Payload structure for message attachments */ +export interface IAttachmentAPI { + color?: string; + text?: string; + ts?: string; + thumb_url?: string; + message_link?: string; + collapsed?: boolean; + author_name?: string; + author_link?: string; + author_icon?: string; + title?: string; + title_link?: string; + title_link_download_true?: string; + image_url?: string; + audio_url?: string; + video_url?: string; + fields?: IAttachmentFieldAPI[]; +} +/** + * Payload structure for attachment field object + * The field property of the attachments allows for “tables” or “columns” to be displayed on messages + */ +export interface IAttachmentFieldAPI { + short?: boolean; + title: string; + value: string; +} +/** Result structure for message endpoints */ +export interface IMessageResultAPI { + ts: number; + channel: string; + message: IMessageReceiptAPI; + success: boolean; +} +/** User object structure for creation endpoints */ +export interface INewUserAPI { + email?: string; + name?: string; + password: string; + username: string; + active?: true; + roles?: string[]; + joinDefaultChannels?: boolean; + requirePasswordChange?: boolean; + sendWelcomeEmail?: boolean; + verified?: true; +} +/** User object structure for queries (not including admin access level) */ +export interface IUserAPI { + _id: string; + type: string; + status: string; + active: boolean; + name: string; + utcOffset: number; + username: string; +} +/** Result structure for user data request (by non-admin) */ +export interface IUserResultAPI { + user: IUserAPI; + success: boolean; +} +/** Room object structure */ +export interface IRoomAPI { + _id: string; + _updatedAt: string; + t: 'c' | 'p' | 'd' | 'l'; + msgs: number; + ts: string; + meta: { + revision: number; + created: number; + version: number; + }; +} +/** Channel result schema */ +export interface IChannelAPI { + _id: string; + name: string; + t: 'c' | 'p' | 'l'; + msgs: number; + u: { + _id: string; + username: string; + }; + ts: string; + default: boolean; +} +/** Group result schema */ +export interface IGroupAPI { + _id: string; + name: string; + usernames: string[]; + t: 'c' | 'p' | 'l'; + msgs: number; + u: { + _id: string; + username: string; + }; + ts: string; + default: boolean; +} +/** Result structure for room creation (e.g. DM) */ +export interface IRoomResultAPI { + room: IRoomAPI; + success: boolean; +} +/** Result structure for channel creation */ +export interface IChannelResultAPI { + channel: IChannelAPI; + success: boolean; +} +/** Result structure for group creation */ +export interface IGroupResultAPI { + group: IGroupAPI; + success: boolean; +} diff --git a/dist/utils/interfaces.js b/dist/utils/interfaces.js new file mode 100644 index 0000000..db91911 --- /dev/null +++ b/dist/utils/interfaces.js @@ -0,0 +1,3 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +//# sourceMappingURL=interfaces.js.map \ No newline at end of file diff --git a/dist/utils/interfaces.js.map b/dist/utils/interfaces.js.map new file mode 100644 index 0000000..eb3b0c9 --- /dev/null +++ b/dist/utils/interfaces.js.map @@ -0,0 +1 @@ +{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/utils/interfaces.ts"],"names":[],"mappings":"","sourcesContent":["/** Payload structure for `chat.postMessage` endpoint */\nexport interface IMessageAPI {\n roomId: string // The room id of where the message is to be sent\n channel?: string // The channel name with the prefix in front of it\n text?: string // The text of the message to send, is optional because of attachments\n alias?: string // This will cause the messenger name to appear as the given alias, but username will still display\n emoji?: string // If provided, this will make the avatar on this message be an emoji\n avatar?: string // If provided, this will make the avatar use the provided image url\n attachments?: IAttachmentAPI[] // See attachment interface below\n}\n\n/** Payload structure for `chat.update` endpoint */\nexport interface IMessageUpdateAPI {\n roomId: string // The room id of where the message is\n msgId: string // The message id to update\n text: string // Updated text for the message\n}\n\n/** Message receipt returned after send (not the same as sent object) */\nexport interface IMessageReceiptAPI {\n _id: string // ID of sent message\n rid: string // Room ID of sent message\n alias: string // ?\n msg: string // Content of message\n parseUrls: boolean // URL parsing enabled on message hooks\n groupable: boolean // Grouping message enabled\n ts: string // Timestamp of message creation\n u: { // User details of sender\n _id: string\n username: string\n }\n _updatedAt: string // Time message last updated\n editedAt?: string // Time updated by edit\n editedBy?: { // User details for the updater\n _id: string\n username: string\n }\n}\n\n/** Payload structure for message attachments */\nexport interface IAttachmentAPI {\n color?: string // The color you want the order on the left side to be, any value background-css supports\n text?: string // The text to display for this attachment, it is different than the message text\n ts?: string // ISO timestamp, displays the time next to the text portion\n thumb_url?: string // An image that displays to the left of the text, looks better when this is relatively small\n message_link?: string // Only applicable if the ts is provided, as it makes the time clickable to this link\n collapsed?: boolean // Causes the image, audio, and video sections to be hiding when collapsed is true\n author_name?: string // Name of the author\n author_link?: string // Providing this makes the author name clickable and points to this link\n author_icon?: string // Displays a tiny icon to the left of the author's name\n title?: string // Title to display for this attachment, displays under the author\n title_link?: string // Providing this makes the title clickable, pointing to this link\n title_link_download_true?: string // When this is true, a download icon appears and clicking this saves the link to file\n image_url?: string // The image to display, will be “big” and easy to see\n audio_url?: string // Audio file to play, only supports what html audio does\n video_url?: string // Video file to play, only supports what html video does\n fields?: IAttachmentFieldAPI[] // An array of Attachment Field Objects\n}\n\n/**\n * Payload structure for attachment field object\n * The field property of the attachments allows for “tables” or “columns” to be displayed on messages\n */\nexport interface IAttachmentFieldAPI {\n short?: boolean // Whether this field should be a short field\n title: string // The title of this field\n value: string // The value of this field, displayed underneath the title value\n}\n\n/** Result structure for message endpoints */\nexport interface IMessageResultAPI {\n ts: number // Seconds since unix epoch\n channel: string // Name of channel without prefix\n message: IMessageReceiptAPI // Sent message\n success: boolean // Send status\n}\n\n/** User object structure for creation endpoints */\nexport interface INewUserAPI {\n email?: string // Email address\n name?: string // Full name\n password: string // User pass\n username: string // Username\n active?: true // Subscription is active\n roles?: string[] // Role IDs\n joinDefaultChannels?: boolean // Auto join channels marked as default\n requirePasswordChange?: boolean // Direct to password form on next login\n sendWelcomeEmail?: boolean // Send new credentials in email\n verified?: true // Email address verification status\n}\n\n/** User object structure for queries (not including admin access level) */\nexport interface IUserAPI {\n _id: string // MongoDB user doc ID\n type: string // user / bot ?\n status: string // online | offline\n active: boolean // Subscription is active\n name: string // Full name\n utcOffset: number // Hours off UTC/GMT\n username: string // Username\n}\n\n/** Result structure for user data request (by non-admin) */\nexport interface IUserResultAPI {\n user: IUserAPI // The requested user\n success: boolean // Status of request\n}\n\n/** Room object structure */\nexport interface IRoomAPI {\n _id: string // Room ID\n _updatedAt: string // ISO timestamp\n t: 'c' | 'p' | 'd' | 'l' // Room type (channel, private, direct, livechat)\n msgs: number // Count of messages in room\n ts: string // ISO timestamp (current time in room?)\n meta: {\n revision: number // ??\n created: number // Unix ms>epoch time\n version: number // ??\n }\n}\n\n/** Channel result schema */\nexport interface IChannelAPI {\n _id: string // Channel ID\n name: string // Channel name\n t: 'c' | 'p' | 'l' // Channel type (channel always c)\n msgs: number // Count of messages in room\n u: {\n _id: string // Owner user ID\n username: string // Owner username\n }\n ts: string // ISO timestamp (current time in room?)\n default: boolean // Is default channel\n}\n\n/** Group result schema */\nexport interface IGroupAPI {\n _id: string // Group ID\n name: string // Group name\n usernames: string[] // Users in group\n t: 'c' | 'p' | 'l' // Group type (private always p)\n msgs: number // Count of messages in room\n u: {\n _id: string // Owner user ID\n username: string // Owner username\n }\n ts: string // ISO timestamp (current time in room?)\n default: boolean // Is default channel (would be false)\n}\n\n/** Result structure for room creation (e.g. DM) */\nexport interface IRoomResultAPI {\n room: IRoomAPI\n success: boolean\n}\n\n/** Result structure for channel creation */\nexport interface IChannelResultAPI {\n channel: IChannelAPI\n success: boolean\n}\n\n/** Result structure for group creation */\nexport interface IGroupResultAPI {\n group: IGroupAPI\n success: boolean\n}\n"]} \ No newline at end of file diff --git a/dist/utils/setup.d.ts b/dist/utils/setup.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/utils/setup.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/utils/setup.js b/dist/utils/setup.js new file mode 100644 index 0000000..3d63e0b --- /dev/null +++ b/dist/utils/setup.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +/** On require, runs the test utils setup method */ +const testing_1 = require("./testing"); +const log_1 = require("../lib/log"); +log_1.silence(); +testing_1.setup().catch((e) => console.error(e)); +//# sourceMappingURL=setup.js.map \ No newline at end of file diff --git a/dist/utils/setup.js.map b/dist/utils/setup.js.map new file mode 100644 index 0000000..431fc9a --- /dev/null +++ b/dist/utils/setup.js.map @@ -0,0 +1 @@ +{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/utils/setup.ts"],"names":[],"mappings":";;AAAA,mDAAmD;AACnD,uCAAiC;AACjC,oCAAoC;AACpC,aAAO,EAAE,CAAA;AACT,eAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["/** On require, runs the test utils setup method */\nimport { setup } from './testing'\nimport { silence } from '../lib/log'\nsilence()\nsetup().catch((e) => console.error(e))\n"]} \ No newline at end of file diff --git a/dist/utils/start.d.ts b/dist/utils/start.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/utils/start.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/utils/start.js b/dist/utils/start.js new file mode 100644 index 0000000..7c17489 --- /dev/null +++ b/dist/utils/start.js @@ -0,0 +1,65 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// Test script uses standard methods and env config to connect and log streams +const config_1 = require("./config"); +const __1 = require(".."); +const delay = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms)); +// Start subscription to log message stream (used for e2e test and demo) +function start() { + return __awaiter(this, void 0, void 0, function* () { + yield __1.driver.connect(); + yield __1.driver.login({ username: config_1.botUser.username, password: config_1.botUser.password }); + yield __1.driver.subscribeToMessages(); + yield __1.driver.respondToMessages((err, msg, msgOpts) => { + if (err) + throw err; + console.log('[respond]', JSON.stringify(msg), JSON.stringify(msgOpts)); + demo(msg).catch((e) => console.error(e)); + }, { + rooms: ['general'], + allPublic: false, + dm: true, + edited: true, + livechat: false + }); + }); +} +// Demo bot-style interactions +// A: Listen for "tell everyone " and send that something to everyone +// B: Listen for "who's online" and tell that person who's online +function demo(message) { + return __awaiter(this, void 0, void 0, function* () { + console.log(message); + if (!message.msg) + return; + if (/tell everyone/i.test(message.msg)) { + const match = message.msg.match(/tell everyone (.*)/i); + if (!match || !match[1]) + return; + const sayWhat = `@${message.u.username} says "${match[1]}"`; + const usernames = yield __1.api.users.allNames(); + for (let username of usernames) { + if (username !== config_1.botUser.username) { + const toWhere = yield __1.driver.getDirectMessageRoomId(username); + yield __1.driver.sendToRoomId(sayWhat, toWhere); // DM ID hax + yield delay(200); // delay to prevent rate-limit error + } + } + } + else if (/who\'?s online/i.test(message.msg)) { + const names = yield __1.api.users.onlineNames(); + const niceNames = names.join(', ').replace(/, ([^,]*)$/, ' and $1'); + yield __1.driver.sendToRoomId(niceNames + ' are online', message.rid); + } + }); +} +start().catch((e) => console.error(e)); +//# sourceMappingURL=start.js.map \ No newline at end of file diff --git a/dist/utils/start.js.map b/dist/utils/start.js.map new file mode 100644 index 0000000..6dec501 --- /dev/null +++ b/dist/utils/start.js.map @@ -0,0 +1 @@ +{"version":3,"file":"start.js","sourceRoot":"","sources":["../../src/utils/start.ts"],"names":[],"mappings":"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","sourcesContent":["// Test script uses standard methods and env config to connect and log streams\nimport { botUser } from './config'\nimport { IMessage } from '../config/messageInterfaces'\nimport { api, driver } from '..'\nconst delay = (ms: number) => new Promise((resolve, reject) => setTimeout(resolve, ms))\n\n// Start subscription to log message stream (used for e2e test and demo)\nasync function start () {\n await driver.connect()\n await driver.login({ username: botUser.username, password: botUser.password })\n await driver.subscribeToMessages()\n await driver.respondToMessages((err, msg, msgOpts) => {\n if (err) throw err\n console.log('[respond]', JSON.stringify(msg), JSON.stringify(msgOpts))\n demo(msg).catch((e) => console.error(e))\n }, {\n rooms: ['general'],\n allPublic: false,\n dm: true,\n edited: true,\n livechat: false\n })\n}\n\n// Demo bot-style interactions\n// A: Listen for \"tell everyone \" and send that something to everyone\n// B: Listen for \"who's online\" and tell that person who's online\nasync function demo (message: IMessage) {\n console.log(message)\n if (!message.msg) return\n if (/tell everyone/i.test(message.msg)) {\n const match = message.msg.match(/tell everyone (.*)/i)\n if (!match || !match[1]) return\n const sayWhat = `@${message.u!.username} says \"${match[1]}\"`\n const usernames = await api.users.allNames()\n for (let username of usernames) {\n if (username !== botUser.username) {\n const toWhere = await driver.getDirectMessageRoomId(username)\n await driver.sendToRoomId(sayWhat, toWhere) // DM ID hax\n await delay(200) // delay to prevent rate-limit error\n }\n }\n } else if (/who\\'?s online/i.test(message.msg)) {\n const names = await api.users.onlineNames()\n const niceNames = names.join(', ').replace(/, ([^,]*)$/, ' and $1')\n await driver.sendToRoomId(niceNames + ' are online', message.rid!)\n }\n}\n\nstart().catch((e) => console.error(e))\n"]} \ No newline at end of file diff --git a/dist/utils/testing.d.ts b/dist/utils/testing.d.ts new file mode 100644 index 0000000..4fe88fc --- /dev/null +++ b/dist/utils/testing.d.ts @@ -0,0 +1,49 @@ +import { IMessageUpdateAPI, IMessageResultAPI, INewUserAPI, IUserResultAPI, IRoomResultAPI, IChannelResultAPI, IGroupResultAPI } from './interfaces'; +import { IMessage } from '../config/messageInterfaces'; +/** Define common attributes for DRY tests */ +export declare const testChannelName = "tests"; +export declare const testPrivateName = "p-tests"; +/** Get information about a user */ +export declare function userInfo(username: string): Promise; +/** Create a user and catch the error if they exist already */ +export declare function createUser(user: INewUserAPI): Promise; +/** Get information about a channel */ +export declare function channelInfo(query: { + roomName?: string; + roomId?: string; +}): Promise; +/** Get information about a private group */ +export declare function privateInfo(query: { + roomName?: string; + roomId?: string; +}): Promise; +/** Get the last messages sent to a channel (in last 10 minutes) */ +export declare function lastMessages(roomId: string, count?: number): Promise; +/** Create a room for tests and catch the error if it exists already */ +export declare function createChannel(name: string, members?: string[], readOnly?: boolean): Promise; +/** Create a private group / room and catch if exists already */ +export declare function createPrivate(name: string, members?: string[], readOnly?: boolean): Promise; +/** Send message from mock user to channel for tests to listen and respond */ +/** @todo Sometimes the post request completes before the change event emits + * the message to the streamer. That's why the interval is used for proof + * of receipt. It would be better for the endpoint to not resolve until + * server side handling is complete. Would require PR to core. + */ +export declare function sendFromUser(payload: any): Promise; +/** Leave user from room, to generate `ul` message (test channel by default) */ +export declare function leaveUser(room?: { + id?: string; + name?: string; +}): Promise; +/** Invite user to room, to generate `au` message (test channel by default) */ +export declare function inviteUser(room?: { + id?: string; + name?: string; +}): Promise; +/** @todo : Join user into room (enter) to generate `uj` message type. */ +/** Update message sent from mock user */ +export declare function updateFromUser(payload: IMessageUpdateAPI): Promise; +/** Create a direct message session with the mock user */ +export declare function setupDirectFromUser(): Promise; +/** Initialise testing instance with the required users for SDK/bot tests */ +export declare function setup(): Promise; diff --git a/dist/utils/testing.js b/dist/utils/testing.js new file mode 100644 index 0000000..0fa3f46 --- /dev/null +++ b/dist/utils/testing.js @@ -0,0 +1,238 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const api_1 = require("../lib/api"); +const config_1 = require("./config"); +/** Define common attributes for DRY tests */ +exports.testChannelName = 'tests'; +exports.testPrivateName = 'p-tests'; +/** Get information about a user */ +function userInfo(username) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.get('users.info', { username }, true); + }); +} +exports.userInfo = userInfo; +/** Create a user and catch the error if they exist already */ +function createUser(user) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.post('users.create', user, true, /already in use/i); + }); +} +exports.createUser = createUser; +/** Get information about a channel */ +function channelInfo(query) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.get('channels.info', query, true); + }); +} +exports.channelInfo = channelInfo; +/** Get information about a private group */ +function privateInfo(query) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.get('groups.info', query, true); + }); +} +exports.privateInfo = privateInfo; +/** Get the last messages sent to a channel (in last 10 minutes) */ +function lastMessages(roomId, count = 1) { + return __awaiter(this, void 0, void 0, function* () { + const now = new Date(); + const latest = now.toISOString(); + const oldest = new Date(now.setMinutes(now.getMinutes() - 10)).toISOString(); + return (yield api_1.get('channels.history', { roomId, latest, oldest, count })).messages; + }); +} +exports.lastMessages = lastMessages; +/** Create a room for tests and catch the error if it exists already */ +function createChannel(name, members = [], readOnly = false) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.post('channels.create', { name, members, readOnly }, true); + }); +} +exports.createChannel = createChannel; +/** Create a private group / room and catch if exists already */ +function createPrivate(name, members = [], readOnly = false) { + return __awaiter(this, void 0, void 0, function* () { + return api_1.post('groups.create', { name, members, readOnly }, true); + }); +} +exports.createPrivate = createPrivate; +/** Send message from mock user to channel for tests to listen and respond */ +/** @todo Sometimes the post request completes before the change event emits + * the message to the streamer. That's why the interval is used for proof + * of receipt. It would be better for the endpoint to not resolve until + * server side handling is complete. Would require PR to core. + */ +function sendFromUser(payload) { + return __awaiter(this, void 0, void 0, function* () { + const user = yield api_1.login({ username: config_1.mockUser.username, password: config_1.mockUser.password }); + const endpoint = (payload.roomId && payload.roomId.indexOf(user.data.userId) !== -1) + ? 'dm.history' + : 'channels.history'; + const roomId = (payload.roomId) + ? payload.roomId + : (yield channelInfo({ roomName: exports.testChannelName })).channel._id; + const messageDefaults = { roomId }; + const data = Object.assign({}, messageDefaults, payload); + const oldest = new Date().toISOString(); + const result = yield api_1.post('chat.postMessage', data, true); + const proof = new Promise((resolve, reject) => { + let looked = 0; + const look = setInterval(() => __awaiter(this, void 0, void 0, function* () { + const { messages } = yield api_1.get(endpoint, { roomId, oldest }); + const found = messages.some((message) => { + return result.message._id === message._id; + }); + if (found || looked > 10) { + clearInterval(look); + if (found) + resolve(); + else + reject('API send from user, proof of receipt timeout'); + } + looked++; + }), 100); + }); + yield proof; + return result; + }); +} +exports.sendFromUser = sendFromUser; +/** Leave user from room, to generate `ul` message (test channel by default) */ +function leaveUser(room = {}) { + return __awaiter(this, void 0, void 0, function* () { + yield api_1.login({ username: config_1.mockUser.username, password: config_1.mockUser.password }); + if (!room.id && !room.name) + room.name = exports.testChannelName; + const roomId = (room.id) + ? room.id + : (yield channelInfo({ roomName: room.name })).channel._id; + return api_1.post('channels.leave', { roomId }); + }); +} +exports.leaveUser = leaveUser; +/** Invite user to room, to generate `au` message (test channel by default) */ +function inviteUser(room = {}) { + return __awaiter(this, void 0, void 0, function* () { + let mockInfo = yield userInfo(config_1.mockUser.username); + yield api_1.login({ username: config_1.apiUser.username, password: config_1.apiUser.password }); + if (!room.id && !room.name) + room.name = exports.testChannelName; + const roomId = (room.id) + ? room.id + : (yield channelInfo({ roomName: room.name })).channel._id; + return api_1.post('channels.invite', { userId: mockInfo.user._id, roomId }); + }); +} +exports.inviteUser = inviteUser; +/** @todo : Join user into room (enter) to generate `uj` message type. */ +/** Update message sent from mock user */ +function updateFromUser(payload) { + return __awaiter(this, void 0, void 0, function* () { + yield api_1.login({ username: config_1.mockUser.username, password: config_1.mockUser.password }); + return api_1.post('chat.update', payload, true); + }); +} +exports.updateFromUser = updateFromUser; +/** Create a direct message session with the mock user */ +function setupDirectFromUser() { + return __awaiter(this, void 0, void 0, function* () { + yield api_1.login({ username: config_1.mockUser.username, password: config_1.mockUser.password }); + return api_1.post('im.create', { username: config_1.botUser.username }, true); + }); +} +exports.setupDirectFromUser = setupDirectFromUser; +/** Initialise testing instance with the required users for SDK/bot tests */ +function setup() { + return __awaiter(this, void 0, void 0, function* () { + console.log('\nPreparing instance for tests...'); + try { + // Verify API user can login + const loginInfo = yield api_1.login(config_1.apiUser); + if (loginInfo.status !== 'success') { + throw new Error(`API user (${config_1.apiUser.username}) could not login`); + } + else { + console.log(`API user (${config_1.apiUser.username}) logged in`); + } + // Verify or create user for bot + let botInfo = yield userInfo(config_1.botUser.username); + if (!botInfo || !botInfo.success) { + console.log(`Bot user (${config_1.botUser.username}) not found`); + botInfo = yield createUser(config_1.botUser); + if (!botInfo.success) { + throw new Error(`Bot user (${config_1.botUser.username}) could not be created`); + } + else { + console.log(`Bot user (${config_1.botUser.username}) created`); + } + } + else { + console.log(`Bot user (${config_1.botUser.username}) exists`); + } + // Verify or create mock user for talking to bot + let mockInfo = yield userInfo(config_1.mockUser.username); + if (!mockInfo || !mockInfo.success) { + console.log(`Mock user (${config_1.mockUser.username}) not found`); + mockInfo = yield createUser(config_1.mockUser); + if (!mockInfo.success) { + throw new Error(`Mock user (${config_1.mockUser.username}) could not be created`); + } + else { + console.log(`Mock user (${config_1.mockUser.username}) created`); + } + } + else { + console.log(`Mock user (${config_1.mockUser.username}) exists`); + } + // Verify or create channel for tests + let testChannelInfo = yield channelInfo({ roomName: exports.testChannelName }); + if (!testChannelInfo || !testChannelInfo.success) { + console.log(`Test channel (${exports.testChannelName}) not found`); + testChannelInfo = yield createChannel(exports.testChannelName, [ + config_1.apiUser.username, config_1.botUser.username, config_1.mockUser.username + ]); + if (!testChannelInfo.success) { + throw new Error(`Test channel (${exports.testChannelName}) could not be created`); + } + else { + console.log(`Test channel (${exports.testChannelName}) created`); + } + } + else { + console.log(`Test channel (${exports.testChannelName}) exists`); + } + // Verify or create private room for tests + let testPrivateInfo = yield privateInfo({ roomName: exports.testPrivateName }); + if (!testPrivateInfo || !testPrivateInfo.success) { + console.log(`Test private room (${exports.testPrivateName}) not found`); + testPrivateInfo = yield createPrivate(exports.testPrivateName, [ + config_1.apiUser.username, config_1.botUser.username, config_1.mockUser.username + ]); + if (!testPrivateInfo.success) { + throw new Error(`Test private room (${exports.testPrivateName}) could not be created`); + } + else { + console.log(`Test private room (${exports.testPrivateName}) created`); + } + } + else { + console.log(`Test private room (${exports.testPrivateName}) exists`); + } + yield api_1.logout(); + } + catch (e) { + throw e; + } + }); +} +exports.setup = setup; +//# sourceMappingURL=testing.js.map \ No newline at end of file diff --git a/dist/utils/testing.js.map b/dist/utils/testing.js.map new file mode 100644 index 0000000..401d4e4 --- /dev/null +++ b/dist/utils/testing.js.map @@ -0,0 +1 @@ +{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../src/utils/testing.ts"],"names":[],"mappings":"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","sourcesContent":["import { get, post, login, logout } from '../lib/api'\nimport { apiUser, botUser, mockUser } from './config'\nimport {\n IMessageAPI,\n IMessageUpdateAPI,\n IMessageResultAPI,\n INewUserAPI,\n IUserResultAPI,\n IRoomResultAPI,\n IChannelResultAPI,\n IGroupResultAPI,\n IMessageReceiptAPI\n} from './interfaces'\nimport { IMessage } from '../config/messageInterfaces'\n\n/** Define common attributes for DRY tests */\nexport const testChannelName = 'tests'\nexport const testPrivateName = 'p-tests'\n\n/** Get information about a user */\nexport async function userInfo (username: string): Promise {\n return get('users.info', { username }, true)\n}\n\n/** Create a user and catch the error if they exist already */\nexport async function createUser (user: INewUserAPI): Promise {\n return post('users.create', user, true, /already in use/i)\n}\n\n/** Get information about a channel */\nexport async function channelInfo (query: { roomName?: string, roomId?: string }): Promise {\n return get('channels.info', query, true)\n}\n\n/** Get information about a private group */\nexport async function privateInfo (query: { roomName?: string, roomId?: string }): Promise {\n return get('groups.info', query, true)\n}\n\n/** Get the last messages sent to a channel (in last 10 minutes) */\nexport async function lastMessages (roomId: string, count: number = 1): Promise {\n const now = new Date()\n const latest = now.toISOString()\n const oldest = new Date(now.setMinutes(now.getMinutes() - 10)).toISOString()\n return (await get('channels.history', { roomId, latest, oldest, count })).messages\n}\n\n/** Create a room for tests and catch the error if it exists already */\nexport async function createChannel (\n name: string,\n members: string[] = [],\n readOnly: boolean = false\n): Promise {\n return post('channels.create', { name, members, readOnly }, true)\n}\n\n/** Create a private group / room and catch if exists already */\nexport async function createPrivate (\n name: string,\n members: string[] = [],\n readOnly: boolean = false\n): Promise {\n return post('groups.create', { name, members, readOnly }, true)\n}\n\n/** Send message from mock user to channel for tests to listen and respond */\n/** @todo Sometimes the post request completes before the change event emits\n * the message to the streamer. That's why the interval is used for proof\n * of receipt. It would be better for the endpoint to not resolve until\n * server side handling is complete. Would require PR to core.\n */\nexport async function sendFromUser (payload: any): Promise {\n const user = await login({ username: mockUser.username, password: mockUser.password })\n const endpoint = (payload.roomId && payload.roomId.indexOf(user.data.userId) !== -1)\n ? 'dm.history'\n : 'channels.history'\n const roomId = (payload.roomId)\n ? payload.roomId\n : (await channelInfo({ roomName: testChannelName })).channel._id\n const messageDefaults: IMessageAPI = { roomId }\n const data: IMessageAPI = Object.assign({}, messageDefaults, payload)\n const oldest = new Date().toISOString()\n const result = await post('chat.postMessage', data, true)\n const proof = new Promise((resolve, reject) => {\n let looked = 0\n const look = setInterval(async () => {\n const { messages } = await get(endpoint, { roomId, oldest })\n const found = messages.some((message: IMessageReceiptAPI) => {\n return result.message._id === message._id\n })\n if (found || looked > 10) {\n clearInterval(look)\n if (found) resolve()\n else reject('API send from user, proof of receipt timeout')\n }\n looked++\n }, 100)\n })\n await proof\n return result\n}\n\n/** Leave user from room, to generate `ul` message (test channel by default) */\nexport async function leaveUser (room: { id?: string, name?: string } = {}): Promise {\n await login({ username: mockUser.username, password: mockUser.password })\n if (!room.id && !room.name) room.name = testChannelName\n const roomId = (room.id)\n ? room.id\n : (await channelInfo({ roomName: room.name })).channel._id\n return post('channels.leave', { roomId })\n}\n\n/** Invite user to room, to generate `au` message (test channel by default) */\nexport async function inviteUser (room: { id?: string, name?: string } = {}): Promise {\n let mockInfo = await userInfo(mockUser.username)\n await login({ username: apiUser.username, password: apiUser.password })\n if (!room.id && !room.name) room.name = testChannelName\n const roomId = (room.id)\n ? room.id\n : (await channelInfo({ roomName: room.name })).channel._id\n return post('channels.invite', { userId: mockInfo.user._id, roomId })\n}\n\n/** @todo : Join user into room (enter) to generate `uj` message type. */\n\n/** Update message sent from mock user */\nexport async function updateFromUser (payload: IMessageUpdateAPI): Promise {\n await login({ username: mockUser.username, password: mockUser.password })\n return post('chat.update', payload, true)\n}\n\n/** Create a direct message session with the mock user */\nexport async function setupDirectFromUser (): Promise {\n await login({ username: mockUser.username, password: mockUser.password })\n return post('im.create', { username: botUser.username }, true)\n}\n\n/** Initialise testing instance with the required users for SDK/bot tests */\nexport async function setup () {\n console.log('\\nPreparing instance for tests...')\n try {\n // Verify API user can login\n const loginInfo = await login(apiUser)\n if (loginInfo.status !== 'success') {\n throw new Error(`API user (${apiUser.username}) could not login`)\n } else {\n console.log(`API user (${apiUser.username}) logged in`)\n }\n\n // Verify or create user for bot\n let botInfo = await userInfo(botUser.username)\n if (!botInfo || !botInfo.success) {\n console.log(`Bot user (${botUser.username}) not found`)\n botInfo = await createUser(botUser)\n if (!botInfo.success) {\n throw new Error(`Bot user (${botUser.username}) could not be created`)\n } else {\n console.log(`Bot user (${botUser.username}) created`)\n }\n } else {\n console.log(`Bot user (${botUser.username}) exists`)\n }\n\n // Verify or create mock user for talking to bot\n let mockInfo = await userInfo(mockUser.username)\n if (!mockInfo || !mockInfo.success) {\n console.log(`Mock user (${mockUser.username}) not found`)\n mockInfo = await createUser(mockUser)\n if (!mockInfo.success) {\n throw new Error(`Mock user (${mockUser.username}) could not be created`)\n } else {\n console.log(`Mock user (${mockUser.username}) created`)\n }\n } else {\n console.log(`Mock user (${mockUser.username}) exists`)\n }\n\n // Verify or create channel for tests\n let testChannelInfo = await channelInfo({ roomName: testChannelName })\n if (!testChannelInfo || !testChannelInfo.success) {\n console.log(`Test channel (${testChannelName}) not found`)\n testChannelInfo = await createChannel(testChannelName, [\n apiUser.username, botUser.username, mockUser.username\n ])\n if (!testChannelInfo.success) {\n throw new Error(`Test channel (${testChannelName}) could not be created`)\n } else {\n console.log(`Test channel (${testChannelName}) created`)\n }\n } else {\n console.log(`Test channel (${testChannelName}) exists`)\n }\n\n // Verify or create private room for tests\n let testPrivateInfo = await privateInfo({ roomName: testPrivateName })\n if (!testPrivateInfo || !testPrivateInfo.success) {\n console.log(`Test private room (${testPrivateName}) not found`)\n testPrivateInfo = await createPrivate(testPrivateName, [\n apiUser.username, botUser.username, mockUser.username\n ])\n if (!testPrivateInfo.success) {\n throw new Error(`Test private room (${testPrivateName}) could not be created`)\n } else {\n console.log(`Test private room (${testPrivateName}) created`)\n }\n } else {\n console.log(`Test private room (${testPrivateName}) exists`)\n }\n\n await logout()\n } catch (e) {\n throw e\n }\n}\n"]} \ No newline at end of file diff --git a/dist/utils/users.d.ts b/dist/utils/users.d.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/dist/utils/users.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/dist/utils/users.js b/dist/utils/users.js new file mode 100644 index 0000000..3ca5207 --- /dev/null +++ b/dist/utils/users.js @@ -0,0 +1,50 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +} +Object.defineProperty(exports, "__esModule", { value: true }); +// Test script uses standard methods and env config to connect and log streams +const api = __importStar(require("../lib/api")); +const log_1 = require("../lib/log"); +log_1.silence(); +function users() { + return __awaiter(this, void 0, void 0, function* () { + console.log(` + +Demo of API user query helpers + +ALL users \`api.users.all()\`: +${JSON.stringify(yield api.users.all(), null, '\t')} + +ALL usernames \`api.users.allNames()\`: +${JSON.stringify(yield api.users.allNames(), null, '\t')} + +ALL IDs \`api.users.allIDs()\`: +${JSON.stringify(yield api.users.allIDs(), null, '\t')} + +ONLINE users \`api.users.online()\`: +${JSON.stringify(yield api.users.online(), null, '\t')} + +ONLINE usernames \`api.users.onlineNames()\`: +${JSON.stringify(yield api.users.onlineNames(), null, '\t')} + +ONLINE IDs \`api.users.onlineIds()\`: +${JSON.stringify(yield api.users.onlineIds(), null, '\t')} + + `); + }); +} +users().catch((e) => console.error(e)); +//# sourceMappingURL=users.js.map \ No newline at end of file diff --git a/dist/utils/users.js.map b/dist/utils/users.js.map new file mode 100644 index 0000000..5e9551f --- /dev/null +++ b/dist/utils/users.js.map @@ -0,0 +1 @@ +{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/utils/users.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,8EAA8E;AAC9E,gDAAiC;AACjC,oCAAoC;AACpC,aAAO,EAAE,CAAA;AAET;;QACE,OAAO,CAAC,GAAG,CAAC;;;;;EAKZ,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;;EAGjD,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;;EAGtD,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;;EAGpD,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;;EAGpD,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;;EAGzD,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC;;GAEtD,CAAC,CAAA;IACJ,CAAC;CAAA;AAED,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA","sourcesContent":["// Test script uses standard methods and env config to connect and log streams\nimport * as api from '../lib/api'\nimport { silence } from '../lib/log'\nsilence()\n\nasync function users () {\n console.log(`\n\nDemo of API user query helpers\n\nALL users \\`api.users.all()\\`:\n${JSON.stringify(await api.users.all(), null, '\\t')}\n\nALL usernames \\`api.users.allNames()\\`:\n${JSON.stringify(await api.users.allNames(), null, '\\t')}\n\nALL IDs \\`api.users.allIDs()\\`:\n${JSON.stringify(await api.users.allIDs(), null, '\\t')}\n\nONLINE users \\`api.users.online()\\`:\n${JSON.stringify(await api.users.online(), null, '\\t')}\n\nONLINE usernames \\`api.users.onlineNames()\\`:\n${JSON.stringify(await api.users.onlineNames(), null, '\\t')}\n\nONLINE IDs \\`api.users.onlineIds()\\`:\n${JSON.stringify(await api.users.onlineIds(), null, '\\t')}\n\n `)\n}\n\nusers().catch((e) => console.error(e))\n"]} \ No newline at end of file diff --git a/docma.json b/docma.json new file mode 100644 index 0000000..6a31a99 --- /dev/null +++ b/docma.json @@ -0,0 +1,79 @@ +{ + "src": [{ + "driver": [ + "dist/index.js", + "dist/lib/driver.js" + ] + }, + { + "guide": "README.md" + }], + "dest": "docs/", + "clean": true, + "jsdoc": { + "encoding": "utf8", + "recurse": false, + "pedantic": false, + "access": null, + "package": null, + "module": true, + "undocumented": false, + "undescribed": false, + "ignored": false, + "hierarchy": true, + "sort": "alphabetic", + "relativePath": null, + "filter": null, + "allowUnknownTags": true, + "plugins": ["plugins/markdown"] + }, + "app": { + "title": "Rocket.Chat.js.SDK", + "routing": "path", + "entrance": "content:guide", + "favicon": "favicon.ico" + }, + "template": { + "options": { + "title": { + "label": "Rocket.Chat.js.SDK", + "href": "/guide" + }, + "symbols": { + "autoLink": true, + "params": "table", + "enums": "list", + "props": "list", + "meta": false + }, + "navbar": { + "menu": [ + { + "iconClass": "fas fa-puzzle-piece", + "label": "Driver Methods", + "href": "/api/driver" + }, + { + "iconClass": "fas fa-book", + "label": "SDK Guide", + "href": "/guide" + }, + { + "iconClass": "fab fa-rocketchat", + "label": "", + "href": "https://github.com/RocketChat/Rocket.Chat.js.SDK", + "target": "_blank" + } + ] + }, + "sidebar": { + "enabled": true, + "outline": "flat" + }, + "logo": { + "dark": "https://upload.wikimedia.org/wikipedia/commons/5/55/RocketChat_Logo_1024x1024.png", + "light": "https://upload.wikimedia.org/wikipedia/commons/5/55/RocketChat_Logo_1024x1024.png" + } + } + } +} \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8b0899b1bf6c4ac4b7e8303bf403025652fcb7a6 GIT binary patch literal 15086 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", + "license": "MIT", + "private": false, + "keywords": [ + "adapter", + "rocketchat", + "rocket", + "chat", + "messaging", + "CUI", + "typescript" + ], + "files": [ + "dist" + ], + "engines": { + "node": "> 8.0.0", + "npm": "> 5.0.0" + }, + "scripts": { + "pretest": "tslint -p . && ts-node src/utils/setup.ts", + "test": "nyc mocha './src/lib/**/*.spec.ts'", + "test:hook": "mocha './**/*.spec.ts'", + "test:debug": "mocha --inspect --debug-brk 'src/**/*.spec.ts'", + "test:package": "preview && mocha 'src/index.spec.ts'", + "start": "ts-node ./src/utils/start", + "docs": "rimraf ./docs && typedoc --options ./typedoc.json ./src/lib", + "js-docs": "./node_modules/.bin/docma -c docma.json", + "prebuild": "npm run test", + "build": "rimraf ./dist/* && tsc && npm run test:package" + }, + "husky": { + "hooks": { + "pre-push": "npm run test:hook" + } + }, + "devDependencies": { + "@types/chai": "^4.1.2", + "@types/mocha": "^2.2.48", + "@types/sinon": "^4.3.0", + "chai": "^4.1.2", + "commitizen": "^2.9.6", + "cz-conventional-changelog": "^2.1.0", + "docma": "^3.2.2", + "dotenv": "^5.0.1", + "husky": "^0.14.3", + "mocha": "^5.0.1", + "nyc": "^11.4.1", + "package-preview": "^1.0.5", + "rimraf": "^2.6.2", + "sinon": "^4.4.2", + "source-map-support": "^0.5.3", + "ts-node": "^5.0.1", + "tslint": "^5.9.1", + "tslint-config-standard": "^7.0.0", + "typedoc": "0.8.0", + "typedoc-plugin-external-module-name": "^1.1.1", + "typescript": "^2.7.2" + }, + "dependencies": { + "@types/lru-cache": "^4.1.0", + "@types/node": "^9.4.6", + "asteroid": "rocketchat/asteroid", + "lru-cache": "^4.1.1", + "node-rest-client": "^3.1.0" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + } +} diff --git a/src/config/asteroidInterfaces.ts b/src/config/asteroidInterfaces.ts new file mode 100644 index 0000000..fa419a7 --- /dev/null +++ b/src/config/asteroidInterfaces.ts @@ -0,0 +1,124 @@ +import { EventEmitter } from 'events' +// import { Map } from 'immutable' + +/** + * Asteroid DDP - add known properties to avoid TS lint errors + */ +export interface IAsteroidDDP extends EventEmitter { + readyState: 1 | 0 +} + +/** + * Asteroid type + * @todo Update with typing from definitely typed (when available) + */ +export interface IAsteroid extends EventEmitter { + connect: () => Promise, + disconnect: () => Promise, + createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise + loginWithLDAP: (...params: any[]) => Promise + loginWithFacebook: (...params: any[]) => Promise + loginWithGoogle: (...params: any[]) => Promise + loginWithTwitter: (...params: any[]) => Promise + loginWithGithub: (...params: any[]) => Promise + loginWithPassword: (usernameOrEmail: string, password: string) => Promise + logout: () => Promise + subscribe: (name: string, ...params: any[]) => ISubscription + subscriptions: ISubscription[], + call: (method: string, ...params: any[]) => IMethodResult + apply: (method: string, params: any[]) => IMethodResult + getCollection: (name: string) => ICollection + resumeLoginPromise: Promise + ddp: IAsteroidDDP +} + +/** + * Asteroid user options type + * @todo Update with typing from definitely typed (when available) + */ +export interface IUserOptions { + username?: string, + email?: string, + password: string +} + +/** + * Asteroid subscription type. + * ID is populated when ready promise resolves. + * @todo Update with typing from definitely typed (when available) + */ +export interface ISubscription { + stop: () => void, + ready: Promise, + id?: string +} + +// Asteroid v1 only +export interface IReady { state: string, value: string } + +/* // v2 +export interface ISubscription extends EventEmitter { + id: string +} +*/ + +/** + * If the method is successful, the `result` promise will be resolved with the + * return value passed by the server. The `updated` promise will be resolved + * with nothing once the server emits the updated message, that tells the client + * that any side-effect that the method execution caused on the database has + * been reflected on the client (for example, if the method caused the insertion + * of an item into a collection, the client has been notified of said + * insertion). + * + * If the method fails, the `result` promise will be rejected with the error + * returned by the server. The `updated` promise will be rejected as well + * (with nothing). + */ +export interface IMethodResult { + result: Promise, + updated: Promise +} + +/** + * + */ +export interface ICollection { + name: string, + insert: (item: any) => ICollectionResult, + update: (id: string, item: any) => ICollectionResult, + remove: (id: string) => ICollectionResult, + reactiveQuery: (selector: object | Function) => IReactiveQuery +} + +/** + * The `local` promise is immediately resolved with the `_id` of the updated + * item. That is, unless an error occurred. In that case, an exception will be + * raised. + * The `remote` promise is resolved with the `_id` of the updated item if the + * remote update is successful. Otherwise it's rejected with the reason of the + * failure. + */ +export interface ICollectionResult { + local: Promise, + remote: Promise +} + +/** + * A reactive subset of a collection. Possible events are: + * `change`: emitted whenever the result of the query changes. The id of the + * item that changed is passed to the handler. + */ +export interface IReactiveQuery { + on: (event: string, handler: Function) => void, + result: any[] +} + +/** Credentials for Asteroid login method */ +export interface ICredentials { + password: string, + username?: string, + email?: string, + ldap?: boolean, + ldapOptions?: object +} diff --git a/src/config/driverInterfaces.ts b/src/config/driverInterfaces.ts new file mode 100644 index 0000000..e9f6503 --- /dev/null +++ b/src/config/driverInterfaces.ts @@ -0,0 +1,45 @@ +/** + * Connection options type + * @param host Rocket.Chat instance Host URL:PORT (without protocol) + * @param timeout How long to wait (ms) before abandoning connection + */ +export interface IConnectOptions { + host?: string, + useSsl?: boolean, + timeout?: number, + integration?: string +} + +/** + * Message respond options + * @param rooms Respond to only selected room/s (names or IDs) + * @param allPublic Respond on all public channels (ignores rooms if true) + * @param dm Respond to messages in DM / private chats + * @param livechat Respond to messages in livechat + * @param edited Respond to edited messages + */ +export interface IRespondOptions { + rooms?: string[], + allPublic?: boolean, + dm?: boolean, + livechat?: boolean, + edited?: boolean +} + +/** + * Loggers need to provide the same set of methods + */ +export interface ILogger { + debug: (...args: any[]) => void + info: (...args: any[]) => void + warning: (...args: any[]) => void + warn: (...args: any[]) => void + error: (...args: any[]) => void +} + +/** + * Error-first callback param type + */ +export interface ICallback { + (error: Error | null, ...args: any[]): void +} diff --git a/src/config/messageInterfaces.ts b/src/config/messageInterfaces.ts new file mode 100644 index 0000000..d0662cf --- /dev/null +++ b/src/config/messageInterfaces.ts @@ -0,0 +1,75 @@ +/** @todo contribute these to @types/rocketchat and require */ + +export interface IMessage { + rid: string | null // room ID + _id?: string // generated by Random.id() + t?: string // type e.g. rm + msg?: string // text content + alias?: string // ?? + emoji?: string // emoji to use as avatar + avatar?: string // url + groupable?: boolean // ? + bot?: any // integration details + urls?: string[] // ? + mentions?: string[] // ? + attachments?: IMessageAttachment[] + reactions?: IMessageReaction + location ?: IMessageLocation + u?: IUser // User that sent the message + editedBy?: IUser // User that edited the message + editedAt?: Date // When the message was edited +} + +export interface IUser { + _id: string + username: string + name?: string +} + +export interface IMessageAttachment { + fields?: IAttachmentField[] + actions?: IMessageAction[] + color?: string + text?: string + ts?: string + thumb_url?: string + message_link?: string + collapsed?: boolean + author_name?: string + author_link?: string + author_icon?: string + title?: string + title_link?: string + title_link_download?: string + image_url?: string + audio_url?: string + video_url?: string +} + +export interface IAttachmentField { + short?: boolean + title?: string + value?: string +} + +export interface IMessageAction { + type?: string + text?: string + url?: string + image_url?: string + is_webview?: boolean + webview_height_ratio?: 'compact' | 'tall' | 'full' + msg?: string + msg_in_chat_window?: boolean + button_alignment?: 'vertical' | 'horizontal' + temporary_buttons?: boolean +} + +export interface IMessageLocation { + type: string // e.g. Point + coordinates: string[] // longitude latitude +} + +export interface IMessageReaction { + [emoji: string]: { usernames: string[] } // emoji: [usernames that reacted] +} diff --git a/src/index.spec.ts b/src/index.spec.ts new file mode 100644 index 0000000..fb4b16e --- /dev/null +++ b/src/index.spec.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' +import * as rocketchat from '@rocket.chat/sdk' + +describe('index:', () => { + it('exports all lib members', () => { + expect(Object.keys(rocketchat)).to.eql([ + 'driver', + 'methodCache', + 'api', + 'settings' + ]) + }) +}) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..85984fa --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +import * as driver from './lib/driver' +import * as methodCache from './lib/methodCache' +import * as api from './lib/api' +import * as settings from './lib/settings' +export { + driver, + methodCache, + api, + settings +} diff --git a/src/lib/api.spec.ts b/src/lib/api.spec.ts new file mode 100644 index 0000000..b1147e2 --- /dev/null +++ b/src/lib/api.spec.ts @@ -0,0 +1,123 @@ +import 'mocha' +import sinon from 'sinon' +import { expect } from 'chai' +import { silence, logger } from './log' +const initEnv = process.env // store configs to restore after tests +import { botUser, mockUser } from '../utils/config' +import * as utils from '../utils/testing' +import * as driver from './driver' +import * as api from './api' + +silence() // suppress log during tests (disable this while developing tests) + +describe('api', () => { + before(async () => { // wait for connection + await driver.connect() + await driver.login() + }) + after(() => process.env = initEnv) + afterEach(() => api.logout()) + describe('.success', () => { + it('returns true when result status is success', () => { + expect(api.success({ status: 'success' })).to.equal(true) + }) + it('returns true when success is true', () => { + expect(api.success({ success: true })).to.equal(true) + }) + it('returns false when result status is not success', () => { + expect(api.success({ status: 'error' })).to.equal(false) + }) + it('returns false when success is not true', () => { + expect(api.success({ success: false })).to.equal(false) + }) + it('returns true if neither status or success given', () => { + expect(api.success({})).to.equal(true) + }) + }) + describe('.getQueryString', () => { + it('converts object to query params string', () => { + expect(api.getQueryString({ + foo: 'bar', + baz: 'qux' + })).to.equal('?foo=bar&baz=qux') + }) + it('returns empty if nothing in object', () => { + expect(api.getQueryString({})).to.equal('') + }) + it('returns nested objects without serialising', () => { + expect(api.getQueryString({ + fields: { 'username': 1 } + })).to.equal('?fields={"username":1}') + }) + }) + describe('.get', () => { + it('returns data from basic call without auth', async () => { + const server = await driver.callMethod('getServerInfo') + const result = await api.get('info', {}, false) + expect(result).to.eql({ + info: { version: server.version }, + success: true + }) + }) + it('returns data from complex calls with auth and parameters', async () => { + await api.login() + const result = await api.get('users.list', { + fields: { 'username': 1 }, + query: { type: { $in: ['user', 'bot'] } } + }, true) + const users = result.users.map((user) => user.username) + expect(users).to.include(botUser.username, mockUser.username) + }) + }) + describe('.login', () => { + it('logs in with the default user without arguments', async () => { + const login = await api.login() + expect(login.data.userId).to.equal(driver.userId) + }) + it('logs in with another user if given credentials', async () => { + await api.login({ + username: mockUser.username, + password: mockUser.password + }) + const mockInfo = await api.get('users.info', { username: mockUser.username }) + expect(api.currentLogin.userId).to.equal(mockInfo.user._id) + }) + it('stores logged in user result', async () => { + await api.login() + expect(api.currentLogin.userId).to.equal(driver.userId) + }) + it('stores user and token in auth headers', async () => { + await api.login() + expect(api.authHeaders['X-User-Id']).to.equal(driver.userId) + expect(api.authHeaders['X-Auth-Token']).to.have.lengthOf(43) + }) + }) + describe('.logout', () => { + it('resets auth headers and clears user ID', async () => { + await api.login().catch(e => console.log('login error', e)) + await api.logout().catch(e => console.log('logout error', e)) + expect(api.authHeaders).to.eql({}) + expect(api.currentLogin).to.eql(null) + }) + }) + describe('.getHeaders', () => { + beforeEach(() => api.clearHeaders()) + it('returns headers for API call', () => { + expect(api.getHeaders()).to.eql({ + 'Content-Type': 'application/json' + }) + }) + it('should fail if called before login when auth required', () => { + expect(() => api.getHeaders(true)).to.throw() + }) + it('should return auth headers if required when logged in', async () => { + api.authHeaders['X-User-Id'] = 'test' + api.authHeaders['X-Auth-Token'] = 'test' + expect(api.getHeaders(true)).to.eql({ + 'Content-Type': 'application/json', + 'X-User-Id': 'test', + 'X-Auth-Token': 'test' + }) + }) + }) +}) diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..0b37c8f --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,219 @@ +import { Client } from 'node-rest-client' +import * as settings from './settings' +import { logger } from './log' +import { IUserAPI } from '../utils/interfaces' + +/** Result object from an API login */ +export interface ILoginResultAPI { + status: string // e.g. 'success' + data: { authToken: string, userId: string } +} + +/** Structure for passing and keeping login credentials */ +export interface ILoginCredentials { + username: string, + password: string +} +export let currentLogin: { + username: string, + userId: string, + authToken: string, + result: ILoginResultAPI +} | null = null + +/** Check for existing login */ +export function loggedIn (): boolean { + return (currentLogin !== null) +} + +/** Initialise client and configs */ +export const client = new Client() +export const host = settings.host + +/** + * Prepend protocol (or put back if removed from env settings for driver) + * Hard code endpoint prefix, because all syntax depends on this version + */ +export const url = ((host.indexOf('http') === -1) + ? host.replace(/^(\/\/)?/, 'http://') + : host) + '/api/v1/' + +/** Convert payload data to query string for GET requests */ +export function getQueryString (data: any) { + if (!data || typeof data !== 'object' || !Object.keys(data).length) return '' + return '?' + Object.keys(data).map((k) => { + const value = (typeof data[k] === 'object') + ? JSON.stringify(data[k]) + : encodeURIComponent(data[k]) + return `${encodeURIComponent(k)}=${value}` + }).join('&') +} + +/** Setup default headers with empty auth for now */ +export const basicHeaders = { 'Content-Type': 'application/json' } +export const authHeaders = { 'X-Auth-Token': '', 'X-User-Id': '' } + +/** Populate auth headers (from response data on login) */ +export function setAuth (authData: {authToken: string, userId: string}) { + authHeaders['X-Auth-Token'] = authData.authToken + authHeaders['X-User-Id'] = authData.userId +} + +/** Join basic headers with auth headers if required */ +export function getHeaders (authRequired = false) { + if (!authRequired) return basicHeaders + if ( + (!('X-Auth-Token' in authHeaders) || !('X-User-Id' in authHeaders)) || + authHeaders['X-Auth-Token'] === '' || + authHeaders['X-User-Id'] === '' + ) { + throw new Error('Auth required endpoint cannot be called before login') + } + return Object.assign({}, basicHeaders, authHeaders) +} + +/** Clear headers so they can't be used without logging in again */ +export function clearHeaders () { + delete authHeaders['X-Auth-Token'] + delete authHeaders['X-User-Id'] +} + +/** Check result data for success, allowing override to ignore some errors */ +export function success (result: any, ignore?: RegExp) { + return ( + ( + typeof result.error === 'undefined' && + typeof result.status === 'undefined' && + typeof result.success === 'undefined' + ) || + (result.status && result.status === 'success') || + (result.success && result.success === true) || + (ignore && result.error && !ignore.test(result.error)) + ) ? true : false +} + +/** + * Do a POST request to an API endpoint. + * If it needs a token, login first (with defaults) to set auth headers. + * @todo Look at why some errors return HTML (caught as buffer) instead of JSON + * @param endpoint The API endpoint (including version) e.g. `chat.update` + * @param data Payload for POST request to endpoint + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +export async function post ( + endpoint: string, + data: any, + auth: boolean = true, + ignore?: RegExp +): Promise { + try { + logger.debug(`[API] POST: ${endpoint}`, JSON.stringify(data)) + if (auth && !loggedIn()) await login() + let headers = getHeaders(auth) + const result = await new Promise((resolve, reject) => { + client.post(url + endpoint, { headers, data }, (result: any) => { + if (Buffer.isBuffer(result)) reject('Result was buffer (HTML, not JSON)') + else if (!success(result, ignore)) reject(result) + else resolve(result) + }).on('error', (err: Error) => reject(err)) + }) + logger.debug('[API] POST result:', result) + return result + } catch (err) { + console.error(err) + logger.error(`[API] POST error (${endpoint}):`, err) + } +} + +/** + * Do a GET request to an API endpoint + * @param endpoint The API endpoint (including version) e.g. `users.info` + * @param data Object to serialise for GET request query string + * @param auth Require auth headers for endpoint, default true + * @param ignore Allows certain matching error messages to not count as errors + */ +export async function get ( + endpoint: string, + data?: any, + auth: boolean = true, + ignore?: RegExp +): Promise { + try { + logger.debug(`[API] GET: ${endpoint}`, data) + if (auth && !loggedIn()) await login() + let headers = getHeaders(auth) + const query = getQueryString(data) + const result = await new Promise((resolve, reject) => { + client.get(url + endpoint + query, { headers }, (result: any) => { + if (Buffer.isBuffer(result)) reject('Result was buffer (HTML, not JSON)') + else if (!success(result, ignore)) reject(result) + else resolve(result) + }).on('error', (err: Error) => reject(err)) + }) + logger.debug('[API] GET result:', result) + return result + } catch (err) { + logger.error(`[API] GET error (${endpoint}):`, err) + } +} + +/** + * Login a user for further API calls + * Result should come back with a token, to authorise following requests. + * Use env default credentials, unless overridden by login arguments. + */ +export async function login (user: ILoginCredentials = { + username: settings.username, + password: settings.password +}): Promise { + logger.info(`[API] Logging in ${user.username}`) + if (currentLogin !== null) { + logger.debug(`[API] Already logged in`) + if (currentLogin.username === user.username) { + return currentLogin.result + } else { + await logout() + } + } + const result = await post('login', user, false) + if (result && result.data && result.data.authToken) { + currentLogin = { + result: result, // keep to return if login requested again for same user + username: user.username, // keep to compare with following login attempt + authToken: result.data.authToken, + userId: result.data.userId + } + setAuth(currentLogin) + logger.info(`[API] Logged in ID ${ currentLogin.userId }`) + return result + } else { + throw new Error(`[API] Login failed for ${user.username}`) + } +} + +/** Logout a user at end of API calls */ +export function logout () { + if (currentLogin === null) { + logger.debug(`[API] Already logged out`) + return Promise.resolve() + } + logger.info(`[API] Logging out ${ currentLogin.username }`) + return get('logout', null, true).then(() => { + clearHeaders() + currentLogin = null + }) +} + +/** Defaults for user queries */ +export const userFields = { name: 1, username: 1, status: 1, type: 1 } + +/** Query helpers for user collection requests */ +export const users: any = { + all: (fields: any = userFields) => get('users.list', { fields }).then((r) => r.users), + allNames: () => get('users.list', { fields: { 'username': 1 } }).then((r) => r.users.map((u: IUserAPI) => u.username)), + allIDs: () => get('users.list', { fields: { '_id': 1 } }).then((r) => r.users.map((u: IUserAPI) => u._id)), + online: (fields: any = userFields) => get('users.list', { fields, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users), + onlineNames: () => get('users.list', { fields: { 'username': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u: IUserAPI) => u.username)), + onlineIds: () => get('users.list', { fields: { '_id': 1 }, query: { 'status': { $ne: 'offline' } } }).then((r) => r.users.map((u: IUserAPI) => u._id)) +} diff --git a/src/lib/driver.spec.ts b/src/lib/driver.spec.ts new file mode 100644 index 0000000..3457d6b --- /dev/null +++ b/src/lib/driver.spec.ts @@ -0,0 +1,458 @@ +import 'mocha' +import sinon from 'sinon' +import { expect } from 'chai' +import { silence } from './log' +import { botUser, mockUser, apiUser } from '../utils/config' +import * as api from './api' +import * as utils from '../utils/testing' +import * as driver from './driver' +import * as methodCache from './methodCache' + +const delay = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms)) +let clock +let tId +let pId +const tName = utils.testChannelName +const pName = utils.testPrivateName + +silence() // suppress log during tests (disable this while developing tests) + +describe('driver', () => { + before(async () => { + const testChannel = await utils.channelInfo({ roomName: tName }) + tId = testChannel.channel._id + const testPrivate = await utils.privateInfo({ roomName: pName }) + pId = testPrivate.group._id + }) + after(async () => { + await api.logout() + await driver.logout() + await driver.disconnect() + }) + describe('.connect', () => { + context('with localhost connection', () => { + it('without args, returns a promise', () => { + const promise = driver.connect() + expect(promise.then).to.be.a('function') + promise.catch((err) => console.error(err)) + return promise + }) + it('accepts an error-first callback, providing asteroid', (done) => { + driver.connect({}, (err, asteroid) => { + expect(err).to.equal(null) + expect(asteroid).to.be.an('object') + done() + }) + }) + it('without url takes localhost as default', (done) => { + driver.connect({}, (err, asteroid) => { + expect(err).to.eql(null) + // const connectionHost = asteroid.endpoint + const connectionHost = asteroid._host + expect(connectionHost).to.contain('localhost:3000') + done() + }) + }) + it('promise resolves with asteroid in successful state', () => { + return driver.connect({}).then((asteroid) => { + const isActive = (asteroid.ddp.readyState === 1) + // const isActive = asteroid.ddp.status === 'connected' + expect(isActive).to.equal(true) + }) + }) + it('provides the asteroid instance to method cache', () => { + return driver.connect().then((asteroid) => { + expect(methodCache.instance).to.eql(asteroid) + }) + }) + }) + context('with timeout, on expiry', () => { + before(() => clock = sinon.useFakeTimers(0)) + after(() => clock.restore()) + it('with url, attempts connection at URL', (done) => { + driver.connect({ host: 'localhost:9999', timeout: 100 }, (err, asteroid) => { + expect(err).to.be.an('error') + const connectionHost = asteroid.endpoint || asteroid._host + expect(connectionHost).to.contain('localhost:9999') + done() + }) + clock.tick(200) + }) + it('returns error', (done) => { + let opts = { host: 'localhost:9999', timeout: 100 } + driver.connect(opts, (err, asteroid) => { + const isActive = (asteroid.ddp.readyState === 1) + expect(err).to.be.an('error') + expect(isActive).to.eql(false) + done() + }) + clock.tick(200) + }) + it('without callback, triggers promise catch', () => { + const promise = driver.connect({ host: 'localhost:9999', timeout: 100 }) + .catch((err) => expect(err).to.be.an('error')) + clock.tick(200) + return promise + }) + it('with callback, provides error to callback', (done) => { + driver.connect({ host: 'localhost:9999', timeout: 100 }, (err) => { + expect(err).to.be.an('error') + done() + }) + clock.tick(200) + }) + }) + }) + + // describe('disconnect', () => { + // Disabled for now, as only Asteroid v2 has a disconnect method + // it('disconnects from asteroid', async () => { + // await driver.connect() + // const asteroid = await driver.connect() + // await driver.disconnect() + // const isActive = asteroid.ddp.readyState === 1 + // // const isActive = asteroid.ddp.status === 'connected' + // expect(isActive).to.equal(false) + // }) + // }) + describe('.login', () => { + it('sets the bot user status to online', async () => { + await driver.connect() + await driver.login() + await utils + const result = await utils.userInfo(botUser.username) + expect(result.user.status).to.equal('online') + }) + }) + describe('.subscribeToMessages', () => { + it('resolves with subscription object', async () => { + await driver.connect() + await driver.login() + const subscription = await driver.subscribeToMessages() + expect(subscription).to.have.property('ready') + // expect(subscription.ready).to.have.property('state', 'fulfilled') ???? + }) + }) + describe('.reactToMessages', () => { + afterEach(() => delay(500)) // avoid rate limit + it('calls callback on every subscription update', async () => { + await driver.connect() + await driver.login() + await driver.subscribeToMessages() + const callback = sinon.spy() + driver.reactToMessages(callback) + await utils.sendFromUser({ text: 'SDK test `reactToMessages` 1' }) + await delay(500) + await utils.sendFromUser({ text: 'SDK test `reactToMessages` 2' }) + expect(callback.callCount).to.equal(2) + }) + it('calls callback with sent message object', async () => { + await driver.connect() + await driver.login() + await driver.subscribeToMessages() + const callback = sinon.spy() + driver.reactToMessages(callback) + await utils.sendFromUser({ text: 'SDK test `reactToMessages` 3' }) + const messageArgs = callback.getCall(0).args[1] + expect(messageArgs.msg).to.equal('SDK test `reactToMessages` 3') + }) + }) + describe('.sendMessage', () => { + before(async () => { + await driver.connect() + await driver.login() + }) + it('sends a custom message', async () => { + const message = driver.prepareMessage({ + rid: tId, + msg: ':point_down:', + emoji: ':point_right:', + reactions: { ':thumbsup:': { usernames: [botUser.username] } }, + groupable: false + }) + await driver.sendMessage(message) + const last = (await utils.lastMessages(tId))[0] + expect(last).to.have.deep.property('reactions', message.reactions) + expect(last).to.have.property('emoji', ':point_right:') + expect(last).to.have.property('msg', ':point_down:') + }) + it('sends a message with actions', async () => { + const attachments = [{ + actions: [ + { type: 'button', text: 'Action 1', msg: 'Testing Action 1', msg_in_chat_window: true }, + { type: 'button', text: 'Action 2', msg: 'Testing Action 2', msg_in_chat_window: true } + ] + }] + await driver.sendMessage({ + rid: tId, + msg: 'SDK test `prepareMessage` actions', + attachments + }) + const last = (await utils.lastMessages(tId))[0] + expect(last.attachments).to.eql(attachments) + }) + }) + describe('.editMessage', () => { + before(async () => { + await driver.connect() + await driver.login() + }) + it('edits the last sent message', async () => { + const original = driver.prepareMessage({ + msg: ':point_down:', + emoji: ':point_right:', + groupable: false, + rid: tId + }) + await driver.sendMessage(original) + const sent = (await utils.lastMessages(tId))[0] + const update = Object.assign({}, original, { + _id: sent._id, + msg: ':point_up:' + }) + await driver.editMessage(update) + const last = (await utils.lastMessages(tId))[0] + expect(last).to.have.property('msg', ':point_up:') + expect(last).to.have.deep.property('editedBy', { + _id: driver.userId, username: botUser.username + }) + }) + }) + describe('.setReaction', () => { + before(async () => { + await driver.connect() + await driver.login() + }) + it('adds emoji reaction to message', async () => { + let sent = await driver.sendToRoomId('test reactions', tId) + if (Array.isArray(sent)) sent = sent[0] // see todo on `sendToRoomId` + await driver.setReaction(':thumbsup:', sent._id) + const last = (await utils.lastMessages(tId))[0] + expect(last.reactions).to.have.deep.property(':thumbsup:', { + usernames: [ botUser.username ] + }) + }) + it('removes if used when emoji reaction exists', async () => { + const sent = await driver.sendMessage(driver.prepareMessage({ + msg: 'test reactions -', + reactions: { ':thumbsup:': { usernames: [botUser.username] } }, + rid: tId + })) + await driver.setReaction(':thumbsup:', sent._id) + const last = (await utils.lastMessages(tId))[0] + expect(last).to.not.have.property('reactions') + }) + }) + describe('.sendToRoomId', () => { + it('sends string to the given room id', async () => { + const result = await driver.sendToRoomId('SDK test `sendToRoomId`', tId) + expect(result).to.include.all.keys(['msg', 'rid', '_id']) + }) + it('sends array of strings to the given room id', async () => { + const result = await driver.sendToRoomId([ + 'SDK test `sendToRoomId` A', + 'SDK test `sendToRoomId` B' + ], tId) + expect(result).to.be.an('array') + expect(result[0]).to.include.all.keys(['msg', 'rid', '_id']) + expect(result[1]).to.include.all.keys(['msg', 'rid', '_id']) + }) + }) + describe('.sendToRoom', () => { + it('sends string to the given room name', async () => { + await driver.connect() + await driver.login() + await driver.subscribeToMessages() + const result = await driver.sendToRoom('SDK test `sendToRoom`', tName) + expect(result).to.include.all.keys(['msg', 'rid', '_id']) + }) + it('sends array of strings to the given room name', async () => { + await driver.connect() + await driver.login() + await driver.subscribeToMessages() + const result = await driver.sendToRoom([ + 'SDK test `sendToRoom` A', + 'SDK test `sendToRoom` B' + ], tName) + expect(result).to.be.an('array') + expect(result[0]).to.include.all.keys(['msg', 'rid', '_id']) + expect(result[1]).to.include.all.keys(['msg', 'rid', '_id']) + }) + }) + describe('.sendDirectToUser', () => { + before(async () => { + await driver.connect() + await driver.login() + }) + it('sends string to the given room name', async () => { + await driver.connect() + await driver.login() + const result = await driver.sendDirectToUser('SDK test `sendDirectToUser`', mockUser.username) + expect(result).to.include.all.keys(['msg', 'rid', '_id']) + }) + it('sends array of strings to the given room name', async () => { + const result = await driver.sendDirectToUser([ + 'SDK test `sendDirectToUser` A', + 'SDK test `sendDirectToUser` B' + ], mockUser.username) + expect(result).to.be.an('array') + expect(result[0]).to.include.all.keys(['msg', 'rid', '_id']) + expect(result[1]).to.include.all.keys(['msg', 'rid', '_id']) + }) + }) + describe('.respondToMessages', () => { + beforeEach(async () => { + await driver.connect() + await driver.login() + await driver.subscribeToMessages() + }) + it('joins rooms if not already joined', async () => { + expect(driver.joinedIds).to.have.lengthOf(0) + await driver.respondToMessages(() => null, { rooms: ['general', tName] }) + expect(driver.joinedIds).to.have.lengthOf(2) + }) + it('ignores messages sent from bot', async () => { + const callback = sinon.spy() + driver.respondToMessages(callback) + await driver.sendToRoomId('SDK test `respondToMessages`', tId) + sinon.assert.notCalled(callback) + }) + it('fires callback on messages in joined rooms', async () => { + const callback = sinon.spy() + driver.respondToMessages(callback, { rooms: [tName] }) + await utils.sendFromUser({ text: 'SDK test `respondToMessages` 1' }) + sinon.assert.calledOnce(callback) + }) + it('by default ignores edited messages', async () => { + const callback = sinon.spy() + const sentMessage = await utils.sendFromUser({ + text: 'SDK test `respondToMessages` sent' + }) + driver.respondToMessages(callback, { rooms: [tName] }) + await utils.updateFromUser({ + roomId: tId, + msgId: sentMessage.message._id, + text: 'SDK test `respondToMessages` edited' + }) + sinon.assert.notCalled(callback) + }) + it('ignores edited messages, after receiving original', async () => { + const callback = sinon.spy() + driver.respondToMessages(callback, { rooms: [tName] }) + const sentMessage = await utils.sendFromUser({ + text: 'SDK test `respondToMessages` sent' + }) + await utils.updateFromUser({ + roomId: tId, + msgId: sentMessage.message._id, + text: 'SDK test `respondToMessages` edited' + }) + sinon.assert.calledOnce(callback) + }) + it('fires callback on edited message if configured', async () => { + const callback = sinon.spy() + const sentMessage = await utils.sendFromUser({ + text: 'SDK test `respondToMessages` sent' + }) + driver.respondToMessages(callback, { edited: true, rooms: [tName] }) + await utils.updateFromUser({ + roomId: tId, + msgId: sentMessage.message._id, + text: 'SDK test `respondToMessages` edited' + }) + sinon.assert.calledOnce(callback) + }) + it('by default ignores DMs', async () => { + const dmResult = await utils.setupDirectFromUser() + const callback = sinon.spy() + driver.respondToMessages(callback, { rooms: [tName] }) + await utils.sendFromUser({ + text: 'SDK test `respondToMessages` DM', + roomId: dmResult.room._id + }) + sinon.assert.notCalled(callback) + }) + it('fires callback on DMs if configured', async () => { + const dmResult = await utils.setupDirectFromUser() + const callback = sinon.spy() + driver.respondToMessages(callback, { dm: true, rooms: [tName] }) + await utils.sendFromUser({ + text: 'SDK test `respondToMessages` DM', + roomId: dmResult.room._id + }) + sinon.assert.calledOnce(callback) + }) + it('fires callback on ul (user leave) message types', async () => { + const callback = sinon.spy() + driver.respondToMessages(callback, { rooms: [tName] }) + await utils.leaveUser() + sinon.assert.calledWithMatch(callback, null, sinon.match({ t: 'ul' })) + await utils.inviteUser() + }) + it('fires callback on au (user added) message types', async () => { + await utils.leaveUser() + const callback = sinon.spy() + driver.respondToMessages(callback, { rooms: [tName] }) + await utils.inviteUser() + sinon.assert.calledWithMatch(callback, null, sinon.match({ t: 'au' })) + }) + // it('appends room name to event meta in channels', async () => { + // const callback = sinon.spy() + // driver.respondToMessages(callback, { dm: true, rooms: [tName] }) + // await utils.sendFromUser({ text: 'SDK test `respondToMessages` DM' }) + // expect(callback.firstCall.args[2].roomName).to.equal(tName) + // }) + // it('room name is undefined in direct messages', async () => { + // const dmResult = await utils.setupDirectFromUser() + // const callback = sinon.spy() + // driver.respondToMessages(callback, { dm: true, rooms: [tName] }) + // await utils.sendFromUser({ + // text: 'SDK test `respondToMessages` DM', + // roomId: dmResult.room._id + // }) + // expect(callback.firstCall.args[2].roomName).to.equal(undefined) + // }) + }) + describe('.getRoomId', () => { + beforeEach(async () => { + await driver.connect() + await driver.login() + }) + it('returns the ID for a channel by ID', async () => { + const room = await driver.getRoomId(tName) + expect(room).to.equal(tId) + }) + it('returns the ID for a private room name', async () => { + const room = await driver.getRoomId(pName) + expect(room).to.equal(pId) + }) + }) + describe('.getRoomName', () => { + beforeEach(async () => { + await driver.connect() + await driver.login() + }) + it('returns the name for a channel by ID', async () => { + const room = await driver.getRoomName(tId) + expect(room).to.equal(tName) + }) + it('returns the name for a private group by ID', async () => { + const room = await driver.getRoomName(pId) + expect(room).to.equal(pName) + }) + it('returns undefined for a DM room', async () => { + const dmResult = await utils.setupDirectFromUser() + const room = await driver.getRoomName(dmResult.room._id) + expect(room).to.equal(undefined) + }) + }) + describe('.joinRooms', () => { + it('joins all the rooms in array, keeping IDs', async () => { + driver.joinedIds.splice(0, driver.joinedIds.length) // clear const array + await driver.connect() + await driver.login() + await driver.joinRooms(['general', tName]) + expect(driver.joinedIds).to.have.members(['GENERAL', tId]) + }) + }) +}) diff --git a/src/lib/driver.ts b/src/lib/driver.ts new file mode 100644 index 0000000..8982901 --- /dev/null +++ b/src/lib/driver.ts @@ -0,0 +1,568 @@ +import { EventEmitter } from 'events' +import Asteroid from 'asteroid' +import * as settings from './settings' +import * as methodCache from './methodCache' +import { Message } from './message' +import { + IConnectOptions, + IRespondOptions, + ICallback, + ILogger +} from '../config/driverInterfaces' +import { + IAsteroid, + ICredentials, + ISubscription, + ICollection +} from '../config/asteroidInterfaces' +import { IMessage } from '../config/messageInterfaces' +import { logger, replaceLog } from './log' +import { IMessageReceiptAPI } from '../utils/interfaces' + +/** Collection names */ +const _messageCollectionName = 'stream-room-messages' +const _messageStreamName = '__my_messages__' + +// CONNECTION SETUP AND CONFIGURE +// ----------------------------------------------------------------------------- + +/** Internal for comparing message update timestamps */ +export let lastReadTime: Date + +/** + * The integration property is applied as an ID on sent messages `bot.i` param + * Should be replaced when connection is invoked by a package using the SDK + * e.g. The Hubot adapter would pass its integration ID with credentials, like: + */ +export const integrationId = settings.integrationId + +/** + * Event Emitter for listening to connection. + * @example + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * driver.events.on('connected', () => console.log('driver connected')) + */ +export const events = new EventEmitter() + +/** + * An Asteroid instance for interacting with Rocket.Chat. + * Variable not initialised until `connect` called. + */ +export let asteroid: IAsteroid + +/** + * Asteroid subscriptions, exported for direct polling by adapters + * Variable not initialised until `prepMeteorSubscriptions` called. + */ +export let subscriptions: ISubscription[] = [] + +/** + * Current user object populated from resolved login + */ +export let userId: string + +/** + * Array of joined room IDs (for reactive queries) + */ +export let joinedIds: string[] = [] + +/** + * Array of messages received from reactive collection + */ +export let messages: ICollection + +/** + * Allow override of default logging with adapter's log instance + */ +export function useLog (externalLog: ILogger) { + replaceLog(externalLog) +} + +/** + * Initialise asteroid instance with given options or defaults. + * Returns promise, resolved with Asteroid instance. Callback follows + * error-first-pattern. Error returned or promise rejected on timeout. + * Removes http/s protocol to get connection hostname if taken from URL. + * @example Use with callback + * import { driver } from '@rocket.chat/sdk' + * driver.connect({}, (err) => { + * if (err) throw err + * else console.log('connected') + * }) + * @example Using promise + * import { driver } from '@rocket.chat/sdk' + * driver.connect() + * .then(() => console.log('connected')) + * .catch((err) => console.error(err)) + */ +export function connect ( + options: IConnectOptions = {}, + callback?: ICallback +): Promise { + return new Promise((resolve, reject) => { + const config = Object.assign({}, settings, options) // override defaults + config.host = config.host.replace(/(^\w+:|^)\/\//, '') + logger.info('[connect] Connecting', config) + asteroid = new Asteroid(config.host, config.useSsl) + + setupMethodCache(asteroid) // init instance for later caching method calls + asteroid.on('connected', () => { + asteroid.resumeLoginPromise.catch(function () { + // pass + }) + events.emit('connected') + }) + asteroid.on('reconnected', () => events.emit('reconnected')) + let cancelled = false + const rejectionTimeout = setTimeout(function () { + logger.info(`[connect] Timeout (${config.timeout})`) + const err = new Error('Asteroid connection timeout') + cancelled = true + events.removeAllListeners('connected') + callback ? callback(err, asteroid) : reject(err) + }, config.timeout) + + // if to avoid condition where timeout happens before listener to 'connected' is added + // and this listener is not removed (because it was added after the removal) + if (!cancelled) { + events.once('connected', () => { + logger.info('[connect] Connected') + // if (cancelled) return asteroid.ddp.disconnect() // cancel if already rejected + clearTimeout(rejectionTimeout) + if (callback) callback(null, asteroid) + resolve(asteroid) + }) + } + }) +} + +/** Remove all active subscriptions, logout and disconnect from Rocket.Chat */ +export function disconnect (): Promise { + logger.info('Unsubscribing, logging out, disconnecting') + unsubscribeAll() + return logout() + .then(() => Promise.resolve()) +} + +// ASYNC AND CACHE METHOD UTILS +// ----------------------------------------------------------------------------- + +/** + * Setup method cache configs from env or defaults, before they are called. + * @param asteroid The asteroid instance to cache method calls + */ +function setupMethodCache (asteroid: IAsteroid): void { + methodCache.use(asteroid) + methodCache.create('getRoomIdByNameOrId', { + max: settings.roomCacheMaxSize, + maxAge: settings.roomCacheMaxAge + }), + methodCache.create('getRoomNameById', { + max: settings.roomCacheMaxSize, + maxAge: settings.roomCacheMaxAge + }) + methodCache.create('createDirectMessage', { + max: settings.dmCacheMaxSize, + maxAge: settings.dmCacheMaxAge + }) +} + +/** + * Wraps method calls to ensure they return a Promise with caught exceptions. + * @param method The Rocket.Chat server method, to call through Asteroid + * @param params Single or array of parameters of the method to call + */ +export function asyncCall (method: string, params: any | any[]): Promise { + if (!Array.isArray(params)) params = [params] // cast to array for apply + logger.info(`[${method}] Calling (async): ${JSON.stringify(params)}`) + return Promise.resolve(asteroid.apply(method, params).result) + .catch((err: Error) => { + logger.error(`[${method}] Error:`, err) + throw err // throw after log to stop async chain + }) + .then((result: any) => { + (result) + ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) + : logger.debug(`[${method}] Success`) + return result + }) +} + +/** + * Call a method as async via Asteroid, or through cache if one is created. + * If the method doesn't have or need parameters, it can't use them for caching + * so it will always call asynchronously. + * @param name The Rocket.Chat server method to call + * @param params Single or array of parameters of the method to call + */ +export function callMethod (name: string, params?: any | any[]): Promise { + return (methodCache.has(name) || typeof params === 'undefined') + ? asyncCall(name, params) + : cacheCall(name, params) +} + +/** + * Wraps Asteroid method calls, passed through method cache if cache is valid. + * @param method The Rocket.Chat server method, to call through Asteroid + * @param key Single string parameters only, required to use as cache key + */ +export function cacheCall (method: string, key: string): Promise { + return methodCache.call(method, key) + .catch((err: Error) => { + logger.error(`[${method}] Error:`, err) + throw err // throw after log to stop async chain + }) + .then((result: any) => { + (result) + ? logger.debug(`[${method}] Success: ${JSON.stringify(result)}`) + : logger.debug(`[${method}] Success`) + return result + }) +} + +// LOGIN AND SUBSCRIBE TO ROOMS +// ----------------------------------------------------------------------------- + +/** Login to Rocket.Chat via Asteroid */ +export function login (credentials: ICredentials = { + username: settings.username, + password: settings.password, + ldap: settings.ldap +}): Promise { + let login: Promise + // if (credentials.ldap) { + // logger.info(`[login] Logging in ${credentials.username} with LDAP`) + // login = asteroid.loginWithLDAP( + // credentials.email || credentials.username, + // credentials.password, + // { ldap: true, ldapOptions: credentials.ldapOptions || {} } + // ) + // } else { + logger.info(`[login] Logging in ${credentials.username}`) + login = asteroid.loginWithPassword( + credentials.email || credentials.username!, + credentials.password + ) + // } + return login + .then((loggedInUserId) => { + userId = loggedInUserId + return loggedInUserId + }) + .catch((err: Error) => { + logger.info('[login] Error:', err) + throw err // throw after log to stop async chain + }) +} + +/** Logout of Rocket.Chat via Asteroid */ +export function logout (): Promise { + return asteroid.logout() + .catch((err: Error) => { + logger.error('[Logout] Error:', err) + throw err // throw after log to stop async chain + }) +} + +/** + * Subscribe to Meteor subscription + * Resolves with subscription (added to array), with ID property + * @todo - 3rd param of asteroid.subscribe is deprecated in Rocket.Chat? + */ +export function subscribe ( + topic: string, + roomId: string +): Promise { + return new Promise((resolve, reject) => { + logger.info(`[subscribe] Preparing subscription: ${topic}: ${roomId}`) + const subscription = asteroid.subscribe(topic, roomId, true) + subscriptions.push(subscription) + return subscription.ready + .then((id) => { + logger.info(`[subscribe] Stream ready: ${id}`) + resolve(subscription) + }) + }) +} + +/** Unsubscribe from Meteor subscription */ +export function unsubscribe (subscription: ISubscription): void { + const index = subscriptions.indexOf(subscription) + if (index === -1) return + subscription.stop() + // asteroid.unsubscribe(subscription.id) // v2 + subscriptions.splice(index, 1) // remove from collection + logger.info(`[${subscription.id}] Unsubscribed`) +} + +/** Unsubscribe from all subscriptions in collection */ +export function unsubscribeAll (): void { + subscriptions.map((s: ISubscription) => unsubscribe(s)) +} + +/** + * Begin subscription to room events for user. + * Older adapters used an option for this method but it was always the default. + */ +export function subscribeToMessages (): Promise { + return subscribe(_messageCollectionName, _messageStreamName) + .then((subscription) => { + messages = asteroid.getCollection(_messageCollectionName) + return subscription + }) +} + +/** + * Once a subscription is created, using `subscribeToMessages` this method + * can be used to attach a callback to changes in the message stream. + * This can be called directly for custom extensions, but for most usage (e.g. + * for bots) the respondToMessages is more useful to only receive messages + * matching configuration. + * + * If the bot hasn't been joined to any rooms at this point, it will attempt to + * join now based on environment config, otherwise it might not receive any + * messages. It doesn't matter that this happens asynchronously because the + * bot's joined rooms can change after the reactive query is set up. + * + * @todo `reactToMessages` should call `subscribeToMessages` if not already + * done, so it's not required as an arbitrary step for simpler adapters. + * Also make `login` call `connect` for the same reason, the way + * `respondToMessages` calls `respondToMessages`, so all that's really + * required is: + * `driver.login(credentials).then(() => driver.respondToMessages(callback))` + * @param callback Function called with every change in subscriptions. + * - Uses error-first callback pattern + * - Second argument is the changed item + * - Third argument is additional attributes, such as `roomType` + */ +export function reactToMessages (callback: ICallback): void { + logger.info(`[reactive] Listening for change events in collection ${messages.name}`) + + messages.reactiveQuery({}).on('change', (_id: string) => { + const changedMessageQuery = messages.reactiveQuery({ _id }) + if (changedMessageQuery.result && changedMessageQuery.result.length > 0) { + const changedMessage = changedMessageQuery.result[0] + if (Array.isArray(changedMessage.args)) { + logger.info(`[received] Message in room ${ changedMessage.args[0].rid }`) + callback(null, changedMessage.args[0], changedMessage.args[1]) + } else { + logger.debug('[received] Update without message args') + } + } else { + logger.debug('[received] Reactive query at ID ${ _id } without results') + } + }) +} + +/** + * Proxy for `reactToMessages` with some filtering of messages based on config. + * + * @param callback Function called after filters run on subscription events. + * - Uses error-first callback pattern + * - Second argument is the changed item + * - Third argument is additional attributes, such as `roomType` + * @param options Sets filters for different event/message types. + */ +export function respondToMessages ( + callback: ICallback, + options: IRespondOptions = {} +): Promise { + const config = Object.assign({}, settings, options) + // return value, may be replaced by async ops + let promise: Promise = Promise.resolve() + + // Join configured rooms if they haven't been already, unless listening to all + // public rooms, in which case it doesn't matter + if ( + !config.allPublic && + joinedIds.length === 0 && + config.rooms && + config.rooms.length > 0 + ) { + promise = joinRooms(config.rooms) + .catch((err) => { + logger.error(`[joinRooms] Failed to join configured rooms (${config.rooms.join(', ')}): ${err.message}`) + }) + } + + lastReadTime = new Date() // init before any message read + reactToMessages(async (err, message, meta) => { + if (err) { + logger.error(`[received] Unable to receive: ${err.message}`) + callback(err) // bubble errors back to adapter + } + + // Ignore bot's own messages + if (message.u._id === userId) return + + // Ignore DMs unless configured not to + const isDM = meta.roomType === 'd' + if (isDM && !config.dm) return + + // Ignore Livechat unless configured not to + const isLC = meta.roomType === 'l' + if (isLC && !config.livechat) return + + // Ignore messages in un-joined public rooms unless configured not to + if (!config.allPublic && !isDM && !meta.roomParticipant) return + + // Set current time for comparison to incoming + let currentReadTime = new Date(message.ts.$date) + + // Ignore edited messages if configured to + if (!config.edited && message.editedAt) return + + // Set read time as time of edit, if message is edited + if (message.editedAt) currentReadTime = new Date(message.editedAt.$date) + + // Ignore messages in stream that aren't new + if (currentReadTime <= lastReadTime) return + + // At this point, message has passed checks and can be responded to + logger.info(`[received] Message ${message._id} from ${message.u.username}`) + lastReadTime = currentReadTime + + // Processing completed, call callback to respond to message + callback(null, message, meta) + }) + return promise +} + +// PREPARE AND SEND MESSAGES +// ----------------------------------------------------------------------------- + +/** Get ID for a room by name (or ID). */ +export function getRoomId (name: string): Promise { + return cacheCall('getRoomIdByNameOrId', name) +} + +/** Get name for a room by ID. */ +export function getRoomName (id: string): Promise { + return cacheCall('getRoomNameById', id) +} + +/** + * Get ID for a DM room by its recipient's name. + * Will create a DM (with the bot) if it doesn't exist already. + * @todo test why create resolves with object instead of simply ID + */ +export function getDirectMessageRoomId (username: string): Promise { + return cacheCall('createDirectMessage', username) + .then((DM) => DM.rid) +} + +/** Join the bot into a room by its name or ID */ +export async function joinRoom (room: string): Promise { + let roomId = await getRoomId(room) + let joinedIndex = joinedIds.indexOf(room) + if (joinedIndex !== -1) { + logger.error(`[joinRoom] room was already joined`) + } else { + await asyncCall('joinRoom', roomId) + joinedIds.push(roomId) + } +} + +/** Exit a room the bot has joined */ +export async function leaveRoom (room: string): Promise { + let roomId = await getRoomId(room) + let joinedIndex = joinedIds.indexOf(room) + if (joinedIndex === -1) { + logger.error(`[leaveRoom] failed because bot has not joined ${room}`) + } else { + await asyncCall('leaveRoom', roomId) + delete joinedIds[joinedIndex] + } +} + +/** Join a set of rooms by array of names or IDs */ +export function joinRooms (rooms: string[]): Promise { + return Promise.all(rooms.map((room) => joinRoom(room))) +} + +/** + * Structure message content, optionally addressing to room ID. + * Accepts message text string or a structured message object. + */ +export function prepareMessage ( + content: string | IMessage, + roomId?: string +): Message { + const message = new Message(content, integrationId) + if (roomId) message.setRoomId(roomId) + return message +} + +/** + * Send a prepared message object (with pre-defined room ID). + * Usually prepared and called by sendMessageByRoomId or sendMessageByRoom. + */ +export function sendMessage (message: IMessage): Promise { + return asyncCall('sendMessage', message) +} + +/** + * Prepare and send string/s to specified room ID. + * @param content Accepts message text string or array of strings. + * @param roomId ID of the target room to use in send. + * @todo Returning one or many gets complicated with type checking not allowing + * use of a property because result may be array, when you know it's not. + * Solution would probably be to always return an array, even for single + * send. This would be a breaking change, should hold until major version. + */ +export function sendToRoomId ( + content: string | string[] | IMessage, + roomId: string +): Promise { + if (!Array.isArray(content)) { + return sendMessage(prepareMessage(content, roomId)) + } else { + return Promise.all(content.map((text) => { + return sendMessage(prepareMessage(text, roomId)) + })) + } +} + +/** + * Prepare and send string/s to specified room name (or ID). + * @param content Accepts message text string or array of strings. + * @param room A name (or ID) to resolve as ID to use in send. + */ +export function sendToRoom ( + content: string | string[] | IMessage, + room: string +): Promise { + return getRoomId(room) + .then((roomId) => sendToRoomId(content, roomId)) +} + +/** + * Prepare and send string/s to a user in a DM. + * @param content Accepts message text string or array of strings. + * @param username Name to create (or get) DM for room ID to use in send. + */ +export function sendDirectToUser ( + content: string | string[] | IMessage, + username: string +): Promise { + return getDirectMessageRoomId(username) + .then((rid) => sendToRoomId(content, rid)) +} + +/** + * Edit an existing message, replacing any attributes with those provided. + * The given message object should have the ID of an existing message. + */ +export function editMessage (message: IMessage): Promise { + return asyncCall('updateMessage', message) +} + +/** + * Send a reaction to an existing message. Simple proxy for method call. + * @param emoji Accepts string like `:thumbsup:` to add 👍 reaction + * @param messageId ID for a previously sent message + */ +export function setReaction (emoji: string, messageId: string) { + return asyncCall('setReaction', [emoji, messageId]) +} diff --git a/src/lib/log.ts b/src/lib/log.ts new file mode 100644 index 0000000..273009a --- /dev/null +++ b/src/lib/log.ts @@ -0,0 +1,42 @@ +import { ILogger } from '../config/driverInterfaces' + +/** Temp logging, should override form adapter's log */ +class InternalLog implements ILogger { + debug (...args: any[]) { + console.log(...args) + } + info (...args: any[]) { + console.log(...args) + } + warning (...args: any[]) { + console.warn(...args) + } + warn (...args: any[]) { // legacy method + return this.warning(...args) + } + error (...args: any[]) { + console.error(...args) + } +} + +let logger: ILogger = new InternalLog() + +function replaceLog (externalLog: ILogger) { + logger = externalLog +} + +function silence () { + replaceLog({ + debug: () => null, + info: () => null, + warn: () => null, + warning: () => null, + error: () => null + }) +} + +export { + logger, + replaceLog, + silence +} diff --git a/src/lib/message.spec.ts b/src/lib/message.spec.ts new file mode 100644 index 0000000..4cdc9af --- /dev/null +++ b/src/lib/message.spec.ts @@ -0,0 +1,47 @@ +import 'mocha' +import sinon from 'sinon' +import { expect } from 'chai' +import { Message } from './message' + +describe('message', () => { + describe('constructor', () => { + it('creates message object from content string', () => { + let message = new Message('hello world', 'test') + expect(message.msg).to.equal('hello world') + }) + it('uses second param as integration ID attribute', () => { + let message = new Message('hello world', 'test') + expect(message.bot.i).to.equal('test') + }) + it('accepts existing message and assigns new properties', () => { + let message = new Message({ + msg: 'hello world', + rid: 'GENERAL' + }, 'test') + expect(message).to.eql({ + msg: 'hello world', + rid: 'GENERAL', + bot: { i: 'test' } + }) + }) + }) + describe('.setRoomId', () => { + it('sets rid property', () => { + let message = new Message('hello world', 'test') + message.setRoomId('111') + expect(message.rid).to.equal('111') + }) + it('updates rid property', () => { + let message = new Message({ + msg: 'hello world', + rid: 'GENERAL' + }, 'test') + message.setRoomId('111') + expect(message.rid).to.equal('111') + }) + it('returns message instance', () => { + let message = new Message('hello world', 'test') + expect(message.setRoomId('111')).to.eql(message) + }) + }) +}) diff --git a/src/lib/message.ts b/src/lib/message.ts new file mode 100644 index 0000000..901b337 --- /dev/null +++ b/src/lib/message.ts @@ -0,0 +1,23 @@ +import { IMessage } from '../config/messageInterfaces' + +// Message class declaration implicitly implements interface +// https://github.com/Microsoft/TypeScript/issues/340 +export interface Message extends IMessage {} + +/** + * Rocket.Chat message class. + * Sets integration param to allow tracing source of automated sends. + * @param content Accepts message text or a preformed message object + * @todo Potential for SDK usage that isn't bots, bot prop should be optional? + */ +export class Message { + constructor (content: string | IMessage, integrationId: string) { + if (typeof content === 'string') this.msg = content + else Object.assign(this, content) + this.bot = { i: integrationId } + } + setRoomId (roomId: string): Message { + this.rid = roomId + return this + } +} diff --git a/src/lib/methodCache.spec.ts b/src/lib/methodCache.spec.ts new file mode 100644 index 0000000..8e6be64 --- /dev/null +++ b/src/lib/methodCache.spec.ts @@ -0,0 +1,150 @@ +import sinon from 'sinon' +import { expect } from 'chai' +import LRU from 'lru-cache' +import * as methodCache from './methodCache' + +// Instance method variance for testing cache +const mockInstance = { call: sinon.stub() } +mockInstance.call.withArgs('methodOne').onCall(0).returns({ result: 'foo' }) +mockInstance.call.withArgs('methodOne').onCall(1).returns({ result: 'bar' }) +mockInstance.call.withArgs('methodTwo', 'key1').returns({ result: 'value1' }) +mockInstance.call.withArgs('methodTwo', 'key2').returns({ result: 'value2' }) + +describe('methodCache', () => { + beforeEach(() => mockInstance.call.resetHistory()) + afterEach(() => methodCache.resetAll()) + describe('.use', () => { + it('calls apply to instance', () => { + methodCache.use(mockInstance) + methodCache.call('methodOne', 'key1') + expect(mockInstance.call.callCount).to.equal(1) + }) + it('accepts a class instance', () => { + class MyClass {} + const myInstance = new MyClass() + const shouldWork = () => methodCache.use(myInstance) + expect(shouldWork).to.not.throw() + }) + }) + describe('.create', () => { + it('returns a cache for method calls', () => { + expect(methodCache.create('anyMethod')).to.be.instanceof(LRU) + }) + it('accepts options overriding defaults', () => { + const cache = methodCache.create('methodOne', { maxAge: 3000 }) + expect(cache.max).to.equal(methodCache.defaults.max) + expect(cache.maxAge).to.equal(3000) + }) + }) + describe('.call', () => { + it('throws if instance not in use', () => { + const badUse = () => methodCache.call('methodOne', 'key1') + expect(badUse).to.throw() + }) + it('throws if method does not exist', () => { + methodCache.use(mockInstance) + const badUse = () => methodCache.call('bad', 'key1') + expect(badUse).to.throw() + }) + it('returns a promise', () => { + methodCache.use(mockInstance) + expect(methodCache.call('methodOne', 'key1').then).to.be.a('function') + }) + it('calls the method with the key', () => { + methodCache.use(mockInstance) + return methodCache.call('methodTwo', 'key1').then((result) => { + expect(result).to.equal('value1') + }) + }) + it('only calls the method once', () => { + methodCache.use(mockInstance) + methodCache.call('methodOne', 'key1') + methodCache.call('methodOne', 'key1') + expect(mockInstance.call.callCount).to.equal(1) + }) + it('returns cached result on subsequent calls', () => { + methodCache.use(mockInstance) + return Promise.all([ + methodCache.call('methodOne', 'key1'), + methodCache.call('methodOne', 'key1') + ]).then((results) => { + expect(results[0]).to.equal(results[1]) + }) + }) + it('calls again if cache expired', () => { + const clock = sinon.useFakeTimers() + methodCache.use(mockInstance) + methodCache.create('methodOne', { maxAge: 10 }) + const result1 = methodCache.call('methodOne', 'key1') + clock.tick(20) + const result2 = methodCache.call('methodOne', 'key1') + clock.restore() + return Promise.all([result1, result2]).then((results) => { + expect(mockInstance.call.callCount).to.equal(2) + expect(results[0]).to.not.equal(results[1]) + }) + }) + }) + describe('.has', () => { + it('returns true if the method cache was created', () => { + methodCache.use(mockInstance) + methodCache.create('methodOne') + expect(methodCache.has('methodOne')).to.equal(true) + }) + it('returns true if the method was called with cache', () => { + methodCache.use(mockInstance) + methodCache.call('methodOne', 'key') + expect(methodCache.has('methodOne')).to.equal(true) + }) + it('returns false if the method is not cached', () => { + methodCache.use(mockInstance) + expect(methodCache.has('methodThree')).to.equal(false) + }) + }) + describe('.get', () => { + it('returns cached result from last call with key', () => { + methodCache.use(mockInstance) + return methodCache.call('methodOne', 'key1').then((result) => { + expect(methodCache.get('methodOne', 'key1')).to.equal(result) + }) + }) + }) + describe('.reset', () => { + it('removes cached results for a method and key', () => { + methodCache.use(mockInstance) + const result1 = methodCache.call('methodOne', 'key1') + methodCache.reset('methodOne', 'key1') + const result2 = methodCache.call('methodOne', 'key1') + expect(result1).not.to.equal(result2) + }) + it('does not remove cache of calls with different key', () => { + methodCache.use(mockInstance) + methodCache.call('methodTwo', 'key1') + methodCache.call('methodTwo', 'key2') + methodCache.reset('methodTwo', 'key1') + const result = methodCache.get('methodTwo', 'key2') + expect(result).to.equal('value2') + }) + it('without key, removes all results for method', () => { + methodCache.use(mockInstance) + methodCache.call('methodTwo', 'key1') + methodCache.call('methodTwo', 'key2') + methodCache.reset('methodTwo') + const result1 = methodCache.get('methodTwo', 'key1') + const result2 = methodCache.get('methodTwo', 'key2') + expect(result1).to.equal(undefined) + expect(result2).to.equal(undefined) + }) + }) + describe('.resetAll', () => { + it('resets all cached methods', () => { + methodCache.use(mockInstance) + methodCache.call('methodOne', 'key1') + methodCache.call('methodTwo', 'key1') + methodCache.resetAll() + methodCache.call('methodOne', 'key1') + methodCache.call('methodTwo', 'key1') + expect(mockInstance.call.callCount).to.equal(4) + }) + }) +}) diff --git a/src/lib/methodCache.ts b/src/lib/methodCache.ts new file mode 100644 index 0000000..c566685 --- /dev/null +++ b/src/lib/methodCache.ts @@ -0,0 +1,90 @@ +import LRU from 'lru-cache' +import { logger } from './log' + +/** @TODO: Remove ! post-fix expression when TypeScript #9619 resolved */ +export let instance: any +export const results: Map> = new Map() +export const defaults: LRU.Options = { + max: 100, + maxAge: 300 * 1000 +} + +/** + * Set the instance to call methods on, with cached results. + * @param instanceToUse Instance of a class + */ +export function use (instanceToUse: object): void { + instance = instanceToUse +} + +/** + * Setup a cache for a method call. + * @param method Method name, for index of cached results + * @param options.max Maximum size of cache + * @param options.maxAge Maximum age of cache + */ +export function create (method: string, options: LRU.Options = {}): LRU.Cache | undefined { + options = Object.assign(defaults, options) + results.set(method, new LRU(options)) + return results.get(method) +} + +/** + * Get results of a prior method call or call and cache. + * @param method Method name, to call on instance in use + * @param key Key to pass to method call and save results against + */ +export function call (method: string, key: string): Promise { + if (!results.has(method)) create(method) // create as needed + const methodCache = results.get(method)! + let callResults + + if (methodCache.has(key)) { + logger.debug(`[${method}] Calling (cached): ${key}`) + // return from cache if key has been used on method before + callResults = methodCache.get(key) + } else { + // call and cache for next time, returning results + logger.debug(`[${method}] Calling (caching): ${key}`) + callResults = instance.call(method, key).result + methodCache.set(key, callResults) + } + return Promise.resolve(callResults) +} + +/** + * Proxy for checking if method has been cached. + * Cache may exist from manual creation, or prior call. + * @param method Method name for cache to get + */ +export function has (method: string): boolean { + return results.has(method) +} + +/** + * Get results of a prior method call. + * @param method Method name for cache to get + * @param key Key for method result set to return + */ +export function get (method: string, key: string): LRU.Cache | undefined { + if (results.has(method)) return results.get(method)!.get(key) +} + +/** + * Reset a cached method call's results (all or only for given key). + * @param method Method name for cache to clear + * @param key Key for method result set to clear + */ +export function reset (method: string, key?: string): void { + if (results.has(method)) { + if (key) return results.get(method)!.del(key) + else return results.get(method)!.reset() + } +} + +/** + * Reset cached results for all methods. + */ +export function resetAll (): void { + results.forEach((cache) => cache.reset()) +} diff --git a/src/lib/settings.spec.ts b/src/lib/settings.spec.ts new file mode 100644 index 0000000..0c44915 --- /dev/null +++ b/src/lib/settings.spec.ts @@ -0,0 +1,76 @@ +import 'mocha' +import { expect } from 'chai' +const initEnv = process.env // store configs to restore after tests + +describe('settings', () => { + beforeEach(() => { + delete process.env.ROCKETCHAT_URL + delete process.env.ROCKETCHAT_USE_SSL + delete process.env.ROCKETCHAT_ROOM + delete process.env.LISTEN_ON_ALL_PUBLIC + delete process.env.RESPOND_TO_DM + delete process.env.RESPOND_TO_LIVECHAT + delete process.env.RESPOND_TO_EDITED + delete require.cache[require.resolve('./settings')] // clear modules memory + }) + afterEach(() => process.env = initEnv) + it('uses localhost URL without SSL if env undefined', () => { + const settings = require('./settings') + expect(settings).to.deep.include({ + host: 'localhost:3000', + useSsl: false, + timeout: 20000 + }) + }) + it('sets SSL from env if defined', () => { + process.env.ROCKETCHAT_USE_SSL = 'true' + const settings = require('./settings') + expect(settings.useSsl).to.equal(true) + }) + it('uses SSL if https protocol URL in env', () => { + process.env.ROCKETCHAT_URL = 'https://localhost:3000' + const settings = require('./settings') + expect(settings.useSsl).to.equal(true) + }) + it('does not use SSL if http protocol URL in env', () => { + process.env.ROCKETCHAT_URL = 'http://localhost:3000' + const settings = require('./settings') + expect(settings.useSsl).to.equal(false) + }) + it('SSL overrides protocol detection', () => { + process.env.ROCKETCHAT_URL = 'https://localhost:3000' + process.env.ROCKETCHAT_USE_SSL = 'false' + const settings = require('./settings') + expect(settings.useSsl).to.equal(false) + }) + it('all respond configs default to false if env undefined', () => { + const settings = require('./settings') + expect(settings).to.deep.include({ + rooms: [], + allPublic: false, + dm: false, + livechat: false, + edited: false + }) + }) + it('inherits config from env settings', () => { + process.env.ROCKETCHAT_ROOM = 'GENERAL' + process.env.LISTEN_ON_ALL_PUBLIC = 'false' + process.env.RESPOND_TO_DM = 'true' + process.env.RESPOND_TO_LIVECHAT = 'true' + process.env.RESPOND_TO_EDITED = 'true' + const settings = require('./settings') + expect(settings).to.deep.include({ + rooms: ['GENERAL'], + allPublic: false, + dm: true, + livechat: true, + edited: true + }) + }) + it('creates room array from csv list', () => { + process.env.ROCKETCHAT_ROOM = `general, foo` + const settings = require('./settings') + expect(settings.rooms).to.eql(['general', 'foo']) + }) +}) diff --git a/src/lib/settings.ts b/src/lib/settings.ts new file mode 100644 index 0000000..816fdb0 --- /dev/null +++ b/src/lib/settings.ts @@ -0,0 +1,30 @@ + +// Login settings - LDAP needs to be explicitly enabled +export let username = process.env.ROCKETCHAT_USER || 'bot' +export let password = process.env.ROCKETCHAT_PASSWORD || 'pass' +export let ldap = (process.env.ROCKETCHAT_AUTH === 'ldap') + +// Connection settings - Enable SSL by default if Rocket.Chat URL contains https +export let host = process.env.ROCKETCHAT_URL || 'localhost:3000' +export let useSsl = (process.env.ROCKETCHAT_USE_SSL) + ? ((process.env.ROCKETCHAT_USE_SSL || '').toString().toLowerCase() === 'true') + : ((process.env.ROCKETCHAT_URL || '').toString().toLowerCase().startsWith('https')) +export let timeout = 20 * 1000 // 20 seconds + +// Respond settings - reactive callback filters for .respondToMessages +export let rooms = (process.env.ROCKETCHAT_ROOM) + ? (process.env.ROCKETCHAT_ROOM || '').split(',').map((room) => room.trim()) + : [] +export let allPublic = (process.env.LISTEN_ON_ALL_PUBLIC || 'false').toLowerCase() === 'true' +export let dm = (process.env.RESPOND_TO_DM || 'false').toLowerCase() === 'true' +export let livechat = (process.env.RESPOND_TO_LIVECHAT || 'false').toLowerCase() === 'true' +export let edited = (process.env.RESPOND_TO_EDITED || 'false').toLowerCase() === 'true' + +// Message attribute settings +export let integrationId = process.env.INTEGRATION_ID || 'js.SDK' + +// Cache settings +export let roomCacheMaxSize = parseInt(process.env.ROOM_CACHE_SIZE || '10', 10) +export let roomCacheMaxAge = 1000 * parseInt(process.env.ROOM_CACHE_MAX_AGE || '300', 10) +export let dmCacheMaxSize = parseInt(process.env.DM_ROOM_CACHE_SIZE || '10', 10) +export let dmCacheMaxAge = 1000 * parseInt(process.env.DM_ROOM_CACHE_MAX_AGE || '100', 10) diff --git a/src/types/Asteroid.d.ts b/src/types/Asteroid.d.ts new file mode 100644 index 0000000..9b4458e --- /dev/null +++ b/src/types/Asteroid.d.ts @@ -0,0 +1 @@ +declare module 'asteroid' \ No newline at end of file diff --git a/src/types/Client.d.ts b/src/types/Client.d.ts new file mode 100644 index 0000000..c7fc520 --- /dev/null +++ b/src/types/Client.d.ts @@ -0,0 +1 @@ +declare module 'node-rest-client' \ No newline at end of file diff --git a/src/types/immutableCollectionMixin.d.ts b/src/types/immutableCollectionMixin.d.ts new file mode 100644 index 0000000..69bf79e --- /dev/null +++ b/src/types/immutableCollectionMixin.d.ts @@ -0,0 +1 @@ +declare module 'asteroid-immutable-collections-mixin' \ No newline at end of file diff --git a/src/utils/.DS_Store b/src/utils/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d93b6c051a8f9f398cdba92661f517a72ece6dc2 GIT binary patch literal 6148 zcmeHKO-lnY5Pi`i1us2%%*mS;|3N72O}zLAw7Y_ff@ME?&QJGEW)v24mQrRQd6W4_ zvJW=N00^6@eE?PfmTZDSl!}PC>bkIC!BFNHF&xJI<1~zYWuj9w$=pU4xv-MArB;S)W z;0!ne&cInQz&l%Hd9CQZGvEw31HTN&`4HIzlVMgYM+du70uUE87oje-gyclSWSAA{ zp#)7OYHF|&BWOD9(c+R}R@8KavNGqfm4AwtP*$fsmT-im=)E)G49pq0)Zt3*{~dps zYLP!r@sTs&44fGQVX=MQZun7lxBmG&xoaca3!8|9XO935epoch time + version: number // ?? + } +} + +/** Channel result schema */ +export interface IChannelAPI { + _id: string // Channel ID + name: string // Channel name + t: 'c' | 'p' | 'l' // Channel type (channel always c) + msgs: number // Count of messages in room + u: { + _id: string // Owner user ID + username: string // Owner username + } + ts: string // ISO timestamp (current time in room?) + default: boolean // Is default channel +} + +/** Group result schema */ +export interface IGroupAPI { + _id: string // Group ID + name: string // Group name + usernames: string[] // Users in group + t: 'c' | 'p' | 'l' // Group type (private always p) + msgs: number // Count of messages in room + u: { + _id: string // Owner user ID + username: string // Owner username + } + ts: string // ISO timestamp (current time in room?) + default: boolean // Is default channel (would be false) +} + +/** Result structure for room creation (e.g. DM) */ +export interface IRoomResultAPI { + room: IRoomAPI + success: boolean +} + +/** Result structure for channel creation */ +export interface IChannelResultAPI { + channel: IChannelAPI + success: boolean +} + +/** Result structure for group creation */ +export interface IGroupResultAPI { + group: IGroupAPI + success: boolean +} diff --git a/src/utils/setup.ts b/src/utils/setup.ts new file mode 100644 index 0000000..aa5e6ad --- /dev/null +++ b/src/utils/setup.ts @@ -0,0 +1,5 @@ +/** On require, runs the test utils setup method */ +import { setup } from './testing' +import { silence } from '../lib/log' +silence() +setup().catch((e) => console.error(e)) diff --git a/src/utils/start.ts b/src/utils/start.ts new file mode 100644 index 0000000..43cfbc8 --- /dev/null +++ b/src/utils/start.ts @@ -0,0 +1,50 @@ +// Test script uses standard methods and env config to connect and log streams +import { botUser } from './config' +import { IMessage } from '../config/messageInterfaces' +import { api, driver } from '..' +const delay = (ms: number) => new Promise((resolve, reject) => setTimeout(resolve, ms)) + +// Start subscription to log message stream (used for e2e test and demo) +async function start () { + await driver.connect() + await driver.login({ username: botUser.username, password: botUser.password }) + await driver.subscribeToMessages() + await driver.respondToMessages((err, msg, msgOpts) => { + if (err) throw err + console.log('[respond]', JSON.stringify(msg), JSON.stringify(msgOpts)) + demo(msg).catch((e) => console.error(e)) + }, { + rooms: ['general'], + allPublic: false, + dm: true, + edited: true, + livechat: false + }) +} + +// Demo bot-style interactions +// A: Listen for "tell everyone " and send that something to everyone +// B: Listen for "who's online" and tell that person who's online +async function demo (message: IMessage) { + console.log(message) + if (!message.msg) return + if (/tell everyone/i.test(message.msg)) { + const match = message.msg.match(/tell everyone (.*)/i) + if (!match || !match[1]) return + const sayWhat = `@${message.u!.username} says "${match[1]}"` + const usernames = await api.users.allNames() + for (let username of usernames) { + if (username !== botUser.username) { + const toWhere = await driver.getDirectMessageRoomId(username) + await driver.sendToRoomId(sayWhat, toWhere) // DM ID hax + await delay(200) // delay to prevent rate-limit error + } + } + } else if (/who\'?s online/i.test(message.msg)) { + const names = await api.users.onlineNames() + const niceNames = names.join(', ').replace(/, ([^,]*)$/, ' and $1') + await driver.sendToRoomId(niceNames + ' are online', message.rid!) + } +} + +start().catch((e) => console.error(e)) diff --git a/src/utils/testing.ts b/src/utils/testing.ts new file mode 100644 index 0000000..33e5c9b --- /dev/null +++ b/src/utils/testing.ts @@ -0,0 +1,214 @@ +import { get, post, login, logout } from '../lib/api' +import { apiUser, botUser, mockUser } from './config' +import { + IMessageAPI, + IMessageUpdateAPI, + IMessageResultAPI, + INewUserAPI, + IUserResultAPI, + IRoomResultAPI, + IChannelResultAPI, + IGroupResultAPI, + IMessageReceiptAPI +} from './interfaces' +import { IMessage } from '../config/messageInterfaces' + +/** Define common attributes for DRY tests */ +export const testChannelName = 'tests' +export const testPrivateName = 'p-tests' + +/** Get information about a user */ +export async function userInfo (username: string): Promise { + return get('users.info', { username }, true) +} + +/** Create a user and catch the error if they exist already */ +export async function createUser (user: INewUserAPI): Promise { + return post('users.create', user, true, /already in use/i) +} + +/** Get information about a channel */ +export async function channelInfo (query: { roomName?: string, roomId?: string }): Promise { + return get('channels.info', query, true) +} + +/** Get information about a private group */ +export async function privateInfo (query: { roomName?: string, roomId?: string }): Promise { + return get('groups.info', query, true) +} + +/** Get the last messages sent to a channel (in last 10 minutes) */ +export async function lastMessages (roomId: string, count: number = 1): Promise { + const now = new Date() + const latest = now.toISOString() + const oldest = new Date(now.setMinutes(now.getMinutes() - 10)).toISOString() + return (await get('channels.history', { roomId, latest, oldest, count })).messages +} + +/** Create a room for tests and catch the error if it exists already */ +export async function createChannel ( + name: string, + members: string[] = [], + readOnly: boolean = false +): Promise { + return post('channels.create', { name, members, readOnly }, true) +} + +/** Create a private group / room and catch if exists already */ +export async function createPrivate ( + name: string, + members: string[] = [], + readOnly: boolean = false +): Promise { + return post('groups.create', { name, members, readOnly }, true) +} + +/** Send message from mock user to channel for tests to listen and respond */ +/** @todo Sometimes the post request completes before the change event emits + * the message to the streamer. That's why the interval is used for proof + * of receipt. It would be better for the endpoint to not resolve until + * server side handling is complete. Would require PR to core. + */ +export async function sendFromUser (payload: any): Promise { + const user = await login({ username: mockUser.username, password: mockUser.password }) + const endpoint = (payload.roomId && payload.roomId.indexOf(user.data.userId) !== -1) + ? 'dm.history' + : 'channels.history' + const roomId = (payload.roomId) + ? payload.roomId + : (await channelInfo({ roomName: testChannelName })).channel._id + const messageDefaults: IMessageAPI = { roomId } + const data: IMessageAPI = Object.assign({}, messageDefaults, payload) + const oldest = new Date().toISOString() + const result = await post('chat.postMessage', data, true) + const proof = new Promise((resolve, reject) => { + let looked = 0 + const look = setInterval(async () => { + const { messages } = await get(endpoint, { roomId, oldest }) + const found = messages.some((message: IMessageReceiptAPI) => { + return result.message._id === message._id + }) + if (found || looked > 10) { + clearInterval(look) + if (found) resolve() + else reject('API send from user, proof of receipt timeout') + } + looked++ + }, 100) + }) + await proof + return result +} + +/** Leave user from room, to generate `ul` message (test channel by default) */ +export async function leaveUser (room: { id?: string, name?: string } = {}): Promise { + await login({ username: mockUser.username, password: mockUser.password }) + if (!room.id && !room.name) room.name = testChannelName + const roomId = (room.id) + ? room.id + : (await channelInfo({ roomName: room.name })).channel._id + return post('channels.leave', { roomId }) +} + +/** Invite user to room, to generate `au` message (test channel by default) */ +export async function inviteUser (room: { id?: string, name?: string } = {}): Promise { + let mockInfo = await userInfo(mockUser.username) + await login({ username: apiUser.username, password: apiUser.password }) + if (!room.id && !room.name) room.name = testChannelName + const roomId = (room.id) + ? room.id + : (await channelInfo({ roomName: room.name })).channel._id + return post('channels.invite', { userId: mockInfo.user._id, roomId }) +} + +/** @todo : Join user into room (enter) to generate `uj` message type. */ + +/** Update message sent from mock user */ +export async function updateFromUser (payload: IMessageUpdateAPI): Promise { + await login({ username: mockUser.username, password: mockUser.password }) + return post('chat.update', payload, true) +} + +/** Create a direct message session with the mock user */ +export async function setupDirectFromUser (): Promise { + await login({ username: mockUser.username, password: mockUser.password }) + return post('im.create', { username: botUser.username }, true) +} + +/** Initialise testing instance with the required users for SDK/bot tests */ +export async function setup () { + console.log('\nPreparing instance for tests...') + try { + // Verify API user can login + const loginInfo = await login(apiUser) + if (loginInfo.status !== 'success') { + throw new Error(`API user (${apiUser.username}) could not login`) + } else { + console.log(`API user (${apiUser.username}) logged in`) + } + + // Verify or create user for bot + let botInfo = await userInfo(botUser.username) + if (!botInfo || !botInfo.success) { + console.log(`Bot user (${botUser.username}) not found`) + botInfo = await createUser(botUser) + if (!botInfo.success) { + throw new Error(`Bot user (${botUser.username}) could not be created`) + } else { + console.log(`Bot user (${botUser.username}) created`) + } + } else { + console.log(`Bot user (${botUser.username}) exists`) + } + + // Verify or create mock user for talking to bot + let mockInfo = await userInfo(mockUser.username) + if (!mockInfo || !mockInfo.success) { + console.log(`Mock user (${mockUser.username}) not found`) + mockInfo = await createUser(mockUser) + if (!mockInfo.success) { + throw new Error(`Mock user (${mockUser.username}) could not be created`) + } else { + console.log(`Mock user (${mockUser.username}) created`) + } + } else { + console.log(`Mock user (${mockUser.username}) exists`) + } + + // Verify or create channel for tests + let testChannelInfo = await channelInfo({ roomName: testChannelName }) + if (!testChannelInfo || !testChannelInfo.success) { + console.log(`Test channel (${testChannelName}) not found`) + testChannelInfo = await createChannel(testChannelName, [ + apiUser.username, botUser.username, mockUser.username + ]) + if (!testChannelInfo.success) { + throw new Error(`Test channel (${testChannelName}) could not be created`) + } else { + console.log(`Test channel (${testChannelName}) created`) + } + } else { + console.log(`Test channel (${testChannelName}) exists`) + } + + // Verify or create private room for tests + let testPrivateInfo = await privateInfo({ roomName: testPrivateName }) + if (!testPrivateInfo || !testPrivateInfo.success) { + console.log(`Test private room (${testPrivateName}) not found`) + testPrivateInfo = await createPrivate(testPrivateName, [ + apiUser.username, botUser.username, mockUser.username + ]) + if (!testPrivateInfo.success) { + throw new Error(`Test private room (${testPrivateName}) could not be created`) + } else { + console.log(`Test private room (${testPrivateName}) created`) + } + } else { + console.log(`Test private room (${testPrivateName}) exists`) + } + + await logout() + } catch (e) { + throw e + } +} diff --git a/src/utils/users.ts b/src/utils/users.ts new file mode 100644 index 0000000..89999d7 --- /dev/null +++ b/src/utils/users.ts @@ -0,0 +1,32 @@ +// Test script uses standard methods and env config to connect and log streams +import * as api from '../lib/api' +import { silence } from '../lib/log' +silence() + +async function users () { + console.log(` + +Demo of API user query helpers + +ALL users \`api.users.all()\`: +${JSON.stringify(await api.users.all(), null, '\t')} + +ALL usernames \`api.users.allNames()\`: +${JSON.stringify(await api.users.allNames(), null, '\t')} + +ALL IDs \`api.users.allIDs()\`: +${JSON.stringify(await api.users.allIDs(), null, '\t')} + +ONLINE users \`api.users.online()\`: +${JSON.stringify(await api.users.online(), null, '\t')} + +ONLINE usernames \`api.users.onlineNames()\`: +${JSON.stringify(await api.users.onlineNames(), null, '\t')} + +ONLINE IDs \`api.users.onlineIds()\`: +${JSON.stringify(await api.users.onlineIds(), null, '\t')} + + `) +} + +users().catch((e) => console.error(e)) diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..43b774f --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,7 @@ +--require dotenv/config +--require ts-node/register +--require source-map-support/register +--recursive +--timeout 3000 +--reporter list +--exit \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5f0314d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,54 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [ "node_modules/@types" ], /* List of folders to include type definitions from. */ + // "types": [ "node" ], /* Type declaration files to be included in compilation. */ + "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + "inlineSources": true /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "node_modules"] +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..61e219b --- /dev/null +++ b/tslint.json @@ -0,0 +1,14 @@ +{ + "extends": "tslint-config-standard", + "linterOptions": { + "exclude": [ + "**/*.json" + ] + }, + "rules": { + "semicolon": [ + true, + "never" + ] + } +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..dd2c3c7 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,10 @@ +{ + "includeDeclarations": "true", + "excludeExternals": "true", + "mode": "modules", + "module": "commonjs", + "target": "ES6", + "out": "./docs", + "theme": "default", + "exclude": "**/*.spec.ts" +} \ No newline at end of file diff --git a/wallaby.js b/wallaby.js new file mode 100644 index 0000000..f073e2d --- /dev/null +++ b/wallaby.js @@ -0,0 +1,23 @@ +module.exports = function (wallaby) { + return { + name: 'Rocket.Chat.js.SDK', + files: [ + "src/**/*.ts", + { pattern: "src/**/*.spec.ts", ignore: true }, + { pattern: "src/**/*.d.ts", ignore: true }, + ], + tests: ["src/**/*.spec.ts"], + testFramework: 'mocha', + env: { + type: 'node' + }, + compilers: { + '**/*.ts?(x)': wallaby.compilers.typeScript({ module: 'commonjs' }) + }, + debug: true, + slowTestThreshold: 200, + delays: { + run: 1000 + } + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..bd5b2e3 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2653 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@pnpm/exec@^1.1.1": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@pnpm/exec/-/exec-1.1.4.tgz#6ab9a3fcbe9b9d9350ab5a23443571f0077bdf5c" + dependencies: + "@pnpm/self-installer" "^2.0.0" + "@types/got" "^7.1.4" + "@types/node" "^9.3.0" + command-exists "^1.2.2" + cross-spawn "^5.1.0" + +"@pnpm/self-installer@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@pnpm/self-installer/-/self-installer-2.0.2.tgz#85bb8764c7ccd223999f6142190062c626e8f287" + +"@sinonjs/formatio@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-2.0.0.tgz#84db7e9eb5531df18a8c5e0bfb6e449e55e654b2" + dependencies: + samsam "1.3.0" + +"@types/chai@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.2.tgz#f1af664769cfb50af805431c407425ed619daa21" + +"@types/events@*": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02" + +"@types/fs-extra@^4.0.0": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.7.tgz#02533262386b5a6b9a49797dc82feffdf269140a" + dependencies: + "@types/node" "*" + +"@types/fs-extra@^5.0.0": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.0.1.tgz#cd856fbbdd6af2c11f26f8928fd8644c9e9616c9" + dependencies: + "@types/node" "*" + +"@types/glob@*": + version "5.0.35" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/got@^7.1.4": + version "7.1.7" + resolved "https://registry.yarnpkg.com/@types/got/-/got-7.1.7.tgz#fd8053a4e7f3e7d37f2aed6ea24f5f19ec4bda76" + dependencies: + "@types/node" "*" + +"@types/handlebars@^4.0.31": + version "4.0.36" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.36.tgz#ff57c77fa1ab6713bb446534ddc4d979707a3a79" + +"@types/highlight.js@^9.1.8": + version "9.12.2" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.2.tgz#6ee7cd395effe5ec80b515d3ff1699068cd0cd1d" + +"@types/load-json-file@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/load-json-file/-/load-json-file-2.0.7.tgz#c887826f5230b7507d5230994d26315c6776be06" + +"@types/lodash@^4.14.37": + version "4.14.104" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" + +"@types/lru-cache@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.0.tgz#ef69ce9c3ebb46bd146f0d80f0c1ce38b0508eae" + +"@types/marked@0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + +"@types/minimatch@^2.0.29": + version "2.0.29" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" + +"@types/mocha@^2.2.48": + version "2.2.48" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.48.tgz#3523b126a0b049482e1c3c11877460f76622ffab" + +"@types/mz@0.0.32", "@types/mz@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.32.tgz#e8248b4e41424c052edc1725dd33650c313a3659" + dependencies: + "@types/node" "*" + +"@types/node@*", "@types/node@^9.4.6": + version "9.4.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" + +"@types/node@^9.3.0": + version "9.4.7" + resolved "http://registry.npmjs.org/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" + +"@types/shelljs@^0.7.0": + version "0.7.8" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.8.tgz#4b4d6ee7926e58d7bca448a50ba442fd9f6715bd" + dependencies: + "@types/glob" "*" + "@types/node" "*" + +"@types/sinon@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-4.3.0.tgz#7f53915994a00ccea24f4e0c24709822ed11a3b1" + +"@types/write-json-file@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/write-json-file/-/write-json-file-2.2.1.tgz#74155aaccbb0d532be21f9d66bebc4ea875a5a62" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-escapes@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +any-promise@^1.0.0, any-promise@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + +append-transform@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" + dependencies: + default-require-extensions "^1.0.0" + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + +assertion-error@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + +asteroid@rocketchat/asteroid: + version "0.6.1" + resolved "https://codeload.github.com/rocketchat/asteroid/tar.gz/a76a53254e381f9487aa2a9be3d874c9a2df6552" + dependencies: + ddp.js "^0.5.0" + faye-websocket "^0.11.0" + q "^1.0.1" + +async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-generator@^6.18.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.18.0, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.18.0, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978" + +bl@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e" + dependencies: + readable-stream "^2.0.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +buffer@^3.0.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-3.6.0.tgz#a72c936f77b96bf52f5f7e7b467180628551defb" + dependencies: + base64-js "0.0.8" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0, builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +bzip2-maybe@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bzip2-maybe/-/bzip2-maybe-1.0.0.tgz#c9aef7008a6b943cbe99cc617125eb4bd478296b" + dependencies: + is-bzip2 "^1.0.0" + peek-stream "^1.1.1" + pumpify "^1.3.5" + through2 "^2.0.1" + unbzip2-stream "^1.0.9" + +cachedir@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-1.2.0.tgz#e9a0a25bb21a2b7a0f766f07c41eb7a311919b97" + dependencies: + os-homedir "^1.0.1" + +caching-transform@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-1.0.1.tgz#6dbdb2f20f8d8fbce79f3e94e9d1742dcdf5c0a1" + dependencies: + md5-hex "^1.2.0" + mkdirp "^0.5.1" + write-file-atomic "^1.1.4" + +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chai@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.1.2.tgz#0f64584ba642f0f2ace2806279f4f06ca23ad73c" + dependencies: + assertion-error "^1.0.1" + check-error "^1.0.1" + deep-eql "^3.0.0" + get-func-name "^2.0.0" + pathval "^1.0.0" + type-detect "^4.0.0" + +chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + dependencies: + ansi-styles "^3.2.0" + escape-string-regexp "^1.0.5" + supports-color "^5.2.0" + +check-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +ci-info@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" + +cli-cursor@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc" + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +color-convert@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +command-exists@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.2.tgz#12819c64faf95446ec0ae07fe6cafb6eb3708b22" + +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + +commander@^2.12.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + +commitizen@^2.9.6: + version "2.9.6" + resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-2.9.6.tgz#c0d00535ef264da7f63737edfda4228983fa2291" + dependencies: + cachedir "^1.1.0" + chalk "1.1.3" + cz-conventional-changelog "1.2.0" + dedent "0.6.0" + detect-indent "4.0.0" + find-node-modules "1.0.4" + find-root "1.0.0" + fs-extra "^1.0.0" + glob "7.1.1" + inquirer "1.2.3" + lodash "4.17.2" + minimist "1.2.0" + path-exists "2.1.0" + shelljs "0.7.6" + strip-json-comments "2.0.1" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.4.7: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +conventional-commit-types@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/conventional-commit-types/-/conventional-commit-types-2.2.0.tgz#5db95739d6c212acbe7b6f656a11b940baa68946" + +convert-source-map@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +core-js@^2.4.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cross-spawn@^4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1, cross-spawn@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +cz-conventional-changelog@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-1.2.0.tgz#2bca04964c8919b23f3fd6a89ef5e6008b31b3f8" + dependencies: + conventional-commit-types "^2.0.0" + lodash.map "^4.5.1" + longest "^1.0.1" + pad-right "^0.2.2" + right-pad "^1.0.1" + word-wrap "^1.0.3" + +cz-conventional-changelog@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cz-conventional-changelog/-/cz-conventional-changelog-2.1.0.tgz#2f4bc7390e3244e4df293e6ba351e4c740a7c764" + dependencies: + conventional-commit-types "^2.0.0" + lodash.map "^4.5.1" + longest "^1.0.1" + right-pad "^1.0.1" + word-wrap "^1.0.3" + +ddp.js@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/ddp.js/-/ddp.js-0.5.0.tgz#f977ee4207838571203bf781dafb571d0aebd66a" + +debug-log@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" + +debug@3.1.0, debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +decamelize-keys@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.0.0, decamelize@^1.1.0, decamelize@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decompress-maybe@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/decompress-maybe/-/decompress-maybe-1.0.0.tgz#adfe78c66cc069e64e824bd1405b85e75e6d1cbb" + dependencies: + bzip2-maybe "^1.0.0" + gunzip-maybe "^1.3.1" + pumpify "^1.3.5" + +dedent@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.6.0.tgz#0e6da8f0ce52838ef5cec5c8f9396b0c1b64a3cb" + +deep-eql@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + dependencies: + type-detect "^4.0.0" + +default-require-extensions@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" + dependencies: + strip-bom "^2.0.0" + +detect-file@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-0.1.0.tgz#4935dedfd9488648e006b0129566e9386711ea63" + dependencies: + fs-exists-sync "^0.1.0" + +detect-indent@4.0.0, detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" + +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + +diff@^3.1.0, diff@^3.2.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" + +doctrine@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" + dependencies: + esutils "^1.1.6" + isarray "0.0.1" + +dotenv@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-5.0.1.tgz#a5317459bd3d79ab88cff6e44057a6a3fbb1fcef" + +duplexify@^3.5.0, duplexify@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esutils@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +expand-tilde@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-1.2.2.tgz#0b81eba897e5a3d31d1c3d102f8f01441e559449" + dependencies: + os-homedir "^1.0.1" + +extend@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +external-editor@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-1.1.1.tgz#12d7b0db850f7ff7e7081baf4005700060c4600b" + dependencies: + extend "^3.0.0" + spawn-sync "^1.0.15" + tmp "^0.0.29" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +faye-websocket@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + dependencies: + websocket-driver ">=0.5.1" + +figures@^1.3.5: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +find-cache-dir@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9" + dependencies: + commondir "^1.0.1" + mkdirp "^0.5.1" + pkg-dir "^1.0.0" + +find-down@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/find-down/-/find-down-0.1.4.tgz#2365628f38249cb0a93bd9c2c851ad942d1e0eeb" + dependencies: + locate-path "^2.0.0" + next-path "^1.0.0" + +find-node-modules@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/find-node-modules/-/find-node-modules-1.0.4.tgz#b6deb3cccb699c87037677bcede2c5f5862b2550" + dependencies: + findup-sync "0.4.2" + merge "^1.2.0" + +find-root@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.0.0.tgz#962ff211aab25c6520feeeb8d6287f8f6e95807a" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +findup-sync@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.4.2.tgz#a8117d0f73124f5a4546839579fe52d7129fb5e5" + dependencies: + detect-file "^0.1.0" + is-glob "^2.0.1" + micromatch "^2.3.7" + resolve-dir "^0.1.0" + +follow-redirects@>=1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" + dependencies: + debug "^3.1.0" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +foreground-child@^1.5.3, foreground-child@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" + dependencies: + cross-spawn "^4" + signal-exit "^3.0.0" + +fs-exists-sync@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs-extra@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.1.2, glob@^7.0.0, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-modules@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" + dependencies: + global-prefix "^0.1.4" + is-windows "^0.2.0" + +global-prefix@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-0.1.5.tgz#8d3bc6b8da3ca8112a160d8d496ff0462bfef78f" + dependencies: + homedir-polyfill "^1.0.0" + ini "^1.3.4" + is-windows "^0.2.0" + which "^1.2.12" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + +gunzip-maybe@^1.3.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.1.tgz#39c72ed89d1b49ba708e18776500488902a52027" + dependencies: + browserify-zlib "^0.1.4" + is-deflate "^1.0.0" + is-gzip "^1.0.0" + peek-stream "^1.1.0" + pumpify "^1.3.3" + through2 "^2.0.3" + +handlebars@^4.0.3, handlebars@^4.0.6: + version "4.0.11" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" + dependencies: + async "^1.4.0" + optimist "^0.6.1" + source-map "^0.4.4" + optionalDependencies: + uglify-js "^2.6" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + +highlight.js@^9.0.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + +homedir-polyfill@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +http-parser-js@>=0.4.0: + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + +husky@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" + dependencies: + is-ci "^1.0.10" + normalize-path "^1.0.0" + strip-indent "^2.0.0" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +inquirer@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-1.2.3.tgz#4dec6f32f37ef7bb0b2ed3f1d1a5c3f545074918" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^1.0.1" + cli-width "^2.0.0" + external-editor "^1.1.0" + figures "^1.3.5" + lodash "^4.3.0" + mute-stream "0.0.6" + pinkie-promise "^2.0.0" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^1.0.1" + strip-ansi "^3.0.0" + through "^2.3.6" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invariant@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-bzip2@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bzip2/-/is-bzip2-1.0.0.tgz#5ee58eaa5a2e9c80e21407bedf23ae5ac091b3fc" + +is-ci@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" + dependencies: + ci-info "^1.0.0" + +is-deflate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-gzip@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-windows@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" + +is-windows@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz#4113c8ff6b7a40a1ef7350b01016331f63afde14" + +istanbul-lib-hook@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b" + dependencies: + append-transform "^0.4.0" + +istanbul-lib-instrument@^1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz#84905bf47f7e0b401d6b840da7bad67086b4aab6" + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.1.2" + semver "^5.3.0" + +istanbul-lib-report@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz#2df12188c0fa77990c0d2176d2d0ba3394188259" + dependencies: + istanbul-lib-coverage "^1.1.2" + mkdirp "^0.5.1" + path-parse "^1.0.5" + supports-color "^3.1.2" + +istanbul-lib-source-maps@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz#20fb54b14e14b3fb6edb6aca3571fd2143db44e6" + dependencies: + debug "^3.1.0" + istanbul-lib-coverage "^1.1.2" + mkdirp "^0.5.1" + rimraf "^2.6.1" + source-map "^0.5.3" + +istanbul-reports@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.4.tgz#5ccba5e22b7b5a5d91d5e0a830f89be334bf97bd" + dependencies: + handlebars "^4.0.3" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.7.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +json-parse-better-errors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + optionalDependencies: + graceful-fs "^4.1.6" + +just-extend@^1.1.27: + version "1.1.27" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + +lodash.map@^4.5.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" + +lodash@4.17.2: + version "4.17.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" + +lodash@^4.13.1, lodash@^4.17.4, lodash@^4.3.0: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +lolex@^2.2.0, lolex@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.3.2.tgz#85f9450425103bf9e7a60668ea25dc43274ca807" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-dir@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" + dependencies: + pify "^3.0.0" + +make-error@^1.1.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.4.tgz#19978ed575f9e9545d2ff8c13e33b5d18a67d535" + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + +marked@^0.3.5: + version "0.3.15" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.15.tgz#de96982e54c880962f5093a2fa93d0866bf73668" + +md5-hex@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4" + dependencies: + md5-o-matic "^0.1.1" + +md5-o-matic@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +meow@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.0.tgz#fd5855dd008db5b92c552082db1c307cba20b29d" + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist "^1.1.3" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + +merge-source-map@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + dependencies: + source-map "^0.6.1" + +merge@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" + +micromatch@^2.3.11, micromatch@^2.3.7: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist-options@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@1.2.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + +mkdirp-promise@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" + dependencies: + mkdirp "*" + +mkdirp@*, mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.1.tgz#759b62c836b0732382a62b6b1fb245ec1bc943ac" + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + +ms@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +mute-stream@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" + +mz@^2.4.0, mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +next-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-path/-/next-path-1.0.0.tgz#822c4580d7abe783df19965b789622ca801603e4" + +nice-try@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" + +nise@^1.2.0: + version "1.2.6" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.2.6.tgz#42b054981a5c869d6c447be5776cc6f137f00ac5" + dependencies: + "@sinonjs/formatio" "^2.0.0" + just-extend "^1.1.27" + lolex "^2.3.2" + path-to-regexp "^1.7.0" + text-encoding "^0.6.4" + +node-rest-client@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-rest-client/-/node-rest-client-3.1.0.tgz#e0beb6dda7b20cc0b67a7847cf12c5fc419c37c3" + dependencies: + debug "~2.2.0" + follow-redirects ">=1.2.0" + xml2js ">=0.2.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + +normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +nyc@^11.4.1: + version "11.4.1" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-11.4.1.tgz#13fdf7e7ef22d027c61d174758f6978a68f4f5e5" + dependencies: + archy "^1.0.0" + arrify "^1.0.1" + caching-transform "^1.0.0" + convert-source-map "^1.3.0" + debug-log "^1.0.1" + default-require-extensions "^1.0.0" + find-cache-dir "^0.1.1" + find-up "^2.1.0" + foreground-child "^1.5.3" + glob "^7.0.6" + istanbul-lib-coverage "^1.1.1" + istanbul-lib-hook "^1.1.0" + istanbul-lib-instrument "^1.9.1" + istanbul-lib-report "^1.1.2" + istanbul-lib-source-maps "^1.2.2" + istanbul-reports "^1.1.3" + md5-hex "^1.2.0" + merge-source-map "^1.0.2" + micromatch "^2.3.11" + mkdirp "^0.5.0" + resolve-from "^2.0.0" + rimraf "^2.5.4" + signal-exit "^3.0.1" + spawn-wrap "^1.4.2" + test-exclude "^4.1.1" + yargs "^10.0.3" + yargs-parser "^8.0.0" + +object-assign@^4.0.1, object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-shim@^0.1.2: + version "0.1.3" + resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917" + +os-tmpdir@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +package-preview@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/package-preview/-/package-preview-1.0.5.tgz#d6408cd0ae7cbbf7c404e84f22d51955bc6933eb" + dependencies: + "@pnpm/exec" "^1.1.1" + "@types/fs-extra" "^5.0.0" + "@types/load-json-file" "^2.0.7" + "@types/mz" "^0.0.32" + "@types/node" "^9.3.0" + "@types/write-json-file" "^2.2.1" + cross-spawn "^6.0.0" + find-down "^0.1.4" + fs-extra "^5.0.0" + graceful-fs "^4.1.11" + load-json-file "^4.0.0" + meow "^4.0.0" + mz "^2.7.0" + rimraf-then "^1.0.1" + symlink-dir "^1.1.0" + unpack-stream "^3.0.0" + write-json-file "^2.3.0" + +pad-right@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/pad-right/-/pad-right-0.2.2.tgz#6fbc924045d244f2a2a244503060d3bfc6009774" + dependencies: + repeat-string "^1.5.2" + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + +path-exists@2.1.0, path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + dependencies: + isarray "0.0.1" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + +pathval@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + +peek-stream@^1.1.0, peek-stream@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.2.tgz#97eb76365bcfd8c89e287f55c8b69d4c3e9bcc52" + dependencies: + duplexify "^3.5.0" + through2 "^2.0.3" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pkg-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" + dependencies: + find-up "^1.0.0" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +progress@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3, pumpify@^1.3.5: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + +q@^1.0.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +quick-lru@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.5, readable-stream@^2.1.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readable-stream@^2.2.2: + version "2.3.4" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + dependencies: + resolve "^1.1.6" + +redent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +resolve-dir@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-0.1.1.tgz#b219259a5602fac5c5c496ad894a6e8cc430261e" + dependencies: + expand-tilde "^1.2.2" + global-modules "^0.2.3" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + +resolve@^1.1.6, resolve@^1.3.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +right-pad@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/right-pad/-/right-pad-1.0.1.tgz#8ca08c2cbb5b55e74dafa96bf7fd1a27d568c8d0" + +rimraf-then@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rimraf-then/-/rimraf-then-1.0.1.tgz#bd4458a79eb561b7548aaec0ac3753ef429fe70b" + dependencies: + any-promise "^1.3.0" + rimraf "2" + +rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + +rx@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" + +safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +samsam@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" + +sax@>=0.6.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +shelljs@0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shelljs@^0.7.0: + version "0.7.8" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +sinon@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.4.2.tgz#c4c41d4bd346e1d33594daec2d5df0548334fc65" + dependencies: + "@sinonjs/formatio" "^2.0.0" + diff "^3.1.0" + lodash.get "^4.4.2" + lolex "^2.2.0" + nise "^1.2.0" + supports-color "^5.1.0" + type-detect "^4.0.5" + +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + dependencies: + is-plain-obj "^1.0.0" + +source-map-support@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76" + dependencies: + source-map "^0.6.0" + +source-map@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.3, source-map@^0.5.7, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spawn-sync@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476" + dependencies: + concat-stream "^1.4.7" + os-shim "^0.1.2" + +spawn-wrap@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" + dependencies: + foreground-child "^1.5.6" + mkdirp "^0.5.0" + os-homedir "^1.0.1" + rimraf "^2.6.2" + signal-exit "^3.0.2" + which "^1.3.0" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +ssri@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + dependencies: + safe-buffer "^5.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + +strip-json-comments@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.1.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^5.1.0, supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" + dependencies: + has-flag "^3.0.0" + +symlink-dir@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/symlink-dir/-/symlink-dir-1.1.2.tgz#eb367da33401e9c6e95b1ca87efd921cef369852" + dependencies: + "@types/mz" "0.0.32" + "@types/node" "^9.3.0" + graceful-fs "^4.1.11" + is-windows "^1.0.0" + mkdirp-promise "^5.0.0" + mz "^2.4.0" + +tar-fs@^1.14.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.0.tgz#e877a25acbcc51d8c790da1c57c9cf439817b896" + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-stream@^1.1.2: + version "1.5.5" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55" + dependencies: + bl "^1.0.0" + end-of-stream "^1.0.0" + readable-stream "^2.0.0" + xtend "^4.0.0" + +test-exclude@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.0.tgz#07e3613609a362c74516a717515e13322ab45b3c" + dependencies: + arrify "^1.0.1" + micromatch "^2.3.11" + object-assign "^4.1.0" + read-pkg-up "^1.0.1" + require-main-filename "^1.0.1" + +text-encoding@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + dependencies: + any-promise "^1.0.0" + +through2@^2.0.1, through2@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tmp@^0.0.29: + version "0.0.29" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" + dependencies: + os-tmpdir "~1.0.1" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +ts-node@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-5.0.1.tgz#78e5d1cb3f704de1b641e43b76be2d4094f06f81" + dependencies: + arrify "^1.0.0" + chalk "^2.3.0" + diff "^3.1.0" + make-error "^1.1.1" + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map-support "^0.5.3" + yn "^2.0.0" + +tslib@^1.0.0, tslib@^1.8.0, tslib@^1.8.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" + +tslint-config-standard@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tslint-config-standard/-/tslint-config-standard-7.0.0.tgz#47bbf25578ed2212456f892d51e1abe884a29f15" + dependencies: + tslint-eslint-rules "^4.1.1" + +tslint-eslint-rules@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-4.1.1.tgz#7c30e7882f26bc276bff91d2384975c69daf88ba" + dependencies: + doctrine "^0.7.2" + tslib "^1.0.0" + tsutils "^1.4.0" + +tslint@^5.9.1: + version "5.9.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.9.1.tgz#1255f87a3ff57eb0b0e1f0e610a8b4748046c9ae" + dependencies: + babel-code-frame "^6.22.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.7.0" + minimatch "^3.0.4" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.12.1" + +tsutils@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-1.9.1.tgz#b9f9ab44e55af9681831d5f28d0aeeaf5c750cb0" + +tsutils@^2.12.1: + version "2.21.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.21.1.tgz#5b23c263233300ed7442b4217855cbc7547c296a" + dependencies: + tslib "^1.8.1" + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +typedoc-default-themes@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz#6dc2433e78ed8bea8e887a3acde2f31785bd6227" + +typedoc-plugin-external-module-name@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/typedoc-plugin-external-module-name/-/typedoc-plugin-external-module-name-1.1.1.tgz#0ef2d6a760b42c703519c474258b6f062983aa83" + +typedoc@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.8.0.tgz#d7172bc6a29964f451b7609c005beadadefe2361" + dependencies: + "@types/fs-extra" "^4.0.0" + "@types/handlebars" "^4.0.31" + "@types/highlight.js" "^9.1.8" + "@types/lodash" "^4.14.37" + "@types/marked" "0.0.28" + "@types/minimatch" "^2.0.29" + "@types/shelljs" "^0.7.0" + fs-extra "^4.0.0" + handlebars "^4.0.6" + highlight.js "^9.0.0" + lodash "^4.13.1" + marked "^0.3.5" + minimatch "^3.0.0" + progress "^2.0.0" + shelljs "^0.7.0" + typedoc-default-themes "^0.5.0" + typescript "2.4.1" + +typescript@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" + +typescript@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" + +uglify-js@^2.6: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +unbzip2-stream@^1.0.9: + version "1.2.5" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz#73a033a567bbbde59654b193c44d48a7e4f43c47" + dependencies: + buffer "^3.0.1" + through "^2.3.6" + +universalify@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7" + +unpack-stream@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/unpack-stream/-/unpack-stream-3.0.1.tgz#e6e31965eb356e1e6971660cf11e6309645020e1" + dependencies: + "@types/node" "^9.3.0" + decompress-maybe "^1.0.0" + ssri "^5.0.0" + tar-fs "^1.14.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +websocket-driver@>=0.5.1: + version "0.7.0" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + dependencies: + http-parser-js ">=0.4.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@^1.2.12, which@^1.2.9, which@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +word-wrap@^1.0.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +write-file-atomic@^1.1.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + +write-file-atomic@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-json-file@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + pify "^3.0.0" + sort-keys "^2.0.0" + write-file-atomic "^2.0.0" + +xml2js@>=0.2.4: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^8.0.0, yargs-parser@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" + dependencies: + camelcase "^4.1.0" + +yargs@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^8.1.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" + +yn@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"