Commit c8a37f6b authored by murtaza98's avatar murtaza98

reorganized files and folder into better structure, add suppor for quick-replies

parent e5a93309
import { import {
IAppAccessors, IAppAccessors,
IConfigurationExtend, IConfigurationExtend,
IEnvironmentRead,
IHttp, IHttp,
ILogger, ILogger,
IModify, IModify,
...@@ -9,39 +8,27 @@ import { ...@@ -9,39 +8,27 @@ import {
IRead, IRead,
} from '@rocket.chat/apps-engine/definition/accessors'; } from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App'; import { App } from '@rocket.chat/apps-engine/definition/App';
import { ILivechatEventContext, IPostLivechatAgentAssigned } from '@rocket.chat/apps-engine/definition/livechat'; import { ILivechatMessage } from '@rocket.chat/apps-engine/definition/livechat';
import { IMessage, IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages'; import { IPostMessageSent } from '@rocket.chat/apps-engine/definition/messages';
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import { PostLivechatAgentAssignedHandler } from './handler/PostLivechatAgentAssignedHandler'; import { settings } from './config/Settings';
import { PostMessageSentHandler } from './handler/PostMessageSentHandler'; import { PostMessageSentHandler } from './handler/PostMessageSentHandler';
import { AppSettings } from './lib/AppSettings';
export class AppsRasaApp extends App implements IPostMessageSent, IPostLivechatAgentAssigned { export class AppsRasaApp extends App implements IPostMessageSent {
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors); super(info, logger, accessors);
} }
public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> { public async executePostMessageSent(message: ILivechatMessage,
await AppSettings.forEach((setting) => configurationExtend.settings.provideSetting(setting));
this.getLogger().log('Apps.Dialogflow App Initialized');
}
public async executePostMessageSent(message: IMessage,
read: IRead, read: IRead,
http: IHttp, http: IHttp,
persis: IPersistence, persis: IPersistence,
modify: IModify): Promise<void> { modify: IModify): Promise<void> {
const handler = new PostMessageSentHandler(this, message, read, http, persis, modify); const handler = new PostMessageSentHandler(this, message, read, http, persis, modify);
try { await handler.run();
await handler.run();
} catch (error) {
this.getLogger().error(error.message);
}
} }
public async executePostLivechatAgentAssigned(context: ILivechatEventContext, read: IRead, http: IHttp, persistence: IPersistence): Promise<void> { protected async extendConfiguration(configuration: IConfigurationExtend): Promise<void> {
const postLivechatAgentAssignedHandler = new PostLivechatAgentAssignedHandler(context, read, http, persistence); await Promise.all(settings.map((setting) => configuration.settings.provideSetting(setting)));
await postLivechatAgentAssignedHandler.run();
} }
} }
{ {
"id": "646b8e7d-f1e1-419e-9478-10d0f5bc74d9", "id": "646b8e7d-f1e1-419e-9478-10d0f5bc74d9",
"version": "0.0.1", "version": "0.0.1",
"requiredApiVersion": "^1.4.0", "requiredApiVersion": "^1.15.0",
"iconFile": "icon.png", "iconFile": "icon.png",
"author": { "author": {
"name": "Rocket.Chat", "name": "Rocket.Chat",
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
}, },
"name": "Apps.Rasa", "name": "Apps.Rasa",
"nameSlug": "appsrasa", "nameSlug": "appsrasa",
"classFile": "AppsRasaApp.ts", "classFile": "RasaApp.ts",
"description": "Integration between Rocket.Chat and the RASA Chatbot platform" "description": "Integration between Rocket.Chat and the RASA Chatbot platform"
} }
\ No newline at end of file
import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings'; import { ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings';
export enum AppSettingId { export enum AppSetting {
RasaBotUsername = 'rasa_bot_username', RasaBotUsername = 'rasa_bot_username',
RasaServerUrl = 'rasa_server_url', RasaServerUrl = 'rasa_server_url',
} RasaServiceUnavailableMessage = 'rasa_service_unavailable_message',
}
export const AppSettings: Array<ISetting> = [ export const settings: Array<ISetting> = [
{ {
id: AppSettingId.RasaBotUsername, id: AppSetting.RasaBotUsername,
public: true, public: true,
type: SettingType.STRING, type: SettingType.STRING,
packageValue: '', packageValue: '',
i18nLabel: 'Bot Username', i18nLabel: 'bot_username',
required: true, required: true,
}, },
{ {
id: AppSettingId.RasaServerUrl, id: AppSetting.RasaServerUrl,
public: true, public: true,
type: SettingType.STRING, type: SettingType.STRING,
packageValue: '', packageValue: '',
i18nLabel: 'Rasa Server Url', 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, required: true,
}, },
{
id: AppSetting.RasaServiceUnavailableMessage,
public: true,
type: SettingType.STRING,
packageValue: '',
i18nLabel: 'rasa_service_unavailable_message',
i18nDescription: 'rasa_service_unavailable_message_description',
required: false,
},
]; ];
export interface IParsedRasaResponse {
messages: Array<string>;
}
export enum Headers {
CONTENT_TYPE_JSON = 'application/json',
ACCEPT_JSON = 'application/json',
}
export interface IRasaMessage {
messages: Array<string | IRasaQuickReplies>;
}
export interface IRasaQuickReplies {
text: string;
quickReplies: Array<IRasaQuickReply>;
}
export interface IRasaQuickReply {
title: string;
payload: string;
}
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);
}
}
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 { 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 { RoomType } from '@rocket.chat/apps-engine/definition/rooms';
import { IUser } from '@rocket.chat/apps-engine/definition/users'; import { AppSetting } from '../config/Settings';
import { IParsedRasaResponse } from '../definition/IParsedRasaResponse'; import { IRasaMessage } from '../enum/Rasa';
import { getAppSetting, getBotUser, getSessionId } from '../helper'; import { createMessage, createRasaMessage } from '../lib/Message';
import { AppSettingId } from '../lib/AppSettings'; import { sendMessage } from '../lib/Rasa';
import { RasaSDK } from '../lib/RasaSDK'; import { getAppSettingValue } from '../lib/Setting';
export class PostMessageSentHandler { export class PostMessageSentHandler {
constructor(private app: IApp, constructor(private app: IApp,
private message: IMessage, private message: ILivechatMessage,
private read: IRead, private read: IRead,
private http: IHttp, private http: IHttp,
private persis: IPersistence, private persis: IPersistence,
private modify: IModify) {} private modify: IModify) {}
public async run() { public async run() {
const SettingBotUsername: string = await getAppSetting(this.read, AppSettingId.RasaBotUsername);
if (this.message.sender.username === SettingBotUsername) { const { text, editedAt, room, token, sender } = this.message;
// this msg was sent by the Bot itself, so no need to respond back const livechatRoom = room as ILivechatRoom;
return;
} else if (this.message.room.type !== RoomType.LIVE_CHAT) { const { id: rid, type, servedBy, isOpen } = livechatRoom;
// check whether this is a Livechat message
const RasaBotUsername: string = await getAppSettingValue(this.read, AppSetting.RasaBotUsername);
if (!type || type !== RoomType.LIVE_CHAT) {
return; 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; return;
} }
// send request to Rasa if (!servedBy || servedBy.username !== RasaBotUsername) {
if (!this.message.text || (this.message.text && this.message.text.trim().length === 0)) { return; } return;
const messageText: string = this.message.text; }
const sessionId = getSessionId(this.message); if (sender.username === RasaBotUsername) {
const rasaSDK: RasaSDK = new RasaSDK(this.http, this.read, this.persis, sessionId, messageText); return;
}
const response: IParsedRasaResponse = await rasaSDK.sendMessage(); if (!text || (text && text.trim().length === 0)) {
for (const message of response.messages) { return;
await this.sendMessageToVisitor(message);
} }
} 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) { await createRasaMessage(rid, this.read, this.modify, response);
const sender: IUser = getBotUser(this.message);
// 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);
} }
} }
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<any> => {
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;
};
{
"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"
}
icon.png

2.6 KB | W: | H:

icon.png

1.67 KB | W: | H:

icon.png
icon.png
icon.png
icon.png
  • 2-up
  • Swipe
  • Onion skin
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,
},
};
};
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<any> => {
const { messages = [] } = rasaMessage;
for (const message of messages) {
const { text, quickReplies } = message as IRasaQuickReplies;
if (text && quickReplies) {
// message is instanceof IRasaQuickReplies
const actions: Array<IMessageAction> = 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<any> => {
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));
});
};
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<IRasaMessage> => {
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<string | IRasaQuickReplies> = [];
response.forEach((message) => {
const { text, buttons } = message;
if (buttons) {
const quickReply: IRasaQuickReplies = {
text,
quickReplies: buttons,
};
messages.push(quickReply);
} else {
messages.push(text);
}
});
return {
messages,
};
};
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<IParsedRasaResponse> {
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<string> = [];
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,
},
};
}
}
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);
};
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<void> {
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<boolean> {
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;
}
}