From 24882e118a26fe410b3637dee21098ac99f05a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Wed, 13 Dec 2017 16:34:10 +0100 Subject: [PATCH] Smarti 0.6.1 (#147) * WIP: Proxy for Smarti stub * WIP: Proxy for Smarti stub added to package ``` curl -X POST \ http://localhost:3000/api/v1/assistify/smarti/conversation/ \ -H 'content-type: application/json' \ -H 'x-auth-token: EKZ2_t7Wfqg5GML8bs44v3uS0d_b7f3rUd9IUYKNpvJ' \ //from login -H 'x-user-id: S4Mew2PDtEPr3Ejne' \ //from login -d '{ "thisIsMy":"JSON-Body" }' ``` * use injected logger for logging in API * Fixes #151 - Misspelled label "jetzt chaten" * Using the configured access token as HTTP header * Using the RC proxy for Smarti * Revert "Feature/#23 title first message to new request (#149)" This reverts commit 484b04cc461af0da7df1d7103de3ba8109e3ec61. * Revert "Fixes #151 - Misspelled label "jetzt chaten"" This reverts commit 0c9ac4f0f25a5863ac55f4c9256ff24ea07769c5. * Fixing lint errors. * Fixing non authorized access. * fixed issue with wrong knowledge provider indexes * improved tailing slashes for URLs * - add authorization checks before routing the API calls - roll back injected logger, since it is not defined * removed unused localization keys * Reducing code for adding a tailing slash to the Smarti URL. * consolidate constants naming * - Use RateLimiter for Smarti requests - Use propagation function for 'onMessage' and 'onClose' - Make Smarti the default knowledge provider - Reordered Smarti backend settings - Added descriptions for Smarti backend settings - Also reload the settings, when reloading the Smarti widget - Refactored file names * Using rate limiter in each proxy method and limit the propagateToSmarti function instead of HTTP.call * - merged proxy and adapter into only one file - only use DDP for Smarti Widget / Rocket.Chat messages (getConversation, getSmartiQueryBuilderResult) - added proxy endpoint for Smarti conversation search - added several comments * Migrate settings * Extract Smarti loader * additional migrations * Do not migrate old settings with other defaults * separated responsibilities * Simplify adapter, proxy, router, widgetBackend * Minor fixes to get it work with Smarti. * Fix Webhook-token not being transmitted * revert unintentional changes to other files after branching off the wron state. Copied all changes from `develop` which were not included in `assistify:ai`-package --- packages/assistify-ai/client/smartiLoader.js | 25 +++ .../client/views/app/tabbar/smarti.js | 58 +---- packages/assistify-ai/config.js | 41 ++-- packages/assistify-ai/package.js | 8 + packages/assistify-ai/server/SmartiProxy.js | 54 +++++ packages/assistify-ai/server/SmartiRouter.js | 36 +++ .../hooks/sendMessageToKnowledgeAdapter.js | 4 +- .../server/lib/KnowledgeAdapterProvider.js | 30 +-- packages/assistify-ai/server/lib/Smarti.js | 207 ------------------ .../assistify-ai/server/lib/SmartiAdapter.js | 97 ++++++++ .../server/methods/SmartiWidgetBackend.js | 138 ++++++++++++ .../server/methods/getSmartiUiScript.js | 75 ------- packages/assistify-ai/server/migrations.js | 33 +++ .../hooks/sendMessageToKnowledgeAdapter.js | 4 +- .../i18n/assistifyAI.de.i18n.yml | 41 ++-- .../i18n/assistifyAI.en.i18n.yml | 37 ++-- 16 files changed, 471 insertions(+), 417 deletions(-) create mode 100644 packages/assistify-ai/client/smartiLoader.js create mode 100644 packages/assistify-ai/server/SmartiProxy.js create mode 100644 packages/assistify-ai/server/SmartiRouter.js delete mode 100644 packages/assistify-ai/server/lib/Smarti.js create mode 100644 packages/assistify-ai/server/lib/SmartiAdapter.js create mode 100644 packages/assistify-ai/server/methods/SmartiWidgetBackend.js delete mode 100644 packages/assistify-ai/server/methods/getSmartiUiScript.js create mode 100644 packages/assistify-ai/server/migrations.js diff --git a/packages/assistify-ai/client/smartiLoader.js b/packages/assistify-ai/client/smartiLoader.js new file mode 100644 index 000000000000..a6d87c8b3b45 --- /dev/null +++ b/packages/assistify-ai/client/smartiLoader.js @@ -0,0 +1,25 @@ +/* globals RocketChat */ + +/** + * Load Smarti script asynchronously to the window. + * This ensures the invalidation as settings are changed and allow the script to live beyond template lifetime. + */ +RocketChat.settings.onload('Assistify_AI_Smarti_Base_URL', function() { + Meteor.call('getSmartiUiScript', function(error, script) { + if (error) { + console.error('could not load Smarti:', error.message); + } else { + // generate a script tag for smarti JS + const doc = document; + const smartiScriptTag = doc.createElement('script'); + smartiScriptTag.type = 'text/javascript'; + smartiScriptTag.async = true; + smartiScriptTag.defer = true; + smartiScriptTag.innerHTML = script; + // insert the smarti script tag as first script tag + const firstScriptTag = doc.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(smartiScriptTag, firstScriptTag); + console.debug('loaded Smarti successfully'); + } + }); +}); diff --git a/packages/assistify-ai/client/views/app/tabbar/smarti.js b/packages/assistify-ai/client/views/app/tabbar/smarti.js index e66f5ea3c19e..ac7fa6c52a23 100644 --- a/packages/assistify-ai/client/views/app/tabbar/smarti.js +++ b/packages/assistify-ai/client/views/app/tabbar/smarti.js @@ -1,4 +1,4 @@ -/* globals TAPi18n */ +/* globals TAPi18n, RocketChat */ Template.AssistifySmarti.onCreated(function() { this.helpRequest = new ReactiveVar(null); @@ -26,6 +26,7 @@ Template.AssistifySmarti.onDestroyed(function() { /** * Create Smarti (as soon as the script is loaded) + * @namespace SmartiWidget */ Template.AssistifySmarti.onRendered(function() { @@ -45,46 +46,23 @@ Template.AssistifySmarti.onRendered(function() { } } else { instance.smartiLoaded.set(true); - const DBS_AI_Redlink_URL = - RocketChat.settings.get('DBS_AI_Redlink_URL').endsWith('/') ? - RocketChat.settings.get('DBS_AI_Redlink_URL') : - `${ RocketChat.settings.get('DBS_AI_Redlink_URL') }/`; - - const SITE_URL_W_SLASH = - RocketChat.settings.get('Site_Url').endsWith('/') ? - RocketChat.settings.get('Site_Url') : - `${ RocketChat.settings.get('Site_Url') }/`; - + const ROCKET_CHAT_URL = RocketChat.settings.get('Site_Url').replace(/\/?$/, '/'); // stripping only the protocol ("http") from the site-url either creates a secure or an insecure websocket connection - const WEBSOCKET_URL = `ws${ SITE_URL_W_SLASH.substring(4) }websocket/`; - - let customSuffix = RocketChat.settings.get('Assistify_AI_DBSearch_Suffix') || ''; - customSuffix = customSuffix.replace(/\r\n|\r|\n/g, ''); - + const WEBSOCKET_URL = `ws${ ROCKET_CHAT_URL.substring(4) }websocket/`; const WIDGET_POSTING_TYPE = RocketChat.settings.get('Assistify_AI_Widget_Posting_Type') || 'postRichText'; - console.log(WIDGET_POSTING_TYPE, RocketChat.settings.get('Assistify_AI_Widget_Posting_Type')); + const SMARTI_CLIENT_NAME = RocketChat.settings.get('Assistify_AI_Smarti_Domain'); const smartiOptions = { socketEndpoint: WEBSOCKET_URL, - smartiEndpoint: DBS_AI_Redlink_URL, + clientName: SMARTI_CLIENT_NAME, channel: instance.data.rid, postings: { type: WIDGET_POSTING_TYPE, cssInputSelector: '.rc-message-box .js-input-message' }, - widget: { - 'query.dbsearch': { - numOfRows: 2, - suffix: customSuffix - }, - 'query.dbsearch.keyword': { - numOfRows: 2, - suffix: customSuffix, - disabled: true - } - }, lang: 'de' }; + console.debug('Initializing Smarti with options: ', JSON.stringify(smartiOptions, null, 2)); instance.smarti = new window.SmartiWidget(instance.find('.smarti-widget'), smartiOptions); } } @@ -131,25 +109,3 @@ Template.AssistifySmarti.helpers({ } }); -/** - * Load Smarti script - */ -RocketChat.settings.onload('DBS_AI_Redlink_URL', function() { - Meteor.call('getSmartiUiScript', function(error, script) { - if (error) { - console.error('could not load Smarti:', error.message); - } else { - // generate a script tag for smarti JS - const doc = document; - const smartiScriptTag = doc.createElement('script'); - smartiScriptTag.type = 'text/javascript'; - smartiScriptTag.async = true; - smartiScriptTag.defer = true; - smartiScriptTag.innerHTML = script; - // insert the smarti script tag as first script tag - const firstScriptTag = doc.getElementsByTagName('script')[0]; - firstScriptTag.parentNode.insertBefore(smartiScriptTag, firstScriptTag); - console.debug('loaded Smarti successfully'); - } - }); -}); diff --git a/packages/assistify-ai/config.js b/packages/assistify-ai/config.js index f513c39282a0..ea531bce621f 100644 --- a/packages/assistify-ai/config.js +++ b/packages/assistify-ai/config.js @@ -1,35 +1,35 @@ +/* globals RocketChat */ + Meteor.startup(() => { const addAISettings = function() { this.section('Knowledge_Base', function() { - this.add('DBS_AI_Enabled', false, { + this.add('Assistify_AI_Enabled', false, { type: 'boolean', public: true, i18nLabel: 'Enabled' }); - this.add('DBS_AI_Source', '', { + this.add('Assistify_AI_Source', '0', { type: 'select', values: [ - {key: '0', i18nLabel: 'DBS_AI_Source_APIAI'}, - {key: '1', i18nLabel: 'DBS_AI_Source_Redlink'}, - {key: '2', i18nLabel: 'DBS_AI_Source_Smarti'} + {key: '0', i18nLabel: 'Assistify_AI_Source_Smarti'}, + {key: '1', i18nLabel: 'Assistify_AI_Source_APIAI'} ], public: true, - i18nLabel: 'DBS_AI_Source' + i18nLabel: 'Assistify_AI_Source' }); - this.add('DBS_AI_Redlink_URL', '', { - type: 'string', - public: true, - i18nLabel: 'DBS_AI_Redlink_URL' + this.add('Assistify_AI_Reload', 'reloadSmarti', { + type: 'action', + actionText: 'Reload_Settings' }); - this.add('DBS_AI_Redlink_Hook_Token', '', { + this.add('Assistify_AI_Smarti_Base_URL', '', { type: 'string', public: true, - i18nLabel: 'DBS_AI_Redlink_Hook_Token' + i18nLabel: 'Assistify_AI_Smarti_Base_URL' }); let domain = RocketChat.settings.get('Site_Url'); @@ -41,10 +41,16 @@ Meteor.startup(() => { domain = domain.substr(0, domain.length - 1); } } - this.add('DBS_AI_Redlink_Domain', domain, { + this.add('Assistify_AI_Smarti_Domain', domain, { type: 'string', public: true, - i18nLabel: 'DBS_AI_Redlink_Domain' + i18nLabel: 'Assistify_AI_Smarti_Domain' + }); + + this.add('Assistify_AI_Smarti_Auth_Token', '', { + type: 'string', + public: true, + i18nLabel: 'Assistify_AI_Smarti_Auth_Token' }); this.add('Assistify_AI_Widget_Posting_Type', '', { @@ -58,9 +64,10 @@ Meteor.startup(() => { i18nLabel: 'Assistify_AI_Widget_Posting_Type' }); - this.add('reload_Assistify', 'reloadSmarti', { - type: 'action', - actionText: 'Reload_Settings' + this.add('Assistify_AI_RocketChat_Webhook_Token', '', { + type: 'string', + public: true, + i18nLabel: 'Assistify_AI_RocketChat_Webhook_Token' }); }); }; diff --git a/packages/assistify-ai/package.js b/packages/assistify-ai/package.js index c6004eaa42b0..2011bf3d6c2e 100755 --- a/packages/assistify-ai/package.js +++ b/packages/assistify-ai/package.js @@ -27,6 +27,13 @@ Package.onUse(function(api) { addDirectory(api, 'server/hooks', 'server'); addDirectory(api, 'server/methods', 'server'); + // Smarti proxy and router + api.addFiles('server/SmartiProxy.js', 'server'); + api.addFiles('server/SmartiRouter.js', 'server'); + + //migration scripts + api.addFiles('server/migrations.js', 'server'); + //Configuration api.addFiles('config.js', 'server'); @@ -36,6 +43,7 @@ Package.onUse(function(api) { //client views addDirectory(api, 'client/views/app/tabbar', 'client'); + api.addFiles('client/smartiLoader.js', 'client'); //styling api.addFiles('client/public/stylesheets/smarti.css', 'client'); diff --git a/packages/assistify-ai/server/SmartiProxy.js b/packages/assistify-ai/server/SmartiProxy.js new file mode 100644 index 000000000000..63eff8db4a15 --- /dev/null +++ b/packages/assistify-ai/server/SmartiProxy.js @@ -0,0 +1,54 @@ +/* globals SystemLogger, RocketChat */ + +/** The HTTP methods. */ +export const verbs = { + get: 'GET', + post: 'POST', + put: 'PUT', + delete: 'DELETE' +}; + +/** + * The proxy propagates the HTTP requests to Smarti. + * All HTTP outbound traffic (from Rocket.Chat to Smarti) should pass the this proxy. + */ +export class SmartiProxy { + + static get smartiAuthToken() { + return RocketChat.settings.get('Assistify_AI_Smarti_Auth_Token'); + } + + static get smartiUrl() { + return RocketChat.settings.get('Assistify_AI_Smarti_Base_URL'); + } + + /** + * Propagates requests to Smarti. + * Make sure all requests to Smarti are using this function. + * + * @param {String} method - the HTTP method to use + * @param {String} path - the path to call + * @param {Object} [body=null] - the payload to pass (optional) + * + * @returns {Object} + */ + static propagateToSmarti(method, path, body = null) { + + const url = `${ SmartiProxy.smartiUrl }${ path }`; + const header = { + 'X-Auth-Token': SmartiProxy.smartiAuthToken, + 'Content-Type': 'application/json; charset=utf-8' + }; + try { + const response = HTTP.call(method, url, {data: body, headers: header}); + if (response.statusCode === 200) { + return response.data || response.content; //.data if it's a json-response + } else { + SystemLogger.debug('Got unexpected result from Smarti', method, 'to', url, 'response', JSON.stringify(response)); + } + } catch (error) { + SystemLogger.error('Could not complete', method, 'to', url); + SystemLogger.debug(error); + } + } +} diff --git a/packages/assistify-ai/server/SmartiRouter.js b/packages/assistify-ai/server/SmartiRouter.js new file mode 100644 index 000000000000..66f2560601f1 --- /dev/null +++ b/packages/assistify-ai/server/SmartiRouter.js @@ -0,0 +1,36 @@ +/* globals SystemLogger, RocketChat */ + +import {SmartiAdapter} from './lib/SmartiAdapter'; + +/** + * The SmartiRouter handles all incoming HTTP requests from Smarti. + * This is the only place, where adding routes to the Rocket.Chat API, related to Smarti + * All HTTP inbound traffic (from Rocket.Chat to Smarti) should pass the this router. + */ + +/** + * Add an incoming webhook '/newConversationResult' to receive answers from Smarti. + * This allows asynchronous callback from Smarti, when analyzing the conversation has finished. + */ +RocketChat.API.v1.addRoute('smarti.result/:_token', {authRequired: false}, { + + post() { + + check(this.bodyParams, Match.ObjectIncluding({ + conversationId: String, + channelId: String + })); + + const rcWebhookToken = RocketChat.settings.get('Assistify_AI_RocketChat_Webhook_Token'); + + //verify token + if (this.urlParams._token && this.urlParams._token === rcWebhookToken) { + + SystemLogger.debug('Smarti - got conversation result:', JSON.stringify(this.bodyParams, null, 2)); + SmartiAdapter.analysisCompleted(this.bodyParams.channelId, this.bodyParams.conversationId, this.bodyParams.token); + return RocketChat.API.v1.success(); + } else { + return RocketChat.API.v1.unauthorized({msg: 'token not valid'}); + } + } +}); diff --git a/packages/assistify-ai/server/hooks/sendMessageToKnowledgeAdapter.js b/packages/assistify-ai/server/hooks/sendMessageToKnowledgeAdapter.js index eca2bb0687e5..bbb2c880ae6c 100755 --- a/packages/assistify-ai/server/hooks/sendMessageToKnowledgeAdapter.js +++ b/packages/assistify-ai/server/hooks/sendMessageToKnowledgeAdapter.js @@ -11,7 +11,7 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { } let knowledgeEnabled = false; - RocketChat.settings.get('DBS_AI_Enabled', function(key, value) { + RocketChat.settings.get('Assistify_AI_Enabled', function(key, value) { knowledgeEnabled = value; }); @@ -37,4 +37,4 @@ RocketChat.callbacks.add('afterSaveMessage', function(message, room) { }); return message; -}, RocketChat.callbacks.priority.LOW, 'dbsAI_OnMessage'); +}, RocketChat.callbacks.priority.LOW, 'Assistify_AI_OnMessage'); diff --git a/packages/assistify-ai/server/lib/KnowledgeAdapterProvider.js b/packages/assistify-ai/server/lib/KnowledgeAdapterProvider.js index 46a17c65ea98..9f1a23b5236d 100644 --- a/packages/assistify-ai/server/lib/KnowledgeAdapterProvider.js +++ b/packages/assistify-ai/server/lib/KnowledgeAdapterProvider.js @@ -1,15 +1,15 @@ /* globals RocketChat */ -import {SmartiAdapterFactory} from './Smarti'; +import {SmartiAdapter} from './SmartiAdapter'; import {ApiAiAdapter} from './AiApiAdapter'; export function getKnowledgeAdapter() { let knowledgeSource = ''; - const KNOWLEDGE_SRC_APIAI = '0'; - const KNOWLEDGE_SRC_SMARTI = '2'; + const KNOWLEDGE_SRC_SMARTI = '0'; + const KNOWLEDGE_SRC_APIAI = '1'; - RocketChat.settings.get('DBS_AI_Source', function(key, value) { + RocketChat.settings.get('Assistify_AI_Source', function(key, value) { knowledgeSource = value; }); @@ -29,28 +29,8 @@ export function getKnowledgeAdapter() { RocketChat.settings.get('Assistify_AI_Apiai_Language', function(key, value) { adapterProps.language = value; }); - return new ApiAiAdapter(adapterProps); case KNOWLEDGE_SRC_SMARTI: - return SmartiAdapterFactory.getInstance(); // buffering done inside the factory method + return SmartiAdapter; } } - - -/** - * Refreshes the adapter instances on change of the configuration - the redlink-adapter factory does that on its own - */ -//todo: refresh adapter instances on change of the configuration. Observe a raw cursor -// Meteor.autorun(()=> { -// RocketChat.settings.get('DBS_AI_Source', function(key) { -// _dbs.apiaiAdapter = undefined; -// }); -// -// RocketChat.settings.get('Assistify_AI_Apiai_Key', function(key) { -// _dbs.apiaiAdapter = undefined; -// }); -// -// RocketChat.settings.get('Assistify_AI_Apiai_Language', function(key) { -// _dbs.apiaiAdapter = undefined; -// }); -// }); diff --git a/packages/assistify-ai/server/lib/Smarti.js b/packages/assistify-ai/server/lib/Smarti.js deleted file mode 100644 index fbf94506a7e9..000000000000 --- a/packages/assistify-ai/server/lib/Smarti.js +++ /dev/null @@ -1,207 +0,0 @@ -/* globals SystemLogger, RocketChat */ - -class SmartiAdapter { - constructor(adapterProps) { - - this.properties = adapterProps; - this.properties.url = this.properties.url.toLowerCase(); - - this.options = {}; - this.options.headers = {}; - this.options.headers['content-Type'] = 'application/json; charset=utf-8'; - if (this.properties.token) { - this.options.headers['authorization'] = `basic ${ this.properties.token }`; - } - if (this.properties.url.substring(0, 4) === 'https') { - this.options.cert = `~/.nodeCaCerts/${ this.properties.url.replace('https', '') }`; - } - } - - onMessage(message) { - - //TODO is this always a new one, what about update - - const helpRequest = RocketChat.models.HelpRequests.findOneByRoomId(message.rid); - - const supportArea = helpRequest ? helpRequest.supportArea : undefined; - - const requestBody = { - webhook_url: this.properties.webhookUrl, - message_id: message._id, - channel_id: message.rid, - user_id: message.u._id, - // username: message.u.username, - text: message.msg, - timestamp: message.ts, - origin: message.origin, - support_area: supportArea - }; - - try { - const options = this.options; - this.options.data = requestBody; - SystemLogger.debug('Smarti - trigger analysis:', JSON.stringify(options, null, 2)); - - const URL = `${ this.properties.url }rocket/${ RocketChat.settings.get('DBS_AI_Redlink_Domain') }`; - SystemLogger.info(`Send post request: ${ URL } with options: ${ JSON.stringify(options, null, 2) }`); - const response = HTTP.post(URL, options); - - if (response.statusCode === 200) { - SystemLogger.debug('Smarti - analysis triggered successfully:', JSON.stringify(response, null, 2)); - } else { - SystemLogger.error('Smarti - analysis triggering failed:', JSON.stringify(response, null, 2)); - } - } catch (e) { - SystemLogger.error(`Smarti response for ${ URL } did not succeed:`, e); - } - } - - onClose(room) { //async - //TODO add options here? - //get conversation id - const m = RocketChat.models.LivechatExternalMessage.findOneById(room._id); - - if (m) { - const URL = `${ this.properties.url }conversation/${ m.conversationId }/publish`; - SystemLogger.info(`Send post request: ${ URL }`); - const response = HTTP.post(URL); - if (response.statusCode === 200) { - SystemLogger.debug('Smarti - closed room successfully:', room._id, JSON.stringify(response, null, 2)); - } else { - SystemLogger.error('Smarti - closing room failed:', room._id, JSON.stringify(response, null, 2)); - } - } else { - SystemLogger.error(`Smarti - closing room failed: No conversation id for room: ${ room._id }`); - } - } - -} - -class SmartiMock extends SmartiAdapter { - - constructor(adapterProps) { - super(adapterProps); - this.properties.url = 'http://localhost:8080'; - delete this.headers.authorization; - } -} - -export class SmartiAdapterFactory { - - constructor() { - this.singleton = undefined; - - /** - * Refreshes the adapter instances on change of the configuration - */ - //todo: validate it works - const factory = this; - RocketChat.settings.onload('DBS_AI_Source', () => { - factory.singleton = null; - }); - - RocketChat.settings.onload('DBS_AI_Redlink_URL', () => { - factory.singleton = null; - }); - - RocketChat.settings.onload('DBS_AI_Redlink_Auth_Token', () => { - factory.singleton = null; - }); - - RocketChat.settings.onload('DBS_AI_Redlink_Hook_Token', () => { - factory.singleton = null; - }); - } - - static getInstance() { - if (this.singleton) { - return this.singleton; - } else { - const adapterProps = { - url: '', - token: '', - language: '' - }; - - const DBS_AI_Redlink_URL = - RocketChat.settings.get('DBS_AI_Redlink_URL').endsWith('/') ? - RocketChat.settings.get('DBS_AI_Redlink_URL') : - `${ RocketChat.settings.get('DBS_AI_Redlink_URL') }/`; - - const SITE_URL_W_SLASH = - RocketChat.settings.get('Site_Url').endsWith('/') ? - RocketChat.settings.get('Site_Url') : - `${ RocketChat.settings.get('Site_Url') }/`; - - adapterProps.url = DBS_AI_Redlink_URL; - - adapterProps.token = RocketChat.settings.get('DBS_AI_Redlink_Auth_Token'); - - adapterProps.webhookUrl = `${ SITE_URL_W_SLASH }api/v1/smarti.result/${ RocketChat.settings.get('DBS_AI_Redlink_Hook_Token') }`; - - SystemLogger.debug(RocketChat.settings); - - const useMock = false; - if (useMock) { //todo: proper mocking - this.singleton = new SmartiMock(adapterProps); - } else { - this.singleton = new SmartiAdapter(adapterProps); - } - return this.singleton; - } - } -} - -/** - * add method to get conversation id via realtime api - */ -Meteor.methods({ - getLastSmartiResult(rid) { - - //Todo: check if the user is allowed to get this results! - - SystemLogger.debug('Smarti - last smarti result requested:', JSON.stringify(rid, '', 2)); - SystemLogger.debug('Smarti - last message:', JSON.stringify(RocketChat.models.LivechatExternalMessage.findOneById(rid), '', 2)); - return RocketChat.models.LivechatExternalMessage.findOneById(rid); - } -}); - -/** - * add incoming webhook - */ -RocketChat.API.v1.addRoute('smarti.result/:token', {authRequired: false}, { - post() { - - check(this.bodyParams, Match.ObjectIncluding({ - conversationId: String, - channelId: String - })); - - //verify token - if (this.urlParams.token && this.urlParams.token === RocketChat.settings.get('DBS_AI_Redlink_Hook_Token')) { - - SystemLogger.debug('Smarti - got conversation result:', JSON.stringify(this.bodyParams, null, 2)); - RocketChat.models.LivechatExternalMessage.update( - { - _id: this.bodyParams.channelId - }, { - rid: this.bodyParams.channelId, - knowledgeProvider: 'smarti', - conversationId: this.bodyParams.conversationId, - token: this.bodyParams.token, - ts: new Date() - }, { - upsert: true - }); - - const m = RocketChat.models.LivechatExternalMessage.findOneById(this.bodyParams.channelId); - RocketChat.Notifications.notifyRoom(this.bodyParams.channelId, 'newConversationResult', m); - return RocketChat.API.v1.success(); - } else { - return RocketChat.API.v1.unauthorized({msg: 'token not valid'}); - } - } -}); - - - diff --git a/packages/assistify-ai/server/lib/SmartiAdapter.js b/packages/assistify-ai/server/lib/SmartiAdapter.js new file mode 100644 index 000000000000..e975264885e1 --- /dev/null +++ b/packages/assistify-ai/server/lib/SmartiAdapter.js @@ -0,0 +1,97 @@ +/* globals SystemLogger, RocketChat */ + +import {SmartiProxy, verbs} from '../SmartiProxy'; + +/** + * The SmartiAdapter handles the interaction with Smarti triggered by Rocket.Chat hooks (not by Smarti widget). + * This adapter has no state, as all settings are fully buffered. Thus, the complete class is static. + */ +export class SmartiAdapter { + + static get rocketWebhookUrl() { + let rocketUrl = RocketChat.settings.get('Site_Url'); + rocketUrl = rocketUrl ? rocketUrl.replace(/\/?$/, '/') : rocketUrl; + return `${ rocketUrl }api/v1/smarti.result/${ RocketChat.settings.get('Assistify_AI_RocketChat_Webhook_Token') }`; + } + + static get smartiKnowledgeDomain() { + return RocketChat.settings.get('Assistify_AI_Smarti_Domain'); + } + + /** + * Event implementation that posts the message to Smarti. + * + * @param {object} message: { + * _id: STRING, + * rid: STRING, + * u: {*}, + * msg: STRING, + * ts: NUMBER, + * origin: STRING + * } + * + * @returns {*} + */ + static onMessage(message) { + + //TODO is this always a new one, what about update + const helpRequest = RocketChat.models.HelpRequests.findOneByRoomId(message.rid); + const supportArea = helpRequest ? helpRequest.supportArea : undefined; + const requestBody = { + // TODO: Should this really be in the responsibility of the Adapter? + webhook_url: SmartiAdapter.rocketWebhookUrl, + message_id: message._id, + channel_id: message.rid, + user_id: message.u._id, + // username: message.u.username, + text: message.msg, + timestamp: message.ts, + origin: message.origin, + support_area: supportArea + }; + return SmartiProxy.propagateToSmarti(verbs.post, `rocket/${ SmartiAdapter.smartiKnowledgeDomain }`, requestBody); + } + + /** + * Event implementation that publishes the conversation in Smarti. + * + * @param room - the room to close + * + * @returns {*} + */ + static onClose(room) { //async + + // get conversation id + const m = RocketChat.models.LivechatExternalMessage.findOneById(room._id); + if (m) { + SmartiProxy.propagateToSmarti(verbs.post, `conversation/${ m.conversationId }/publish`); + } else { + SystemLogger.error(`Smarti - closing room failed: No conversation id for room: ${ room._id }`); + } + } + + /** + * This method provides an implementation for a hook registering an asynchronously sent response from Smarti to RocketChat + * + * @param roomId + * @param smartiConversationId + * @param token + */ + static analysisCompleted(roomId, smartiConversationId, token) { + RocketChat.models.LivechatExternalMessage.update( + { + _id: roomId + }, { + rid: roomId, + knowledgeProvider: 'smarti', + conversationId: smartiConversationId, + token, + ts: new Date() + }, { + upsert: true + } + ); + + RocketChat.Notifications.notifyRoom(roomId, 'newConversationResult', RocketChat.models.LivechatExternalMessage.findOneById(roomId)); + } +} diff --git a/packages/assistify-ai/server/methods/SmartiWidgetBackend.js b/packages/assistify-ai/server/methods/SmartiWidgetBackend.js new file mode 100644 index 000000000000..26b04bc00cda --- /dev/null +++ b/packages/assistify-ai/server/methods/SmartiWidgetBackend.js @@ -0,0 +1,138 @@ +/* globals SystemLogger, RocketChat */ + +import {SmartiProxy, verbs} from '../SmartiProxy'; + +/** @namespace RocketChat.RateLimiter.limitFunction */ + +/** + * The SmartiWidgetBackend handles all interactions triggered by the Smarti widget (not by Rocket.Chat hooks). + * These 'Meteor.methods' are made available to be accessed via DDP, to be used in the Smarti widget. + */ +Meteor.methods({ + + /** + * Returns the conversation Id for the given client and its channel. + * + * @param {String} channelId - the channel Id + * + * @returns {String} - the conversation Id + */ + getConversationId(channelId) { + + const clientDomain = RocketChat.settings.get('Assistify_AI_Smarti_Domain'); + return RocketChat.RateLimiter.limitFunction( + SmartiProxy.propagateToSmarti, 5, 1000, { + userId(userId) { + return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); + } + } + )(verbs.get, `rocket/${ clientDomain }/${ channelId }/conversationid`); + // Smarti only returns the plain id (no JSON Object), therefore we do not get an response.data obeject. + // use body instead + // TODO: Smarti release 0.7.0 should return a valid JSON + }, + + /** + * Returns the analyzed conversation by id. + * + * @param {String} conversationId - the conversation to retrieve + * + * @returns {Object} - the analysed conversation + */ + getConversation(conversationId) { + + return RocketChat.RateLimiter.limitFunction( + SmartiProxy.propagateToSmarti, 5, 1000, { + userId(userId) { + return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); + } + } + )(verbs.get, `conversation/${ conversationId }`); + }, + + /** + * Returns the query builder results for the given conversation (used by Smarti widget) + * + * @param {String} conversationId - the conversation id to get results for + * @param {Number} templateIndex - the index of the template to get the results for + * @param {String} creator - the creator providing the suggestions + * @param {Number} start - the offset of the suggestion results (pagination) + * + * @returns {Object} - the suggestions + */ + getQueryBuilderResult(conversationId, templateIndex, creator, start) { + + return RocketChat.RateLimiter.limitFunction( + SmartiProxy.propagateToSmarti, 5, 1000, { + userId(userId) { + return !RocketChat.authz.hasPermission(userId, 'send-many-messages'); + } + } + )(verbs.get, `conversation/${ conversationId }/template/${ templateIndex }/${ creator }?start=${ start }`); + } +}); + + +//////////////////////////////////////////// +//////// LOAD THE SMARTI JavaScript //////// +//////////////////////////////////////////// + +// TODO: Prevent writing JavaScript into a inline