forked from van-ibm/watsonworkspace-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ae38a2f
Showing
9 changed files
with
481 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
node_modules/ | ||
public/ | ||
.settings | ||
.project | ||
manifest.yml | ||
*.log | ||
.npmrc | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
function toString (fields) { | ||
return fields.reduce((accumulator, currentValue) => accumulator + ' ' + currentValue) | ||
} | ||
|
||
exports.addMessageFocus = `mutation AddMessageFocus($input: AddFocusInput!) { | ||
addMessageFocus(input: $input) { | ||
message { | ||
id | ||
annotations | ||
} | ||
} | ||
} | ||
}` | ||
|
||
exports.createTargetedMessage = `mutation CreateTargetedMessage($input: CreateTargetedMessageInput!) { | ||
createTargetedMessage(input: $input) { | ||
successful | ||
} | ||
} | ||
` | ||
|
||
exports.getMessage = (fields) => { | ||
// TODO make sure the id field is always present | ||
return `query GetMessage($id: ID!) { | ||
message(id: $id) { | ||
${toString(fields)} | ||
} | ||
}` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
'use strict' | ||
|
||
const graphql = require('./graphql') | ||
const request = require('request-promise') | ||
const logger = require('winston') | ||
const oauth = require('./oauth') | ||
const Promise = require('bluebird') | ||
const ui = require('./ui') | ||
|
||
const baseUrl = 'https://api.watsonwork.ibm.com' | ||
|
||
// export the logger to allow logger.level to be set | ||
exports.logger = logger | ||
|
||
function pick (property, promise) { | ||
return new Promise((resolve, reject) => { | ||
promise.then(response => resolve(response[property])) | ||
.catch(err => reject(err)) | ||
}) | ||
} | ||
|
||
function map (property, fn, promise) { | ||
return new Promise((resolve, reject) => { | ||
promise.then(response => { | ||
if (response[property]) { | ||
response[property] = response[property].map(fn) | ||
} else { | ||
logger.warn(`Map requested on missing property '${property}'`) | ||
} | ||
resolve(response) | ||
}) | ||
.catch(err => reject(err)) | ||
}) | ||
} | ||
|
||
function jsonify (obj) { | ||
return JSON.parse(obj) | ||
} | ||
|
||
exports.authenticate = (appId, appSecret) => { | ||
return new Promise((resolve, reject) => { | ||
const retry = 10000 | ||
let errors = 0 | ||
|
||
oauth.run( | ||
appId, | ||
appSecret, | ||
(err, token) => { | ||
if (err) { | ||
logger.error(`Failed to get JWT token; retrying in ${retry / 1000} seconds`) | ||
errors++ | ||
if (errors > 10) { | ||
reject(new Error(`Too many JWT token attempts giving up`)) | ||
} | ||
setTimeout(exports.authenticate, retry) | ||
return | ||
} | ||
|
||
// the token is stored in process.env to be shared with other modules | ||
process.env.jwtToken = token() | ||
resolve(token()) | ||
}) | ||
}) | ||
} | ||
|
||
exports.sendGraphql = (query) => { | ||
const headers = { | ||
'Content-Type': typeof query === 'string' ? 'application/json' : 'application/json', | ||
'x-graphql-view': 'PUBLIC, BETA' | ||
} | ||
|
||
return pick('data', exports.sendRequest(`graphql`, 'POST', headers, query)) | ||
} | ||
|
||
exports.sendRequest = (route, method, headers, body) => { | ||
// add the auth header for convenience | ||
headers.Authorization = `Bearer ${process.env.jwtToken}` | ||
|
||
const options = { | ||
method: method, | ||
uri: `${baseUrl}/${route}`, | ||
headers: headers, | ||
body: body, | ||
json: typeof body === 'object' | ||
} | ||
|
||
logger.verbose(`${method} to '${route}'`) | ||
logger.debug(headers) | ||
logger.debug(JSON.stringify(body, null, 1)) | ||
|
||
return request(options) | ||
} | ||
|
||
exports.getMessage = (id, fields) => { | ||
const json = { | ||
query: graphql.getMessage(fields), | ||
variables: { | ||
id: id | ||
} | ||
} | ||
|
||
return map('annotations', jsonify, pick('message', exports.sendGraphql(json))) | ||
} | ||
|
||
exports.sendMessage = (spaceId, content) => { | ||
logger.verbose(`Sending message to conversation '${spaceId}'`) | ||
|
||
const body = { | ||
type: 'appMessage', | ||
version: '1', | ||
annotations: [] | ||
} | ||
|
||
// determine the type of content the user is tying to send | ||
const type = typeof content | ||
switch (type) { | ||
case 'string': | ||
body.annotations.push(ui.message(content)) | ||
break | ||
case 'object': | ||
if (Array.isArray(content)) { | ||
body.annotations = content | ||
} else { | ||
body.annotations = [content] | ||
} | ||
break | ||
default: | ||
logger.error(`Error sending message of type '${type}'`) | ||
} | ||
|
||
return exports.sendRequest(`v1/spaces/${spaceId}/messages`, 'POST', {}, body) | ||
} | ||
|
||
exports.addMessageFocus = (message, phrase, lens, category, actions, payload) => { | ||
let id | ||
let pos = -1 | ||
|
||
if (message.id) { | ||
id = message.id | ||
} else { | ||
id = message.messageId | ||
} | ||
|
||
// the message's content differs based on how the message was created | ||
if (message.annotations && message.annotations[0].type === 'generic') { | ||
// app created | ||
pos = message.annotations[0].text.indexOf(phrase) | ||
} else { | ||
// user created | ||
pos = message.content.indexOf(phrase) | ||
} | ||
|
||
logger.info(`Adding message focus to message '${id}'`) | ||
|
||
const json = { | ||
query: graphql.addMessageFocus, | ||
variables: { | ||
input: { | ||
messageId: id, | ||
messageFocus: { | ||
phrase: phrase, | ||
lens: lens, | ||
category: category, | ||
actions: actions, | ||
confidence: 0.99, | ||
payload: payload, | ||
start: pos, | ||
end: pos + phrase.length, | ||
version: 1, | ||
hidden: false | ||
} | ||
} | ||
} | ||
} | ||
|
||
return exports.sendGraphql(json) | ||
} | ||
|
||
exports.sendTargetedMessage = (userId, annotation, items) => { | ||
logger.info(`Sending targetted message to user ${userId}`) | ||
|
||
const input = { | ||
conversationId: annotation.conversationId, | ||
targetUserId: userId, | ||
targetDialogId: annotation.targetDialogId | ||
} | ||
|
||
if (!Array.isArray(items)) { | ||
items = [items] | ||
} | ||
|
||
// check the type of user interface | ||
if (items[0].genericAnnotation) { | ||
// TODO allow an array of UI elements? | ||
input.annotations = items | ||
} else { | ||
// TODO allow an array of UI elements? | ||
input.attachments = items | ||
} | ||
|
||
const json = { | ||
query: graphql.createTargetedMessage, | ||
variables: { | ||
input: input | ||
} | ||
} | ||
|
||
return exports.sendGraphql(json) | ||
} | ||
|
||
exports.ui = ui |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
'use strict' | ||
|
||
const logger = require('winston') | ||
const jsonwebtoken = require('jsonwebtoken') | ||
const request = require('request') | ||
|
||
// Obtain an OAuth token for the app, repeat at regular intervals before the | ||
// token expires. Returns a function that will always return a current | ||
// valid token. | ||
exports.run = (appId, secret, cb) => { | ||
let tok | ||
|
||
// Return the current token | ||
const current = () => tok | ||
|
||
// Return the time to live of a token | ||
const ttl = (tok) => | ||
Math.max(0, jsonwebtoken.decode(tok).exp * 1000 - Date.now()) | ||
|
||
// Refresh the token | ||
const refresh = (cb) => { | ||
logger.info(`Requesting token for appId '${appId}' and secret '${secret.replace(/.*/, '*')}'`) | ||
|
||
request.post('https://api.watsonwork.ibm.com/oauth/token', { | ||
auth: { | ||
user: appId, | ||
pass: secret | ||
}, | ||
json: true, | ||
form: { | ||
grant_type: 'client_credentials' | ||
} | ||
}, (err, res) => { | ||
if (err || res.statusCode !== 200) { | ||
logger.error(`Error (${res.statusCode}) requesting token error '${err}'`) | ||
logger.error(res.body) | ||
|
||
cb(err || new Error(res.statusCode), current) | ||
return | ||
} | ||
|
||
// Save the fresh token | ||
logger.info(`Successfully requested token`) | ||
tok = res.body.access_token | ||
|
||
// Schedule next refresh a bit before the token expires | ||
const t = ttl(tok) | ||
logger.verbose('Token time-to-live', t) | ||
setTimeout(() => { refresh(cb) }, Math.max(0, t - 60000)).unref() | ||
|
||
// Return a function that'll return the current token | ||
cb(undefined, current) | ||
}) | ||
} | ||
|
||
// Obtain initial token | ||
setImmediate(() => refresh(cb)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"name": "watsonworkspace-sdk", | ||
"version": "0.0.1", | ||
"description": "An unofficial IBM Watson Workspace SDK", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "jasmine" | ||
}, | ||
"keywords": [ | ||
"watsonworkspace", | ||
"ibm", | ||
"workspace" | ||
], | ||
"author": "[email protected]", | ||
"license": "Apache-2.0", | ||
"devDependencies": { | ||
"dotenv": "^4.0.0", | ||
"jasmine": "^2.8.0" | ||
}, | ||
"dependencies": { | ||
"bluebird": "^3.5.1", | ||
"jsonwebtoken": "^8.1.0", | ||
"request": "^2.83.0", | ||
"request-promise": "^4.2.2", | ||
"winston": "^2.4.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# IBM Watson Workspace Javascript SDK | ||
|
||
An unofficial IBM Watson Workspace Javascript SDK. | ||
|
||
## Usage | ||
|
||
Include the SDK using Node.js require statements, authenticate, and begin running API commands. | ||
|
||
```Javascript | ||
const ww = require('watsonworkspace-sdk') | ||
|
||
it('authenticate', function (done) { | ||
ww.authenticate(process.env.APP_ID, process.env.APP_SECRET) | ||
.then(token => expect(token).not.toBe(null)) | ||
.catch(error => expect(error).toBeUndefined()) | ||
.finally(() => done()) | ||
}) | ||
|
||
var messageId | ||
|
||
it('sendMessage', function (done) { | ||
ww.sendMessage(spaceId, 'Hello from Watson Workspace SDK') | ||
.then(message => { | ||
messageId = message.id | ||
expect(message).not.toBe(null) | ||
}) | ||
.catch(error => expect(error).toBeUndefined()) | ||
.finally(() => done()) | ||
}) | ||
``` | ||
|
||
If using watsonworkspace-bot, you do not need to authenticate. |
Oops, something went wrong.