├── it ├── mocha.opts └── pingpong.js ├── .npmignore ├── test ├── mocha.opts ├── resources │ ├── publicKey.pem │ └── privateKey.pem ├── fakes.js ├── adapter-test.js ├── nock-server.js └── symphony-test.js ├── whitesource.config.json ├── .babelrc ├── NOTICE ├── .gitignore ├── .codeclimate.yml ├── .flowconfig ├── .eslintrc ├── flow-typed └── npm │ ├── hubot_vx.x.x.js │ ├── uuid_v3.x.x.js │ ├── log_vx.x.x.js │ ├── html-entities_vx.x.x.js │ ├── yargs_vx.x.x.js │ ├── request_vx.x.x.js │ ├── backoff_vx.x.x.js │ ├── nock_v10.x.x.js │ ├── mocha_v6.x.x.js │ ├── memoizee_vx.x.x.js │ └── chai_v4.x.x.js ├── .travis.yml ├── src ├── message.js ├── diagnostic.js ├── adapter.js └── symphony.js ├── package.json ├── .github └── CONTRIBUTING.md ├── README.md └── LICENSE.txt /it/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-register 2 | --recursive 3 | --timeout 10000 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .codeclimate.yml 2 | .eslintrc 3 | .flowconfig 4 | .travis.yml 5 | it/* 6 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require coffee-script/register 2 | --require babel-register 3 | --recursive -------------------------------------------------------------------------------- /whitesource.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "checkPolicies":true, 3 | "productName":"Hubot Symphony", 4 | "projectName":"Hubot Symphony", 5 | "devDep": false 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "es2016" 5 | ], 6 | "plugins": [ 7 | "transform-class-properties", 8 | "transform-flow-strip-types" 9 | ] 10 | } -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | hubot-symphony - FINOS 2 | Copyright 2016-2019 Jon Freedman 3 | 4 | This product includes software developed at 5 | The Symphony Software Foundation (http://symphony.foundation). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | npm-debug.log 4 | cov-int 5 | .nyc_output 6 | coverage 7 | lib 8 | hubot-symphony.iml 9 | certs 10 | **/*-compiled.js 11 | **/*-compiled.js.map 12 | 13 | ws-log* 14 | ws-ls* -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | fixme: 5 | enabled: true 6 | ratings: 7 | paths: 8 | - it/** 9 | - src/** 10 | - test/** 11 | exclude_paths: 12 | - flow-typed/** -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/config-chain/.* 3 | .*/node_modules/findup/.* 4 | .*/node_modules/nock/.* 5 | .*/node_modules/npmconf/.* 6 | 7 | [include] 8 | 9 | [libs] 10 | flow-typed 11 | 12 | [options] 13 | module.use_strict=true -------------------------------------------------------------------------------- /test/resources/publicKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0NarCFmJxLhNdtmgR/G6FpeNo 3 | J2fbAiIgS20M1xu0Rwg+QLdBaOopigewF94tcaSgLb4VDCchdySsHolPareyy1mz 4 | zsfyk2Zi6/TigptGyB8j015hnH+D/U8KgxUAzQMaLXDhYUSyYrVHqpbaaYysqYEx 5 | qg0SeHN09o6afk8KDQIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": [ 8 | "eslint:recommended", 9 | "google", 10 | "plugin:flowtype/recommended", 11 | "plugin:mocha/recommended" 12 | ], 13 | "plugins": [ 14 | "flowtype", 15 | "mocha" 16 | ], 17 | "rules": { 18 | "max-len": [ 19 | 2, 20 | 120, 21 | 2 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /flow-typed/npm/hubot_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1c04fe8c2cff5d0721ea258b4e3731eb 2 | // flow-typed version: <>/hubot_v2.19.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'hubot' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'hubot' { 17 | declare module.exports: any; 18 | } 19 | -------------------------------------------------------------------------------- /flow-typed/npm/uuid_v3.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: c07f382c8238bb78e545b60dd4f097a6 2 | // flow-typed version: 27f92307d3/uuid_v3.x.x/flow_>=v0.33.x 3 | 4 | declare module 'uuid' { 5 | declare function v1(options?: {| 6 | node?: number[], 7 | clockseq?: number, 8 | msecs?: number | Date, 9 | nsecs?: number, 10 | |}, buffer?: number[] | Buffer, offset?: number): string; 11 | declare function v4(options?: {| 12 | random?: number[], 13 | rng?: () => number[] | Buffer, 14 | |}, buffer?: number[] | Buffer, offset?: number): string; 15 | } 16 | -------------------------------------------------------------------------------- /test/resources/privateKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC0NarCFmJxLhNdtmgR/G6FpeNoJ2fbAiIgS20M1xu0Rwg+QLdB 3 | aOopigewF94tcaSgLb4VDCchdySsHolPareyy1mzzsfyk2Zi6/TigptGyB8j015h 4 | nH+D/U8KgxUAzQMaLXDhYUSyYrVHqpbaaYysqYExqg0SeHN09o6afk8KDQIDAQAB 5 | AoGACTvOug8nkCEKQPz9rB5BE3wCgO2z9pbPZNQ4jDXhZ4VUOMxcF2/mv6Yg6rbu 6 | XWm7Q7HUUYPD5YUTTfZqlrUjIGIjnLGXbFmj8wM+INcvGQ0Rx9YON1Xkn/GAoroy 7 | zCCUrg9sLFLM7NWjYHhZCCGcen2R9xMRixzOxedjIrygd4ECQQDZ4DvBPlUBVHx8 8 | lv7SBMaOoDEiphmLkuDNb8Sx518TrxNVOo3uNvaTjHxHME/xOA8FuwSHYPWrqsoN 9 | FBjyb0VdAkEA074rkqLuJaPECbvOPmQq0+LmgUwSsVPfjlxIMWyT5XvF4kHu5Xb3 10 | iQGZ2yD6LsrtEuQqAdgCs1RuEEeSLxRccQJAGsnhTv9VAFbc/4ypRDVmHH9By1rU 11 | 5T5n+Zp2etFR9V+fZulOLi3/32B0n2QnUCduYWv/QI4BZtwW/8iq0JQx2QJBAICM 12 | JFHanm+1c29hV/2ivCl0x/HZKEQFomP//EgdHdClCuaolosyZWcE1M4mwBwmUDU2 13 | 1ZXW+RS7/jHd8Y6pctECQEL2ZNMRHkSOMIkGTNy2s3oFqLSPO6sUKuQSfCsvct1Y 14 | VWkzvw2CY+fBhLcC0dfI+AquAgP8tM8zMTEtWcBoauY= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | 5 | env: 6 | # setup for integration tests & turn off any internal logging in tests 7 | - SYMPHONY_HOST=foundation-dev.symphony.com SYMPHONY_KM_HOST=foundation-dev-api.symphony.com SYMPHONY_SESSIONAUTH_HOST=foundation-dev-api.symphony.com SYMPHONY_AGENT_HOST=foundation-dev-api.symphony.com HUBOT_SYMPHONY_LOG_LEVEL=alert 8 | 9 | before_script: 10 | # fetch certificates for integration tests against foundation-dev pod 11 | - "if [[ $TRAVIS_PULL_REQUEST -eq 'false' ]]; then curl -s https://raw.githubusercontent.com/symphonyoss/contrib-toolbox/master/scripts/download-files.sh | bash; fi;" 12 | 13 | script: 14 | # run flow typing 15 | - npm run flow 16 | # execute tests with coverage instrumentation 17 | - npm run test-cov 18 | # execute integration tests against foundation-dev pod 19 | - "if [[ $TRAVIS_PULL_REQUEST -eq 'false' ]]; then npm run-script it; fi;" 20 | - npm run build 21 | # Cannot run on external PRs due to https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions 22 | - "if [[ $TRAVIS_PULL_REQUEST -eq 'false' ]]; then npm install ; npm run whitesource; fi;" 23 | # Break the build, if any Whitesource policy violation is found 24 | - "if [[ -e 'ws-log-policy-violations.json' ]]; then echo 'Found Whitesource Policy violation, build failed.' ; exit -1; fi;" 25 | 26 | after_success: 27 | # publish coverage results to coveralls 28 | - 'cat ./coverage/lcov.info | ./node_modules/.bin/coveralls' 29 | # publish coverage results to codeclimate 30 | - 'cat ./coverage/lcov.info | ./node_modules/.bin/codeclimate-test-reporter' 31 | # deploy to npm if required 32 | - npx semantic-release 33 | -------------------------------------------------------------------------------- /flow-typed/npm/log_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: d23dad0eca28a2b7066768d35f06459f 2 | // flow-typed version: <>/log_v1.4.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'log' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'log' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'log/examples/file' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'log/examples/reader' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'log/examples/stdout' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'log/lib/log' { 38 | declare module.exports: any; 39 | } 40 | 41 | // Filename aliases 42 | declare module 'log/examples/file.js' { 43 | declare module.exports: $Exports<'log/examples/file'>; 44 | } 45 | declare module 'log/examples/reader.js' { 46 | declare module.exports: $Exports<'log/examples/reader'>; 47 | } 48 | declare module 'log/examples/stdout.js' { 49 | declare module.exports: $Exports<'log/examples/stdout'>; 50 | } 51 | declare module 'log/index' { 52 | declare module.exports: $Exports<'log'>; 53 | } 54 | declare module 'log/index.js' { 55 | declare module.exports: $Exports<'log'>; 56 | } 57 | declare module 'log/lib/log.js' { 58 | declare module.exports: $Exports<'log/lib/log'>; 59 | } 60 | -------------------------------------------------------------------------------- /flow-typed/npm/html-entities_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: b21f11712600936274d0dff4d875da0b 2 | // flow-typed version: <>/html-entities_v1.2.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'html-entities' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'html-entities' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'html-entities/lib/html4-entities' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'html-entities/lib/html5-entities' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'html-entities/lib/xml-entities' { 34 | declare module.exports: any; 35 | } 36 | 37 | // Filename aliases 38 | declare module 'html-entities/index' { 39 | declare module.exports: $Exports<'html-entities'>; 40 | } 41 | declare module 'html-entities/index.js' { 42 | declare module.exports: $Exports<'html-entities'>; 43 | } 44 | declare module 'html-entities/lib/html4-entities.js' { 45 | declare module.exports: $Exports<'html-entities/lib/html4-entities'>; 46 | } 47 | declare module 'html-entities/lib/html5-entities.js' { 48 | declare module.exports: $Exports<'html-entities/lib/html5-entities'>; 49 | } 50 | declare module 'html-entities/lib/xml-entities.js' { 51 | declare module.exports: $Exports<'html-entities/lib/xml-entities'>; 52 | } 53 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | import {TextMessage} from 'hubot'; 20 | import type {SymphonyMessageV2Type} from './symphony'; 21 | 22 | /** 23 | * Represents a V2Message received from Symphony for use within Hubot 24 | */ 25 | export class V2Message extends TextMessage { 26 | symphonyMessage: SymphonyMessageV2Type; 27 | room: string; 28 | 29 | /** 30 | * @param {Object} user Hubot user 31 | * @param {SymphonyMessageV2Type} symphonyMessage Message from Symphony 32 | * @constructor 33 | */ 34 | constructor(user: Object, symphonyMessage: SymphonyMessageV2Type) { 35 | super(user, V2Message._getMessageText(symphonyMessage), symphonyMessage.id); 36 | this.symphonyMessage = symphonyMessage; 37 | this.room = symphonyMessage.streamId; 38 | } 39 | 40 | /** 41 | * @param {SymphonyMessageV2Type} symphonyMessage Message from Symphony 42 | * @return {string} message text contained within messageML tag 43 | * @private 44 | */ 45 | static _getMessageText(symphonyMessage: SymphonyMessageV2Type): string { 46 | const match = /(.*)<\/messageML>/i.exec(symphonyMessage.message); 47 | if (match === undefined || match === null) { 48 | return symphonyMessage.message; 49 | } 50 | return match[1]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-symphony", 3 | "description": "Hubot adapter for Symphony", 4 | "main": "lib/adapter.js", 5 | "scripts": { 6 | "precommit": "npm test", 7 | "prepush": "npm test", 8 | "build": "babel src -d lib", 9 | "commitmsg": "validate-commit-msg", 10 | "diagnostic": "babel-node src/diagnostic.js", 11 | "flow": "flow", 12 | "eslint": "eslint src/*.js test/*.js it/*.js", 13 | "test": "mocha test/*.js", 14 | "test-cov": "nyc mocha test/*.js", 15 | "it": "mocha it/*.js", 16 | "whitesource": "node node_modules/whitesource/bin/whitesource.js run" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/symphonyoss/hubot-symphony.git" 21 | }, 22 | "keywords": [ 23 | "symphonyoss", 24 | "hubot", 25 | "adapter", 26 | "symphony" 27 | ], 28 | "author": "Jon Freedman", 29 | "license": "Apache-2.0", 30 | "bugs": { 31 | "url": "https://github.com/symphonyoss/hubot-symphony/issues" 32 | }, 33 | "homepage": "https://github.com/symphonyoss/hubot-symphony", 34 | "dependencies": { 35 | "backoff": "2.5.0", 36 | "html-entities": "1.2.1", 37 | "log": "1.4.0", 38 | "memoizee": "0.4.14", 39 | "request": "2.88.2" 40 | }, 41 | "devDependencies": { 42 | "babel-cli": "6.26.0", 43 | "babel-eslint": "10.1.0", 44 | "babel-plugin-transform-class-properties": "6.24.1", 45 | "babel-plugin-transform-flow-strip-types": "6.22.0", 46 | "babel-preset-env": "1.7.0", 47 | "babel-preset-es2016": "6.24.1", 48 | "babel-register": "6.26.0", 49 | "busboy": "0.3.1", 50 | "chai": "4.2.0", 51 | "codeclimate-test-reporter": "0.5.1", 52 | "coffee-script": "1.12.7", 53 | "coveralls": "3.1.0", 54 | "cross-env": "7.0.2", 55 | "cz-conventional-changelog": "3.3.0", 56 | "eslint": "7.12.1", 57 | "eslint-config-canonical": "24.4.4", 58 | "eslint-config-google": "0.14.0", 59 | "eslint-plugin-flowtype": "5.2.0", 60 | "eslint-plugin-mocha": "8.0.0", 61 | "flow-bin": "0.131.0", 62 | "ghooks": "2.0.4", 63 | "hubot": "2.19.0", 64 | "husky": "4.3.0", 65 | "mocha": "7.2.0", 66 | "nock": "9.0.22", 67 | "nyc": "15.1.0", 68 | "semantic-release": "17.2.3", 69 | "uuid": "3.3.3", 70 | "validate-commit-msg": "2.14.0", 71 | "yargs": "16.1.1", 72 | "whitesource": "20.9.1" 73 | }, 74 | "config": { 75 | "commitizen": { 76 | "path": "./node_modules/cz-conventional-changelog" 77 | }, 78 | "ghooks": { 79 | "commit-msg": "validate-commit-msg" 80 | }, 81 | "validate-commit-msg": { 82 | "helpMessage": "This repo is Commitizen friendly, commit using git cz or see https://github.com/stevemao/conventional-changelog-angular/blob/master/convention.md" 83 | } 84 | }, 85 | "nyc": { 86 | "include": [ 87 | "src/*.js" 88 | ], 89 | "exclude": [ 90 | "**/*-compiled.js", 91 | "**/*-compiled.js.map" 92 | ], 93 | "require": [ 94 | "babel-register" 95 | ], 96 | "reporter": [ 97 | "lcov", 98 | "text" 99 | ], 100 | "sourceMap": true, 101 | "instrument": true 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /flow-typed/npm/yargs_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e912a6c7b078ec14f9a54969f4ad3f14 2 | // flow-typed version: <>/yargs_v7.0.2/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'yargs' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'yargs' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'yargs/lib/apply-extends' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'yargs/lib/argsert' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'yargs/lib/assign' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'yargs/lib/command' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'yargs/lib/completion' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'yargs/lib/levenshtein' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'yargs/lib/obj-filter' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'yargs/lib/usage' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'yargs/lib/validation' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'yargs/lib/yerror' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'yargs/yargs' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'yargs/index' { 71 | declare module.exports: $Exports<'yargs'>; 72 | } 73 | declare module 'yargs/index.js' { 74 | declare module.exports: $Exports<'yargs'>; 75 | } 76 | declare module 'yargs/lib/apply-extends.js' { 77 | declare module.exports: $Exports<'yargs/lib/apply-extends'>; 78 | } 79 | declare module 'yargs/lib/argsert.js' { 80 | declare module.exports: $Exports<'yargs/lib/argsert'>; 81 | } 82 | declare module 'yargs/lib/assign.js' { 83 | declare module.exports: $Exports<'yargs/lib/assign'>; 84 | } 85 | declare module 'yargs/lib/command.js' { 86 | declare module.exports: $Exports<'yargs/lib/command'>; 87 | } 88 | declare module 'yargs/lib/completion.js' { 89 | declare module.exports: $Exports<'yargs/lib/completion'>; 90 | } 91 | declare module 'yargs/lib/levenshtein.js' { 92 | declare module.exports: $Exports<'yargs/lib/levenshtein'>; 93 | } 94 | declare module 'yargs/lib/obj-filter.js' { 95 | declare module.exports: $Exports<'yargs/lib/obj-filter'>; 96 | } 97 | declare module 'yargs/lib/usage.js' { 98 | declare module.exports: $Exports<'yargs/lib/usage'>; 99 | } 100 | declare module 'yargs/lib/validation.js' { 101 | declare module.exports: $Exports<'yargs/lib/validation'>; 102 | } 103 | declare module 'yargs/lib/yerror.js' { 104 | declare module.exports: $Exports<'yargs/lib/yerror'>; 105 | } 106 | declare module 'yargs/yargs.js' { 107 | declare module.exports: $Exports<'yargs/yargs'>; 108 | } 109 | -------------------------------------------------------------------------------- /flow-typed/npm/request_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: e2881decbea569d5d97a1d3094326607 2 | // flow-typed version: <>/request_v2.81.0npm/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'request' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'request' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'request/lib/auth' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'request/lib/cookies' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'request/lib/getProxyFromURI' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'request/lib/har' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'request/lib/helpers' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'request/lib/multipart' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'request/lib/oauth' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'request/lib/querystring' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'request/lib/redirect' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'request/lib/tunnel' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'request/request' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'request/index' { 71 | declare module.exports: $Exports<'request'>; 72 | } 73 | declare module 'request/index.js' { 74 | declare module.exports: $Exports<'request'>; 75 | } 76 | declare module 'request/lib/auth.js' { 77 | declare module.exports: $Exports<'request/lib/auth'>; 78 | } 79 | declare module 'request/lib/cookies.js' { 80 | declare module.exports: $Exports<'request/lib/cookies'>; 81 | } 82 | declare module 'request/lib/getProxyFromURI.js' { 83 | declare module.exports: $Exports<'request/lib/getProxyFromURI'>; 84 | } 85 | declare module 'request/lib/har.js' { 86 | declare module.exports: $Exports<'request/lib/har'>; 87 | } 88 | declare module 'request/lib/helpers.js' { 89 | declare module.exports: $Exports<'request/lib/helpers'>; 90 | } 91 | declare module 'request/lib/multipart.js' { 92 | declare module.exports: $Exports<'request/lib/multipart'>; 93 | } 94 | declare module 'request/lib/oauth.js' { 95 | declare module.exports: $Exports<'request/lib/oauth'>; 96 | } 97 | declare module 'request/lib/querystring.js' { 98 | declare module.exports: $Exports<'request/lib/querystring'>; 99 | } 100 | declare module 'request/lib/redirect.js' { 101 | declare module.exports: $Exports<'request/lib/redirect'>; 102 | } 103 | declare module 'request/lib/tunnel.js' { 104 | declare module.exports: $Exports<'request/lib/tunnel'>; 105 | } 106 | declare module 'request/request.js' { 107 | declare module.exports: $Exports<'request/request'>; 108 | } 109 | -------------------------------------------------------------------------------- /test/fakes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | /* eslint-disable require-jsdoc */ 19 | 20 | import {Response, User} from 'hubot'; 21 | import EventEmitter from 'events'; 22 | import {V2Message} from '../src/message'; 23 | import Log from 'log'; 24 | 25 | type LoggerType = { 26 | error: (string) => void, 27 | info: (string) => void, 28 | debug: (string) => void 29 | }; 30 | 31 | type BrainType = { 32 | userForId: (string, UserPropertiesType) => User 33 | }; 34 | 35 | type UserPropertiesType = { 36 | room: string 37 | }; 38 | 39 | const logger: Log = new Log(process.env.HUBOT_SYMPHONY_LOG_LEVEL || process.env.HUBOT_LOG_LEVEL || 'info'); 40 | 41 | class FakeRobot extends EventEmitter { 42 | logs: Map>; 43 | logger: LoggerType; 44 | users: Map; 45 | brain: BrainType; 46 | received: Array; 47 | Response: Response; 48 | 49 | constructor() { 50 | super(); 51 | 52 | // echo any errors 53 | this.on('error', function(err: Error) { 54 | logger.error(err); 55 | }); 56 | 57 | // required to allow nested functions to access robot state 58 | let self = this; 59 | 60 | // no-op the logging 61 | this.logs = new Map(); 62 | this.logger = { 63 | error: function(message: string) { 64 | self._log('error', message); 65 | }, 66 | info: function(message: string) { 67 | self._log('info', message); 68 | }, 69 | debug: function(message: string) { 70 | self._log('debug', message); 71 | }, 72 | }; 73 | 74 | // save user details in brain 75 | this.users = new Map(); 76 | this.brain = { 77 | userForId: function(id: string, options: UserPropertiesType): User { 78 | let user = self.users.get(id); 79 | if (user === undefined) { 80 | logger.debug(`Creating userId ${id} = ${JSON.stringify(options)}`); 81 | user = new User(id, options); 82 | self.users.set(id, user); 83 | } 84 | if (options && options.room && (!user.room || user.room !== options.room)) { 85 | logger.debug(`Updating userId ${id} = ${JSON.stringify(options)}`); 86 | user = new User(id, options); 87 | self.users.set(id, user); 88 | } 89 | return user; 90 | }, 91 | }; 92 | 93 | // record all received messages 94 | this.received = []; 95 | 96 | this.Response = Response; 97 | } 98 | 99 | _log(level: string, message: string) { 100 | let messages = this.logs.get(level); 101 | if (messages === undefined) { 102 | messages = []; 103 | this.logs.set(level, messages); 104 | } 105 | messages.push(message); 106 | logger[level](message); 107 | } 108 | 109 | receive(msg: V2Message) { 110 | this.received.push(msg); 111 | super.emit('received'); 112 | } 113 | } 114 | 115 | module.exports = FakeRobot; 116 | -------------------------------------------------------------------------------- /flow-typed/npm/backoff_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 35dbd6e51e31b9e20b35d103feccb37c 2 | // flow-typed version: <>/backoff_v2.5.0/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'backoff' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'backoff' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'backoff/lib/backoff' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'backoff/lib/function_call' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'backoff/lib/strategy/exponential' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'backoff/lib/strategy/fibonacci' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'backoff/lib/strategy/strategy' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'backoff/tests/api' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'backoff/tests/backoff_strategy' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'backoff/tests/backoff' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'backoff/tests/exponential_backoff_strategy' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'backoff/tests/fibonacci_backoff_strategy' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'backoff/tests/function_call' { 66 | declare module.exports: any; 67 | } 68 | 69 | // Filename aliases 70 | declare module 'backoff/index' { 71 | declare module.exports: $Exports<'backoff'>; 72 | } 73 | declare module 'backoff/index.js' { 74 | declare module.exports: $Exports<'backoff'>; 75 | } 76 | declare module 'backoff/lib/backoff.js' { 77 | declare module.exports: $Exports<'backoff/lib/backoff'>; 78 | } 79 | declare module 'backoff/lib/function_call.js' { 80 | declare module.exports: $Exports<'backoff/lib/function_call'>; 81 | } 82 | declare module 'backoff/lib/strategy/exponential.js' { 83 | declare module.exports: $Exports<'backoff/lib/strategy/exponential'>; 84 | } 85 | declare module 'backoff/lib/strategy/fibonacci.js' { 86 | declare module.exports: $Exports<'backoff/lib/strategy/fibonacci'>; 87 | } 88 | declare module 'backoff/lib/strategy/strategy.js' { 89 | declare module.exports: $Exports<'backoff/lib/strategy/strategy'>; 90 | } 91 | declare module 'backoff/tests/api.js' { 92 | declare module.exports: $Exports<'backoff/tests/api'>; 93 | } 94 | declare module 'backoff/tests/backoff_strategy.js' { 95 | declare module.exports: $Exports<'backoff/tests/backoff_strategy'>; 96 | } 97 | declare module 'backoff/tests/backoff.js' { 98 | declare module.exports: $Exports<'backoff/tests/backoff'>; 99 | } 100 | declare module 'backoff/tests/exponential_backoff_strategy.js' { 101 | declare module.exports: $Exports<'backoff/tests/exponential_backoff_strategy'>; 102 | } 103 | declare module 'backoff/tests/fibonacci_backoff_strategy.js' { 104 | declare module.exports: $Exports<'backoff/tests/fibonacci_backoff_strategy'>; 105 | } 106 | declare module 'backoff/tests/function_call.js' { 107 | declare module.exports: $Exports<'backoff/tests/function_call'>; 108 | } 109 | -------------------------------------------------------------------------------- /src/diagnostic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | // / !pragma coverage-skip-block /// 20 | 21 | import Log from 'log'; 22 | import Symphony from './symphony'; 23 | import NockServer from '../test/nock-server'; 24 | 25 | const logger: Log = new Log(process.env.HUBOT_SYMPHONY_LOG_LEVEL || process.env.HUBOT_LOG_LEVEL || 'info'); 26 | 27 | let argv = require('yargs') 28 | .usage('Usage: $0 --publicKey [key1.pem] --privateKey [key2.pem] --passphrase [changeit] ' + 29 | '--host [host.symphony.com] --kmhost [keymanager.host.com] --agenthost [agent.host.com] ' + 30 | '--sessionhost [session.host.com]') 31 | .demand(['publicKey', 'privateKey', 'host', 'passphrase']) 32 | .argv; 33 | 34 | let nock: NockServer; 35 | 36 | if (argv.runOffline) { 37 | logger.info('Instantiating nock server...'); 38 | nock = new NockServer({host: 'https://foundation.symphony.com'}); 39 | } 40 | 41 | logger.info(`Running diagnostics against https://${argv.host}`); 42 | 43 | let symphony = new Symphony({ 44 | host: argv.host, 45 | privateKey: argv.privateKey, 46 | publicKey: argv.publicKey, 47 | passphrase: argv.passphrase, 48 | keyManagerHost: argv.kmhost || argv.host, 49 | agentHost: argv.agenthost || argv.host, 50 | sessionAuthHost: argv.sessionhost || argv.host, 51 | }); 52 | 53 | logger.info('Connection initiated, starting tests...'); 54 | 55 | // check tokens 56 | symphony.sessionAuth() 57 | .then((response) => { 58 | logger.info(`Session token: ${response.token}`); 59 | }) 60 | .catch((err) => { 61 | logger.error(`Failed to fetch session token: ${err}`); 62 | }); 63 | symphony.keyAuth() 64 | .then((response) => { 65 | logger.info(`Key manager token: ${response.token}`); 66 | }) 67 | .catch((err) => { 68 | logger.error(`Failed to fetch key manager token: ${err}`); 69 | }); 70 | 71 | // who am i 72 | symphony.whoAmI() 73 | .then((response) => { 74 | logger.info(`UserId: ${response.userId}`); 75 | return symphony.getUser({userId: response.userId}); 76 | }) 77 | .then((response) => { 78 | logger.info(`My name is ${response.displayName} [${response.emailAddress}]`); 79 | }) 80 | .catch((err) => { 81 | logger.error(`Failed to fetch userId: ${err}`); 82 | }); 83 | 84 | // read message... 85 | symphony.createDatafeed() 86 | .then((response) => { 87 | logger.info(`Created datafeed: ${response.id}`); 88 | logger.info('You should send me a message...'); 89 | return symphony.readDatafeed(response.id); 90 | }) 91 | .then((response) => { 92 | for (const msg of response) { 93 | if (msg.v2messageType === 'V2Message') { 94 | logger.info(`Received '${msg.message}'`); 95 | } 96 | } 97 | if (argv.runOffline) { 98 | nock.close(); 99 | } 100 | process.exit(0); 101 | }) 102 | .catch((err) => { 103 | logger.error(`Was it something I said? You didn't send me a message...: ${err}`); 104 | }); 105 | -------------------------------------------------------------------------------- /it/pingpong.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | const assert = require('chai').assert; 20 | import {describe, it} from 'mocha'; 21 | import Log from 'log'; 22 | import Symphony from '../src/symphony'; 23 | 24 | const logger: Log = new Log(process.env.HUBOT_SYMPHONY_LOG_LEVEL || process.env.HUBOT_LOG_LEVEL || 'info'); 25 | 26 | describe('Foundation Open Developer Platform integration tests', () => { 27 | const getEnv = function(key: string): string { 28 | const value = process.env[key]; 29 | if (value) { 30 | return value; 31 | } 32 | throw new Error(`${key} undefined`); 33 | }; 34 | 35 | it('should send a message from user account and receive it on bot account', () => { 36 | // create two separate connections so we can send a message from one account to another 37 | const botConnection = new Symphony({ 38 | host: getEnv('SYMPHONY_HOST'), 39 | privateKey: getEnv('BOT_USER_KEY'), 40 | publicKey: getEnv('BOT_USER_CERT'), 41 | passphrase: getEnv('BOT_USER_PASSWORD'), 42 | keyManagerHost: getEnv('SYMPHONY_KM_HOST'), 43 | agentHost: getEnv('SYMPHONY_AGENT_HOST'), 44 | sessionAuthHost: getEnv('SYMPHONY_SESSIONAUTH_HOST'), 45 | }); 46 | const userConnection = new Symphony({ 47 | host: getEnv('SYMPHONY_HOST'), 48 | privateKey: getEnv('SENDER_USER_KEY'), 49 | publicKey: getEnv('SENDER_USER_CERT'), 50 | passphrase: getEnv('SENDER_USER_PASSWORD'), 51 | keyManagerHost: getEnv('SYMPHONY_KM_HOST'), 52 | agentHost: getEnv('SYMPHONY_AGENT_HOST'), 53 | sessionAuthHost: getEnv('SYMPHONY_SESSIONAUTH_HOST'), 54 | }); 55 | 56 | logger.info('Connections initiated, starting tests...'); 57 | 58 | // print bot & user account diagnostics, send message from user -> bot and verify receipt 59 | userConnection.whoAmI() 60 | .then((response) => { 61 | return userConnection.getUser({userId: response.userId}); 62 | }) 63 | .then((response) => { 64 | return botConnection.whoAmI(); 65 | }) 66 | .then((response) => { 67 | return botConnection.getUser({userId: response.userId}); 68 | }) 69 | .then((response) => { 70 | const botUserId = response.id; 71 | return botConnection.createDatafeed() 72 | .then((response) => { 73 | const datafeedId = response.id; 74 | logger.info(`Created datafeed: ${datafeedId}`); 75 | // get conversation between user & bot 76 | return userConnection.createIM(botUserId) 77 | .then((response) => { 78 | const conversationId = response.id; 79 | logger.info(`Created conversation: ${conversationId}`); 80 | // send ping from user to bot 81 | return userConnection.sendMessage(conversationId, 'ping'); 82 | }) 83 | .then((response) => { 84 | return botConnection.readDatafeed(datafeedId); 85 | }); 86 | }); 87 | }) 88 | .then((response) => { 89 | assert.include(response.map((m) => m.message), 'ping'); 90 | }); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hubot Symphony 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | # Contributor License Agreement (CLA) 9 | A CLA is a document that specifies how a project is allowed to use your 10 | contribution; they are commonly used in many open source projects. 11 | 12 | **_All_ contributions to _all_ projects hosted by [FINOS](https://www.finos.org/) 13 | must be made with a 14 | [Foundation CLA](https://finosfoundation.atlassian.net/wiki/spaces/FINOS/pages/83034172/Contribute) 15 | in place, and there are [additional legal requirements](https://finosfoundation.atlassian.net/wiki/spaces/FINOS/pages/75530375/Legal+Requirements) 16 | that must also be met.** 17 | 18 | _NOTE:_ Commits and pull requests to FINOS repositories will only be accepted from those contributors with an active, executed Individual Contributor License Agreement (ICLA) with FINOS OR who are covered under an existing and active Corporate Contribution License Agreement (CCLA) executed with FINOS. Commits from individuals not covered under an ICLA or CCLA will be flagged and blocked by the FINOS Clabot tool. Please note that some CCLAs require individuals/employees to be explicitly named on the CCLA. 19 | 20 | As a result, PRs submitted to the Hubot Symphony project cannot be accepted until you have a CLA in place with the Foundation. 21 | 22 | *Need an ICLA? Unsure if you are covered under an existing CCLA? Email [help@finos.org](mailto:help@finos.org)* 23 | 24 | # Contributing Issues 25 | 26 | ## Prerequisites 27 | 28 | * [ ] Have you [searched for duplicates](https://github.com/symphonyoss/hubot-symphony/issues?utf8=%E2%9C%93&q=)? A simple search for exception error messages or a summary of the unexpected behaviour should suffice. 29 | * [ ] Are you running the latest version? 30 | * [ ] Are you sure this is a bug or missing capability? 31 | 32 | ## Raising an Issue 33 | * Create your issue [here](https://github.com/symphonyoss/hubot-symphony/issues/new). 34 | * New issues contain two templates in the description: bug report and enhancement request. Please pick the most appropriate for your issue, **then delete the other**. 35 | * Please also tag the new issue with either "Bug" or "Enhancement". 36 | * Please use [Markdown formatting](https://help.github.com/categories/writing-on-github/) 37 | liberally to assist in readability. 38 | * [Code fences](https://help.github.com/articles/creating-and-highlighting-code-blocks/) for exception stack traces and log entries, for example, massively improve readability. 39 | 40 | # Contributing Pull Requests (Code & Docs) 41 | To make review of PRs easier, please: 42 | 43 | * Please make sure your PRs will merge cleanly - PRs that don't are unlikely to be accepted. 44 | * For code contributions, follow the existing code layout. 45 | * For documentation contributions, follow the general structure, language, and tone of the [existing docs](https://github.com/symphonyoss/hubot-symphony/wiki). 46 | * Keep commits small and cohesive - if you have multiple contributions, please submit them as independent commits (and ideally as independent PRs too). 47 | * Reference issue #s if your PR has anything to do with an issue (even if it doesn't address it). 48 | * Minimise non-functional changes (e.g. whitespace). 49 | * Ensure all new files include a header comment block containing the [Apache License v2.0 and your copyright information](http://www.apache.org/licenses/LICENSE-2.0#apply). 50 | * If necessary (e.g. due to 3rd party dependency licensing requirements), update the [NOTICE file](https://github.com/symphonyoss/hubot-symphony/blob/master/NOTICE) with any new attribution or other notices 51 | 52 | ## Commit and PR Messages 53 | 54 | * **Reference issues, wiki pages, and pull requests liberally!** 55 | * Use the present tense ("Add feature" not "Added feature") 56 | * Use the imperative mood ("Move button left..." not "Moves button left...") 57 | * Limit the first line to 72 characters or less -------------------------------------------------------------------------------- /flow-typed/npm/nock_v10.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 870c10d697546bfbbf7ee15f6b5197ed 2 | // flow-typed version: c6154227d1/nock_v10.x.x/flow_>=v0.104.x 3 | 4 | declare type $npm$nock$Path = string | RegExp | ((url: string) => boolean); 5 | declare type $npm$nock$Parameter = 6 | | string 7 | | RegExp 8 | | Array 9 | | Object 10 | | ((body: Object | Array) => boolean); 11 | 12 | declare type $npm$nock$RecordedCall = { 13 | scope: string, 14 | method: string, 15 | path: string, 16 | body: any, 17 | status: number, 18 | response: any, 19 | headers: Object, 20 | reqheader: Object, 21 | ... 22 | }; 23 | 24 | declare class $npm$nock$Recorder { 25 | rec(options?: { 26 | dont_print?: boolean, 27 | output_objects?: boolean, 28 | enable_reqheaders_recording?: boolean, 29 | logging?: (content: any) => any, 30 | use_separator: boolean, 31 | ... 32 | }): void; 33 | play(): $npm$nock$RecordedCall[]; 34 | } 35 | 36 | declare type $npm$nock$InterceptorOptions = { 37 | hostname: string, 38 | path: string, 39 | method: string, 40 | proto: string, 41 | ... 42 | }; 43 | 44 | declare class $npm$nock$NockBack { 45 | static (path: string, cb: (cb: Function) => any): void; 46 | fixtures: string; 47 | setMode(mode: string): void; 48 | } 49 | 50 | declare class $npm$nock$Nock { 51 | static ( 52 | url: string | RegExp, 53 | options?: { 54 | reqheaders?: Object, 55 | badheaders?: string[], 56 | filteringScope?: (scope: string) => boolean, 57 | allowUnmocked?: boolean, 58 | ... 59 | } 60 | ): $npm$nock$Nock; 61 | static restore(): void; 62 | static cleanAll(): void; 63 | static disableNetConnect(): void; 64 | static enableNetConnect(path?: $npm$nock$Path): void; 65 | static load(path: string): $npm$nock$RecordedCall[]; 66 | // TODO: type definitions 67 | static definitions(path: string): any; 68 | static define(nocks: any): any; 69 | static removeInterceptor( 70 | interceptor: $npm$nock$Nock | $Shape<$npm$nock$InterceptorOptions> 71 | ): void; 72 | static emitter: events$EventEmitter; 73 | static recorder: $npm$nock$Recorder; 74 | static back: $npm$nock$NockBack; 75 | get(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 76 | post(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 77 | put(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 78 | head(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 79 | delete(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 80 | patch(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 81 | merge(path: $npm$nock$Path, parameter?: $npm$nock$Parameter): this; 82 | query(query: Object | boolean | ((query: Object) => boolean)): this; 83 | reply( 84 | code: number, 85 | data?: 86 | | (( 87 | uri: string, 88 | body: mixed, 89 | cb: (error: ?Error, result: mixed) => any 90 | ) => mixed) 91 | | mixed, 92 | header?: Object 93 | ): this; 94 | reply( 95 | fn: ( 96 | uri: string, 97 | body: mixed, 98 | cb: (error: ?Error, result: mixed) => any 99 | ) => mixed 100 | ): this; 101 | replyWithFile(code: number, path: string): this; 102 | replyWithError(error: mixed): this; 103 | basicAuth(auth: { 104 | user: string, 105 | pass: string, 106 | ... 107 | }): this; 108 | defaultReplyHeaders(header: { [key: string]: string | ((req: any, res: any, body: any) => any), ... }): this; 109 | replyContentLength(): this; 110 | replyDate(date?: Date): this; 111 | intercept( 112 | path: $npm$nock$Path, 113 | verb: string, 114 | parameter?: $npm$nock$Parameter, 115 | options?: any 116 | ): this; 117 | times(number: number): this; 118 | once(): this; 119 | twice(): this; 120 | thrice(): this; 121 | delayBody(delay: number): this; 122 | delayConnection(delay: number): this; 123 | delay(delay: number | { 124 | head: number, 125 | body: number, 126 | ... 127 | }): this; 128 | socketDelay(delay: number): this; 129 | filtering$npm$nock$Path(path: RegExp, replace: string): this; 130 | filtering$npm$nock$Path(fn: (path: string) => string): this; 131 | filteringRequestBody(body: RegExp, replace: string): this; 132 | filteringRequestBody(fn: (path: string) => string): this; 133 | matchHeader(header: string, value: mixed | ((value: mixed) => boolean)): this; 134 | optionally(optional?: boolean): this; 135 | persist(): this; 136 | 137 | done(): void; 138 | isDone(): boolean; 139 | static isDone(): boolean; 140 | pendingMocks(): string[]; 141 | static pendingMocks(): string[]; 142 | activeMocks(): string[]; 143 | static activeMocks(): string[]; 144 | log(logFn: Function): this; 145 | } 146 | 147 | declare module 'nock' { 148 | declare module.exports: typeof $npm$nock$Nock; 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-symphony 2 | 3 | [Hubot](http://hubot.github.com/) adapter for [Symphony](https://symphony.com) hosted by the [Symphony Program](https://finosfoundation.atlassian.net/wiki/spaces/SYM/overview) part of [FINOS](https://www.finos.org/) 4 | 5 | Hubot is a [chatops](http://lmgtfy.com/?q=chatops+hubot) tool developed by GitHub, with this adapter you can get up and running with a programmable bot written in JavaScript/Coffescript [in a few minutes](http://blog.symphony.foundation/run-a-symphony-bot-in-less-than-three-minutes-on-docker). This project wraps a small number of the Symphony REST APIs required for two-way bot communication and user lookup together with offline test cases, the adapter is in use both by Symphony clients and by Symphony themselves. 6 | 7 | In mid-2018 Symphony released their own JavaScript API together with a Yeoman generator which facilitates creating simple bots, unless you wish to make use of existing Hubot scripts it's recommended to use this instead. See the developer site [here](https://symphony-developers.symphony.com/) and [symphony-api-client-node](https://www.npmjs.com/package/symphony-api-client-node). 8 | 9 | [![FINOS - Incubating](https://cdn.jsdelivr.net/gh/finos/contrib-toolbox@master/images/badge-incubating.svg)](https://finosfoundation.atlassian.net/wiki/display/FINOS/Incubating) 10 | 11 | [![Build Status](https://travis-ci.org/symphonyoss/hubot-symphony.svg?branch=master)](https://travis-ci.org/symphonyoss/hubot-symphony) 12 | [![Coverage Status](https://coveralls.io/repos/github/symphonyoss/hubot-symphony/badge.svg?branch=master)](https://coveralls.io/github/symphonyoss/hubot-symphony) 13 | [![Code Climate](https://codeclimate.com/github/symphonyoss/hubot-symphony/badges/gpa.svg)](https://codeclimate.com/github/symphonyoss/hubot-symphony) 14 | [![Greenkeeper badge](https://badges.greenkeeper.io/symphonyoss/hubot-symphony.svg)](https://greenkeeper.io/) 15 | 16 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 17 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 18 | 19 | [![NPM](https://nodei.co/npm/hubot-symphony.png?downloads=true&stars=true)](https://nodei.co/npm/hubot-symphony/) 20 | 21 | ## Usage 22 | You must pass the following environment variables to hubot 23 | * `HUBOT_SYMPHONY_HOST` set to the url of your pod without the https:// prefix 24 | * `HUBOT_SYMPHONY_PUBLIC_KEY` set to the location of your bot account .pem public key file 25 | * `HUBOT_SYMPHONY_PRIVATE_KEY` set to the location of your bot account .pem private key file 26 | * `HUBOT_SYMPHONY_PASSPHRASE` set to the passphrase associated with your bot account private key 27 | 28 | There are also optional arguments which should be used if you are running on-premise 29 | * `HUBOT_SYMPHONY_KM_HOST` set to the url of your key manager without the https:// prefix 30 | * `HUBOT_SYMPHONY_AGENT_HOST` set to the url of your agent without the https:// prefix 31 | * `HUBOT_SYMPHONY_SESSIONAUTH_HOST` set to the url of your session auth without the https:// prefix 32 | 33 | These arguments are passed through to the NodeJs request module as described [here](https://github.com/request/request#tlsssl-protocol). 34 | 35 | ### Non-standard messaging 36 | 37 | If you want to send a rich message you can call send just pass messageML directly to the send method instead of plaintext. The various supported tags are documented [here](https://rest-api.symphony.com/docs/message-format). If you want to send [Structured Objects](https://rest-api.symphony.com/v1.47/docs/objects) you can call send with an Object instead of a String (note the text must be valid messageML). 38 | 39 | ``` 40 | module.exports = (robot) -> 41 | robot.respond /pug me/i, (msg) -> 42 | msg.http("http://pugme.herokuapp.com/random") 43 | .get() (err, res, body) -> 44 | pug = JSON.parse(body).pug 45 | // send url as text 46 | msg.send pug 47 | // send url as link 48 | msg.send "" 49 | // send url as a card 50 | msg.send "
PUG!

" 51 | // send message with a structured object 52 | msg.send { 53 | text: myMessageML, 54 | data: myStructuredObjectJson 55 | } 56 | ``` 57 | 58 | 59 | If you want to send a direct message to a user in response to a webhook you can interact with the adapter via the robot variable: 60 | ``` 61 | module.exports = (robot) -> 62 | robot.router.post '/hubot/webhook', (req, res) -> 63 | email = req.params.email 64 | message = req.params.message 65 | robot.adapter.sendDirectMessageToEmail(email, message) 66 | res.send 'OK' 67 | ``` 68 | 69 | ### Diagnostics 70 | A simple diagnostic script is included to help confirm that you have all the necessary pieces to get started. You can run this as follows: 71 | 72 | ``` 73 | git clone https://github.com/symphonyoss/hubot-symphony.git 74 | cd hubot-symphony 75 | npm install 76 | npm run diagnostic -- --publicKey [key1.pem] --privateKey [key2.pem] --passphrase [changeit] --host [host.symphony.com] 77 | ``` 78 | 79 | If you are running on-premise you can add optional fifth / sixth / seventh arguments 80 | 81 | ``` 82 | git clone https://github.com/symphonyoss/hubot-symphony.git 83 | cd hubot-symphony 84 | npm install 85 | npm run diagnostic -- --publicKey [key1.pem] --privateKey [key2.pem] --passphrase [changeit] --host [host.symphony.com] --kmhost [keymanager.host.com] --agenthost [agent.host.com] --sessionhost [session.host.com] 86 | ``` 87 | 88 | If the script runs as expected it will obtain and log both session and key manager tokens, look up and log some details of the bot account and then create a datafeed and poll. If you send a message using the Symphony client to the bot account you should see the details logged. 89 | 90 | ### Whitesource reports 91 | 92 | To check security and legal compliance, the build integrates with Whitesource to submit and validate the list of third-party packages used by the build. 93 | 94 | Simply run the following commands from the root project folder. 95 | ``` 96 | export WHITESOURCE_API_KEY= 97 | npm install ; npm run whitesource 98 | ``` 99 | 100 | The `` can be retrieved from the [WhiteSource project dashboard](https://saas.whitesourcesoftware.com/Wss/WSS.html#!home). 101 | 102 | If any issue is found, a file called `ws-log-policy-violations.json` will be generated in root project folder; if no issue is found, metrics will be sent to the [WhiteSource project dashboard](https://saas.whitesourcesoftware.com/Wss/WSS.html#!home) (available to project committers). 103 | 104 | ### Contribute 105 | 106 | Contributions are accepted via GitHub pull requests. All contributors must be covered by contributor license agreements to comply with the [Code Contribution Process](https://symphonyoss.atlassian.net/wiki/display/FM/Code+Contribution+Process). 107 | 108 | 1. Fork it () 109 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 110 | 3. Read our [contribution guidelines](.github/CONTRIBUTING.md) and [Community Code of Conduct](https://www.finos.org/code-of-conduct) 111 | 4. Commit your changes (`git commit -am 'Add some fooBar'`) 112 | 5. Push to the branch (`git push origin feature/fooBar`) 113 | 6. Create a new Pull Request 114 | 115 | ## License 116 | 117 | The code in this repository is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 118 | 119 | Copyright 2016-2019 Jon Freedman 120 | 121 | #### Note 122 | The privateKey.pem and publicKey.pem files under test/resources have been generated at random and are not real keys. 123 | -------------------------------------------------------------------------------- /test/adapter-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | const assert = require('chai').assert; 20 | import SymphonyAdapter from "../src/adapter"; 21 | import NockServer from "./nock-server"; 22 | import FakeRobot from "./fakes"; 23 | 24 | process.env['HUBOT_SYMPHONY_HOST'] = 'foundation.symphony.com'; 25 | process.env['HUBOT_SYMPHONY_PUBLIC_KEY'] = './test/resources/publicKey.pem'; 26 | process.env['HUBOT_SYMPHONY_PRIVATE_KEY'] = './test/resources/privateKey.pem'; 27 | process.env['HUBOT_SYMPHONY_PASSPHRASE'] = 'changeit'; 28 | 29 | describe('Constructor test', () => { 30 | for (const constructorProp of [ 31 | 'HUBOT_SYMPHONY_HOST', 'HUBOT_SYMPHONY_PUBLIC_KEY', 'HUBOT_SYMPHONY_PRIVATE_KEY', 'HUBOT_SYMPHONY_PASSPHRASE', 32 | ]) { 33 | it(`should throw on construction if ${constructorProp} missing`, () => { 34 | let prop = process.env[constructorProp]; 35 | delete process.env[constructorProp]; 36 | assert.throws(SymphonyAdapter.use, new RegExp(`${constructorProp} undefined`)); 37 | process.env[constructorProp] = prop; 38 | }); 39 | } 40 | }); 41 | 42 | describe('Adapter test suite with helloWorld message', () => { 43 | let nock: NockServer; 44 | 45 | beforeEach(() => { 46 | nock = new NockServer({host: 'https://foundation.symphony.com'}); 47 | }); 48 | 49 | afterEach(() => { 50 | nock.close(); 51 | }); 52 | 53 | it('should connect and receive message', (done) => { 54 | let robot = new FakeRobot(); 55 | let adapter = SymphonyAdapter.use(robot); 56 | adapter.on('connected', () => { 57 | assert.isDefined(adapter.symphony); 58 | robot.on('received', () => { 59 | assert.include(robot.received.map((m) => m.text), 'Hello World'); 60 | adapter.close(); 61 | done(); 62 | }); 63 | }); 64 | adapter.run(); 65 | }); 66 | 67 | it('should retry on http 400 errors when reading datafeed', (done) => { 68 | nock.datafeedReadHttp400Count = 1; 69 | let robot = new FakeRobot(); 70 | let adapter = SymphonyAdapter.use(robot); 71 | adapter.on('connected', () => assert.isDefined(adapter.symphony)); 72 | robot.on('error', () => { 73 | adapter.on('connected', () => { 74 | robot.on('received', () => { 75 | assert.include(robot.received.map((m) => m.text), 'Hello World'); 76 | adapter.close(); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | adapter.run(); 82 | }); 83 | 84 | it('should retry if datafeed cannot be created', (done) => { 85 | nock.datafeedCreateHttp400Count = 1; 86 | let robot = new FakeRobot(); 87 | let adapter = SymphonyAdapter.use(robot); 88 | adapter.on('connected', () => { 89 | assert.isDefined(adapter.symphony); 90 | robot.on('received', () => { 91 | assert.include(robot.received.map((m) => m.text), 'Hello World'); 92 | adapter.close(); 93 | done(); 94 | }); 95 | }); 96 | adapter.run(); 97 | }); 98 | }); 99 | 100 | describe('Adapter test suite', () => { 101 | let nock: NockServer; 102 | 103 | beforeEach(() => { 104 | nock = new NockServer({host: 'https://foundation.symphony.com', startWithHelloWorldMessage: false}); 105 | }); 106 | 107 | afterEach(() => { 108 | nock.close(); 109 | }); 110 | 111 | const expectMessage = (send: string, receive: string, done: () => void) => { 112 | let robot = new FakeRobot(); 113 | let adapter = SymphonyAdapter.use(robot); 114 | adapter.on('connected', () => { 115 | assert.isDefined(adapter.symphony); 116 | let envelope = {room: nock.streamId}; 117 | adapter.send(envelope, send); 118 | adapter.close(); 119 | }); 120 | nock.on('received', () => { 121 | const received = nock.messages.map((m) => m.message); 122 | assert.include(received, receive, `Received ${JSON.stringify(received)}`); 123 | done(); 124 | }); 125 | adapter.run(); 126 | }; 127 | 128 | it('should send with no adornment', (done) => { 129 | expectMessage('foo bar', 'foo bar', done); 130 | }); 131 | 132 | it('should send MESSAGEML', (done) => { 133 | expectMessage('foo bar', 'foo bar', done); 134 | }); 135 | 136 | it('should send MESSAGEML with newlines', (done) => { 137 | expectMessage(`foo 138 | bar`, `foo 139 | bar`, done); 140 | }); 141 | 142 | it('should reply with @mention', (done) => { 143 | let robot = new FakeRobot(); 144 | let adapter = SymphonyAdapter.use(robot); 145 | adapter.on('connected', () => { 146 | assert.isDefined(adapter.symphony); 147 | let envelope = { 148 | room: nock.streamId, 149 | user: { 150 | emailAddress: 'johndoe@symphony.com', 151 | }, 152 | }; 153 | adapter.reply(envelope, 'foo bar baz'); 154 | adapter.close(); 155 | }); 156 | nock.on('received', () => { 157 | const messageTexts = nock.messages.map((m) => m.message); 158 | assert.include(messageTexts, 'foo bar baz'); 159 | done(); 160 | }); 161 | adapter.run(); 162 | }); 163 | 164 | it('should escape xml chars in reply', (done) => { 165 | let robot = new FakeRobot(); 166 | let adapter = SymphonyAdapter.use(robot); 167 | adapter.on('connected', () => { 168 | assert.isDefined(adapter.symphony); 169 | let envelope = { 170 | room: nock.streamId, 171 | user: { 172 | emailAddress: 'johndoe@symphony.com', 173 | }, 174 | }; 175 | adapter.reply(envelope, '<&>'); 176 | adapter.close(); 177 | }); 178 | nock.on('received', () => { 179 | const messageTexts = nock.messages.map((m) => m.message); 180 | assert.include(messageTexts, '<&>'); 181 | done(); 182 | }); 183 | adapter.run(); 184 | }); 185 | 186 | it('should exit datafeed cannot be created', (done) => { 187 | nock.datafeedCreateHttp400Count = 1; 188 | let robot = new FakeRobot(); 189 | let adapter = SymphonyAdapter.use(robot, { 190 | failConnectAfter: 1, 191 | shutdownFunc: () => done(), 192 | }); 193 | adapter.run(); 194 | }); 195 | 196 | it('should send direct message to username', (done) => { 197 | let robot = new FakeRobot(); 198 | let adapter = SymphonyAdapter.use(robot); 199 | adapter.on('connected', () => { 200 | assert.isDefined(adapter.symphony); 201 | adapter.sendDirectMessageToUsername(nock.realUserName, 'username message'); 202 | adapter.close(); 203 | }); 204 | nock.on('received', () => { 205 | assert.include(nock.messages.map((m) => m.message), 'username message'); 206 | done(); 207 | }); 208 | adapter.run(); 209 | }); 210 | 211 | it('should send direct message to email', (done) => { 212 | let robot = new FakeRobot(); 213 | let adapter = SymphonyAdapter.use(robot); 214 | adapter.on('connected', () => { 215 | assert.isDefined(adapter.symphony); 216 | adapter.sendDirectMessageToEmail(nock.realUserEmail, 'email message'); 217 | adapter.close(); 218 | }); 219 | nock.on('received', () => { 220 | assert.include(nock.messages.map((m) => m.message), 'email message'); 221 | done(); 222 | }); 223 | adapter.run(); 224 | }); 225 | 226 | it('should send direct message to id', (done) => { 227 | let robot = new FakeRobot(); 228 | let adapter = SymphonyAdapter.use(robot); 229 | adapter.on('connected', () => { 230 | assert.isDefined(adapter.symphony); 231 | adapter.sendDirectMessageToUserId(nock.realUserId, 'id message'); 232 | adapter.close(); 233 | }); 234 | nock.on('received', () => { 235 | assert.include(nock.messages.map((m) => m.message), 'id message'); 236 | done(); 237 | }); 238 | adapter.run(); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /flow-typed/npm/mocha_v6.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 98b5dfd18f535c2d50c7499f0d11e354 2 | // flow-typed version: c6154227d1/mocha_v6.x.x/flow_>=v0.104.x 3 | 4 | declare interface $npm$mocha$SetupOptions { 5 | slow?: number; 6 | timeout?: number; 7 | ui?: string; 8 | globals?: Array; 9 | reporter?: any; 10 | bail?: boolean; 11 | ignoreLeaks?: boolean; 12 | grep?: any; 13 | } 14 | 15 | declare type $npm$mocha$done = (error?: any) => any; 16 | 17 | // declare interface $npm$mocha$SuiteCallbackContext { 18 | // timeout(ms: number): void; 19 | // retries(n: number): void; 20 | // slow(ms: number): void; 21 | // } 22 | 23 | // declare interface $npm$mocha$TestCallbackContext { 24 | // skip(): void; 25 | // timeout(ms: number): void; 26 | // retries(n: number): void; 27 | // slow(ms: number): void; 28 | // [index: string]: any; 29 | // } 30 | 31 | declare interface $npm$mocha$Suite { 32 | parent: $npm$mocha$Suite; 33 | title: string; 34 | fullTitle(): string; 35 | } 36 | 37 | declare interface $npm$mocha$ContextDefinition { 38 | (description: string, callback: (/* this: $npm$mocha$SuiteCallbackContext */) => void): $npm$mocha$Suite; 39 | only(description: string, callback: (/* this: $npm$mocha$SuiteCallbackContext */) => void): $npm$mocha$Suite; 40 | skip(description: string, callback: (/* this: $npm$mocha$SuiteCallbackContext */) => void): void; 41 | timeout(ms: number): void; 42 | } 43 | 44 | declare interface $npm$mocha$TestDefinition { 45 | (expectation: string, callback?: (/* this: $npm$mocha$TestCallbackContext, */ done: $npm$mocha$done) => mixed): $npm$mocha$Test; 46 | only(expectation: string, callback?: (/* this: $npm$mocha$TestCallbackContext, */ done: $npm$mocha$done) => mixed): $npm$mocha$Test; 47 | skip(expectation: string, callback?: (/* this: $npm$mocha$TestCallbackContext, */ done: $npm$mocha$done) => mixed): void; 48 | timeout(ms: number): void; 49 | state: 'failed' | 'passed'; 50 | } 51 | 52 | declare interface $npm$mocha$Runner {} 53 | 54 | declare class $npm$mocha$BaseReporter { 55 | stats: { 56 | suites: number, 57 | tests: number, 58 | passes: number, 59 | pending: number, 60 | failures: number, 61 | ... 62 | }; 63 | 64 | constructor(runner: $npm$mocha$Runner): $npm$mocha$BaseReporter; 65 | } 66 | 67 | declare class $npm$mocha$DocReporter extends $npm$mocha$BaseReporter {} 68 | declare class $npm$mocha$DotReporter extends $npm$mocha$BaseReporter {} 69 | declare class $npm$mocha$HTMLReporter extends $npm$mocha$BaseReporter {} 70 | declare class $npm$mocha$HTMLCovReporter extends $npm$mocha$BaseReporter {} 71 | declare class $npm$mocha$JSONReporter extends $npm$mocha$BaseReporter {} 72 | declare class $npm$mocha$JSONCovReporter extends $npm$mocha$BaseReporter {} 73 | declare class $npm$mocha$JSONStreamReporter extends $npm$mocha$BaseReporter {} 74 | declare class $npm$mocha$LandingReporter extends $npm$mocha$BaseReporter {} 75 | declare class $npm$mocha$ListReporter extends $npm$mocha$BaseReporter {} 76 | declare class $npm$mocha$MarkdownReporter extends $npm$mocha$BaseReporter {} 77 | declare class $npm$mocha$MinReporter extends $npm$mocha$BaseReporter {} 78 | declare class $npm$mocha$NyanReporter extends $npm$mocha$BaseReporter {} 79 | declare class $npm$mocha$ProgressReporter extends $npm$mocha$BaseReporter { 80 | constructor(runner: $npm$mocha$Runner, options?: { 81 | open?: string, 82 | complete?: string, 83 | incomplete?: string, 84 | close?: string, 85 | ... 86 | }): $npm$mocha$ProgressReporter; 87 | } 88 | declare class $npm$mocha$SpecReporter extends $npm$mocha$BaseReporter {} 89 | declare class $npm$mocha$TAPReporter extends $npm$mocha$BaseReporter {} 90 | declare class $npm$mocha$XUnitReporter extends $npm$mocha$BaseReporter { 91 | constructor(runner: $npm$mocha$Runner, options?: any): $npm$mocha$XUnitReporter; 92 | } 93 | 94 | declare class $npm$mocha$Mocha { 95 | currentTest: $npm$mocha$TestDefinition; 96 | constructor(options?: { 97 | grep?: RegExp, 98 | ui?: string, 99 | reporter?: string, 100 | timeout?: number, 101 | reporterOptions?: any, 102 | slow?: number, 103 | bail?: boolean, 104 | ... 105 | }): $npm$mocha$Mocha; 106 | setup(options: $npm$mocha$SetupOptions): this; 107 | bail(value?: boolean): this; 108 | addFile(file: string): this; 109 | reporter(name: string): this; 110 | reporter(reporter: (runner: $npm$mocha$Runner, options: any) => any): this; 111 | ui(value: string): this; 112 | grep(value: string): this; 113 | grep(value: RegExp): this; 114 | invert(): this; 115 | ignoreLeaks(value: boolean): this; 116 | checkLeaks(): this; 117 | throwError(error: Error): void; 118 | growl(): this; 119 | globals(value: string): this; 120 | globals(values: Array): this; 121 | useColors(value: boolean): this; 122 | useInlineDiffs(value: boolean): this; 123 | timeout(value: number): this; 124 | slow(value: number): this; 125 | enableTimeouts(value: boolean): this; 126 | asyncOnly(value: boolean): this; 127 | noHighlighting(value: boolean): this; 128 | run(onComplete?: (failures: number) => void): $npm$mocha$Runner; 129 | 130 | static reporters: { 131 | Doc: $npm$mocha$DocReporter, 132 | Dot: $npm$mocha$DotReporter, 133 | HTML: $npm$mocha$HTMLReporter, 134 | HTMLCov: $npm$mocha$HTMLCovReporter, 135 | JSON: $npm$mocha$JSONReporter, 136 | JSONCov: $npm$mocha$JSONCovReporter, 137 | JSONStream: $npm$mocha$JSONStreamReporter, 138 | Landing: $npm$mocha$LandingReporter, 139 | List: $npm$mocha$ListReporter, 140 | Markdown: $npm$mocha$MarkdownReporter, 141 | Min: $npm$mocha$MinReporter, 142 | Nyan: $npm$mocha$NyanReporter, 143 | Progress: $npm$mocha$ProgressReporter, 144 | ... 145 | }; 146 | } 147 | 148 | // declare interface $npm$mocha$HookCallbackContext { 149 | // skip(): void; 150 | // timeout(ms: number): void; 151 | // [index: string]: any; 152 | // } 153 | 154 | declare interface $npm$mocha$Runnable { 155 | title: string; 156 | fn: Function; 157 | async: boolean; 158 | sync: boolean; 159 | timedOut: boolean; 160 | } 161 | 162 | declare interface $npm$mocha$Test extends $npm$mocha$Runnable { 163 | parent: $npm$mocha$Suite; 164 | pending: boolean; 165 | state: 'failed' | 'passed' | void; 166 | fullTitle(): string; 167 | timeout(ms: number): void; 168 | } 169 | 170 | // declare interface $npm$mocha$BeforeAndAfterContext extends $npm$mocha$HookCallbackContext { 171 | // currentTest: $npm$mocha$Test; 172 | // } 173 | 174 | declare var mocha: $npm$mocha$Mocha; 175 | declare var describe: $npm$mocha$ContextDefinition; 176 | declare var xdescribe: $npm$mocha$ContextDefinition; 177 | declare var context: $npm$mocha$ContextDefinition; 178 | declare var suite: $npm$mocha$ContextDefinition; 179 | declare var it: $npm$mocha$TestDefinition; 180 | declare var xit: $npm$mocha$TestDefinition; 181 | declare var test: $npm$mocha$TestDefinition; 182 | declare var specify: $npm$mocha$TestDefinition; 183 | 184 | declare function run(): void; 185 | 186 | declare function setup(callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 187 | declare function teardown(callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 188 | declare function suiteSetup(callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 189 | declare function suiteTeardown(callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 190 | declare function before(callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 191 | declare function before(description: string, callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 192 | declare function after(callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 193 | declare function after(description: string, callback: (/* this: $npm$mocha$HookCallbackContext, */ done: $npm$mocha$done) => mixed): void; 194 | declare function beforeEach(callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 195 | declare function beforeEach(description: string, callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 196 | declare function afterEach(callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 197 | declare function afterEach(description: string, callback: (/* this: $npm$mocha$BeforeAndAfterContext, */ done: $npm$mocha$done) => mixed): void; 198 | 199 | declare module "mocha" { 200 | declare export var mocha: typeof mocha; 201 | declare export var describe: typeof describe; 202 | declare export var xdescribe: typeof xdescribe; 203 | declare export var context: typeof context; 204 | declare export var suite: typeof suite; 205 | declare export var it: typeof it; 206 | declare export var xit: typeof xit; 207 | declare export var test: typeof test; 208 | declare export var specify: typeof specify; 209 | 210 | declare export var run: typeof run; 211 | 212 | declare export var setup: typeof setup; 213 | declare export var teardown: typeof teardown; 214 | declare export var suiteSetup: typeof suiteSetup; 215 | declare export var suiteTeardown: typeof suiteTeardown; 216 | declare export var before: typeof before; 217 | declare export var before: typeof before; 218 | declare export var after: typeof after; 219 | declare export var after: typeof after; 220 | declare export var beforeEach: typeof beforeEach; 221 | declare export var beforeEach: typeof beforeEach; 222 | declare export var afterEach: typeof afterEach; 223 | declare export var afterEach: typeof afterEach; 224 | 225 | declare export default $npm$mocha$Mocha; 226 | } 227 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /flow-typed/npm/memoizee_vx.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 653958968581121dd5817c20c85ee03b 2 | // flow-typed version: <>/memoizee_v0.4.4/flow_v0.42.0 3 | 4 | /** 5 | * This is an autogenerated libdef stub for: 6 | * 7 | * 'memoizee' 8 | * 9 | * Fill this stub out by replacing all the `any` types. 10 | * 11 | * Once filled out, we encourage you to share your work with the 12 | * community by sending a pull request to: 13 | * https://github.com/flowtype/flow-typed 14 | */ 15 | 16 | declare module 'memoizee' { 17 | declare module.exports: any; 18 | } 19 | 20 | /** 21 | * We include stubs for each file inside this npm package in case you need to 22 | * require those files directly. Feel free to delete any files that aren't 23 | * needed. 24 | */ 25 | declare module 'memoizee/benchmark/fibonacci' { 26 | declare module.exports: any; 27 | } 28 | 29 | declare module 'memoizee/ext/async' { 30 | declare module.exports: any; 31 | } 32 | 33 | declare module 'memoizee/ext/dispose' { 34 | declare module.exports: any; 35 | } 36 | 37 | declare module 'memoizee/ext/max-age' { 38 | declare module.exports: any; 39 | } 40 | 41 | declare module 'memoizee/ext/max' { 42 | declare module.exports: any; 43 | } 44 | 45 | declare module 'memoizee/ext/promise' { 46 | declare module.exports: any; 47 | } 48 | 49 | declare module 'memoizee/ext/ref-counter' { 50 | declare module.exports: any; 51 | } 52 | 53 | declare module 'memoizee/lib/configure-map' { 54 | declare module.exports: any; 55 | } 56 | 57 | declare module 'memoizee/lib/methods' { 58 | declare module.exports: any; 59 | } 60 | 61 | declare module 'memoizee/lib/registered-extensions' { 62 | declare module.exports: any; 63 | } 64 | 65 | declare module 'memoizee/lib/resolve-length' { 66 | declare module.exports: any; 67 | } 68 | 69 | declare module 'memoizee/lib/resolve-normalize' { 70 | declare module.exports: any; 71 | } 72 | 73 | declare module 'memoizee/lib/resolve-resolve' { 74 | declare module.exports: any; 75 | } 76 | 77 | declare module 'memoizee/lib/weak' { 78 | declare module.exports: any; 79 | } 80 | 81 | declare module 'memoizee/methods-plain' { 82 | declare module.exports: any; 83 | } 84 | 85 | declare module 'memoizee/methods' { 86 | declare module.exports: any; 87 | } 88 | 89 | declare module 'memoizee/normalizers/get-1' { 90 | declare module.exports: any; 91 | } 92 | 93 | declare module 'memoizee/normalizers/get-fixed' { 94 | declare module.exports: any; 95 | } 96 | 97 | declare module 'memoizee/normalizers/get-primitive-fixed' { 98 | declare module.exports: any; 99 | } 100 | 101 | declare module 'memoizee/normalizers/get' { 102 | declare module.exports: any; 103 | } 104 | 105 | declare module 'memoizee/normalizers/primitive' { 106 | declare module.exports: any; 107 | } 108 | 109 | declare module 'memoizee/plain' { 110 | declare module.exports: any; 111 | } 112 | 113 | declare module 'memoizee/profile' { 114 | declare module.exports: any; 115 | } 116 | 117 | declare module 'memoizee/test/ext/async' { 118 | declare module.exports: any; 119 | } 120 | 121 | declare module 'memoizee/test/ext/dispose' { 122 | declare module.exports: any; 123 | } 124 | 125 | declare module 'memoizee/test/ext/max-age' { 126 | declare module.exports: any; 127 | } 128 | 129 | declare module 'memoizee/test/ext/max' { 130 | declare module.exports: any; 131 | } 132 | 133 | declare module 'memoizee/test/ext/promise' { 134 | declare module.exports: any; 135 | } 136 | 137 | declare module 'memoizee/test/ext/ref-counter' { 138 | declare module.exports: any; 139 | } 140 | 141 | declare module 'memoizee/test/index' { 142 | declare module.exports: any; 143 | } 144 | 145 | declare module 'memoizee/test/lib/configure-map' { 146 | declare module.exports: any; 147 | } 148 | 149 | declare module 'memoizee/test/lib/methods' { 150 | declare module.exports: any; 151 | } 152 | 153 | declare module 'memoizee/test/lib/registered-extensions' { 154 | declare module.exports: any; 155 | } 156 | 157 | declare module 'memoizee/test/lib/resolve-length' { 158 | declare module.exports: any; 159 | } 160 | 161 | declare module 'memoizee/test/lib/resolve-normalize' { 162 | declare module.exports: any; 163 | } 164 | 165 | declare module 'memoizee/test/lib/resolve-resolve' { 166 | declare module.exports: any; 167 | } 168 | 169 | declare module 'memoizee/test/lib/weak' { 170 | declare module.exports: any; 171 | } 172 | 173 | declare module 'memoizee/test/methods-plain' { 174 | declare module.exports: any; 175 | } 176 | 177 | declare module 'memoizee/test/methods' { 178 | declare module.exports: any; 179 | } 180 | 181 | declare module 'memoizee/test/normalizers/get-1' { 182 | declare module.exports: any; 183 | } 184 | 185 | declare module 'memoizee/test/normalizers/get-fixed' { 186 | declare module.exports: any; 187 | } 188 | 189 | declare module 'memoizee/test/normalizers/get-primitive-fixed' { 190 | declare module.exports: any; 191 | } 192 | 193 | declare module 'memoizee/test/normalizers/get' { 194 | declare module.exports: any; 195 | } 196 | 197 | declare module 'memoizee/test/normalizers/primitive' { 198 | declare module.exports: any; 199 | } 200 | 201 | declare module 'memoizee/test/plain' { 202 | declare module.exports: any; 203 | } 204 | 205 | declare module 'memoizee/test/profile' { 206 | declare module.exports: any; 207 | } 208 | 209 | declare module 'memoizee/test/weak-plain' { 210 | declare module.exports: any; 211 | } 212 | 213 | declare module 'memoizee/test/weak' { 214 | declare module.exports: any; 215 | } 216 | 217 | declare module 'memoizee/weak-plain' { 218 | declare module.exports: any; 219 | } 220 | 221 | declare module 'memoizee/weak' { 222 | declare module.exports: any; 223 | } 224 | 225 | // Filename aliases 226 | declare module 'memoizee/benchmark/fibonacci.js' { 227 | declare module.exports: $Exports<'memoizee/benchmark/fibonacci'>; 228 | } 229 | declare module 'memoizee/ext/async.js' { 230 | declare module.exports: $Exports<'memoizee/ext/async'>; 231 | } 232 | declare module 'memoizee/ext/dispose.js' { 233 | declare module.exports: $Exports<'memoizee/ext/dispose'>; 234 | } 235 | declare module 'memoizee/ext/max-age.js' { 236 | declare module.exports: $Exports<'memoizee/ext/max-age'>; 237 | } 238 | declare module 'memoizee/ext/max.js' { 239 | declare module.exports: $Exports<'memoizee/ext/max'>; 240 | } 241 | declare module 'memoizee/ext/promise.js' { 242 | declare module.exports: $Exports<'memoizee/ext/promise'>; 243 | } 244 | declare module 'memoizee/ext/ref-counter.js' { 245 | declare module.exports: $Exports<'memoizee/ext/ref-counter'>; 246 | } 247 | declare module 'memoizee/index' { 248 | declare module.exports: $Exports<'memoizee'>; 249 | } 250 | declare module 'memoizee/index.js' { 251 | declare module.exports: $Exports<'memoizee'>; 252 | } 253 | declare module 'memoizee/lib/configure-map.js' { 254 | declare module.exports: $Exports<'memoizee/lib/configure-map'>; 255 | } 256 | declare module 'memoizee/lib/methods.js' { 257 | declare module.exports: $Exports<'memoizee/lib/methods'>; 258 | } 259 | declare module 'memoizee/lib/registered-extensions.js' { 260 | declare module.exports: $Exports<'memoizee/lib/registered-extensions'>; 261 | } 262 | declare module 'memoizee/lib/resolve-length.js' { 263 | declare module.exports: $Exports<'memoizee/lib/resolve-length'>; 264 | } 265 | declare module 'memoizee/lib/resolve-normalize.js' { 266 | declare module.exports: $Exports<'memoizee/lib/resolve-normalize'>; 267 | } 268 | declare module 'memoizee/lib/resolve-resolve.js' { 269 | declare module.exports: $Exports<'memoizee/lib/resolve-resolve'>; 270 | } 271 | declare module 'memoizee/lib/weak.js' { 272 | declare module.exports: $Exports<'memoizee/lib/weak'>; 273 | } 274 | declare module 'memoizee/methods-plain.js' { 275 | declare module.exports: $Exports<'memoizee/methods-plain'>; 276 | } 277 | declare module 'memoizee/methods.js' { 278 | declare module.exports: $Exports<'memoizee/methods'>; 279 | } 280 | declare module 'memoizee/normalizers/get-1.js' { 281 | declare module.exports: $Exports<'memoizee/normalizers/get-1'>; 282 | } 283 | declare module 'memoizee/normalizers/get-fixed.js' { 284 | declare module.exports: $Exports<'memoizee/normalizers/get-fixed'>; 285 | } 286 | declare module 'memoizee/normalizers/get-primitive-fixed.js' { 287 | declare module.exports: $Exports<'memoizee/normalizers/get-primitive-fixed'>; 288 | } 289 | declare module 'memoizee/normalizers/get.js' { 290 | declare module.exports: $Exports<'memoizee/normalizers/get'>; 291 | } 292 | declare module 'memoizee/normalizers/primitive.js' { 293 | declare module.exports: $Exports<'memoizee/normalizers/primitive'>; 294 | } 295 | declare module 'memoizee/plain.js' { 296 | declare module.exports: $Exports<'memoizee/plain'>; 297 | } 298 | declare module 'memoizee/profile.js' { 299 | declare module.exports: $Exports<'memoizee/profile'>; 300 | } 301 | declare module 'memoizee/test/ext/async.js' { 302 | declare module.exports: $Exports<'memoizee/test/ext/async'>; 303 | } 304 | declare module 'memoizee/test/ext/dispose.js' { 305 | declare module.exports: $Exports<'memoizee/test/ext/dispose'>; 306 | } 307 | declare module 'memoizee/test/ext/max-age.js' { 308 | declare module.exports: $Exports<'memoizee/test/ext/max-age'>; 309 | } 310 | declare module 'memoizee/test/ext/max.js' { 311 | declare module.exports: $Exports<'memoizee/test/ext/max'>; 312 | } 313 | declare module 'memoizee/test/ext/promise.js' { 314 | declare module.exports: $Exports<'memoizee/test/ext/promise'>; 315 | } 316 | declare module 'memoizee/test/ext/ref-counter.js' { 317 | declare module.exports: $Exports<'memoizee/test/ext/ref-counter'>; 318 | } 319 | declare module 'memoizee/test/index.js' { 320 | declare module.exports: $Exports<'memoizee/test/index'>; 321 | } 322 | declare module 'memoizee/test/lib/configure-map.js' { 323 | declare module.exports: $Exports<'memoizee/test/lib/configure-map'>; 324 | } 325 | declare module 'memoizee/test/lib/methods.js' { 326 | declare module.exports: $Exports<'memoizee/test/lib/methods'>; 327 | } 328 | declare module 'memoizee/test/lib/registered-extensions.js' { 329 | declare module.exports: $Exports<'memoizee/test/lib/registered-extensions'>; 330 | } 331 | declare module 'memoizee/test/lib/resolve-length.js' { 332 | declare module.exports: $Exports<'memoizee/test/lib/resolve-length'>; 333 | } 334 | declare module 'memoizee/test/lib/resolve-normalize.js' { 335 | declare module.exports: $Exports<'memoizee/test/lib/resolve-normalize'>; 336 | } 337 | declare module 'memoizee/test/lib/resolve-resolve.js' { 338 | declare module.exports: $Exports<'memoizee/test/lib/resolve-resolve'>; 339 | } 340 | declare module 'memoizee/test/lib/weak.js' { 341 | declare module.exports: $Exports<'memoizee/test/lib/weak'>; 342 | } 343 | declare module 'memoizee/test/methods-plain.js' { 344 | declare module.exports: $Exports<'memoizee/test/methods-plain'>; 345 | } 346 | declare module 'memoizee/test/methods.js' { 347 | declare module.exports: $Exports<'memoizee/test/methods'>; 348 | } 349 | declare module 'memoizee/test/normalizers/get-1.js' { 350 | declare module.exports: $Exports<'memoizee/test/normalizers/get-1'>; 351 | } 352 | declare module 'memoizee/test/normalizers/get-fixed.js' { 353 | declare module.exports: $Exports<'memoizee/test/normalizers/get-fixed'>; 354 | } 355 | declare module 'memoizee/test/normalizers/get-primitive-fixed.js' { 356 | declare module.exports: $Exports<'memoizee/test/normalizers/get-primitive-fixed'>; 357 | } 358 | declare module 'memoizee/test/normalizers/get.js' { 359 | declare module.exports: $Exports<'memoizee/test/normalizers/get'>; 360 | } 361 | declare module 'memoizee/test/normalizers/primitive.js' { 362 | declare module.exports: $Exports<'memoizee/test/normalizers/primitive'>; 363 | } 364 | declare module 'memoizee/test/plain.js' { 365 | declare module.exports: $Exports<'memoizee/test/plain'>; 366 | } 367 | declare module 'memoizee/test/profile.js' { 368 | declare module.exports: $Exports<'memoizee/test/profile'>; 369 | } 370 | declare module 'memoizee/test/weak-plain.js' { 371 | declare module.exports: $Exports<'memoizee/test/weak-plain'>; 372 | } 373 | declare module 'memoizee/test/weak.js' { 374 | declare module.exports: $Exports<'memoizee/test/weak'>; 375 | } 376 | declare module 'memoizee/weak-plain.js' { 377 | declare module.exports: $Exports<'memoizee/weak-plain'>; 378 | } 379 | declare module 'memoizee/weak.js' { 380 | declare module.exports: $Exports<'memoizee/weak'>; 381 | } 382 | -------------------------------------------------------------------------------- /flow-typed/npm/chai_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 9322dedab9c26ccfc457c88842d09bac 2 | // flow-typed version: c6154227d1/chai_v4.x.x/flow_>=v0.104.x 3 | 4 | declare module "chai" { 5 | declare type ExpectChain = { 6 | and: ExpectChain, 7 | at: ExpectChain, 8 | be: ExpectChain, 9 | been: ExpectChain, 10 | have: ExpectChain, 11 | has: ExpectChain, 12 | is: ExpectChain, 13 | of: ExpectChain, 14 | same: ExpectChain, 15 | that: ExpectChain, 16 | to: ExpectChain, 17 | which: ExpectChain, 18 | with: ExpectChain, 19 | not: ExpectChain, 20 | deep: ExpectChain, 21 | any: ExpectChain, 22 | all: ExpectChain, 23 | own: ExpectChain, 24 | a: ExpectChain & ((type: string, message?: string) => ExpectChain), 25 | an: ExpectChain & ((type: string, message?: string) => ExpectChain), 26 | include: ExpectChain & ((value: mixed, message?: string) => ExpectChain), 27 | includes: ExpectChain & ((value: mixed, message?: string) => ExpectChain), 28 | deepInclude: ExpectChain & (value: mixed) => ExpectChain, 29 | contain: ExpectChain & ((value: mixed, message?: string) => ExpectChain), 30 | contains: ExpectChain & ((value: mixed, message?: string) => ExpectChain), 31 | eq: (value: T, message?: string) => ExpectChain, 32 | eql: (value: T, message?: string) => ExpectChain, 33 | equal: (value: T, message?: string) => ExpectChain, 34 | equals: (value: T, message?: string) => ExpectChain, 35 | above: (value: T & number, message?: string) => ExpectChain, 36 | gt: (value: T & number, message?: string) => ExpectChain, 37 | greaterThan: (value: T & number, message?: string) => ExpectChain, 38 | least: (value: T & number, message?: string) => ExpectChain, 39 | below: (value: T & number, message?: string) => ExpectChain, 40 | lessThan: (value: T & number, message?: string) => ExpectChain, 41 | lt: (value: T & number, message?: string) => ExpectChain, 42 | most: (value: T & number, message?: string) => ExpectChain, 43 | within: (start: T & number, finish: T & number, message?: string) => ExpectChain, 44 | instanceof: (constructor: mixed, message?: string) => ExpectChain, 45 | instanceOf: (constructor: mixed, message?: string) => ExpectChain, 46 | nested: ExpectChain, 47 | property:

( 48 | name: string, 49 | value?: P, 50 | message?: string 51 | ) => ExpectChain

& ((name: string) => ExpectChain), 52 | length: ExpectChain & ((value: number, message?: string) => ExpectChain), 53 | lengthOf: (value: number, message?: string) => ExpectChain, 54 | match: (regex: RegExp, message?: string) => ExpectChain, 55 | matches: (regex: RegExp, message?: string) => ExpectChain, 56 | string: (string: string, message?: string) => ExpectChain, 57 | key: (key: string) => ExpectChain, 58 | keys: ( 59 | key: string | Array, 60 | ...keys: Array 61 | ) => ExpectChain, 62 | throw: ( 63 | err?: Class | Error | RegExp | string, 64 | errMsgMatcher?: RegExp | string, 65 | msg?: string 66 | ) => ExpectChain, 67 | respondTo: (method: string, message?: string) => ExpectChain, 68 | itself: ExpectChain, 69 | satisfy: (method: (value: T) => boolean, message?: string) => ExpectChain, 70 | closeTo: (expected: T & number, delta: number, message?: string) => ExpectChain, 71 | members: (set: mixed, message?: string) => ExpectChain, 72 | oneOf: (list: Array, message?: string) => ExpectChain, 73 | change: (obj: mixed, key: string, message?: string) => ExpectChain, 74 | increase: (obj: mixed, key: string, message?: string) => ExpectChain, 75 | decrease: (obj: mixed, key: string, message?: string) => ExpectChain, 76 | by: (delta: number, message?: string) => ExpectChain, 77 | ordered: ExpectChain, 78 | // dirty-chai 79 | ok: () => ExpectChain, 80 | true: () => ExpectChain, 81 | false: () => ExpectChain, 82 | null: () => ExpectChain, 83 | undefined: () => ExpectChain, 84 | exist: () => ExpectChain, 85 | empty: () => ExpectChain, 86 | extensible: () => ExpectChain, 87 | sealed: () => ExpectChain, 88 | frozen: () => ExpectChain, 89 | NaN: () => ExpectChain, 90 | // chai-immutable 91 | size: (n: number) => ExpectChain, 92 | // sinon-chai 93 | called: () => ExpectChain, 94 | callCount: (n: number) => ExpectChain, 95 | calledOnce: () => ExpectChain, 96 | calledTwice: () => ExpectChain, 97 | calledThrice: () => ExpectChain, 98 | calledBefore: (spy: mixed) => ExpectChain, 99 | calledAfter: (spy: mixed) => ExpectChain, 100 | calledImmediatelyBefore: (spy: mixed) => ExpectChain, 101 | calledImmediatelyAfter: (spy: mixed) => ExpectChain, 102 | calledWith: (...args: Array) => ExpectChain, 103 | calledOnceWith: (...args: Array) => ExpectChain, 104 | calledWithMatch: (...args: Array) => ExpectChain, 105 | calledWithExactly: (...args: Array) => ExpectChain, 106 | calledOnceWithExactly: (...args: Array) => ExpectChain, 107 | returned: (returnVal: mixed) => ExpectChain, 108 | alwaysReturned: (returnVal: mixed) => ExpectChain, 109 | // chai-as-promised 110 | eventually: ExpectChain, 111 | resolvedWith: (value: mixed) => Promise & ExpectChain, 112 | resolved: () => Promise & ExpectChain, 113 | rejectedWith: ( 114 | value: mixed, 115 | errMsgMatcher?: RegExp | string, 116 | msg?: string 117 | ) => Promise & ExpectChain, 118 | rejected: () => Promise & ExpectChain, 119 | notify: (callback: () => mixed) => ExpectChain, 120 | fulfilled: () => Promise & ExpectChain, 121 | // chai-subset 122 | containSubset: (obj: {...} | Array< {...} >) => ExpectChain, 123 | // chai-redux-mock-store 124 | dispatchedActions: ( 125 | actions: Array<{...} | ((action: {...}) => any)> 126 | ) => ExpectChain, 127 | dispatchedTypes: (actions: Array) => ExpectChain, 128 | // chai-enzyme 129 | attr: (key: string, val?: any) => ExpectChain, 130 | data: (key: string, val?: any) => ExpectChain, 131 | prop: (key: string, val?: any) => ExpectChain, 132 | state: (key: string, val?: any) => ExpectChain, 133 | value: (val: string) => ExpectChain, 134 | className: (val: string) => ExpectChain, 135 | text: (val: string) => ExpectChain, 136 | // chai-karma-snapshot 137 | matchSnapshot: (lang?: any, update?: boolean, msg?: any) => ExpectChain, 138 | ... 139 | }; 140 | 141 | 142 | declare var expect: { 143 | (actual: T, message?: string): ExpectChain, 144 | fail: ((message?: string) => void) & ((actual: any, expected: any, message?: string, operator?: string) => void), 145 | ... 146 | }; 147 | 148 | declare function use(plugin: (chai: Object, utils: Object) => void): void; 149 | 150 | declare class assert { 151 | static (expression: mixed, message?: string): void; 152 | static fail( 153 | actual: mixed, 154 | expected: mixed, 155 | message?: string, 156 | operator?: string 157 | ): void; 158 | 159 | static isOk(object: mixed, message?: string): void; 160 | static isNotOk(object: mixed, message?: string): void; 161 | 162 | static empty(object: mixed, message?: string): void; 163 | static isEmpty(object: mixed, message?: string): void; 164 | static notEmpty(object: mixed, message?: string): void; 165 | static isNotEmpty(object: mixed, message?: string): void; 166 | 167 | static equal(actual: mixed, expected: mixed, message?: string): void; 168 | static notEqual(actual: mixed, expected: mixed, message?: string): void; 169 | 170 | static strictEqual(act: mixed, exp: mixed, msg?: string): void; 171 | static notStrictEqual(act: mixed, exp: mixed, msg?: string): void; 172 | 173 | static deepEqual(act: mixed, exp: mixed, msg?: string): void; 174 | static notDeepEqual(act: mixed, exp: mixed, msg?: string): void; 175 | 176 | static ok(val: mixed, msg?: string): void; 177 | static isTrue(val: mixed, msg?: string): void; 178 | static isNotTrue(val: mixed, msg?: string): void; 179 | static isFalse(val: mixed, msg?: string): void; 180 | static isNotFalse(val: mixed, msg?: string): void; 181 | 182 | static isNull(val: mixed, msg?: string): void; 183 | static isNotNull(val: mixed, msg?: string): void; 184 | 185 | static isUndefined(val: mixed, msg?: string): void; 186 | static isDefined(val: mixed, msg?: string): void; 187 | 188 | static isNaN(val: mixed, msg?: string): void; 189 | static isNotNaN(val: mixed, msg?: string): void; 190 | 191 | static isAbove(val: number, abv: number, msg?: string): void; 192 | static isBelow(val: number, blw: number, msg?: string): void; 193 | 194 | static isAtMost(val: number, atmst: number, msg?: string): void; 195 | static isAtLeast(val: number, atlst: number, msg?: string): void; 196 | 197 | static isFunction(val: mixed, msg?: string): void; 198 | static isNotFunction(val: mixed, msg?: string): void; 199 | 200 | static isObject(val: mixed, msg?: string): void; 201 | static isNotObject(val: mixed, msg?: string): void; 202 | 203 | static isArray(val: mixed, msg?: string): void; 204 | static isNotArray(val: mixed, msg?: string): void; 205 | 206 | static isString(val: mixed, msg?: string): void; 207 | static isNotString(val: mixed, msg?: string): void; 208 | 209 | static isNumber(val: mixed, msg?: string): void; 210 | static isNotNumber(val: mixed, msg?: string): void; 211 | 212 | static isBoolean(val: mixed, msg?: string): void; 213 | static isNotBoolean(val: mixed, msg?: string): void; 214 | 215 | static typeOf(val: mixed, type: string, msg?: string): void; 216 | static notTypeOf(val: mixed, type: string, msg?: string): void; 217 | 218 | static instanceOf(val: mixed, constructor: Class< * >, msg?: string): void; 219 | static notInstanceOf(val: mixed, constructor: Class< * >, msg?: string): void; 220 | 221 | static include(exp: string, inc: mixed, msg?: string): void; 222 | static include(exp: Array, inc: T, msg?: string): void; 223 | 224 | static deepInclude(exp: string, inc: mixed, msg?: string): void; 225 | static deepInclude(exp: Array, inc: T, msg?: string): void; 226 | 227 | static notInclude(exp: string, inc: mixed, msg?: string): void; 228 | static notInclude(exp: Array, inc: T, msg?: string): void; 229 | 230 | static match(exp: mixed, re: RegExp, msg?: string): void; 231 | static notMatch(exp: mixed, re: RegExp, msg?: string): void; 232 | 233 | static property(obj: Object, prop: string, msg?: string): void; 234 | static notProperty(obj: Object, prop: string, msg?: string): void; 235 | static deepProperty(obj: Object, prop: string, msg?: string): void; 236 | static notDeepProperty(obj: Object, prop: string, msg?: string): void; 237 | 238 | static propertyVal( 239 | obj: Object, 240 | prop: string, 241 | val: mixed, 242 | msg?: string 243 | ): void; 244 | static propertyNotVal( 245 | obj: Object, 246 | prop: string, 247 | val: mixed, 248 | msg?: string 249 | ): void; 250 | 251 | static deepPropertyVal( 252 | obj: Object, 253 | prop: string, 254 | val: mixed, 255 | msg?: string 256 | ): void; 257 | static deepPropertyNotVal( 258 | obj: Object, 259 | prop: string, 260 | val: mixed, 261 | msg?: string 262 | ): void; 263 | 264 | static lengthOf(exp: mixed, len: number, msg?: string): void; 265 | 266 | static throws( 267 | func: () => any, 268 | err?: Class | Error | RegExp | string, 269 | errorMsgMatcher?: string | RegExp, 270 | msg?: string 271 | ): void; 272 | static doesNotThrow( 273 | func: () => any, 274 | err?: Class | Error | RegExp | string, 275 | errorMsgMatcher?: string | RegExp, 276 | msg?: string 277 | ): void; 278 | 279 | static closeTo( 280 | actual: number, 281 | expected: number, 282 | delta: number, 283 | msg?: string 284 | ): void; 285 | static approximately( 286 | actual: number, 287 | expected: number, 288 | delta: number, 289 | msg?: string 290 | ): void; 291 | 292 | // chai-immutable 293 | static sizeOf(val: mixed, length: number): void; 294 | } 295 | 296 | declare var config: { 297 | includeStack: boolean, 298 | showDiff: boolean, 299 | truncateThreshold: number, 300 | ... 301 | }; 302 | } 303 | -------------------------------------------------------------------------------- /src/adapter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | import {Adapter, Robot} from 'hubot'; 20 | import memoize from 'memoizee'; 21 | import backoff from 'backoff'; 22 | import Backoff from 'backoff/lib/backoff'; 23 | import XmlEntities from 'html-entities/lib/xml-entities'; 24 | import type {GetUserArgsType, SymphonyMessageV2Type} from './symphony'; 25 | import Symphony from './symphony'; 26 | import {V2Message} from './message'; 27 | 28 | const entities = new XmlEntities(); 29 | 30 | type AdapterOptionsTypeWithNulls = { 31 | failConnectAfter?: number, 32 | backoff?: Backoff, 33 | shutdownFunc?: () => void 34 | }; 35 | 36 | type AdapterOptionsType = { 37 | failConnectAfter: number, 38 | backoff: Backoff, 39 | shutdownFunc: () => void 40 | }; 41 | 42 | type SimpleMessageEnvelopeType = { 43 | room: string 44 | }; 45 | 46 | type MessageEnvelopeType = { 47 | room: string, 48 | user: { 49 | emailAddress: string 50 | } 51 | }; 52 | 53 | type MessageType = { 54 | text: string, 55 | data: string 56 | }; 57 | 58 | type MessageTypeOrString = MessageType | string; 59 | 60 | type HubotUserType = { 61 | name: string; 62 | displayName: string; 63 | emailAddress: string; 64 | room: string; 65 | } 66 | 67 | /** 68 | * Hubot adapter for Symphony 69 | * @author Jon Freedman 70 | */ 71 | class SymphonyAdapter extends Adapter { 72 | robot: Robot; 73 | symphony: Symphony; 74 | backoff: Backoff; 75 | _userLookup: (GetUserArgsType, ?string) => Promise; 76 | 77 | /** 78 | * @param {Robot} robot Hubot robot 79 | * @param {AdapterOptionsType} options Configuration options that may be overridden for testing 80 | * @constructor 81 | */ 82 | constructor(robot: Robot, options: AdapterOptionsType) { 83 | super(robot); 84 | this.robot = robot; 85 | 86 | if (process.env.HUBOT_SYMPHONY_HOST === undefined || process.env.HUBOT_SYMPHONY_HOST === null) { 87 | throw new Error('HUBOT_SYMPHONY_HOST undefined'); 88 | } 89 | if (process.env.HUBOT_SYMPHONY_PUBLIC_KEY === undefined || process.env.HUBOT_SYMPHONY_PUBLIC_KEY === null) { 90 | throw new Error('HUBOT_SYMPHONY_PUBLIC_KEY undefined'); 91 | } 92 | if (process.env.HUBOT_SYMPHONY_PRIVATE_KEY === undefined || process.env.HUBOT_SYMPHONY_PRIVATE_KEY === null) { 93 | throw new Error('HUBOT_SYMPHONY_PRIVATE_KEY undefined'); 94 | } 95 | if (process.env.HUBOT_SYMPHONY_PASSPHRASE === undefined || process.env.HUBOT_SYMPHONY_PASSPHRASE === null) { 96 | throw new Error('HUBOT_SYMPHONY_PASSPHRASE undefined'); 97 | } 98 | 99 | this.backoff = options.backoff; 100 | this.backoff.on('backoff', (num, delay) => { 101 | if (num > 0) { 102 | this.robot.logger.info(`Re-attempting to create datafeed - attempt ${num} after ${delay}ms`); 103 | } 104 | }); 105 | this.backoff.on('ready', () => { 106 | this.symphony.createDatafeed() 107 | .then((response) => { 108 | if (response.id) { 109 | this.robot.logger.info(`Created datafeed: ${response.id}`); 110 | this.removeAllListeners('poll'); 111 | this.on('poll', this._pollDatafeed); 112 | this.emit('poll', response.id, () => { 113 | this.backoff.reset(); 114 | this.robot.logger.debug('Successfully polled datafeed so resetting backoff'); 115 | }); 116 | this.robot.logger.debug('First poll event emitted'); 117 | this.emit('connected'); 118 | this.robot.logger.debug('connected event emitted'); 119 | } else { 120 | this.robot.emit('error', new Error(`Unable to create datafeed: ${JSON.stringify(response)}`)); 121 | this.backoff.backoff(); 122 | } 123 | }) 124 | .catch((error) => { 125 | this.robot.emit('error', new Error(`Unable to create datafeed: ${error}`)); 126 | this.backoff.backoff(); 127 | }); 128 | }); 129 | this.backoff.on('fail', () => { 130 | this.robot.logger.info('Shutting down...'); 131 | if (options.shutdownFunc) { 132 | options.shutdownFunc(); 133 | } 134 | }); 135 | // will time out reconnecting after ~10min 136 | this.robot.logger.info(`Reconnect attempts = ${options.failConnectAfter}`); 137 | this.backoff.failAfter(options.failConnectAfter); 138 | } 139 | 140 | /** 141 | * Send one or more messages to a room in Symphony, can be called with strings or objects of the form 142 | *

143 |    * {
144 |    *   text: string,
145 |    *   data: string
146 |    * }
147 |    * 
148 | * 149 | * @param {SimpleMessageEnvelopeType} envelope 150 | * @param {Array.} messages 151 | */ 152 | send(envelope: SimpleMessageEnvelopeType, ...messages: Array) { 153 | this.robot.logger.debug(`Sending ${messages.length} messages to ${envelope.room}`); 154 | for (const message of messages) { 155 | if (typeof message === 'string') { 156 | let messageML = message; 157 | const match = /([\s\S]*)<\/messageML>/i.exec(messageML); 158 | if (match === undefined || match === null) { 159 | messageML = `${messageML}<\/messageML>`; 160 | } 161 | this.symphony.sendMessage(envelope.room, messageML); 162 | } else { 163 | this.symphony.sendMessageWithStructuredObjects(envelope.room, message.text, message.data); 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * Send one or more messages to a user in Symphony based on their username, can be called with strings or objects of 170 | * the form 171 | *

172 |    * {
173 |    *   text: string,
174 |    *   format: string
175 |    * }
176 |    * 
177 | * 178 | * @param {string} username 179 | * @param {Array.} messages 180 | */ 181 | sendDirectMessageToUsername(username: string, ...messages: Array) { 182 | this.robot.logger.debug(`Sending direct message to username: ${username}`); 183 | this._userLookup({username: username}, undefined) 184 | .then((response) => { 185 | this.sendDirectMessageToUserId(response.id, ...messages); 186 | }); 187 | } 188 | 189 | /** 190 | * Send one or more messages to a user in Symphony based on their email address, can be called with strings or objects 191 | * of the form 192 | *

193 |    * {
194 |    *   text: string,
195 |    *   format: string
196 |    * }
197 |    * 
198 | * 199 | * @param {string} email 200 | * @param {Array.} messages 201 | */ 202 | sendDirectMessageToEmail(email: string, ...messages: Array) { 203 | this.robot.logger.debug(`Sending direct message to email: ${email}`); 204 | this._userLookup({emailAddress: email}, undefined) 205 | .then((response) => { 206 | this.sendDirectMessageToUserId(response.id, ...messages); 207 | }); 208 | } 209 | 210 | /** 211 | * Send one or more messages to a user in Symphony based on their user id, can be called with strings or objects of 212 | * the form 213 | *

214 |    * {
215 |    *   text: string,
216 |    *   format: string
217 |    * }
218 |    * 
219 | * 220 | * @param {number} userId Symphony user id 221 | * @param {Array.} messages 222 | */ 223 | sendDirectMessageToUserId(userId: number, ...messages: Array) { 224 | this.symphony.createIM(userId) 225 | .then((response) => { 226 | this.send({room: response.id}, ...messages); 227 | }); 228 | } 229 | 230 | /** 231 | * Reply with one or more messages to a room in Symphony, can only be called with strings as each message is prefixed 232 | * with an @mention 233 | * 234 | * @param {MessageEnvelopeType} envelope 235 | * @param {Array.} messages 236 | */ 237 | reply(envelope: MessageEnvelopeType, ...messages: Array) { 238 | this.robot.logger.debug( 239 | `Sending ${messages.length} reply messages to ${envelope.user.emailAddress} in ${envelope.room}` 240 | ); 241 | for (const message of messages) { 242 | const mml = `${entities.encode(message)}`; 243 | this.symphony.sendMessage(envelope.room, mml); 244 | } 245 | } 246 | 247 | /** 248 | * Connect to Symphony 249 | */ 250 | run() { 251 | this.robot.logger.info('Initialising...'); 252 | 253 | const getEnv = function(key: string, defaultVal: ?string): string { 254 | const value = process.env[key]; 255 | if (value) { 256 | return value; 257 | } 258 | if (defaultVal) { 259 | return defaultVal; 260 | } 261 | throw new Error(`${key} undefined`); 262 | }; 263 | const host: string = getEnv('HUBOT_SYMPHONY_HOST'); 264 | this.symphony = new Symphony({ 265 | host: host, 266 | privateKey: getEnv('HUBOT_SYMPHONY_PRIVATE_KEY'), 267 | publicKey: getEnv('HUBOT_SYMPHONY_PUBLIC_KEY'), 268 | passphrase: getEnv('HUBOT_SYMPHONY_PASSPHRASE'), 269 | keyManagerHost: getEnv('HUBOT_SYMPHONY_KM_HOST', host), 270 | sessionAuthHost: getEnv('HUBOT_SYMPHONY_SESSIONAUTH_HOST', host), 271 | agentHost: getEnv('HUBOT_SYMPHONY_AGENT_HOST', host), 272 | }); 273 | this.symphony.whoAmI() 274 | .then((response) => { 275 | this.robot.userId = response.userId; 276 | this.symphony.getUser({userId: response.userId}) 277 | .then((response) => { 278 | this.robot.displayName = response.displayName; 279 | this.robot.logger.info(`Connected as ${response.displayName}`); 280 | }); 281 | }) 282 | .catch((error) => { 283 | this.robot.emit('error', new Error(`Unable to resolve identity: ${error}`)); 284 | }); 285 | // cache user details for an hour 286 | const hourlyRefresh = memoize(this._getUser.bind(this), {maxAge: 3600000, length: 2}); 287 | this._userLookup = function(query: GetUserArgsType, streamId: ?string): Promise { 288 | return hourlyRefresh(query, streamId); 289 | }; 290 | this._createDatafeed(); 291 | } 292 | 293 | /** 294 | * Clean up 295 | */ 296 | close() { 297 | this.robot.logger.debug('Removing datafeed poller'); 298 | this.removeListener('poll', this._pollDatafeed); 299 | } 300 | 301 | /** 302 | * Attempt to create a new datafeed 303 | * @private 304 | */ 305 | _createDatafeed() { 306 | this.backoff.backoff(); 307 | } 308 | 309 | /** 310 | * Poll datafeed for zero or more messages. Ignores anything that is not a V2Message. 311 | * 312 | * @param {string} id Datafeed id 313 | * @param {?function} onSuccess no-arg callback called if poll completes without error 314 | * @private 315 | */ 316 | _pollDatafeed(id: string, onSuccess: () => void) { 317 | // defer execution to ensure we don't go into an infinite polling loop 318 | const self = this; 319 | process.nextTick(() => { 320 | self.robot.logger.debug(`Polling datafeed ${id}`); 321 | self.symphony.readDatafeed(id) 322 | .then((response) => { 323 | if (response) { 324 | self.robot.logger.debug(`Received ${response.length || 0} datafeed messages`); 325 | for (const msg of response) { 326 | if (msg.v2messageType === 'V2Message') { 327 | self._receiveMessage(msg); 328 | } 329 | } 330 | } 331 | this.emit('poll', id); 332 | if (onSuccess !== undefined) { 333 | onSuccess(); 334 | } 335 | }) 336 | .catch((error) => { 337 | self.robot.emit('error', new Error(`Unable to read datafeed ${id}: ${error}`)); 338 | self._createDatafeed(); 339 | }); 340 | }); 341 | } 342 | 343 | /** 344 | * Process a message and convert to a {@link V2Message} for use by Hubot. 345 | * 346 | * @param {SymphonyMessageV2Type} message 347 | * @private 348 | */ 349 | _receiveMessage(message: SymphonyMessageV2Type) { 350 | // ignore anything the bot said 351 | if (message.fromUserId !== this.robot.userId) { 352 | this._userLookup({userId: message.fromUserId}, message.streamId) 353 | .then((response) => { 354 | const v2 = new V2Message(response, message); 355 | this.robot.logger.debug(`Received '${v2.text}' from ${v2.user.name}`); 356 | this.receive(v2); 357 | }) 358 | .catch((error) => { 359 | this.robot.emit('error', new Error(`Unable to fetch user details: ${error}`)); 360 | }); 361 | } 362 | } 363 | 364 | /** 365 | * 366 | * @param {GetUserArgsType} query 367 | * @param {string} streamId 368 | * @return {Promise.} Hubot user 369 | * @private 370 | */ 371 | _getUser(query: GetUserArgsType, streamId: string): Promise { 372 | return this.symphony.getUser(query) 373 | .then((response) => { 374 | // record basic user details in hubot's brain, setting the room causes the brain to update each time we're seen 375 | // in a new conversation 376 | const userId = response.id; 377 | const existing = this.robot.brain.userForId(userId); 378 | existing.name = response.username; 379 | existing.displayName = response.displayName; 380 | existing.emailAddress = response.emailAddress; 381 | if (streamId) { 382 | existing.room = streamId; 383 | } 384 | this.robot.brain.userForId(userId, existing); 385 | return existing; 386 | }); 387 | } 388 | } 389 | 390 | exports.use = (robot: Robot, optionsWithNulls: AdapterOptionsTypeWithNulls = {}) => { 391 | const options = { 392 | failConnectAfter: optionsWithNulls.failConnectAfter || 23, 393 | backoff: optionsWithNulls.backoff || backoff.exponential({ 394 | initialDelay: 10, 395 | maxDelay: 60000, 396 | }), 397 | shutdownFunc: optionsWithNulls.shutdownFunc || function(): void { 398 | process.exit(1); 399 | }, 400 | }; 401 | return new SymphonyAdapter(robot, options); 402 | }; 403 | -------------------------------------------------------------------------------- /test/nock-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | /* eslint-disable require-jsdoc */ 19 | 20 | import EventEmitter from 'events'; 21 | import querystring from 'querystring'; 22 | import nock from 'nock'; 23 | import uuid from 'uuid'; 24 | import Busboy from 'busboy'; 25 | import Log from 'log'; 26 | import type {EchoType, SymphonyMessageV2Type, SymphonyMessageV4Type, RoomInfoType, RoomInfoAlternateType} from '../src/symphony'; 27 | 28 | const logger: Log = new Log(process.env.HUBOT_SYMPHONY_LOG_LEVEL || process.env.HUBOT_LOG_LEVEL || 'info'); 29 | 30 | type ConstructorArgsType = { 31 | host: string, 32 | kmHost?: string, 33 | agentHost?: string, 34 | sessionAuthHost?: string, 35 | startWithHelloWorldMessage?: boolean 36 | }; 37 | 38 | type SymphonyCreateMessageV2PayloadType = { 39 | message: string, 40 | format: string 41 | }; 42 | 43 | type SymphonyCreateMessageV4PayloadType = { 44 | message: string, 45 | data?: string 46 | }; 47 | 48 | type KeyValuePairType = { 49 | key: string, 50 | value: string 51 | }; 52 | 53 | type SymphonyCreateRoomPayloadType = { 54 | name: string, 55 | description: string, 56 | keywords: Array, 57 | membersCanInvite: boolean, 58 | discoverable: boolean, 59 | public: boolean, 60 | readOnly: boolean, 61 | copyProtected: boolean 62 | }; 63 | 64 | type SymphonyUpdateRoomPayloadType = { 65 | name: string, 66 | description: string, 67 | keywords: Array, 68 | membersCanInvite: boolean, 69 | discoverable: boolean, 70 | copyProtected: boolean 71 | }; 72 | 73 | class NockServer extends EventEmitter { 74 | messages: Array; 75 | host: string; 76 | streamId: string; 77 | firstMessageTimestamp: string; 78 | datafeedId: string; 79 | realUserId: number; 80 | realUserName: string; 81 | realUserEmail: string; 82 | realUserDisplayName: string; 83 | botUserId: number; 84 | botUserName: string; 85 | botUserEmail: string; 86 | botUserDisplayName: string; 87 | _datafeedCreateHttp400Count: number; 88 | _datafeedReadHttp400Count: number; 89 | 90 | constructor(args: ConstructorArgsType) { 91 | super(); 92 | 93 | this.messages = []; 94 | this.host = args.host; 95 | this._datafeedCreateHttp400Count = 0; 96 | this._datafeedReadHttp400Count = 0; 97 | 98 | let kmHost = args.kmHost || args.host; 99 | let agentHost = args.agentHost || args.host; 100 | let sessionAuthHost = args.sessionAuthHost || agentHost; 101 | logger.info(`Setting up mocks for ${this.host} / ${kmHost} / ${agentHost} / ${sessionAuthHost}`); 102 | 103 | let self = this; 104 | 105 | this.streamId = 'WLwnGbzxIdU8ZmPUjAs_bn___qulefJUdA'; 106 | 107 | this.firstMessageTimestamp = '1461808889185'; 108 | 109 | this.realUserId = 7215545078229; 110 | this.realUserName = 'johndoe'; 111 | this.realUserEmail = 'johndoe@symphony.com'; 112 | this.realUserDisplayName = 'John Doe'; 113 | 114 | let realUserObject = { 115 | id: self.realUserId, 116 | emailAddress: self.realUserEmail, 117 | firstName: 'John', 118 | lastName: 'Doe', 119 | username: self.realUserName, 120 | displayName: self.realUserDisplayName, 121 | }; 122 | 123 | this.botUserId = 7696581411197; 124 | this.botUserName = 'mozart'; 125 | this.botUserEmail = 'mozart@symphony.com'; 126 | this.botUserDisplayName = 'Mozart'; 127 | 128 | let botUserObject = { 129 | id: self.realUserId, 130 | emailAddress: self.botUserEmail, 131 | firstName: 'Wolfgang Amadeus', 132 | lastName: 'Mozart', 133 | username: self.botUserName, 134 | displayName: self.botUserDisplayName, 135 | }; 136 | 137 | this.datafeedId = '1234'; 138 | 139 | if (args.startWithHelloWorldMessage || args.startWithHelloWorldMessage === undefined) { 140 | this.messages.push({ 141 | id: '-sfAvIPTTmyrpORkBuvL_3___qulZoKedA', 142 | timestamp: self.firstMessageTimestamp, 143 | v2messageType: 'V2Message', 144 | streamId: self.streamId, 145 | message: 'Hello World', 146 | fromUserId: self.realUserId, 147 | }); 148 | } 149 | 150 | nock.disableNetConnect(); 151 | 152 | let checkHeaderMissing = function(val: string): boolean { 153 | return val === undefined || val === null; 154 | }; 155 | 156 | /* eslint-disable no-unused-vars */ 157 | const defaultScope = nock(this.host) 158 | /* eslint-enable no-unused-vars */ 159 | .matchHeader('sessionToken', checkHeaderMissing) 160 | .matchHeader('keyManagerToken', checkHeaderMissing) 161 | .post('/agent/v1/util/echo') 162 | .reply(401, { 163 | code: 401, 164 | message: 'Invalid session', 165 | }); 166 | 167 | /* eslint-disable no-unused-vars */ 168 | const authScope = nock(sessionAuthHost) 169 | /* eslint-enable no-unused-vars */ 170 | .matchHeader('sessionToken', checkHeaderMissing) 171 | .matchHeader('keyManagerToken', checkHeaderMissing) 172 | .post('/sessionauth/v1/authenticate') 173 | .reply(200, { 174 | name: 'sessionToken', 175 | token: 'SESSION_TOKEN', 176 | }); 177 | 178 | /* eslint-disable no-unused-vars */ 179 | const keyAuthScope = nock(kmHost) 180 | /* eslint-enable no-unused-vars */ 181 | .matchHeader('sessionToken', checkHeaderMissing) 182 | .matchHeader('keyManagerToken', checkHeaderMissing) 183 | .post('/keyauth/v1/authenticate') 184 | .reply(200, { 185 | name: 'keyManagerToken', 186 | token: 'KEY_MANAGER_TOKEN', 187 | }); 188 | 189 | /* eslint-disable no-unused-vars */ 190 | const podScope = nock(this.host) 191 | /* eslint-enable no-unused-vars */ 192 | .persist() 193 | .matchHeader('sessionToken', 'SESSION_TOKEN') 194 | .matchHeader('keyManagerToken', checkHeaderMissing) 195 | .get('/pod/v1/sessioninfo') 196 | .reply(200, { 197 | userId: self.botUserId, 198 | }) 199 | .get(`/pod/v2/user?uid=${self.realUserId}&local=true`) 200 | .reply(200, realUserObject) 201 | .get(`/pod/v2/user?email=${self.realUserEmail}&local=true`) 202 | .reply(200, realUserObject) 203 | .get(`/pod/v2/user?username=${self.realUserName}&local=true`) 204 | .reply(200, realUserObject) 205 | .get(`/pod/v2/user?uid=${self.botUserId}&local=true`) 206 | .reply(200, botUserObject) 207 | .get(`/pod/v2/user?email=${self.botUserEmail}&local=true`) 208 | .reply(200, botUserObject) 209 | .get(`/pod/v2/user?username=${self.botUserName}&local=true`) 210 | .reply(200, botUserObject) 211 | .post('/pod/v1/im/create', [self.realUserId]) 212 | .reply(200, { 213 | id: self.streamId, 214 | }) 215 | .post('/pod/v2/room/create') 216 | .reply(200, function(uri: string, requestBody: SymphonyCreateRoomPayloadType): RoomInfoType { 217 | return { 218 | roomAttributes: requestBody, 219 | roomSystemInfo: { 220 | id: self.streamId, 221 | creationDate: 1464448273802, 222 | createdByUserId: self.botUserId, 223 | active: true, 224 | }, 225 | }; 226 | }) 227 | .get(`/pod/v2/room/${self.streamId}/info`) 228 | .reply(200, { 229 | roomAttributes: { 230 | name: 'foo', 231 | description: 'bar', 232 | keywords: [{key: 'x', value: 'y'}], 233 | membersCanInvite: false, 234 | discoverable: false, 235 | readOnly: false, 236 | copyProtected: false, 237 | public: false, 238 | }, 239 | roomSystemInfo: { 240 | id: self.streamId, 241 | creationDate: 1464448273802, 242 | createdByUserId: self.botUserId, 243 | active: true, 244 | }, 245 | }) 246 | .post(`/pod/v1/room/${self.streamId}/setActive`) 247 | .query(function(query): boolean { 248 | return query.hasOwnProperty('active'); 249 | }) 250 | .reply(200, function(uri: string, requestBody: mixed): RoomInfoAlternateType { 251 | const query = querystring.parse(uri.substring(uri.indexOf('?') + 1)); 252 | return { 253 | roomAttributes: { 254 | name: 'foo', 255 | description: 'bar', 256 | keywords: [{key: 'x', value: 'y'}], 257 | membersCanInvite: false, 258 | discoverable: false, 259 | }, 260 | roomSystemInfo: { 261 | id: self.streamId, 262 | creationDate: 1464448273802, 263 | createdByUserId: self.botUserId, 264 | active: query.active == 'true', 265 | }, 266 | immutableRoomAttributes: { 267 | readOnly: false, 268 | copyProtected: false, 269 | public: false, 270 | }, 271 | }; 272 | }) 273 | .post(`/pod/v2/room/${self.streamId}/update`) 274 | .reply(200, function(uri: string, requestBody: SymphonyUpdateRoomPayloadType): RoomInfoType { 275 | return { 276 | roomAttributes: { 277 | name: requestBody.name, 278 | description: requestBody.description, 279 | keywords: requestBody.keywords, 280 | membersCanInvite: requestBody.membersCanInvite, 281 | discoverable: requestBody.discoverable, 282 | copyProtected: requestBody.copyProtected, 283 | readOnly: false, 284 | public: false, 285 | }, 286 | roomSystemInfo: { 287 | id: self.streamId, 288 | creationDate: 1464448273802, 289 | createdByUserId: self.botUserId, 290 | active: true, 291 | }, 292 | }; 293 | }) 294 | .get(`/pod/v2/room/${self.streamId}/membership/list`) 295 | .reply(200, [ 296 | { 297 | id: self.botUserId, 298 | owner: true, 299 | joinDate: 1461426797875, 300 | }, 301 | { 302 | id: self.realUserId, 303 | owner: false, 304 | joinDate: 1461430710531, 305 | }, 306 | ]) 307 | .post(`/pod/v1/room/${self.streamId}/membership/add`) 308 | .reply(200, { 309 | format: 'TEXT', 310 | message: 'Member added', 311 | }) 312 | .post(`/pod/v1/room/${self.streamId}/membership/remove`) 313 | .reply(200, { 314 | format: 'TEXT', 315 | message: 'Member removed', 316 | }) 317 | .post(`/pod/v1/room/${self.streamId}/membership/promoteOwner`) 318 | .reply(200, { 319 | format: 'TEXT', 320 | message: 'Member promoted to owner', 321 | }) 322 | .post(`/pod/v1/room/${self.streamId}/membership/demoteOwner`) 323 | .reply(200, { 324 | format: 'TEXT', 325 | message: 'Member demoted to participant', 326 | }); 327 | 328 | /* eslint-disable no-unused-vars */ 329 | const agentScope = nock(agentHost) 330 | /* eslint-enable no-unused-vars */ 331 | .persist() 332 | .matchHeader('sessionToken', 'SESSION_TOKEN') 333 | .matchHeader('keyManagerToken', 'KEY_MANAGER_TOKEN') 334 | .post('/agent/v1/util/echo') 335 | .reply(200, function(uri: string, requestBody: EchoType): EchoType { 336 | return requestBody; 337 | }) 338 | .post(`/agent/v2/stream/${self.streamId}/message/create`) 339 | .reply(200, function(uri: string, requestBody: SymphonyCreateMessageV2PayloadType): SymphonyMessageV2Type { 340 | const message = { 341 | id: uuid.v1(), 342 | timestamp: new Date().valueOf().toString(), 343 | v2messageType: 'V2Message', 344 | streamId: self.streamId, 345 | message: requestBody.message, 346 | attachments: [], 347 | fromUserId: self.botUserId, 348 | }; 349 | self._receiveMessage(message); 350 | return message; 351 | }) 352 | .post(`/agent/v4/stream/${self.streamId}/message/create`) 353 | .reply(200, function(uri: string, requestBody: string, cb) { 354 | new Promise((resolve) => { 355 | const busboy = new Busboy({headers: this.req.headers}); 356 | let parts = {}; 357 | busboy.on('field', (fieldname, val) => { 358 | parts[fieldname] = val; 359 | }); 360 | busboy.on('finish', () => { 361 | resolve(parts); 362 | }); 363 | busboy.end(requestBody); 364 | }).then((parts) => { 365 | let messageML = parts.message; 366 | const match = /([\s\S]*)<\/messageML>/i.exec(messageML); 367 | if (match === undefined || match === null) { 368 | messageML = `${messageML}<\/messageML>`; 369 | } 370 | const message = { 371 | messageId: uuid.v1(), 372 | timestamp: new Date().valueOf().toString(), 373 | message: messageML, 374 | attachments: [], 375 | user: { 376 | userId: self.botUserId, 377 | displayName: self.botUserDisplayName, 378 | email: self.botUserEmail, 379 | username: self.botUserName, 380 | }, 381 | stream: { 382 | streamId: self.streamId, 383 | } 384 | }; 385 | self._receiveMessage({ 386 | id: message.messageId, 387 | timestamp: message.timestamp, 388 | v2messageType: 'V2Message', 389 | streamId: message.stream.streamId, 390 | message: message.message, 391 | attachments: message.attachments, 392 | fromUserId: message.user.userId, 393 | }); 394 | cb(null, message); 395 | }) 396 | }) 397 | .get(`/agent/v2/stream/${self.streamId}/message`) 398 | .reply(200, function(uri: string, requestBody: mixed) { 399 | return self.messages; 400 | }) 401 | .post('/agent/v1/datafeed/create') 402 | .reply(function(uri: string, requestBody: mixed) { 403 | if (self._datafeedCreateHttp400Count-- > 0) { 404 | return [400, null]; 405 | } 406 | return [200, {id: self.datafeedId}]; 407 | }) 408 | .get(`/agent/v2/datafeed/${self.datafeedId}/read`) 409 | .reply(function(uri: string, requestBody: mixed) { 410 | if (self._datafeedReadHttp400Count-- > 0) { 411 | return [400, null]; 412 | } 413 | if (self.messages.length == 0) { 414 | return [204, null]; 415 | } 416 | let copy = self.messages; 417 | self.messages = []; 418 | return [200, copy]; 419 | }); 420 | } 421 | 422 | set datafeedCreateHttp400Count(count: number) { 423 | this._datafeedCreateHttp400Count = count; 424 | } 425 | 426 | set datafeedReadHttp400Count(count: number) { 427 | this._datafeedReadHttp400Count = count; 428 | } 429 | 430 | close() { 431 | logger.info(`Cleaning up nock for ${this.host}`); 432 | nock.cleanAll(); 433 | } 434 | 435 | _receiveMessage(msg: SymphonyMessageV2Type) { 436 | logger.debug(`Received ${JSON.stringify(msg)}`); 437 | this.messages.push(msg); 438 | super.emit('received'); 439 | } 440 | } 441 | 442 | module.exports = NockServer; 443 | -------------------------------------------------------------------------------- /test/symphony-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | const assert = require('chai').assert; 20 | import {TextListener} from 'hubot'; 21 | import type {RoomMembershipType} from '../src/symphony'; 22 | import Symphony from '../src/symphony'; 23 | import {V2Message} from '../src/message'; 24 | import NockServer from './nock-server'; 25 | import FakeRobot from './fakes'; 26 | 27 | describe('On-premise key manager / agent', () => { 28 | let nock: NockServer; 29 | let symphony: Symphony; 30 | 31 | beforeEach(() => { 32 | nock = new NockServer({ 33 | host: 'https://foundation.symphony.com', 34 | kmHost: 'https://keymanager.notsymphony.com', 35 | agentHost: 'https://agent.alsonotsymphony.com', 36 | sessionAuthHost: 'https://foundation-api.symphony.com', 37 | }); 38 | symphony = new Symphony({ 39 | host: 'foundation.symphony.com', 40 | privateKey: './test/resources/privateKey.pem', 41 | publicKey: './test/resources/publicKey.pem', 42 | passphrase: 'changeit', 43 | keyManagerHost: 'keymanager.notsymphony.com', 44 | agentHost: 'agent.alsonotsymphony.com', 45 | sessionAuthHost: 'foundation-api.symphony.com', 46 | }); 47 | }); 48 | 49 | afterEach(() => { 50 | nock.close(); 51 | }); 52 | 53 | it('should connect to separate key manager / agent url', (done) => { 54 | const msg = {message: 'bar'}; 55 | symphony.echo(msg) 56 | .then((response) => { 57 | assert.deepEqual(msg, response); 58 | done(); 59 | }) 60 | .catch((error) => { 61 | done(`Failed with error ${error}`); 62 | }); 63 | }); 64 | 65 | it('should connect to separate pod url', (done) => { 66 | symphony.whoAmI() 67 | .then((response) => { 68 | assert.equal(nock.botUserId, response.userId); 69 | done(); 70 | }) 71 | .catch((error) => { 72 | done(`Failed with error ${error}`); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('REST API test suite', () => { 78 | let nock: NockServer; 79 | let symphony: Symphony; 80 | 81 | beforeEach(() => { 82 | nock = new NockServer({host: 'https://foundation.symphony.com'}); 83 | symphony = new Symphony({ 84 | host: 'foundation.symphony.com', 85 | privateKey: './test/resources/privateKey.pem', 86 | publicKey: './test/resources/publicKey.pem', 87 | passphrase: 'changeit', 88 | }); 89 | }); 90 | 91 | afterEach(() => { 92 | nock.close(); 93 | }); 94 | 95 | it('echo should obtain session and key tokens and echo response', (done) => { 96 | const msg = {message: 'bar'}; 97 | symphony.echo(msg) 98 | .then((response) => { 99 | assert.deepEqual(msg, response); 100 | done(); 101 | }) 102 | .catch((error) => { 103 | done(`Failed with error ${error}`); 104 | }); 105 | }); 106 | 107 | it('whoAmI should get userId', (done) => { 108 | symphony.whoAmI() 109 | .then((response) => { 110 | assert.equal(nock.botUserId, response.userId); 111 | done(); 112 | }) 113 | .catch((error) => { 114 | done(`Failed with error ${error}`); 115 | }); 116 | }); 117 | 118 | for (const [label, func] of [ 119 | ['getUser by email should expose user details', () => symphony.getUser({emailAddress: nock.realUserEmail})], 120 | ['getUser by username should expose user details', () => symphony.getUser({username: nock.realUserName})], 121 | ['getUser by userId should expose user details', () => symphony.getUser({userId: nock.realUserId})], 122 | ]) { 123 | it(label, (done) => { 124 | func() 125 | .then((response) => { 126 | assert.equal(nock.realUserId, response.id); 127 | assert.equal(nock.realUserName, response.username); 128 | assert.equal(nock.realUserEmail, response.emailAddress); 129 | done(); 130 | }) 131 | .catch((error) => { 132 | done(`Failed with error ${error}`); 133 | }); 134 | }); 135 | } 136 | 137 | it('sendMessage should obtain session and key tokens and get message ack', (done) => { 138 | const msg = 'Testing 123...'; 139 | symphony.sendMessage(nock.streamId, msg) 140 | .then((response) => { 141 | assert.equal(msg, response.message); 142 | assert.equal(nock.botUserId, response.user.userId); 143 | done(); 144 | }) 145 | .catch((error) => { 146 | done(`Failed with error ${error}`); 147 | }); 148 | }); 149 | 150 | it('getMessages should get all messages', (done) => { 151 | const msg = 'Yo!'; 152 | symphony.sendMessage(nock.streamId, msg) 153 | .then((response) => { 154 | assert.equal(msg, response.message); 155 | return symphony.getMessages(nock.streamId); 156 | }) 157 | .then((response) => { 158 | assert.isAtLeast(response.length, 2); 159 | assert.include(response.map((m) => m.message), 'Hello World'); 160 | assert.include(response.map((m) => m.message), msg); 161 | done(); 162 | }) 163 | .catch((error) => { 164 | done(`Failed with error ${error}`); 165 | }); 166 | }); 167 | 168 | it('createDatafeed should generate a datafeed id', (done) => { 169 | symphony.createDatafeed() 170 | .then((response) => { 171 | assert.equal(nock.datafeedId, response.id); 172 | done(); 173 | }) 174 | .catch((error) => { 175 | done(`Failed with error ${error}`); 176 | }); 177 | }); 178 | 179 | it('readDatafeed should pull messages', (done) => { 180 | const msg1 = 'foo'; 181 | const msg2 = 'bar'; 182 | symphony.createDatafeed() 183 | .then((initialResponse) => { 184 | // ensure that any previous message state is drained 185 | return symphony.readDatafeed(initialResponse.id) 186 | .then((response) => { 187 | return symphony.sendMessage(nock.streamId, msg1); 188 | }) 189 | .then((response) => { 190 | assert.equal(msg1, response.message); 191 | return symphony.readDatafeed(initialResponse.id); 192 | }) 193 | .then((response) => { 194 | assert.equal(1, response.length); 195 | assert.equal(msg1, response[0].message); 196 | return symphony.sendMessage(nock.streamId, msg2); 197 | }) 198 | .then((response) => { 199 | assert.equal(msg2, response.message); 200 | return symphony.readDatafeed(initialResponse.id); 201 | }) 202 | .then((response) => { 203 | assert.equal(1, response.length); 204 | assert.equal(msg2, response[0].message); 205 | done(); 206 | }); 207 | }) 208 | .catch((error) => { 209 | done(`Failed with error ${error}`); 210 | }); 211 | }); 212 | 213 | it('readDatafeed should not fail if no messages are available', (done) => { 214 | symphony.createDatafeed() 215 | .then((initialResponse) => { 216 | // ensure that any previous message state is drained 217 | symphony.readDatafeed(initialResponse.id) 218 | .then(() => { 219 | return symphony.readDatafeed(initialResponse.id); 220 | }) 221 | .then((response) => { 222 | assert.isUndefined(response); 223 | done(); 224 | }); 225 | }) 226 | .catch((error) => { 227 | done(`Failed with error ${error}`); 228 | }); 229 | }); 230 | 231 | it('createIM should generate a stream id', (done) => { 232 | symphony.createIM(nock.realUserId) 233 | .then((response) => { 234 | assert.equal(nock.streamId, response.id); 235 | done(); 236 | }) 237 | .catch((error) => { 238 | done(`Failed with error ${error}`); 239 | }); 240 | }); 241 | 242 | it('createRoom should generate room info', (done) => { 243 | const roomInfo = { 244 | name: 'foo', 245 | description: 'bar', 246 | keywords: new Map([['x', 'y']]), 247 | features: { 248 | membersCanInvite: true, 249 | }, 250 | }; 251 | symphony.createRoom(roomInfo) 252 | .then((response) => { 253 | assert.equal('foo', response.roomAttributes.name); 254 | assert.equal('bar', response.roomAttributes.description); 255 | assert.deepEqual([{key: 'x', value: 'y'}], response.roomAttributes.keywords); 256 | assert.isTrue(response.roomAttributes.membersCanInvite); 257 | assert.isFalse(response.roomAttributes.discoverable); 258 | assert.isFalse(response.roomAttributes.readOnly); 259 | assert.isFalse(response.roomAttributes.copyProtected); 260 | assert.isFalse(response.roomAttributes.public); 261 | assert.equal(nock.streamId, response.roomSystemInfo.id); 262 | assert.equal(nock.botUserId, response.roomSystemInfo.createdByUserId); 263 | assert.isTrue(response.roomSystemInfo.active); 264 | done(); 265 | }) 266 | .catch((error) => { 267 | done(`Failed with error ${error}`); 268 | }); 269 | }); 270 | 271 | it('getRoomInfo should return room info', (done) => { 272 | symphony.getRoomInfo(nock.streamId) 273 | .then((response) => { 274 | assert.equal('foo', response.roomAttributes.name); 275 | assert.equal('bar', response.roomAttributes.description); 276 | assert.deepEqual([{key: 'x', value: 'y'}], response.roomAttributes.keywords); 277 | assert.isFalse(response.roomAttributes.membersCanInvite); 278 | assert.isFalse(response.roomAttributes.discoverable); 279 | assert.isFalse(response.roomAttributes.readOnly); 280 | assert.isFalse(response.roomAttributes.copyProtected); 281 | assert.isFalse(response.roomAttributes.public); 282 | assert.equal(nock.streamId, response.roomSystemInfo.id); 283 | assert.equal(nock.botUserId, response.roomSystemInfo.createdByUserId); 284 | assert.isTrue(response.roomSystemInfo.active); 285 | done(); 286 | }) 287 | .catch((error) => { 288 | done(`Failed with error ${error}`); 289 | }); 290 | }); 291 | 292 | it('updateRoom should return room info', (done) => { 293 | const roomInfo = { 294 | name: 'foo1', 295 | description: 'bar2', 296 | keywords: new Map([['x', 'y3']]), 297 | features: { 298 | discoverable: true, 299 | }, 300 | }; 301 | symphony.updateRoom(nock.streamId, roomInfo) 302 | .then((response) => { 303 | assert.equal('foo1', response.roomAttributes.name); 304 | assert.equal('bar2', response.roomAttributes.description); 305 | assert.deepEqual([{key: 'x', value: 'y3'}], response.roomAttributes.keywords); 306 | assert.isFalse(response.roomAttributes.membersCanInvite); 307 | assert.isTrue(response.roomAttributes.discoverable); 308 | assert.isFalse(response.roomAttributes.readOnly); 309 | assert.isFalse(response.roomAttributes.copyProtected); 310 | assert.isFalse(response.roomAttributes.public); 311 | assert.equal(nock.streamId, response.roomSystemInfo.id); 312 | assert.equal(nock.botUserId, response.roomSystemInfo.createdByUserId); 313 | assert.isTrue(response.roomSystemInfo.active); 314 | done(); 315 | }) 316 | .catch((error) => { 317 | done(`Failed with error ${error}`); 318 | }); 319 | }); 320 | 321 | it('setRoomActiveStatus should return room info', (done) => { 322 | symphony.setRoomActiveStatus(nock.streamId, false) 323 | .then((response) => { 324 | assert.equal('foo', response.roomAttributes.name); 325 | assert.equal('bar', response.roomAttributes.description); 326 | assert.deepEqual([{key: 'x', value: 'y'}], response.roomAttributes.keywords); 327 | assert.isFalse(response.roomAttributes.membersCanInvite); 328 | assert.isFalse(response.roomAttributes.discoverable); 329 | assert.equal(nock.streamId, response.roomSystemInfo.id); 330 | assert.equal(nock.botUserId, response.roomSystemInfo.createdByUserId); 331 | assert.isFalse(response.roomSystemInfo.active); 332 | assert.isFalse(response.immutableRoomAttributes.readOnly); 333 | assert.isFalse(response.immutableRoomAttributes.copyProtected); 334 | assert.isFalse(response.immutableRoomAttributes.public); 335 | done(); 336 | }) 337 | .catch((error) => { 338 | done(`Failed with error ${error}`); 339 | }); 340 | }); 341 | 342 | it('getMembers should return members', (done) => { 343 | symphony.getMembers(nock.streamId) 344 | .then((response: Array) => { 345 | assert.lengthOf(response, 2); 346 | assert.deepInclude(response, { 347 | id: nock.botUserId, 348 | owner: true, 349 | joinDate: 1461426797875, 350 | }, 'contains bot user'); 351 | assert.deepInclude(response, { 352 | id: nock.realUserId, 353 | owner: false, 354 | joinDate: 1461430710531, 355 | }, 'contains real user'); 356 | done(); 357 | }) 358 | .catch((error) => { 359 | done(`Failed with error ${error}`); 360 | }); 361 | }); 362 | 363 | for (const [label, func, message] of [ 364 | [ 365 | 'addMember should acknowledge addition', 366 | () => symphony.addMember(nock.streamId, nock.realUserId), 'Member added', 367 | ], 368 | [ 369 | 'removeMember should acknowledge removal', 370 | () => symphony.removeMember(nock.streamId, nock.realUserId), 'Member removed', 371 | ], 372 | [ 373 | 'promoteMember should acknowledge promotion', 374 | () => symphony.promoteMember(nock.streamId, nock.realUserId), 'Member promoted to owner', 375 | ], 376 | [ 377 | 'demoteMember should acknowledge demotion', 378 | () => symphony.demoteMember(nock.streamId, nock.realUserId), 'Member demoted to participant', 379 | ], 380 | ]) { 381 | it(label, (done) => { 382 | func() 383 | .then((response) => { 384 | assert.equal(response.format, 'TEXT'); 385 | assert.equal(response.message, message); 386 | done(); 387 | }) 388 | .catch((error) => { 389 | done(`Failed with error ${error}`); 390 | }); 391 | }); 392 | } 393 | }); 394 | 395 | describe('Object model test suite', () => { 396 | for (const text of ['Hello World', 'Hello World']) { 397 | it(`parse a V2Message containing '${text}'`, () => { 398 | const msg = { 399 | id: 'foobar', 400 | timestamp: '1464629912263', 401 | v2messageType: 'V2Message', 402 | streamId: 'baz', 403 | message: text, 404 | fromUserId: 12345, 405 | }; 406 | const user = { 407 | id: 12345, 408 | name: 'johndoe', 409 | displayName: 'John Doe', 410 | }; 411 | const v2 = new V2Message(user, msg); 412 | assert.equal('Hello World', v2.text); 413 | assert.equal('foobar', v2.id); 414 | assert.equal(12345, v2.user.id); 415 | assert.equal('johndoe', v2.user.name); 416 | assert.equal('John Doe', v2.user.displayName); 417 | assert.equal('baz', v2.room); 418 | }); 419 | } 420 | 421 | it('regex test', (done) => { 422 | const msg = { 423 | id: 'foobar', 424 | timestamp: '1464629912263', 425 | v2messageType: 'V2Message', 426 | streamId: 'baz', 427 | message: 'butler ping', 428 | fromUserId: 12345, 429 | }; 430 | const user = { 431 | id: 12345, 432 | name: 'johndoe', 433 | displayName: 'John Doe', 434 | }; 435 | const robot = new FakeRobot(); 436 | const callback = () => { 437 | done(); 438 | }; 439 | const listener = new TextListener(robot, /^\s*[@]?butler[:,]?\s*(?:PING$)/i, {}, callback); 440 | listener.call(new V2Message(user, msg)); 441 | }); 442 | }); 443 | -------------------------------------------------------------------------------- /src/symphony.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Jon Freedman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // @flow 18 | 19 | import fs from 'fs'; 20 | import request from 'request'; 21 | import querystring from 'querystring'; 22 | import memoize from 'memoizee'; 23 | import Log from 'log'; 24 | 25 | const logger: Log = new Log(process.env.HUBOT_SYMPHONY_LOG_LEVEL || process.env.HUBOT_LOG_LEVEL || 'info'); 26 | 27 | type ConstructorArgsType = { 28 | host: string, 29 | privateKey: string, 30 | publicKey: string, 31 | passphrase: string, 32 | keyManagerHost?: string, 33 | sessionAuthHost?: string, 34 | agentHost?: string 35 | }; 36 | 37 | export type AuthenticateResponseType = { 38 | name: string, 39 | token: string 40 | }; 41 | 42 | export type EchoType = { 43 | message: string 44 | }; 45 | 46 | export type SymphonyUserIdType = { 47 | userId: number 48 | }; 49 | 50 | export type GetUserArgsType = { 51 | userId?: number, 52 | username?: string, 53 | emailAddress?: string 54 | }; 55 | 56 | export type SymphonyUserType = { 57 | id: number, 58 | emailAddress: string, 59 | firstName: string, 60 | lastName: string, 61 | displayName: string, 62 | company: string, 63 | username: string 64 | }; 65 | 66 | export type SymphonyAttachmentType = { 67 | id: string, 68 | name: string, 69 | size: number 70 | }; 71 | 72 | export type SymphonyMessageV2Type = { 73 | id: string, 74 | timestamp: string, 75 | v2messageType: string, 76 | streamId: string, 77 | message: string, 78 | attachments?: Array, 79 | fromUserId: number 80 | }; 81 | 82 | export type SymphonyMessageV4Type = { 83 | messageId: string, 84 | timestamp: string, 85 | message: string, 86 | data?: string, 87 | attachments?: Array, 88 | user: { 89 | userId: number, 90 | displayName: string, 91 | email: string, 92 | username: string 93 | }, 94 | stream: { 95 | streamId: string 96 | } 97 | }; 98 | 99 | export type CreateDatafeedResponseType = { 100 | id: string 101 | }; 102 | 103 | export type CreateIMResponseType = { 104 | id: string 105 | }; 106 | 107 | type KeyValuePairType = { 108 | key: string, 109 | value: string 110 | }; 111 | 112 | type CreateRoomFeaturesType = { 113 | membersCanInvite?: boolean, 114 | discoverable?: boolean, 115 | public?: boolean, 116 | readOnly?: boolean, 117 | copyProtected?: boolean 118 | }; 119 | 120 | export type CreateRoomType = { 121 | name: string, 122 | description: string, 123 | keywords: Map, 124 | features?: CreateRoomFeaturesType 125 | }; 126 | 127 | type UpdateRoomFeaturesType = { 128 | membersCanInvite?: boolean, 129 | discoverable?: boolean, 130 | copyProtected?: boolean 131 | }; 132 | 133 | export type UpdateRoomType = { 134 | name: string, 135 | description: string, 136 | keywords: Map, 137 | features?: UpdateRoomFeaturesType 138 | }; 139 | 140 | export type RoomInfoType = { 141 | roomAttributes: { 142 | name: string, 143 | keywords: Array, 144 | description: string, 145 | membersCanInvite: boolean, 146 | discoverable: boolean, 147 | readOnly: boolean, 148 | copyProtected: boolean, 149 | public: boolean 150 | }, 151 | roomSystemInfo: { 152 | id: string, 153 | creationDate: number, 154 | createdByUserId: number, 155 | active: boolean 156 | } 157 | }; 158 | 159 | export type RoomInfoAlternateType = { 160 | roomAttributes: { 161 | name: string, 162 | keywords: Array, 163 | description: string, 164 | membersCanInvite: boolean, 165 | discoverable: boolean 166 | }, 167 | roomSystemInfo: { 168 | id: string, 169 | creationDate: number, 170 | createdByUserId: number, 171 | active: boolean 172 | }, 173 | immutableRoomAttributes: { 174 | readOnly: boolean, 175 | copyProtected: boolean, 176 | public: boolean 177 | } 178 | }; 179 | 180 | export type RoomMembershipType = { 181 | id: number, 182 | owner: boolean, 183 | joinDate: number 184 | }; 185 | 186 | export type RoomMemberActionType = { 187 | format: string, 188 | message: string 189 | } 190 | 191 | type HttpHeaderType = { 192 | [key: string]: string 193 | }; 194 | 195 | type HttpResponseType = { 196 | statusCode: number 197 | }; 198 | 199 | /** 200 | * Wrapper of Symphony REST API 201 | * @author Jon Freedman 202 | */ 203 | class Symphony { 204 | host: string; 205 | keyManagerHost: string; 206 | sessionAuthHost: string; 207 | agentHost: string; 208 | privateKey: string; 209 | publicKey: string; 210 | passphrase: string; 211 | sessionAuth: () => Promise; 212 | keyAuth: () => Promise; 213 | 214 | /** 215 | * @param {ConstructorArgsType} args 216 | * @constructor 217 | */ 218 | constructor(args: ConstructorArgsType) { 219 | this.host = args.host; 220 | this.keyManagerHost = args.keyManagerHost || args.host; 221 | this.sessionAuthHost = args.sessionAuthHost || args.host; 222 | this.agentHost = args.agentHost || args.host; 223 | this.privateKey = args.privateKey; 224 | this.publicKey = args.publicKey; 225 | this.passphrase = args.passphrase; 226 | logger.info(`Connecting to ${this.host}`); 227 | if (this.keyManagerHost !== this.host) { 228 | logger.info(`Using separate KeyManager ${this.keyManagerHost}`); 229 | } 230 | if (this.sessionAuthHost !== this.host) { 231 | logger.info(`Using separate SessionAuth ${this.sessionAuthHost}`); 232 | } 233 | if (this.agentHost !== this.host) { 234 | logger.info(`Using separate Agent ${this.agentHost}`); 235 | } 236 | // refresh tokens on a weekly basis 237 | const weeklyRefresh = memoize(this._httpPost.bind(this), {maxAge: 604800000, length: 2}); 238 | this.sessionAuth = function(): Promise { 239 | return weeklyRefresh(this.sessionAuthHost, '/sessionauth/v1/authenticate'); 240 | }; 241 | this.keyAuth = function(): Promise { 242 | return weeklyRefresh(this.keyManagerHost, '/keyauth/v1/authenticate'); 243 | }; 244 | Promise.all([this.sessionAuth(), this.keyAuth()]).then((values: Array) => { 245 | const [sessionToken, kmToken] = values; 246 | logger.info(`Initialising with sessionToken: ${sessionToken.token} and keyManagerToken: ${kmToken.token}`); 247 | }); 248 | } 249 | 250 | /** 251 | * A test endpoint, which simply returns the input provided. 252 | * 253 | * See {@link https://rest-api.symphony.com/docs/echo|Echo} 254 | * 255 | * @param {EchoType} body 256 | * @return {Promise.} 257 | */ 258 | echo(body: EchoType): Promise { 259 | return this._httpAgentPost('/agent/v1/util/echo', body); 260 | } 261 | 262 | /** 263 | * Returns the userId of the calling user. 264 | * 265 | * See {@link https://rest-api.symphony.com/docs/session-info|Session User} 266 | * 267 | * @return {Promise.} 268 | */ 269 | whoAmI(): Promise { 270 | return this._httpPodGet('/pod/v1/sessioninfo'); 271 | } 272 | 273 | /** 274 | * Lookup a user by userId, email, or username. Searches are performed locally. 275 | * 276 | * See {@link https://rest-api.symphony.com/docs/user-lookup|User Lookup} 277 | * 278 | * @param {GetUserArgsType} args 279 | * @return {Promise.} 280 | */ 281 | getUser(args: GetUserArgsType): Promise { 282 | if (args.userId !== undefined && args.userId !== null) { 283 | return this._httpPodGet(`/pod/v2/user?uid=${args.userId}&local=true`); 284 | } 285 | if (args.username !== undefined && args.username !== null) { 286 | return this._httpPodGet(`/pod/v2/user?username=${args.username}&local=true`); 287 | } 288 | if (args.emailAddress !== undefined && args.emailAddress !== null) { 289 | return this._httpPodGet(`/pod/v2/user?email=${args.emailAddress}&local=true`); 290 | } 291 | return Promise.reject('No valid user argument supplied'); 292 | } 293 | 294 | /** 295 | * Posts a message to an existing stream. 296 | * 297 | * See {@link https://rest-api.symphony.com/docs/create-message-v4|Create Message} 298 | * 299 | * @param {string} streamId 300 | * @param {string} message 301 | * @return {Promise.} 302 | */ 303 | sendMessage(streamId: string, message: string): Promise { 304 | const formData = { 305 | message: { 306 | value: message, 307 | options: { 308 | contentType: 'text/plain' 309 | } 310 | } 311 | }; 312 | return this._httpAgentPost(`/agent/v4/stream/${streamId}/message/create`, undefined, formData); 313 | } 314 | 315 | /** 316 | * Posts a message with Structured Objects payload to an existing stream. 317 | * 318 | * See {@link https://rest-api.symphony.com/docs/create-message-v4|Create Message} 319 | * 320 | * @param {string} streamId 321 | * @param {string} message 322 | * @param {string} data 323 | * @return {Promise.} 324 | */ 325 | sendMessageWithStructuredObjects(streamId: string, message: string, data: string): Promise { 326 | const formData = { 327 | message: { 328 | value: message, 329 | options: { 330 | contentType: 'text/plain' 331 | } 332 | }, 333 | data: { 334 | value: data, 335 | options: { 336 | contentType: 'text/plain' 337 | } 338 | } 339 | }; 340 | return this._httpAgentPost(`/agent/v4/stream/${streamId}/message/create`, undefined, formData); 341 | } 342 | 343 | /** 344 | * Get messages from an existing stream (IM, MIM, or chatroom). Additionally returns any attachments associated with 345 | * the message. 346 | * 347 | * See {@link https://rest-api.symphony.com/docs/messages-v2|Messages} 348 | * 349 | * @param {string} streamId 350 | * @return {Promise.>} 351 | */ 352 | getMessages(streamId: string): Promise> { 353 | return this._httpAgentGet(`/agent/v2/stream/${streamId}/message`); 354 | } 355 | 356 | /** 357 | * Create a new real time messages / events stream ("datafeed"). The datafeed provides messages and events from all 358 | * conversations that the user is in. Returns the ID of the datafeed that has just been created. This ID should then 359 | * be used as input to the {@link Symphony#readDatafeed} endpoint. 360 | * 361 | * See {@link https://rest-api.symphony.com/docs/create-messagesevents-stream|Create Messages/Events Stream} 362 | * 363 | * @return {Promise.} 364 | */ 365 | createDatafeed(): Promise { 366 | return this._httpAgentPost('/agent/v1/datafeed/create', undefined); 367 | } 368 | 369 | /** 370 | * Read messages from a given real time messages / events stream ("datafeed"). The datafeed provides messages and 371 | * events from all conversations that the user is in. 372 | * 373 | * See {@link https://rest-api.symphony.com/docs/read-messagesevents-stream|Read Messages/Events Stream} 374 | * 375 | * @param {string} datafeedId 376 | * @return {Promise.>} 377 | */ 378 | readDatafeed(datafeedId: string): Promise> { 379 | return this._httpAgentGet(`/agent/v2/datafeed/${datafeedId}/read`); 380 | } 381 | 382 | /** 383 | * Creates a new single-party instant message conversation or returns an existing IM or MIM between the specified user 384 | * and the calling user. 385 | * 386 | * See {@link https://rest-api.symphony.com/docs/create-im-or-mim|Create IM or MIM} 387 | * 388 | * @param {number} userId 389 | * @return {Promise.} 390 | */ 391 | createIM(userId: number): Promise { 392 | return this._httpPodPost('/pod/v1/im/create', [userId]); 393 | } 394 | 395 | /** 396 | * Creates a new internal chatroom. 397 | * 398 | * See {@link https://rest-api.symphony.com/docs/create-room-v2|Create Room} 399 | * 400 | * @param {CreateRoomType} roomInfo 401 | * @return {Promise.} 402 | */ 403 | createRoom(roomInfo: CreateRoomType): Promise { 404 | const getFeature = function(feature: string): boolean { 405 | if (roomInfo.features) { 406 | return roomInfo.features[feature] || false; 407 | } 408 | return false; 409 | }; 410 | const body = { 411 | name: roomInfo.name, 412 | description: roomInfo.description, 413 | keywords: [], 414 | membersCanInvite: getFeature('membersCanInvite'), 415 | discoverable: getFeature('discoverable'), 416 | public: getFeature('public'), 417 | readOnly: getFeature('readOnly'), 418 | copyProtected: getFeature('copyProtected'), 419 | }; 420 | for (const [key, value] of roomInfo.keywords.entries()) { 421 | body.keywords.push({ 422 | key: key, 423 | value: value, 424 | }); 425 | } 426 | return this._httpPodPost('/pod/v2/room/create', body); 427 | } 428 | 429 | /** 430 | * Returns information about a particular chat room. 431 | * 432 | * See {@link https://rest-api.symphony.com/docs/room-info-v2|Room Info} 433 | * 434 | * @param {string} roomId 435 | * @return {Promise.} 436 | */ 437 | getRoomInfo(roomId: string): Promise { 438 | return this._httpPodGet(`/pod/v2/room/${roomId}/info`); 439 | } 440 | 441 | /** 442 | * Updates the attributes of an existing chat room. 443 | * 444 | * See {@link https://rest-api.symphony.com/docs/update-room-v2|Update Room} 445 | * 446 | * @param {string} roomId 447 | * @param {UpdateRoomType} roomInfo 448 | * @return {Promise.} 449 | */ 450 | updateRoom(roomId: string, roomInfo: UpdateRoomType): Promise { 451 | const getFeature = function(feature: string): boolean { 452 | if (roomInfo.features) { 453 | return roomInfo.features[feature] || false; 454 | } 455 | return false; 456 | }; 457 | const body = { 458 | name: roomInfo.name, 459 | description: roomInfo.description, 460 | keywords: [], 461 | membersCanInvite: getFeature('membersCanInvite'), 462 | discoverable: getFeature('discoverable'), 463 | copyProtected: getFeature('copyProtected'), 464 | }; 465 | for (const [key, value] of roomInfo.keywords.entries()) { 466 | body.keywords.push({ 467 | key: key, 468 | value: value, 469 | }); 470 | } 471 | return this._httpPodPost(`/pod/v2/room/${roomId}/update`, body); 472 | } 473 | 474 | /** 475 | * Deactivate or reactivate a chatroom. At creation, a new chatroom is active. 476 | * 477 | * See {@link https://rest-api.symphony.com/docs/de-or-re-activate-room|De/Re-activate Room} 478 | * 479 | * @param {string} roomId 480 | * @param {boolean} status 481 | * @return {Promise.} 482 | */ 483 | setRoomActiveStatus(roomId: string, status: boolean): Promise { 484 | return this._httpPodPost(`/pod/v1/room/${roomId}/setActive?${querystring.stringify({active: status})}`); 485 | } 486 | 487 | /** 488 | * Returns a list of all the current members of a stream (IM, MIM, or chatroom). 489 | * 490 | * See {@link https://rest-api.symphony.com/docs/stream-members|Stream Members} 491 | * 492 | * @param {string} roomId 493 | * @return {Promise.} 494 | */ 495 | getMembers(roomId: string): Promise> { 496 | return this._httpPodGet(`/pod/v2/room/${roomId}/membership/list`); 497 | } 498 | 499 | /** 500 | * Adds a new member to an existing room. 501 | * 502 | * See {@link https://rest-api.symphony.com/docs/add-member|Add Member} 503 | * 504 | * @param {string} roomId 505 | * @param {number} userId 506 | * @return {Promise.} 507 | */ 508 | addMember(roomId: string, userId: number): Promise { 509 | return this._httpPodPost(`/pod/v1/room/${roomId}/membership/add`, {id: userId}); 510 | } 511 | 512 | /** 513 | * Removes an existing member from an existing room. 514 | * 515 | * See {@link https://rest-api.symphony.com/docs/remove-member|Remove Member} 516 | * 517 | * @param {string} roomId 518 | * @param {number} userId 519 | * @return {Promise.} 520 | */ 521 | removeMember(roomId: string, userId: number): Promise { 522 | return this._httpPodPost(`/pod/v1/room/${roomId}/membership/remove`, {id: userId}); 523 | } 524 | 525 | /** 526 | * Promotes user to owner of the chat room. 527 | * 528 | * See {@link https://rest-api.symphony.com/docs/promote-owner|Promote Owner} 529 | * 530 | * @param {string} roomId 531 | * @param {number} userId 532 | * @return {Promise.} 533 | */ 534 | promoteMember(roomId: string, userId: number): Promise { 535 | return this._httpPodPost(`/pod/v1/room/${roomId}/membership/promoteOwner`, {id: userId}); 536 | } 537 | 538 | /** 539 | * Demotes room owner to a participant in the chat room. 540 | * 541 | * See {@link https://rest-api.symphony.com/docs/demote-owner|Demote Owner} 542 | * 543 | * @param {string} roomId 544 | * @param {number} userId 545 | * @return {Promise.} 546 | */ 547 | demoteMember(roomId: string, userId: number): Promise { 548 | return this._httpPodPost(`/pod/v1/room/${roomId}/membership/demoteOwner`, {id: userId}); 549 | } 550 | 551 | /** 552 | * Make a HTTP GET call against the Symphony pod API and return a Promise of the response. 553 | * 554 | * @param {string} path Symphony API path 555 | * @return {Promise.} 556 | * @template T 557 | * @private 558 | */ 559 | _httpPodGet(path: string): Promise { 560 | return this.sessionAuth().then((sessionToken: AuthenticateResponseType): Promise => { 561 | let headers = { 562 | sessionToken: sessionToken.token, 563 | }; 564 | return this._httpGet(this.host, path, headers); 565 | }); 566 | } 567 | 568 | /** 569 | * Make a HTTP POST call against the Symphony pod API and return a Promise of the response. 570 | * 571 | * @param {string} path Symphony API path 572 | * @param {mixed} body Message payload if appropriate 573 | * @return {Promise.} 574 | * @template T 575 | * @private 576 | */ 577 | _httpPodPost(path: string, body: ?mixed): Promise { 578 | return this.sessionAuth().then((sessionToken: AuthenticateResponseType): Promise => { 579 | let headers = { 580 | sessionToken: sessionToken.token, 581 | }; 582 | return this._httpPost(this.host, path, headers, body); 583 | }); 584 | } 585 | 586 | /** 587 | * Make a HTTP GET call against the Symphony agent API and return a Promise of the response. 588 | * 589 | * @param {string} path Symphony API path 590 | * @return {Promise.} 591 | * @template T 592 | * @private 593 | */ 594 | _httpAgentGet(path: string): Promise { 595 | return Promise.all([this.sessionAuth(), this.keyAuth()]) 596 | .then((values: Array): Promise => { 597 | const [sessionToken, keyManagerToken] = values; 598 | let headers = { 599 | sessionToken: sessionToken.token, 600 | keyManagerToken: keyManagerToken.token, 601 | }; 602 | return this._httpGet(this.agentHost, path, headers); 603 | }); 604 | } 605 | 606 | /** 607 | * Make a HTTP POST call against the Symphony agent API and return a Promise of the response. 608 | * 609 | * @param {string} path Symphony API path 610 | * @param {mixed} body Message payload if appropriate 611 | * @param {mixed} formData Form payload if appropriate 612 | * @return {Promise.} 613 | * @template T 614 | * @private 615 | */ 616 | _httpAgentPost(path: string, body: ?mixed, formData: ?mixed): Promise { 617 | return Promise.all([this.sessionAuth(), this.keyAuth()]) 618 | .then((values: Array): Promise => { 619 | const [sessionToken, keyManagerToken] = values; 620 | let headers = { 621 | sessionToken: sessionToken.token, 622 | keyManagerToken: keyManagerToken.token, 623 | }; 624 | return this._httpPost(this.agentHost, path, headers, body, formData); 625 | }); 626 | } 627 | 628 | /** 629 | * Make a HTTP GET call against Symphony and return a Promise of the response. 630 | * 631 | * @param {string} host Symphony host 632 | * @param {string} path Symphony API path 633 | * @param {HttpHeaderType} headers HTTP headers 634 | * @return {Promise.} 635 | * @template T 636 | * @private 637 | */ 638 | _httpGet(host: string, path: string, headers: HttpHeaderType = {}): Promise { 639 | return this._httpRequest('GET', host, path, headers, undefined); 640 | } 641 | 642 | /** 643 | * Make a HTTP POST call against Symphony and return a Promise of the response. 644 | * 645 | * @param {string} host Symphony host 646 | * @param {string} path Symphony API path 647 | * @param {HttpHeaderType} headers HTTP headers 648 | * @param {mixed} body Message payload if appropriate 649 | * @param {mixed} formData Form payload if appropriate 650 | * @return {Promise.} 651 | * @template T 652 | * @private 653 | */ 654 | _httpPost(host: string, path: string, headers: HttpHeaderType = {}, body: ?mixed, formData: ?mixed): Promise { 655 | return this._httpRequest('POST', host, path, headers, body, formData); 656 | } 657 | 658 | /** 659 | * Make a HTTP method call against Symphony and return a Promise of the response. 660 | * 661 | * @param {string} method HTTP method verb 662 | * @param {string} host Symphony host 663 | * @param {string} path Symphony API path 664 | * @param {HttpHeaderType} headers HTTP headers 665 | * @param {mixed} body Message payload if appropriate 666 | * @param {mixed} formData Form payload if appropriate 667 | * @return {Promise.} 668 | * @template T 669 | * @private 670 | */ 671 | _httpRequest(method: string, host: string, path: string, headers: HttpHeaderType, body: ?mixed, formData: ?mixed): Promise { 672 | let self = this; 673 | return new Promise((resolve, reject) => { 674 | let options = { 675 | baseUrl: `https://${host}`, 676 | url: path, 677 | json: true, 678 | headers: headers, 679 | method: method, 680 | key: fs.readFileSync(self.privateKey), 681 | cert: fs.readFileSync(self.publicKey), 682 | passphrase: self.passphrase, 683 | body: undefined, 684 | formData: undefined, 685 | }; 686 | if (body !== undefined && body !== null) { 687 | options.body = body; 688 | } 689 | if (formData !== undefined && formData !== null) { 690 | options.formData = formData; 691 | } 692 | logger.debug(`sending ${options.method} to https://${host}${path} [body: ${JSON.stringify(options.body) || 'undefined'}] [formData: ${JSON.stringify(options.formData) || 'undefined'}]`); 693 | request(options, (err, res: HttpResponseType, data: T) => { 694 | if (err !== undefined && err !== null) { 695 | const statusCode = res ? res.statusCode : 'unknown'; 696 | logger.warning(`received ${statusCode} error response from https://${host}${path}: ${err}`); 697 | reject(new Error(err)); 698 | } else if (Math.floor(res.statusCode / 100) !== 2) { 699 | const err = `received ${res.statusCode} response from https://${host}${path}: ${JSON.stringify(data) || 'undefined'}`; 700 | logger.warning(err); 701 | reject(new Error(err)); 702 | } else { 703 | logger.debug(`received ${res.statusCode} response from https://${host}${path}: ${JSON.stringify(data) || 'undefined'}`); 704 | resolve(data); 705 | } 706 | }); 707 | }); 708 | } 709 | } 710 | 711 | module.exports = Symphony; 712 | --------------------------------------------------------------------------------