diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1f80d12 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules +dist +.github +i18n +.* \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..650dc32 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + "no-case-declarations": "off", + "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] + }, +}; diff --git a/.gitignore b/.gitignore index 6db8a93..6abc5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ Temporary Items .editorconfig .vscode + +# related to rc-apps cli +.rcappsconfig \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..20d0d06 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npm run lint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..801eb0a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +dist +.* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3f5f64a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "useTabs": true, + "tabWidth": 4 +} diff --git a/app.json b/app.json index c300f4f..c65f992 100644 --- a/app.json +++ b/app.json @@ -1,6 +1,6 @@ { "id": "21b7d3ba-031b-41d9-8ff2-fbbfa081ae90", - "version": "1.2.3", + "version": "1.2.5", "requiredApiVersion": "^1.17.0", "iconFile": "icon.png", "author": { @@ -11,7 +11,7 @@ "name": "Dialogflow", "nameSlug": "dialogflow", "classFile": "DialogflowApp.ts", - "description": "Integration between Rocket.Chat and the Dialogflow Chatbot platform", + "description": "Integration between Rocket.Chat and the Dialogflow Chatbot platform. Relevant documentation of this app can be found [here](https://docs.rocket.chat/guides/app-guides/omnichannel-apps/dialogflow-app)", "implements": [ "IPostMessageSent", "IPostLivechatAgentAssigned", @@ -19,5 +19,5 @@ "IPostLivechatRoomClosed", "IUIKitLivechatInteractionHandler" ], - "commitHash": "5b598b81b19d0702a9291d58f9ed4c81e23cb5d0" + "commitHash": "4217396b4410a11d93540c83814702659c9e3dad" } \ No newline at end of file diff --git a/config/Settings.ts b/config/Settings.ts index e6dde6a..678e4f2 100644 --- a/config/Settings.ts +++ b/config/Settings.ts @@ -1,4 +1,8 @@ -import { ISetting, SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import { + ISetting, + ISettingSelectValue, + SettingType, +} from '@rocket.chat/apps-engine/definition/settings'; export enum AppSetting { DialogflowBotList = 'agents', @@ -85,6 +89,36 @@ const agentConfigTemplate = JSON.stringify( }, }], null, '\t'); +export const LanguageCode: Array = [ + { key: 'zh-CN', i18nLabel: 'Chinese - Simplified' }, + { key: 'da', i18nLabel: 'Danish' }, + { key: 'nl', i18nLabel: 'Dutch' }, + { key: 'en', i18nLabel: 'English' }, + { key: 'en-AU', i18nLabel: 'English - Australia' }, + { key: 'en-CA', i18nLabel: 'English - Canada' }, + { key: 'en-GB', i18nLabel: 'English - Great Britain' }, + { key: 'en-IN', i18nLabel: 'English - India' }, + { key: 'en-US', i18nLabel: 'English - US' }, + { key: 'fr-CA', i18nLabel: 'French - Canada' }, + { key: 'fr-FR', i18nLabel: 'French - France' }, + { key: 'de', i18nLabel: 'German' }, + { key: 'hi', i18nLabel: 'Hindi' }, + { key: 'id', i18nLabel: 'Indonesian' }, + { key: 'it', i18nLabel: 'Italian' }, + { key: 'ja', i18nLabel: 'Japanese' }, + { key: 'ko', i18nLabel: 'Korean' }, + { key: 'no', i18nLabel: 'Norwegian' }, + { key: 'pl', i18nLabel: 'Polish' }, + { key: 'pt-BR', i18nLabel: 'Portuguese - Brazil' }, + { key: 'pt', i18nLabel: 'Portuguese - Portugal' }, + { key: 'ru', i18nLabel: 'Russian' }, + { key: 'es', i18nLabel: 'Spanish' }, + { key: 'es-ES', i18nLabel: 'Spanish - Spain' }, + { key: 'sv', i18nLabel: 'Swedish' }, + { key: 'tr', i18nLabel: 'Turkish' }, + { key: 'uk', i18nLabel: 'Ukrainian' }, +]; + export const settings: Array = [ { diff --git a/docs/QuickReplies.md b/docs/QuickReplies.md index a92c9a3..383e130 100644 --- a/docs/QuickReplies.md +++ b/docs/QuickReplies.md @@ -25,6 +25,7 @@ | **Param Name** | **Param Type** | **Description** | **Dependency** | **Acceptable Values** | **Example** | |:--------------:|:--------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------:|:---------------------:|:---------------------------------------:| | `text` | String | Title of the quick replies action. | Required | Any | ``` "text": "Start Chat" ``` | +| `url` | String | Redirect Url in-case you wish to redirect user somewhere if they click on the button.
⚠️ Recommended to only use with Livechat conversations | Optional | Any | ``` "text": "Start Chat" ``` | | `actionId` | String | Id of the quick replies action. | Optional | Any | ``` "actionId": "sflaia-start-chat" ``` | | `buttonStyle` | String | Button style of your quick replies action. Use `danger` to render a red colour action and `primary` for an action that matches your Livechat Bar colour. | Optional | `danger` or `primary` | ``` "buttonStyle": "primary" ``` | diff --git a/endpoints/FulfillmentsEndpoint.ts b/endpoints/FulfillmentsEndpoint.ts index 9e67f60..79280c0 100644 --- a/endpoints/FulfillmentsEndpoint.ts +++ b/endpoints/FulfillmentsEndpoint.ts @@ -25,7 +25,7 @@ export class FulfillmentsEndpoint extends ApiEndpoint { HttpStatusCode.OK, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { fulfillmentMessages: [] }); - } catch (error) { + } catch (error: any) { this.app.getLogger().error(Logs.ENDPOINT_REQUEST_PROCESSING_ERROR, error); return createHttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { error: error.message }); } @@ -35,7 +35,6 @@ export class FulfillmentsEndpoint extends ApiEndpoint { const message: IDialogflowMessage = Dialogflow.parseRequest(request.content); if (!message) { throw new Error(Logs.INVALID_REQUEST_CONTENT); } if (!message.sessionId) { throw new Error(Logs.INVALID_SESSION_ID); } - await createDialogflowMessage(message.sessionId, read, modify, message, this.app); await this.handleBotTyping(read, modify, message.sessionId, message); } diff --git a/endpoints/IncomingEndpoint.ts b/endpoints/IncomingEndpoint.ts index dc045f9..033dc4d 100644 --- a/endpoints/IncomingEndpoint.ts +++ b/endpoints/IncomingEndpoint.ts @@ -16,22 +16,23 @@ import { closeChat, performHandover } from '../lib/Room'; export class IncomingEndpoint extends ApiEndpoint { public path = 'incoming'; - public async post(request: IApiRequest, - endpoint: IApiEndpointInfo, - read: IRead, - modify: IModify, - http: IHttp, - persis: IPersistence): Promise { + public async post( + request: IApiRequest, + endpoint: IApiEndpointInfo, + read: IRead, + modify: IModify, + http: IHttp, + persis: IPersistence): Promise { this.app.getLogger().info(Logs.ENDPOINT_RECEIVED_REQUEST); try { - const { statusCode = HttpStatusCode.OK, data = null } = await this.processRequest(read, modify, persis, http, request.content); - return createHttpResponse(statusCode, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { ...data ? { ...data } : { result: Response.SUCCESS } }); - } catch (error) { - this.app.getLogger().error(Logs.ENDPOINT_REQUEST_PROCESSING_ERROR, error); - return createHttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { error: error.message }); + const { statusCode = HttpStatusCode.OK, data = null } = await this.processRequest(read, modify, persis, http, request.content); + return createHttpResponse(statusCode, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { ...data ? { ...data } : { result: Response.SUCCESS } }); + } catch (error: any) { + this.app.getLogger().error(Logs.ENDPOINT_REQUEST_PROCESSING_ERROR, error); + return createHttpResponse(HttpStatusCode.INTERNAL_SERVER_ERROR, { 'Content-Type': Headers.CONTENT_TYPE_JSON }, { error: error.message }); + } } - } private async processRequest(read: IRead, modify: IModify, @@ -95,6 +96,9 @@ export class IncomingEndpoint extends ApiEndpoint { throw new Error(Logs.INVALID_ENDPOINT_ACTION); } - return { statusCode: HttpStatusCode.OK, data: { result: Response.SUCCESS }}; + return { + statusCode: HttpStatusCode.OK, + data: { result: Response.SUCCESS }, + }; } } diff --git a/enum/Dialogflow.ts b/enum/Dialogflow.ts index 69fface..52aa326 100644 --- a/enum/Dialogflow.ts +++ b/enum/Dialogflow.ts @@ -39,10 +39,11 @@ export interface IDialogflowAction { export interface IDialogflowQuickReplyOptions { text: string; + url?: string; actionId?: string; buttonStyle?: ButtonStyle; data?: { - [prop: string]: any; + [prop: string]: string; }; } diff --git a/handler/ExecuteLivechatBlockActionHandler.ts b/handler/ExecuteLivechatBlockActionHandler.ts index 802ec0c..b557277 100644 --- a/handler/ExecuteLivechatBlockActionHandler.ts +++ b/handler/ExecuteLivechatBlockActionHandler.ts @@ -1,27 +1,52 @@ -import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { + IHttp, + IModify, + IPersistence, + IRead, +} from '@rocket.chat/apps-engine/definition/accessors'; import { IApp } from '@rocket.chat/apps-engine/definition/IApp'; import { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; -import { IUIKitResponse, UIKitLivechatBlockInteractionContext } from '@rocket.chat/apps-engine/definition/uikit'; +import { + IUIKitResponse, + UIKitLivechatBlockInteractionContext, +} from '@rocket.chat/apps-engine/definition/uikit'; import { UIKitIncomingInteractionContainerType } from '@rocket.chat/apps-engine/definition/uikit/UIKitIncomingInteractionContainer'; import { IUser } from '@rocket.chat/apps-engine/definition/users'; import { AppSetting, DefaultMessage } from '../config/Settings'; import { ActionIds } from '../enum/ActionIds'; -import { createLivechatMessage, createMessage, deleteAllActionBlocks } from '../lib/Message'; +import { + createLivechatMessage, + createMessage, + deleteAllActionBlocks, +} from '../lib/Message'; import { closeChat, performHandover } from '../lib/Room'; import { getLivechatAgentConfig } from '../lib/Settings'; export class ExecuteLivechatBlockActionHandler { - constructor(private readonly app: IApp, - private context: UIKitLivechatBlockInteractionContext, - private read: IRead, - private http: IHttp, - private persistence: IPersistence, - private modify: IModify) {} + constructor( + private readonly app: IApp, + private context: UIKitLivechatBlockInteractionContext, + private read: IRead, + private http: IHttp, + private persistence: IPersistence, + private modify: IModify, + ) {} public async run(): Promise { try { const interactionData = this.context.getInteractionData(); - const { visitor, room, container: { id, type }, value, actionId } = interactionData; + const { + visitor, + room, + container: { id, type }, + value, + actionId, + } = interactionData; + + if (!value) { + // most likely, this button has a url to open. So we don't need to do anything here. + return this.context.getInteractionResponder().successResponse(); + } if (type !== UIKitIncomingInteractionContainerType.MESSAGE) { return this.context.getInteractionResponder().successResponse(); @@ -63,6 +88,10 @@ export class ExecuteLivechatBlockActionHandler { await deleteAllActionBlocks(this.modify, appUser, id); } + if (hideQuickRepliesSetting) { + await deleteAllActionBlocks(this.modify, appUser, id); + } + return this.context.getInteractionResponder().successResponse(); } catch (error) { this.app.getLogger().error(error); diff --git a/handler/OnSettingUpdatedHandler.ts b/handler/OnSettingUpdatedHandler.ts index cc57c11..2ab60e4 100644 --- a/handler/OnSettingUpdatedHandler.ts +++ b/handler/OnSettingUpdatedHandler.ts @@ -7,7 +7,11 @@ import { getError } from '../lib/Helper'; import { getAppSettingValue } from '../lib/Settings'; export class OnSettingUpdatedHandler { - constructor(private readonly app: IApp, private readonly read: IRead, private readonly http: IHttp) {} + constructor( + private readonly app: IApp, + private readonly read: IRead, + private readonly http: IHttp, + ) {} public async run() { try { @@ -29,7 +33,7 @@ export class OnSettingUpdatedHandler { try { await Dialogflow.generateNewAccessToken(this.http, clientEmail, privateKey); this.app.getLogger().info(Logs.GOOGLE_AUTH_SUCCESS); - } catch (error) { + } catch (error: any) { console.error(Logs.HTTP_REQUEST_ERROR, getError(error)); this.app.getLogger().error(error.message); } @@ -37,7 +41,7 @@ export class OnSettingUpdatedHandler { } } - } catch (e) { + } catch (e: any) { this.app.getLogger().error(Logs.AGENT_CONFIG_FORMAT_ERROR); console.error(Logs.AGENT_CONFIG_FORMAT_ERROR, e); throw new Error(e); diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index 6f0e38d..992c440 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -1,6 +1,14 @@ -import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { + IHttp, + IModify, + IPersistence, + IRead, +} from '@rocket.chat/apps-engine/definition/accessors'; import { IApp } from '@rocket.chat/apps-engine/definition/IApp'; -import { ILivechatMessage, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; +import { + ILivechatMessage, + ILivechatRoom, +} from '@rocket.chat/apps-engine/definition/livechat'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; import { AppSetting } from '../config/Settings'; import { DialogflowRequestType, IDialogflowMessage, IDialogflowQuickReplies, LanguageCode, Message } from '../enum/Dialogflow'; diff --git a/i18n/en.json b/i18n/en.json index 09b985b..883614c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -32,6 +32,34 @@ "dialogflow_hide_quick_replies_description": "If enabled, then all quick-replies will hide when a visitor clicks on any one of them", "dialogflow_handover_failed_message": "Handover Failed Message", "dialogflow_handover_failed_message_description": "The Bot will send this message to Visitor if the handover failed because no agents were online", + "dialogflow_default_language_description": "Define the language in which you'd be interacting with the Bot", + "dialogflow_chinese_simplified": "Chinese - Simplified", + "dialogflow_danish": "Danish", + "dialogflow_dutch": "Dutch", + "dialogflow_english": "English", + "dialogflow_english_australia": "English - Australia", + "dialogflow_english_canada": "English - Canada", + "dialogflow_english_great_britain": "English - Great Britain", + "dialogflow_english_india": "English - India", + "dialogflow_english_us": "English - US", + "dialogflow_french_canada": "French - Canada", + "dialogflow_french_france": "French - France", + "dialogflow_german": "German", + "dialogflow_hindi": "Hindi", + "dialogflow_indonesian": "Indonesian", + "dialogflow_italian": "Italian", + "dialogflow_japanese": "Japanese", + "dialogflow_korean": "Korean", + "dialogflow_norwegian": "Norwegian", + "dialogflow_polish": "Polish", + "dialogflow_portuguese-brazil": "Portuguese - Brazil", + "dialogflow_portuguese-portugal": "Portuguese - Portugal", + "dialogflow_russian": "Russian", + "dialogflow_spanish": "Spanish", + "dialogflow_spanish-spain": "Spanish - Spain", + "dialogflow_swedish": "Swedish", + "dialogflow_turkish": "Turkish", + "dialogflow_ukrainian": "Ukrainian", "dialogflow_enable_chat_closed_by_visitor_event": "Enable Dialogflow Event When Visitor Ends Chat", "dialogflow_enable_chat_closed_by_visitor_event_description": "If enabled, then a dialogflow event will be triggered when visitor ends chat", "dialogflow_chat_closed_by_visitor_event_name": "Dialogflow Event Name When Visitor Ends Chat", diff --git a/lib/Dialogflow.ts b/lib/Dialogflow.ts index a5275e1..1ea61a6 100644 --- a/lib/Dialogflow.ts +++ b/lib/Dialogflow.ts @@ -1,4 +1,4 @@ -import { IHttp, IHttpRequest, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { IHttp, IHttpRequest, IModify, IRead } from '@rocket.chat/apps-engine/definition/accessors'; import { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat/ILivechatRoom'; import { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { createSign } from 'crypto'; @@ -69,7 +69,7 @@ class DialogflowClass { try { const response = await http.post(serverURL, httpRequestContent); return await this.parseCXRequest(read, response.data, sessionId); - } catch (error) { + } catch (error: any) { const errorContent = `${Logs.HTTP_REQUEST_ERROR}: { roomID: ${sessionId} } ${getError(error)}`; console.error(errorContent); throw new Error(error); @@ -94,7 +94,7 @@ class DialogflowClass { try { const response = await http.post(serverURL, httpRequestContent); return this.parseRequest(response.data, sessionId); - } catch (error) { + } catch (error: any) { const errorContent = `${Logs.HTTP_REQUEST_ERROR}: { roomID: ${sessionId} } ${getError(error)}`; console.error(errorContent); throw new Error(error); @@ -141,7 +141,7 @@ class DialogflowClass { } throw Error(Logs.ACCESS_TOKEN_ERROR); } - } catch (error) { + } catch (error: any) { const errorContent = `${Logs.HTTP_REQUEST_ERROR}: { roomID: ${sessionId || 'N/A'} } ${getError(error)}`; console.error(errorContent); throw new Error(error); @@ -216,8 +216,8 @@ class DialogflowClass { // some error occurred. Dialogflow's response has a error field containing more info abt error throw Error(`An Error occurred while connecting to Dialogflow's REST API\ Error Details:- - message:- ${response.error.message}\ - status:- ${response.error.message}\ + message:- ${response.error?.message}\ + status:- ${response.error?.message}\ Try checking the google credentials in App Setting and your internet connection`); } } @@ -232,7 +232,7 @@ class DialogflowClass { const { session, queryResult } = response; if (queryResult) { - const { responseMessages, match: { matchType }, diagnosticInfo } = queryResult; + const { responseMessages, diagnosticInfo } = queryResult; // Check array of event names from app settings for fallbacks const parsedMessage: IDialogflowMessage = { diff --git a/lib/Helper.ts b/lib/Helper.ts index 512ec6e..6a1eb97 100644 --- a/lib/Helper.ts +++ b/lib/Helper.ts @@ -20,27 +20,37 @@ export const base64urlEncode = (str: any) => { return base64EncodeData(utf8str, utf8str.length, Base64.BASE64_DICTIONARY, Base64.BASE64_PAD); }; -export const base64EncodeData = (data: string, len: number, b64x: string, b64pad: string) => { +export const base64EncodeData = ( + data: string, + len: number, + b64x: string, + b64pad: string, +) => { let dst = ''; let i: number; for (i = 0; i <= len - 3; i += 3) { - dst += b64x.charAt(data.charCodeAt(i) >>> 2); - dst += b64x.charAt(((data.charCodeAt(i) & 3) << 4) | (data.charCodeAt(i + 1) >>> 4)); - dst += b64x.charAt(((data.charCodeAt(i + 1) & 15) << 2) | (data.charCodeAt(i + 2) >>> 6)); + dst += b64x.charAt( + ((data.charCodeAt(i) & 3) << 4) | (data.charCodeAt(i + 1) >>> 4), + ); + dst += b64x.charAt( + ((data.charCodeAt(i + 1) & 15) << 2) | + (data.charCodeAt(i + 2) >>> 6), + ); dst += b64x.charAt(data.charCodeAt(i + 2) & 63); - } if (len % 3 === 2) { dst += b64x.charAt(data.charCodeAt(i) >>> 2); - dst += b64x.charAt(((data.charCodeAt(i) & 3) << 4) | (data.charCodeAt(i + 1) >>> 4)); - dst += b64x.charAt(((data.charCodeAt(i + 1) & 15) << 2)); + dst += b64x.charAt( + ((data.charCodeAt(i) & 3) << 4) | (data.charCodeAt(i + 1) >>> 4), + ); + dst += b64x.charAt((data.charCodeAt(i + 1) & 15) << 2); dst += b64pad; } else if (len % 3 === 1) { dst += b64x.charAt(data.charCodeAt(i) >>> 2); - dst += b64x.charAt(((data.charCodeAt(i) & 3) << 4)); + dst += b64x.charAt((data.charCodeAt(i) & 3) << 4); dst += b64pad; dst += b64pad; } @@ -50,12 +60,15 @@ export const base64EncodeData = (data: string, len: number, b64x: string, b64pad export const uuid = (): string => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); }; export const escapeRegExp = (str: string): string => { - return str.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&'); // $& means the whole matched string + return str.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&'); // $& means the whole matched string }; + +export const stringifyError = (error: Error): string => + JSON.stringify(error, Object.getOwnPropertyNames(error)); diff --git a/lib/Http.ts b/lib/Http.ts index 0f6983e..8c7306a 100644 --- a/lib/Http.ts +++ b/lib/Http.ts @@ -13,7 +13,11 @@ export const createHttpRequest = (headers, data) => { }; }; -export const createHttpResponse = (status: HttpStatusCode, headers: object, payload: object): IApiResponse => { +export const createHttpResponse = ( + status: HttpStatusCode, + headers: object, + payload: object, +): IApiResponse => { return { status, headers: { diff --git a/lib/Message.ts b/lib/Message.ts index 772bd16..dccac2c 100644 --- a/lib/Message.ts +++ b/lib/Message.ts @@ -33,8 +33,14 @@ export const createDialogflowMessage = async (rid: string, read: IRead, modify: text: payload.text, type: TextObjectType.PLAINTEXT, }, - value: payload.text, - ...payload.buttonStyle && { style: payload.buttonStyle }, + // if the button is a link, then we don't need to pass any value as clicking the button will open the link + value: payload.url ? '' : payload.text, + ...(payload.buttonStyle && { + style: payload.buttonStyle, + }), + ...(payload.url && { + url: payload.url, + }), }; if (payload.actionId && payload.actionId === ActionIds.PERFORM_HANDOVER) { @@ -50,6 +56,16 @@ export const createDialogflowMessage = async (rid: string, read: IRead, modify: elements, }); + if (text) { + blocks.addSectionBlock({ + text: blocks.newMarkdownTextObject(text), + }); + } + + blocks.addActionsBlock({ + elements, + }); + data.blocks = blocks; } @@ -223,14 +239,17 @@ export const createLivechatMessage = async (app: IApp, rid: string, read: IRead, export const deleteAllActionBlocks = async (modify: IModify, appUser: IUser, msgId: string): Promise => { const msgBuilder = await modify.getUpdater().message(msgId, appUser); - - const withoutActionBlocks: Array = msgBuilder.getBlocks().filter( - (block) => (!( - block.type === BlockType.ACTIONS && - (block as IActionsBlock).elements.some((element) => (element.type === BlockElementType.BUTTON)) - ) - )); - + const withoutActionBlocks: Array = msgBuilder + .getBlocks() + .filter( + (block) => + !( + block.type === BlockType.ACTIONS && + (block as IActionsBlock).elements.some( + (element) => element.type === BlockElementType.BUTTON, + ) + ), + ); msgBuilder.setEditor(appUser).setBlocks(withoutActionBlocks); return modify.getUpdater().finish(msgBuilder); }; @@ -240,17 +259,25 @@ export const removeQuotedMessage = async (read: IRead, room: IRoom, message: str throw new Error('Error! message text undefined'); } - let serverUrl: string | undefined = await getServerSettingValue(read, ServerSetting.SITE_URL); + let serverUrl: string | undefined = await getServerSettingValue( + read, + ServerSetting.SITE_URL, + ); serverUrl = serverUrl && serverUrl.trim(); if (!serverUrl) { throw new Error('Error! Getting server url'); } - serverUrl = serverUrl.endsWith('/') ? - serverUrl.substr(0, serverUrl.length - 1) : - serverUrl; + serverUrl = serverUrl.endsWith('/') + ? serverUrl.substr(0, serverUrl.length - 1) + : serverUrl; - const pattern = new RegExp(`\\[\\s*\\]\\(${ escapeRegExp(serverUrl) }\\/live\\/${ escapeRegExp(room.id) }\\?msg=.*\\)`, 'gi'); + const pattern = new RegExp( + `\\[\\s*\\]\\(${escapeRegExp(serverUrl)}\\/live\\/${escapeRegExp( + room.id, + )}\\?msg=.*\\)`, + 'gi', + ); if (message.match(pattern)) { return message.replace(pattern, ''); diff --git a/lib/Settings.ts b/lib/Settings.ts index 0bf8621..33a3e8e 100644 --- a/lib/Settings.ts +++ b/lib/Settings.ts @@ -4,11 +4,16 @@ import { Logs } from '../enum/Logs'; import { getPersistentAgentConfigToRoom } from './Persistence'; export const getAppSettingValue = async (read: IRead, id: string) => { - return id && await read.getEnvironmentReader().getSettings().getValueById(id); + return ( + id && (await read.getEnvironmentReader().getSettings().getValueById(id)) + ); }; export const getServerSettingValue = async (read: IRead, id: string) => { - return id && (await read.getEnvironmentReader().getServerSettings().getValueById(id)); + return ( + id && + (await read.getEnvironmentReader().getServerSettings().getValueById(id)) + ); }; export const getLivechatAgentConfig = async (read: IRead, sessionId: string, type?: string) => { @@ -37,7 +42,7 @@ export const getLivechatAgentConfig = async (read: IRead, sessionId: string, typ console.error(Logs.NO_AGENT_IN_CONFIG_WITH_CURRENT_AGENT_NAME, agentName); throw Error(`Agent ${ agentName } not found in Dialogflow Agent Endpoints`); - } catch (e) { + } catch (e: any) { console.error(Logs.AGENT_CONFIG_FORMAT_ERROR); throw new Error(e); } diff --git a/lib/SynchronousHandover.ts b/lib/SynchronousHandover.ts index 434400d..3740a82 100644 --- a/lib/SynchronousHandover.ts +++ b/lib/SynchronousHandover.ts @@ -10,15 +10,29 @@ import { getLivechatAgentConfig } from './Settings'; export const incFallbackIntentAndSendResponse = async (app: IApp, read: IRead, modify: IModify, sessionId: string, dialogflowMessage?: () => any) => { const fallbackThreshold = (await getLivechatAgentConfig(read, sessionId, AppSetting.DialogflowFallbackResponsesLimit)) as number; - if (!fallbackThreshold || (fallbackThreshold && fallbackThreshold === 0)) { return; } - - const room: ILivechatRoom = await read.getRoomReader().getById(sessionId) as ILivechatRoom; - if (!room) { throw new Error(Logs.INVALID_ROOM_ID); } + if (!fallbackThreshold || (fallbackThreshold && fallbackThreshold === 0)) { + return; + } - const { fallbackCount: oldFallbackCount } = room.customFields as any; - const newFallbackCount: number = oldFallbackCount ? oldFallbackCount + 1 : 1; + const room: ILivechatRoom = (await read + .getRoomReader() + .getById(sessionId)) as ILivechatRoom; + if (!room) { + throw new Error(Logs.INVALID_ROOM_ID); + } - await updateRoomCustomFields(sessionId, { fallbackCount: newFallbackCount }, read, modify); + let newFallbackCount = 0; + if (room?.customFields) { + const { fallbackCount: oldFallbackCount } = room.customFields; + newFallbackCount = oldFallbackCount ? oldFallbackCount + 1 : 1; + + await updateRoomCustomFields( + sessionId, + { fallbackCount: newFallbackCount }, + read, + modify, + ); + } if (newFallbackCount === fallbackThreshold) { const targetDepartmentName: string | undefined = await getLivechatAgentConfig(read, sessionId, AppSetting.FallbackTargetDepartment); @@ -40,6 +54,10 @@ export const incFallbackIntentAndSendResponse = async (app: IApp, read: IRead, m } }; -export const resetFallbackIntent = async (read: IRead, modify: IModify, sessionId: string) => { +export const resetFallbackIntent = async ( + read: IRead, + modify: IModify, + sessionId: string, +) => { await updateRoomCustomFields(sessionId, { fallbackCount: 0 }, read, modify); }; diff --git a/package-lock.json b/package-lock.json index 931da73..4c2c6d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,29 +3,31 @@ "lockfileVersion": 1, "dependencies": { "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", "dev": true, "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.0", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/highlight": "^7.16.7" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", + "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } } }, "@rocket.chat/apps-engine": { @@ -99,7 +101,7 @@ "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", "dev": true }, "chalk": { @@ -231,9 +233,9 @@ "dev": true }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dev": true, "requires": { "has": "^1.0.3" @@ -271,18 +273,18 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "once": { @@ -301,9 +303,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "prettier": { @@ -313,13 +315,14 @@ "dev": true }, "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "dev": true, "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "semver": { @@ -349,6 +352,12 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -374,15 +383,17 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.29.0" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" + }, + "dependencies": { + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } } }, "typescript": { diff --git a/tsconfig.json b/tsconfig.json index 1fe4878..88f4c83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,23 @@ { - "compilerOptions": { - "target": "es2017", - "module": "commonjs", - "moduleResolution": "node", - "declaration": false, - "noImplicitAny": false, - "removeComments": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - }, - "include": [ - "**/*.ts" - ] - } + "compilerOptions":{ + "target":"es2018", + "module":"commonjs", + "moduleResolution":"node", + "declaration":false, + "noImplicitAny":false, + "removeComments":true, + "strictNullChecks":true, + "noImplicitReturns":true, + "emitDecoratorMetadata":true, + "experimentalDecorators":true, + "strict":true, + "strictPropertyInitialization":false, + "allowJs":false, + "noUnusedLocals":true, + "noUnusedParameters":true, + "noFallthroughCasesInSwitch":false + }, + "include":[ + "**/*.ts" + ] +} diff --git a/tslint.json b/tslint.json index 0d443fd..eeb9230 100644 --- a/tslint.json +++ b/tslint.json @@ -1,16 +1,19 @@ { - "extends": "tslint:recommended", - "rules": { - "array-type": [true, "generic"], - "member-access": true, - "no-console": [false], - "no-duplicate-variable": true, - "object-literal-sort-keys": false, - "quotemark": [true, "single"], - "max-line-length": [true, { - "limit": 160, - "ignore-pattern": "^import | *export .*? {" - }], - "no-bitwise": false - } - } + "extends": "tslint:recommended", + "rules": { + "array-type": [true, "generic"], + "member-access": true, + "no-console": [false], + "no-duplicate-variable": true, + "object-literal-sort-keys": false, + "quotemark": [true, "single"], + "max-line-length": [ + true, + { + "limit": 160, + "ignore-pattern": "^import | *export .*? {" + } + ], + "no-bitwise": false + } +} diff --git a/types/misc.ts b/types/misc.ts new file mode 100644 index 0000000..929c114 --- /dev/null +++ b/types/misc.ts @@ -0,0 +1,8 @@ +import { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; +import { IBlock } from '@rocket.chat/apps-engine/definition/uikit'; + +export interface IMessageParam { + text?: string; + blocks?: Array; + attachment?: IMessageAttachment; +}