diff --git a/AppsRasaApp.ts b/AppsRasaApp.ts deleted file mode 100644 index 7ffed0b64c2f06b16d8abe353a01a313f8171e0c..0000000000000000000000000000000000000000 --- a/AppsRasaApp.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - IAppAccessors, - IConfigurationExtend, - IEnvironmentRead, - IHttp, - ILogger, - IModify, - IPersistence, - IRead, -} from '@rocket.chat/apps-engine/definition/accessors'; -import { App } from '@rocket.chat/apps-engine/definition/App'; -import { ILivechatEventContext, IPostLivechatAgentAssigned } from '@rocket.chat/apps-engine/definition/livechat'; -import { IMessage, IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages'; -import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; -import { PostLivechatAgentAssignedHandler } from './handler/PostLivechatAgentAssignedHandler'; -import { PostMessageSentHandler } from './handler/PostMessageSentHandler'; -import { AppSettings } from './lib/AppSettings'; - -export class AppsRasaApp extends App implements IPostMessageSent, IPostLivechatAgentAssigned { - constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { - super(info, logger, accessors); - } - - public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise { - await AppSettings.forEach((setting) => configurationExtend.settings.provideSetting(setting)); - this.getLogger().log('Apps.Dialogflow App Initialized'); - } - - public async executePostMessageSent(message: IMessage, - read: IRead, - http: IHttp, - persis: IPersistence, - modify: IModify): Promise { - const handler = new PostMessageSentHandler(this, message, read, http, persis, modify); - try { - await handler.run(); - } catch (error) { - this.getLogger().error(error.message); - } - } - - public async executePostLivechatAgentAssigned(context: ILivechatEventContext, read: IRead, http: IHttp, persistence: IPersistence): Promise { - const postLivechatAgentAssignedHandler = new PostLivechatAgentAssignedHandler(context, read, http, persistence); - await postLivechatAgentAssignedHandler.run(); - } - -} diff --git a/RasaApp.ts b/RasaApp.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ccf6ed5ea85857ee227ef80ee2d00207a01cfb2 --- /dev/null +++ b/RasaApp.ts @@ -0,0 +1,34 @@ +import { + IAppAccessors, + IConfigurationExtend, + IHttp, + ILogger, + IModify, + IPersistence, + IRead, +} from '@rocket.chat/apps-engine/definition/accessors'; +import { App } from '@rocket.chat/apps-engine/definition/App'; +import { ILivechatMessage } from '@rocket.chat/apps-engine/definition/livechat'; +import { IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages'; +import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; +import { settings } from './config/Settings'; +import { PostMessageSentHandler } from './handler/PostMessageSentHandler'; + +export class AppsRasaApp extends App implements IPostMessageSent { + constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { + super(info, logger, accessors); + } + + public async executePostMessageSent(message: ILivechatMessage, + read: IRead, + http: IHttp, + persis: IPersistence, + modify: IModify): Promise { + const handler = new PostMessageSentHandler(this, message, read, http, persis, modify); + await handler.run(); + } + + protected async extendConfiguration(configuration: IConfigurationExtend): Promise { + await Promise.all(settings.map((setting) => configuration.settings.provideSetting(setting))); + } +} diff --git a/app.json b/app.json index 32f4ff8196ba3dace107189aa7591a63a7506fad..a2bc7fb92f623be1bd05ec82dfb051ad7aeba5ae 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "id": "646b8e7d-f1e1-419e-9478-10d0f5bc74d9", "version": "0.0.1", - "requiredApiVersion": "^1.4.0", + "requiredApiVersion": "^1.15.0", "iconFile": "icon.png", "author": { "name": "Rocket.Chat", @@ -10,6 +10,6 @@ }, "name": "Apps.Rasa", "nameSlug": "appsrasa", - "classFile": "AppsRasaApp.ts", + "classFile": "RasaApp.ts", "description": "Integration between Rocket.Chat and the RASA Chatbot platform" -} \ No newline at end of file +} diff --git a/config/Settings.ts b/config/Settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9793f76d08b9f72fb974335916b1d097aab4afc --- /dev/null +++ b/config/Settings.ts @@ -0,0 +1,35 @@ +import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings'; + +export enum AppSetting { + RasaBotUsername = 'rasa_bot_username', + RasaServerUrl = 'rasa_server_url', + RasaServiceUnavailableMessage = 'rasa_service_unavailable_message', + } + +export const settings: Array = [ + { + id: AppSetting.RasaBotUsername, + public: true, + type: SettingType.STRING, + packageValue: '', + i18nLabel: 'bot_username', + required: true, + }, + { + id: AppSetting.RasaServerUrl, + public: true, + type: SettingType.STRING, + packageValue: '', + i18nLabel: 'rasa_server_url', + required: true, + }, + { + id: AppSetting.RasaServiceUnavailableMessage, + public: true, + type: SettingType.STRING, + packageValue: '', + i18nLabel: 'rasa_service_unavailable_message', + i18nDescription: 'rasa_service_unavailable_message_description', + required: false, + }, +]; diff --git a/definition/IParsedRasaResponse.ts b/definition/IParsedRasaResponse.ts deleted file mode 100644 index a9186d4dd56985c0296172dbba0b137beadea8e1..0000000000000000000000000000000000000000 --- a/definition/IParsedRasaResponse.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IParsedRasaResponse { - messages: Array; -} diff --git a/enum/Http.ts b/enum/Http.ts new file mode 100644 index 0000000000000000000000000000000000000000..968729ddccfd2eef5919af4d7e4f72b51dc86a21 --- /dev/null +++ b/enum/Http.ts @@ -0,0 +1,4 @@ +export enum Headers { + CONTENT_TYPE_JSON = 'application/json', + ACCEPT_JSON = 'application/json', +} diff --git a/enum/Rasa.ts b/enum/Rasa.ts new file mode 100644 index 0000000000000000000000000000000000000000..40bdfaf1923fcb31f9f22a95ab81339575089ad9 --- /dev/null +++ b/enum/Rasa.ts @@ -0,0 +1,13 @@ +export interface IRasaMessage { + messages: Array; +} + +export interface IRasaQuickReplies { + text: string; + quickReplies: Array; +} + +export interface IRasaQuickReply { + title: string; + payload: string; +} diff --git a/handler/PostLivechatAgentAssignedHandler.ts b/handler/PostLivechatAgentAssignedHandler.ts deleted file mode 100644 index d6adb6275dbbc93c39c0eb765d8f3263aab61a37..0000000000000000000000000000000000000000 --- a/handler/PostLivechatAgentAssignedHandler.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ILivechatEventContext, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; - -import { IHttp, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { getAppSetting } from '../helper'; -import { AppSettingId } from '../lib/AppSettings'; -import { AppPersistence } from '../lib/persistence'; - -export class PostLivechatAgentAssignedHandler { - constructor(private context: ILivechatEventContext, - private read: IRead, - private http: IHttp, - private persis: IPersistence) {} - - public async run() { - const SettingBotUsername: string = await getAppSetting(this.read, AppSettingId.RasaBotUsername); - if (SettingBotUsername !== this.context.agent.username) { return; } - - await this.saveVisitorSession(); - } - - /** - * - * @description - save visitor.token and session id. - * - This will provide a mapping between visitor.token n session id. - * - This is required for implementing `perform-handover` webhooks since it requires a Visitor object - * which can be obtained from using visitor.token we save here in Persistant storage - */ - private async saveVisitorSession() { - const persistence = new AppPersistence(this.persis, this.read.getPersistenceReader()); - - const lroom = this.context.room as ILivechatRoom; - if (!lroom) { throw new Error('Error!! Could not create session. room object is undefined'); } - - // session Id for Dialogflow will be the same as Room Id - const sessionId = lroom.id; - await persistence.saveSessionId(sessionId); - } -} diff --git a/handler/PostMessageSentHandler.ts b/handler/PostMessageSentHandler.ts index e2200397e5342195116628c4751b9f56fc9d0936..fa0ed3bbd58c35ab87ad16dd01c1965e2029e882 100644 --- a/handler/PostMessageSentHandler.ts +++ b/handler/PostMessageSentHandler.ts @@ -1,54 +1,63 @@ import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; import { IApp } from '@rocket.chat/apps-engine/definition/IApp'; -import { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import { ILivechatMessage, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; -import { IUser } from '@rocket.chat/apps-engine/definition/users'; -import { IParsedRasaResponse } from '../definition/IParsedRasaResponse'; -import { getAppSetting, getBotUser, getSessionId } from '../helper'; -import { AppSettingId } from '../lib/AppSettings'; -import { RasaSDK } from '../lib/RasaSDK'; +import { AppSetting } from '../config/Settings'; +import { IRasaMessage } from '../enum/Rasa'; +import { createMessage, createRasaMessage } from '../lib/Message'; +import { sendMessage } from '../lib/Rasa'; +import { getAppSettingValue } from '../lib/Setting'; export class PostMessageSentHandler { constructor(private app: IApp, - private message: IMessage, + private message: ILivechatMessage, private read: IRead, private http: IHttp, private persis: IPersistence, private modify: IModify) {} public async run() { - const SettingBotUsername: string = await getAppSetting(this.read, AppSettingId.RasaBotUsername); - if (this.message.sender.username === SettingBotUsername) { - // this msg was sent by the Bot itself, so no need to respond back - return; - } else if (this.message.room.type !== RoomType.LIVE_CHAT) { - // check whether this is a Livechat message + + const { text, editedAt, room, token, sender } = this.message; + const livechatRoom = room as ILivechatRoom; + + const { id: rid, type, servedBy, isOpen } = livechatRoom; + + const RasaBotUsername: string = await getAppSettingValue(this.read, AppSetting.RasaBotUsername); + + if (!type || type !== RoomType.LIVE_CHAT) { return; - } else if (SettingBotUsername !== getBotUser(this.message).username) { - // check whether the bot is currently handling the Visitor, if not then return back + } + + if (!isOpen || !token || editedAt || !text) { return; } - // send request to Rasa - if (!this.message.text || (this.message.text && this.message.text.trim().length === 0)) { return; } - const messageText: string = this.message.text; + if (!servedBy || servedBy.username !== RasaBotUsername) { + return; + } - const sessionId = getSessionId(this.message); - const rasaSDK: RasaSDK = new RasaSDK(this.http, this.read, this.persis, sessionId, messageText); + if (sender.username === RasaBotUsername) { + return; + } - const response: IParsedRasaResponse = await rasaSDK.sendMessage(); - for (const message of response.messages) { - await this.sendMessageToVisitor(message); + if (!text || (text && text.trim().length === 0)) { + return; } - } + let response: IRasaMessage; + try { + response = await sendMessage(this.read, this.http, rid, text); + } catch (error) { + this.app.getLogger().error(`Error occurred while using Rasa Rest API. Details:- ${error.message}`); + + const serviceUnavailable: string = await getAppSettingValue(this.read, AppSetting.RasaServiceUnavailableMessage); + await createMessage(rid, this.read, this.modify, { text: serviceUnavailable ? serviceUnavailable : '' }); + + return; + } - private async sendMessageToVisitor(message: string) { - const sender: IUser = getBotUser(this.message); + await createRasaMessage(rid, this.read, this.modify, response); - // build the message for Livechat widget - const builder = this.modify.getNotifier().getMessageBuilder(); - builder.setRoom(this.message.room).setText(message).setSender(sender); - await this.modify.getCreator().finish(builder); } } diff --git a/helper.ts b/helper.ts deleted file mode 100644 index 8a80dce3340765d089283ea736fcc2d688728d45..0000000000000000000000000000000000000000 --- a/helper.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IRead } from '@rocket.chat/apps-engine/definition/accessors'; - -import { IMessage } from '@rocket.chat/apps-engine/definition/messages'; - -import { IUser } from '@rocket.chat/apps-engine/definition/users'; - -import { ILivechatMessage, ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; - -export const getAppSetting = async (read: IRead, id: string): Promise => { - return (await read.getEnvironmentReader().getSettings().getById(id)).value; -}; - -export const getBotUser = (message: IMessage): IUser => { - const lroom: ILivechatRoom = getLivechatRoom(message); - if (!lroom.servedBy) { throw Error('Error!! Room.servedBy field is undefined'); } - return lroom.servedBy; -}; - -export const getLivechatRoom = (message: IMessage): ILivechatRoom => { - return ((message as ILivechatMessage).room as ILivechatRoom); -}; - -/** - * @description: Returns a session Id. Session Id is used to maintain sessions of Dialogflow. - * Note that the Session Id is the same as Room Id - */ -export const getSessionId = (message: IMessage): string => { - return getLivechatRoom(message).id; -}; diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 0000000000000000000000000000000000000000..08eaa95f0b50425eaa771326a9f891ae6663800c --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,6 @@ +{ + "bot_username": "Bot Username", + "rasa_server_url": "Rasa Server Url", + "rasa_service_unavailable_message": "Service Unavailable Message", + "rasa_service_unavailable_message_description": "The Bot will send this message to Visitor if service is unavailable" +} diff --git a/icon.png b/icon.png index a7c0cc49a64bd06d47e4e0b507d26338e8d92582..1471c06c2d290013c6ef245cb71635fc8d732527 100644 Binary files a/icon.png and b/icon.png differ diff --git a/lib/AppSettings.ts b/lib/AppSettings.ts deleted file mode 100644 index ad7dd5bc04636138c7f7db780bd96af54f6d2b72..0000000000000000000000000000000000000000 --- a/lib/AppSettings.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings'; - -export enum AppSettingId { - RasaBotUsername = 'rasa_bot_username', - RasaServerUrl = 'rasa_server_url', -} - -export const AppSettings: Array = [ - { - id: AppSettingId.RasaBotUsername, - public: true, - type: SettingType.STRING, - packageValue: '', - i18nLabel: 'Bot Username', - required: true, - }, - { - id: AppSettingId.RasaServerUrl, - public: true, - type: SettingType.STRING, - packageValue: '', - i18nLabel: 'Rasa Server Url', - i18nDescription: 'Here enter the RASA url where the RASA server is hosted. Make sure to add `/webhooks/rest/webhook` to the end of url. Eg: https://efee760b.ngrok.io/webhooks/rest/webhook', - required: true, - }, -]; diff --git a/lib/Http.ts b/lib/Http.ts new file mode 100644 index 0000000000000000000000000000000000000000..89250517ecc1414c5c8adf3032f211893013adb0 --- /dev/null +++ b/lib/Http.ts @@ -0,0 +1,25 @@ +import { HttpStatusCode } from '@rocket.chat/apps-engine/definition/accessors'; +import { IApiResponse } from '@rocket.chat/apps-engine/definition/api'; + +export const createHttpRequest = (headers, data) => { + return { + headers: { + ...headers, + }, + data: { + ...data, + }, + }; +}; + +export const createHttpResponse = (status: HttpStatusCode, headers: object, payload: object): IApiResponse => { + return { + status, + headers: { + ...headers, + }, + content: { + ...payload, + }, + }; +}; diff --git a/lib/Message.ts b/lib/Message.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffa678c2bfd4256bc3f104a8eaeec2796b4d940b --- /dev/null +++ b/lib/Message.ts @@ -0,0 +1,70 @@ +import { IModify, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { IMessageAction, IMessageAttachment, MessageActionType, MessageProcessingType } from '@rocket.chat/apps-engine/definition/messages'; +import { AppSetting } from '../config/Settings'; +import { IRasaMessage, IRasaQuickReplies, IRasaQuickReply } from '../enum/Rasa'; +import { getAppSettingValue } from './Setting'; + +export const createRasaMessage = async (rid: string, read: IRead, modify: IModify, rasaMessage: IRasaMessage): Promise => { + const { messages = [] } = rasaMessage; + + for (const message of messages) { + const { text, quickReplies } = message as IRasaQuickReplies; + + if (text && quickReplies) { + // message is instanceof IRasaQuickReplies + const actions: Array = quickReplies.map((payload: IRasaQuickReply) => ({ + type: MessageActionType.BUTTON, + text: payload.title, + msg: payload.payload, + msg_in_chat_window: true, + msg_processing_type: MessageProcessingType.SendMessage, + } as IMessageAction)); + const attachment: IMessageAttachment = { actions }; + await createMessage(rid, read, modify, { text, attachment }); + } else { + // message is instanceof string + await createMessage(rid, read, modify, { text: message }); + } + } +}; + +export const createMessage = async (rid: string, read: IRead, modify: IModify, message: any ): Promise => { + if (!message) { + return; + } + + const botUserName = await getAppSettingValue(read, AppSetting.RasaBotUsername); + if (!botUserName) { + this.app.getLogger().error('The Bot Username setting is not defined.'); + return; + } + + const sender = await read.getUserReader().getByUsername(botUserName); + if (!sender) { + this.app.getLogger().error('The Bot User does not exist.'); + return; + } + + const room = await read.getRoomReader().getById(rid); + if (!room) { + this.app.getLogger().error(`Invalid room id ${rid}`); + return; + } + + const msg = modify.getCreator().startMessage().setRoom(room).setSender(sender); + const { text, attachment } = message; + + if (text) { + msg.setText(text); + } + + if (attachment) { + msg.addAttachment(attachment); + } + + return new Promise(async (resolve) => { + modify.getCreator().finish(msg) + .then((result) => resolve(result)) + .catch((error) => console.error(error)); + }); +}; diff --git a/lib/Rasa.ts b/lib/Rasa.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fdc346cdbdebbdb24b98d2c1023b32c0a73f722 --- /dev/null +++ b/lib/Rasa.ts @@ -0,0 +1,46 @@ +import { IHttp, IHttpRequest, IRead } from '@rocket.chat/apps-engine/definition/accessors'; +import { AppSetting } from '../config/Settings'; +import { Headers } from '../enum/Http'; +import { IRasaMessage, IRasaQuickReplies, IRasaQuickReply } from '../enum/Rasa'; +import { createHttpRequest } from './Http'; +import { getAppSettingValue } from './Setting'; + +export const sendMessage = async (read: IRead, http: IHttp, sender: string, message: string): Promise => { + const rasaServerUrl = await getAppSettingValue(read, AppSetting.RasaServerUrl); + if (!rasaServerUrl) { throw new Error('Error! Rasa server url setting empty'); } + + const httpRequestContent: IHttpRequest = createHttpRequest( + { 'Content-Type': Headers.CONTENT_TYPE_JSON }, + { sender, message }, + ); + + const rasaWebhookUrl = `${rasaServerUrl}/webhooks/rest/webhook`; + const response = await http.post(rasaWebhookUrl, httpRequestContent); + if (response.statusCode !== 200) { throw Error(`Error occured while interacting with Rasa Rest API. Details: ${response.content}`); } + + const parsedMessage = parseRasaResponse(response.data); + return parsedMessage; +}; + +export const parseRasaResponse = (response: any): IRasaMessage => { + if (!response) { throw new Error('Error Parsing Rasa\'s Response. Data is undefined'); } + + const messages: Array = []; + + response.forEach((message) => { + const { text, buttons } = message; + if (buttons) { + const quickReply: IRasaQuickReplies = { + text, + quickReplies: buttons, + }; + messages.push(quickReply); + } else { + messages.push(text); + } + }); + + return { + messages, + }; +}; diff --git a/lib/RasaSDK.ts b/lib/RasaSDK.ts deleted file mode 100644 index 13a8b78c182fc7f0313026e99827139b87c1d9d2..0000000000000000000000000000000000000000 --- a/lib/RasaSDK.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { IHttp, IHttpRequest, IHttpResponse, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { IParsedRasaResponse } from '../definition/IParsedRasaResponse'; -import { getAppSetting } from '../helper'; -import { AppSettingId } from './AppSettings'; - -export class RasaSDK { - - constructor(private http: IHttp, - private read: IRead, - private persis: IPersistence, - private sessionId: string, - private messageText: string) {} - - public async sendMessage(): Promise { - const rasaServerUrl = await getAppSetting(this.read, AppSettingId.RasaServerUrl); - - const httpRequestContent: IHttpRequest = this.buildRasaHTTPRequest(); - - // send request to dialogflow - const response = await this.http.post(rasaServerUrl, httpRequestContent); - if (response.statusCode !== 200) { throw Error(`Error occured while interacting with Rasa Rest API. Details: ${response.content}`); } - - const parsedMessage = this.parseRasaResponse(response); - return parsedMessage; - } - - private parseRasaResponse(response: IHttpResponse): IParsedRasaResponse { - if (!response.content) { throw new Error('Error Parsing Dialogflow\'s Response. Content is undefined'); } - const responseJSON = JSON.parse(response.content); - - const messages: Array = []; - - responseJSON.forEach((element) => { - messages.push(element.text); - }); - - return { - messages, - }; - } - - private buildRasaHTTPRequest(): IHttpRequest { - return { - headers: { - 'Content-Type': 'application/json', - }, - data: { - sender: this.sessionId, - message: this.messageText, - }, - }; - } -} diff --git a/lib/Setting.ts b/lib/Setting.ts new file mode 100644 index 0000000000000000000000000000000000000000..7510da2668b8966c4189380d8603f7ac64185812 --- /dev/null +++ b/lib/Setting.ts @@ -0,0 +1,5 @@ +import { IRead } from '@rocket.chat/apps-engine/definition/accessors'; + +export const getAppSettingValue = async (read: IRead, id: string) => { + return id && await read.getEnvironmentReader().getSettings().getValueById(id); +}; diff --git a/lib/persistence.ts b/lib/persistence.ts deleted file mode 100644 index c8c7093c03b6c6f26d62288e3f2ac54c9d34a72f..0000000000000000000000000000000000000000 --- a/lib/persistence.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { IPersistence, IPersistenceRead } from '@rocket.chat/apps-engine/definition/accessors'; -import { RocketChatAssociationModel, RocketChatAssociationRecord } from '@rocket.chat/apps-engine/definition/metadata'; - -export class AppPersistence { - constructor(private readonly persistence: IPersistence, private readonly persistenceRead: IPersistenceRead) {} - - public async saveSessionId(sessionId: string): Promise { - const sessionIdAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, sessionId); - const sessionAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, 'session-Id'); - - await this.persistence.updateByAssociations([sessionIdAssociation, sessionAssociation], { - sessionId, - }, true); - } - - public async checkIfSessionExists(sessionId: string): Promise { - const sessionIdAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, sessionId); - const sessionAssociation = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, 'session-Id'); - - const [result] = await this.persistenceRead.readByAssociations([sessionIdAssociation, sessionAssociation]); - return result && (result as any).sessionId; - } -}