wingbot
Version:
Enterprise Messaging Bot Conversation Engine
290 lines (259 loc) • 9.56 kB
JavaScript
/**
* @author Ales Kalfas
*/
'use strict';
const { withParams } = require('webalize');
const { default: fetch, Headers } = require('node-fetch');
const BotAppSender = require('./BotAppSender');
const extractText = require('./transcript/extractText');
/** @typedef {import('./transcript/transcriptFromHistory').Transcript} Transcript */
/**
* @typedef OrchestratorClientOptions
* @property {string} [apiUrl]
* @property {Promise<string>|string} [secret]
* @property {string} [senderId]
* @property {string} [pageId]
* @property {string} [appId]
* @property {Function} [fetch]
* @property {boolean} [mock]
*/
class OrchestratorClient {
/**
* @param {OrchestratorClientOptions} options
*/
constructor (options) {
this._apiUrl = options.apiUrl;
this._secret = Promise.resolve(options.secret);
this._appId = options.appId;
this._senderId = options.senderId;
this._pageId = options.pageId;
this._fetch = options.fetch || fetch;
this._mock = !!options.mock;
}
/**
* Returns conversation token used to authorize and continue in a conversation
*
* @param {number} expirationInSeconds
* @returns {Promise<{conversationToken: string, expirationInSeconds: number}>}
*/
async getConversationToken (expirationInSeconds) {
const res = await this._send({
query: `query GetConversationToken($senderId: String!, $pageId: String!, $expirationInSeconds: Int!) {
chat {
conversationToken(senderId: $senderId, pageId: $pageId, expirationInSeconds: $expirationInSeconds)
}
}`,
variables: {
senderId: this._senderId,
pageId: this._pageId,
expirationInSeconds
}
});
const conversationToken = res.data
&& res.data
&& res.data.chat
&& res.data.chat.conversationToken;
return {
expirationInSeconds,
conversationToken
};
}
/**
* @returns {Promise<Transcript[]>}
*/
async getTranscript () {
const events = await this.getChatHistory();
return events
.map((e) => {
const text = extractText(e);
return {
fromBot: e.sender
? e.sender.id !== this._senderId
: e.recipient.id === this._senderId,
text,
timestamp: e.timestamp
};
})
.filter((ret) => !!ret.text);
}
/**
*
* @returns {Promise<object[]>}
*/
async getChatHistory () {
const res = await this._send({
query: `query FetchHistory (
$pageId: String!
$senderId: String!
) {
chat {
history (
pageId: $pageId,
senderId: $senderId
) {
events {
sender {
id
}
recipient {
id
}
mid
timestamp
sender_action
set_context
postback {
payload
title
}
expected {
input {
type
}
}
persona {
name
profile_pic_url
}
message {
text
is_confident
attachments {
type
payload {
url
template_type
image_aspect_ratio
text
buttons {
type
title
payload
url
}
elements {
default_action {
type
title
payload
url
}
title
subtitle
image_url
buttons {
type
title
payload
url
}
}
}
}
quick_replies {
content_type
title
payload
}
}
}
nextStartAt
nextEndAt
}
}
}`,
variables: {
senderId: this._senderId,
pageId: this._pageId
}
});
if (res.errors && res.errors[0]) {
throw new Error(res.errors[0].message);
}
const events = (res.data
&& res.data
&& res.data.chat
&& res.data.chat.history
&& res.data.chat.history.events) || [];
return events;
}
/**
* Create and add conversation token to url as a query param.
*
* E.g.:
* http://www.xxx.cz/aaa?param1=lambada
* =>
* http://www.xxx.cz/aaa?param1=lambada&token=aSDasjdasjdas
*
* @param {string} url
* @param {number} expirationInSeconds
* @param {string} tokenName - Query parameter name to store token
*/
async addConversationTokenToUrl (url, expirationInSeconds, tokenName = 'wbchtoken') {
const { conversationToken } = await this.getConversationToken(expirationInSeconds);
return withParams(url, {
query: { [tokenName]: conversationToken }
});
}
/**
* @param {object} payload
*/
async _send (payload) {
if (this._mock) {
const [, queryName] = payload.query.match(/^[a-z]+\s+([a-z]+)/i);
switch (queryName) {
case 'GetConversationToken':
return {
data: {
chat: {
conversationToken: 'mock-token'
}
}
};
case 'FetchHistory':
return {
data: {
chat: {
history: {
events: [
{
sender: {
id: this._senderId || 'sender'
},
recipient: {
id: this._pageId || 'sender'
},
message: {
text: 'Hello'
},
timestamp: 1234567890123
}
]
}
}
}
};
default:
return {};
}
}
const body = JSON.stringify(payload);
const token = await BotAppSender.signBody(
body,
this._secret,
this._appId
);
const headers = new Headers();
headers.set('Authorization', token);
headers.set('Content-Type', 'application/json');
const response = await this._fetch(this._apiUrl, { headers, body, method: 'POST' });
const responseJson = response.json();
const { errors = null } = responseJson;
if (errors) {
const [{ error, description = '', code = 500 }] = errors;
throw new Error(`[${code}] ${error} ${description}`);
}
return responseJson;
}
}
module.exports = OrchestratorClient;