main
lot 1 year ago
commit 5c3c0e66d4

@ -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"
}
}

8
.gitignore vendored

@ -0,0 +1,8 @@
/node_modules
/yarn-error.log
/.nyc_output
/.env
/shrinkwrap.yaml
/package-lock.json
/docs/
.DS_Store

@ -0,0 +1,20 @@
{
"extension": [
".ts"
],
"require": [
"ts-node/register"
],
"include": [
"src/lib/**/*.ts"
],
"exclude": [
"**/*.d.ts",
"**/*.spec.ts"
],
"reporter": [
"lcovonly",
"text"
],
"all": true
}

@ -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
}
]
}

@ -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
}
}

@ -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.

@ -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 <something>` - 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.

@ -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

@ -0,0 +1,110 @@
/// <reference types="node" />
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<void>;
disconnect: () => Promise<void>;
createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise<any>;
loginWithLDAP: (...params: any[]) => Promise<any>;
loginWithFacebook: (...params: any[]) => Promise<any>;
loginWithGoogle: (...params: any[]) => Promise<any>;
loginWithTwitter: (...params: any[]) => Promise<any>;
loginWithGithub: (...params: any[]) => Promise<any>;
loginWithPassword: (usernameOrEmail: string, password: string) => Promise<any>;
logout: () => Promise<null>;
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<string>;
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<IReady>;
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<any>;
updated: Promise<any>;
}
/**
*
*/
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<any>;
remote: Promise<any>;
}
/**
* 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;
}

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=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<void>,\n disconnect: () => Promise<void>,\n createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise<any>\n loginWithLDAP: (...params: any[]) => Promise<any>\n loginWithFacebook: (...params: any[]) => Promise<any>\n loginWithGoogle: (...params: any[]) => Promise<any>\n loginWithTwitter: (...params: any[]) => Promise<any>\n loginWithGithub: (...params: any[]) => Promise<any>\n loginWithPassword: (usernameOrEmail: string, password: string) => Promise<any>\n logout: () => Promise<null>\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<string>\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<IReady>,\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<any>,\n updated: Promise<any>\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<any>,\n remote: Promise<any>\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"]}

@ -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;
}

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=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"]}

@ -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[];
};
}

@ -0,0 +1,4 @@
"use strict";
/** @todo contribute these to @types/rocketchat and require */
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=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"]}

5
dist/index.d.ts vendored

@ -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 };

18
dist/index.js vendored

@ -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

1
dist/index.js.map vendored

@ -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"]}

87
dist/lib/api.d.ts vendored

@ -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<any>;
/**
* 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<any>;
/**
* 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<ILoginResultAPI>;
/** Logout a user at end of API calls */
export declare function logout(): Promise<void>;
/** 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;

218
dist/lib/api.js vendored

@ -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

File diff suppressed because one or more lines are too long

201
dist/lib/driver.d.ts vendored

@ -0,0 +1,201 @@
/// <reference types="node" />
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 <caption>Use with callback</caption>
* import { driver } from '@rocket.chat/sdk'
* driver.connect({}, (err) => {
* if (err) throw err
* else console.log('connected')
* })
* @example <caption>Using promise</caption>
* 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<IAsteroid>;
/** Remove all active subscriptions, logout and disconnect from Rocket.Chat */
export declare function disconnect(): Promise<void>;
/**
* 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<any>;
/**
* 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<any>;
/**
* 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<any>;
/** Login to Rocket.Chat via Asteroid */
export declare function login(credentials?: ICredentials): Promise<any>;
/** Logout of Rocket.Chat via Asteroid */
export declare function logout(): Promise<void | null>;
/**
* 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<ISubscription>;
/** 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<ISubscription>;
/**
* 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<void | void[]>;
/** Get ID for a room by name (or ID). */
export declare function getRoomId(name: string): Promise<string>;
/** Get name for a room by ID. */
export declare function getRoomName(id: string): Promise<string>;
/**
* 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<string>;
/** Join the bot into a room by its name or ID */
export declare function joinRoom(room: string): Promise<void>;
/** Exit a room the bot has joined */
export declare function leaveRoom(room: string): Promise<void>;
/** Join a set of rooms by array of names or IDs */
export declare function joinRooms(rooms: string[]): Promise<void[]>;
/**
* 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<IMessageReceiptAPI>;
/**
* 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<IMessageReceiptAPI[] | IMessageReceiptAPI>;
/**
* 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<IMessageReceiptAPI[] | IMessageReceiptAPI>;
/**
* 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<IMessageReceiptAPI[] | IMessageReceiptAPI>;
/**
* 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<IMessage>;
/**
* 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<any>;

666
dist/lib/driver.js vendored

@ -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<Asteroid>}
* @throws {Error} Asteroid connection timeout
* @example <caption>Usage with callback</caption>
* import { driver } from '@rocket.chat/sdk'
* driver.connect({}, (err) => {
* if (err) throw err
* else console.log('connected')
* })
* @example <caption>Usage with promise</caption>
* 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<UserID>}
*/
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>} - 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>} - 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<roomId>}
*/
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<roomName>}
*/
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<roomId>}
* @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<message>}
*/
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<messageObject>}
*/
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<messageObject>|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<messageObject>}
*/
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<messageObject>}
*/
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<message>}
*/
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<message>}
*/
function setReaction(emoji, messageId) {
return asyncCall('setReaction', [emoji, messageId]);
}
exports.setReaction = setReaction;
//# sourceMappingURL=driver.js.map

File diff suppressed because one or more lines are too long

5
dist/lib/log.d.ts vendored

@ -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 };

37
dist/lib/log.js vendored

@ -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

@ -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"]}

@ -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;
}

@ -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

@ -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"]}

@ -0,0 +1,46 @@
/// <reference types="lru-cache" />
import LRU from 'lru-cache';
/** @TODO: Remove ! post-fix expression when TypeScript #9619 resolved */
export declare let instance: any;
export declare const results: Map<string, LRU.Cache<string, any>>;
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<string, any> | 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<any>;
/**
* 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<string, any> | 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;

@ -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

@ -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<string, LRU.Cache<string, any>> = 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<string, any> | 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<any> {\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<string, any> | 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"]}

@ -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;

@ -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

@ -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"]}

@ -0,0 +1,4 @@
import { INewUserAPI } from './interfaces';
export declare const apiUser: INewUserAPI;
export declare const botUser: INewUserAPI;
export declare const mockUser: INewUserAPI;

@ -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

@ -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"]}

@ -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;
}

@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=interfaces.js.map

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
export {};

@ -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

@ -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"]}

@ -0,0 +1 @@
export {};

@ -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 <something>" 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

@ -0,0 +1 @@
{"version":3,"file":"start.js","sourceRoot":"","sources":["../../src/utils/start.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,8EAA8E;AAC9E,qCAAkC;AAElC,0BAAgC;AAChC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAA;AAEvF,wEAAwE;AACxE;;QACE,MAAM,UAAM,CAAC,OAAO,EAAE,CAAA;QACtB,MAAM,UAAM,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,gBAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,gBAAO,CAAC,QAAQ,EAAE,CAAC,CAAA;QAC9E,MAAM,UAAM,CAAC,mBAAmB,EAAE,CAAA;QAClC,MAAM,UAAM,CAAC,iBAAiB,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;YACnD,EAAE,CAAC,CAAC,GAAG,CAAC;gBAAC,MAAM,GAAG,CAAA;YAClB,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;YACtE,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;QAC1C,CAAC,EAAE;YACD,KAAK,EAAE,CAAC,SAAS,CAAC;YAClB,SAAS,EAAE,KAAK;YAChB,EAAE,EAAE,IAAI;YACR,MAAM,EAAE,IAAI;YACZ,QAAQ,EAAE,KAAK;SAChB,CAAC,CAAA;IACJ,CAAC;CAAA;AAED,8BAA8B;AAC9B,gFAAgF;AAChF,iEAAiE;AACjE,cAAqB,OAAiB;;QACpC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QACpB,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;YAAC,MAAM,CAAA;QACxB,EAAE,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAA;YACtD,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAAC,MAAM,CAAA;YAC/B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,CAAE,CAAC,QAAQ,UAAU,KAAK,CAAC,CAAC,CAAC,GAAG,CAAA;YAC5D,MAAM,SAAS,GAAG,MAAM,OAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAA;YAC5C,GAAG,CAAC,CAAC,IAAI,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;gBAC/B,EAAE,CAAC,CAAC,QAAQ,KAAK,gBAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;oBAClC,MAAM,OAAO,GAAG,MAAM,UAAM,CAAC,sBAAsB,CAAC,QAAQ,CAAC,CAAA;oBAC7D,MAAM,UAAM,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA,CAAC,YAAY;oBACxD,MAAM,KAAK,CAAC,GAAG,CAAC,CAAA,CAAC,oCAAoC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,CAAC,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,MAAM,OAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAA;YAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,SAAS,CAAC,CAAA;YACnE,MAAM,UAAM,CAAC,YAAY,CAAC,SAAS,GAAG,aAAa,EAAE,OAAO,CAAC,GAAI,CAAC,CAAA;QACpE,CAAC;IACH,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 { 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 <something>\" 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"]}

@ -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<IUserResultAPI>;
/** Create a user and catch the error if they exist already */
export declare function createUser(user: INewUserAPI): Promise<IUserResultAPI>;
/** Get information about a channel */
export declare function channelInfo(query: {
roomName?: string;
roomId?: string;
}): Promise<IChannelResultAPI>;
/** Get information about a private group */
export declare function privateInfo(query: {
roomName?: string;
roomId?: string;
}): Promise<IGroupResultAPI>;
/** Get the last messages sent to a channel (in last 10 minutes) */
export declare function lastMessages(roomId: string, count?: number): Promise<IMessage[]>;
/** Create a room for tests and catch the error if it exists already */
export declare function createChannel(name: string, members?: string[], readOnly?: boolean): Promise<IChannelResultAPI>;
/** Create a private group / room and catch if exists already */
export declare function createPrivate(name: string, members?: string[], readOnly?: boolean): Promise<IGroupResultAPI>;
/** 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<IMessageResultAPI>;
/** Leave user from room, to generate `ul` message (test channel by default) */
export declare function leaveUser(room?: {
id?: string;
name?: string;
}): Promise<Boolean>;
/** Invite user to room, to generate `au` message (test channel by default) */
export declare function inviteUser(room?: {
id?: string;
name?: string;
}): Promise<Boolean>;
/** @todo : Join user into room (enter) to generate `uj` message type. */
/** Update message sent from mock user */
export declare function updateFromUser(payload: IMessageUpdateAPI): Promise<IMessageResultAPI>;
/** Create a direct message session with the mock user */
export declare function setupDirectFromUser(): Promise<IRoomResultAPI>;
/** Initialise testing instance with the required users for SDK/bot tests */
export declare function setup(): Promise<void>;

@ -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

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
export {};

@ -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

@ -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"]}

@ -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"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@ -0,0 +1,79 @@
{
"name": "@rocket.chat/sdk",
"version": "0.2.9-1",
"description": "Node.js SDK for Rocket.Chat. Application interface for server methods and message streams.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": "https://github.com/RocketChat/Rocket.Chat.js.SDK.git",
"author": "Tim Kinnane <tim.kinnane@rocket.chat>",
"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"
}
}
}

@ -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<void>,
disconnect: () => Promise<void>,
createUser: (usernameOrEmail: string, password: string, profile: IUserOptions) => Promise<any>
loginWithLDAP: (...params: any[]) => Promise<any>
loginWithFacebook: (...params: any[]) => Promise<any>
loginWithGoogle: (...params: any[]) => Promise<any>
loginWithTwitter: (...params: any[]) => Promise<any>
loginWithGithub: (...params: any[]) => Promise<any>
loginWithPassword: (usernameOrEmail: string, password: string) => Promise<any>
logout: () => Promise<null>
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<string>
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<IReady>,
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<any>,
updated: Promise<any>
}
/**
*
*/
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<any>,
remote: Promise<any>
}
/**
* 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
}

@ -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
}

@ -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]
}

@ -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'
])
})
})

@ -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
}

@ -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'
})
})
})
})

@ -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<any> {
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<any> {
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<ILoginResultAPI> {
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))
}

@ -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])
})
})
})

@ -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 <caption>Use with callback</caption>
* import { driver } from '@rocket.chat/sdk'
* driver.connect({}, (err) => {
* if (err) throw err
* else console.log('connected')
* })
* @example <caption>Using promise</caption>
* 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<IAsteroid> {
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<void> {
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<any> {
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<any> {
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<any> {
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<any> {
let login: Promise<any>
// 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<void | null> {
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<ISubscription> {
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<ISubscription> {
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<void | void[]> {
const config = Object.assign({}, settings, options)
// return value, may be replaced by async ops
let promise: Promise<void | void[]> = 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<string> {
return cacheCall('getRoomIdByNameOrId', name)
}
/** Get name for a room by ID. */
export function getRoomName (id: string): Promise<string> {
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<string> {
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<void> {
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<void> {
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<void[]> {
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<IMessageReceiptAPI> {
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<IMessageReceiptAPI[] | IMessageReceiptAPI> {
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<IMessageReceiptAPI[] | IMessageReceiptAPI> {
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<IMessageReceiptAPI[] | IMessageReceiptAPI> {
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<IMessage> {
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])
}

@ -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
}

@ -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)
})
})
})

@ -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
}
}

@ -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)
})
})
})

@ -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<string, LRU.Cache<string, any>> = 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<string, any> | 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<any> {
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<string, any> | 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())
}

@ -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'])
})
})

@ -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)

@ -0,0 +1 @@
declare module 'asteroid'

@ -0,0 +1 @@
declare module 'node-rest-client'

@ -0,0 +1 @@
declare module 'asteroid-immutable-collections-mixin'

BIN
src/utils/.DS_Store vendored

Binary file not shown.

@ -0,0 +1,35 @@
import { INewUserAPI } from './interfaces'
// The API user, should be provisioned on build with local Rocket.Chat
export const apiUser: INewUserAPI = {
username: process.env.ADMIN_USERNAME || 'admin',
password: process.env.ADMIN_PASS || 'pass'
}
// The Bot user, will attempt to login and run methods in tests
export const botUser: INewUserAPI = {
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
export const mockUser: INewUserAPI = {
email: 'mock@localhost',
name: 'Mock User',
password: 'mock',
username: 'mock',
active: true,
roles: ['user'],
joinDefaultChannels: true,
requirePasswordChange: false,
sendWelcomeEmail: false,
verified: true
}

@ -0,0 +1,168 @@
/** Payload structure for `chat.postMessage` endpoint */
export interface IMessageAPI {
roomId: string // The room id of where the message is to be sent
channel?: string // The channel name with the prefix in front of it
text?: string // The text of the message to send, is optional because of attachments
alias?: string // This will cause the messenger name to appear as the given alias, but username will still display
emoji?: string // If provided, this will make the avatar on this message be an emoji
avatar?: string // If provided, this will make the avatar use the provided image url
attachments?: IAttachmentAPI[] // See attachment interface below
}
/** Payload structure for `chat.update` endpoint */
export interface IMessageUpdateAPI {
roomId: string // The room id of where the message is
msgId: string // The message id to update
text: string // Updated text for the message
}
/** Message receipt returned after send (not the same as sent object) */
export interface IMessageReceiptAPI {
_id: string // ID of sent message
rid: string // Room ID of sent message
alias: string // ?
msg: string // Content of message
parseUrls: boolean // URL parsing enabled on message hooks
groupable: boolean // Grouping message enabled
ts: string // Timestamp of message creation
u: { // User details of sender
_id: string
username: string
}
_updatedAt: string // Time message last updated
editedAt?: string // Time updated by edit
editedBy?: { // User details for the updater
_id: string
username: string
}
}
/** Payload structure for message attachments */
export interface IAttachmentAPI {
color?: string // The color you want the order on the left side to be, any value background-css supports
text?: string // The text to display for this attachment, it is different than the message text
ts?: string // ISO timestamp, displays the time next to the text portion
thumb_url?: string // An image that displays to the left of the text, looks better when this is relatively small
message_link?: string // Only applicable if the ts is provided, as it makes the time clickable to this link
collapsed?: boolean // Causes the image, audio, and video sections to be hiding when collapsed is true
author_name?: string // Name of the author
author_link?: string // Providing this makes the author name clickable and points to this link
author_icon?: string // Displays a tiny icon to the left of the author's name
title?: string // Title to display for this attachment, displays under the author
title_link?: string // Providing this makes the title clickable, pointing to this link
title_link_download_true?: string // When this is true, a download icon appears and clicking this saves the link to file
image_url?: string // The image to display, will be “big” and easy to see
audio_url?: string // Audio file to play, only supports what html audio does
video_url?: string // Video file to play, only supports what html video does
fields?: IAttachmentFieldAPI[] // An array of Attachment Field Objects
}
/**
* 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 // Whether this field should be a short field
title: string // The title of this field
value: string // The value of this field, displayed underneath the title value
}
/** Result structure for message endpoints */
export interface IMessageResultAPI {
ts: number // Seconds since unix epoch
channel: string // Name of channel without prefix
message: IMessageReceiptAPI // Sent message
success: boolean // Send status
}
/** User object structure for creation endpoints */
export interface INewUserAPI {
email?: string // Email address
name?: string // Full name
password: string // User pass
username: string // Username
active?: true // Subscription is active
roles?: string[] // Role IDs
joinDefaultChannels?: boolean // Auto join channels marked as default
requirePasswordChange?: boolean // Direct to password form on next login
sendWelcomeEmail?: boolean // Send new credentials in email
verified?: true // Email address verification status
}
/** User object structure for queries (not including admin access level) */
export interface IUserAPI {
_id: string // MongoDB user doc ID
type: string // user / bot ?
status: string // online | offline
active: boolean // Subscription is active
name: string // Full name
utcOffset: number // Hours off UTC/GMT
username: string // Username
}
/** Result structure for user data request (by non-admin) */
export interface IUserResultAPI {
user: IUserAPI // The requested user
success: boolean // Status of request
}
/** Room object structure */
export interface IRoomAPI {
_id: string // Room ID
_updatedAt: string // ISO timestamp
t: 'c' | 'p' | 'd' | 'l' // Room type (channel, private, direct, livechat)
msgs: number // Count of messages in room
ts: string // ISO timestamp (current time in room?)
meta: {
revision: number // ??
created: number // Unix ms>epoch 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
}

@ -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))

@ -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 <something>" 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))

@ -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<IUserResultAPI> {
return get('users.info', { username }, true)
}
/** Create a user and catch the error if they exist already */
export async function createUser (user: INewUserAPI): Promise<IUserResultAPI> {
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<IChannelResultAPI> {
return get('channels.info', query, true)
}
/** Get information about a private group */
export async function privateInfo (query: { roomName?: string, roomId?: string }): Promise<IGroupResultAPI> {
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<IMessage[]> {
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<IChannelResultAPI> {
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<IGroupResultAPI> {
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<IMessageResultAPI> {
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<Boolean> {
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<Boolean> {
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<IMessageResultAPI> {
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<IRoomResultAPI> {
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
}
}

@ -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))

@ -0,0 +1,7 @@
--require dotenv/config
--require ts-node/register
--require source-map-support/register
--recursive
--timeout 3000
--reporter list
--exit

@ -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"]
}

@ -0,0 +1,14 @@
{
"extends": "tslint-config-standard",
"linterOptions": {
"exclude": [
"**/*.json"
]
},
"rules": {
"semicolon": [
true,
"never"
]
}
}

@ -0,0 +1,10 @@
{
"includeDeclarations": "true",
"excludeExternals": "true",
"mode": "modules",
"module": "commonjs",
"target": "ES6",
"out": "./docs",
"theme": "default",
"exclude": "**/*.spec.ts"
}

@ -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
}
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save