Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
van-ibm committed Oct 16, 2017
0 parents commit ae38a2f
Show file tree
Hide file tree
Showing 9 changed files with 481 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
public/
.settings
.project
manifest.yml
*.log
.npmrc
.env
29 changes: 29 additions & 0 deletions graphql.js
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)}
}
}`
}
211 changes: 211 additions & 0 deletions index.js
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
58 changes: 58 additions & 0 deletions oauth.js
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))
}
27 changes: 27 additions & 0 deletions package.json
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"
}
}
32 changes: 32 additions & 0 deletions readme.md
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.
Loading

0 comments on commit ae38a2f

Please sign in to comment.