feat: rename offline to web

This commit is contained in:
Mohamed Marrouchi 2024-10-22 14:03:15 +01:00
parent 1fc58f4496
commit a3d7b83511
41 changed files with 375 additions and 410 deletions

View File

@ -11,7 +11,7 @@ import { Request, Response } from 'express';
import { SubscriberService } from '@/chat/services/subscriber.service';
import { LIVE_CHAT_TEST_CHANNEL_NAME } from '@/extensions/channels/live-chat-tester/settings';
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
import { WEB_CHANNEL_NAME } from '@/extensions/channels/web/settings';
import { LoggerService } from '@/logger/logger.service';
import {
SocketGet,
@ -72,7 +72,7 @@ export class ChannelService {
/**
* Retrieves the appropriate channel handler based on the channel name.
*
* @param channelName - The name of the channel (messenger, offline, ...).
* @param channelName - The name of the channel (messenger, web, ...).
* @returns The handler for the specified channel.
*/
public getChannelHandler<T extends ChannelName, C extends ChannelHandler<T>>(
@ -99,19 +99,19 @@ export class ChannelService {
}
/**
* Handles a websocket request for the offline channel.
* Handles a websocket request for the web channel.
*
* @param req - The websocket request object.
* @param res - The websocket response object.
*/
@SocketGet(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
@SocketPost(`/webhook/${OFFLINE_CHANNEL_NAME}/`)
handleWebsocketForOffline(
@SocketGet(`/webhook/${WEB_CHANNEL_NAME}/`)
@SocketPost(`/webhook/${WEB_CHANNEL_NAME}/`)
handleWebsocketForWebChannel(
@SocketReq() req: SocketRequest,
@SocketRes() res: SocketResponse,
) {
this.logger.log('Channel notification (Offline Socket) : ', req.method);
const handler = this.getChannelHandler(OFFLINE_CHANNEL_NAME);
this.logger.log('Channel notification (Web Socket) : ', req.method);
const handler = this.getChannelHandler(WEB_CHANNEL_NAME);
return handler.handle(req, res);
}
@ -165,7 +165,7 @@ export class ChannelService {
);
// Update session (end user is both a user + subscriber)
req.session.offline = {
req.session.web = {
profile: testSubscriber,
isSocket: true,
messageQueue: [],

View File

@ -16,7 +16,7 @@ const baseLabel: Label = {
name: '',
label_id: {
messenger: '',
offline: '',
web: '',
dimelo: '',
twitter: '',
},
@ -30,7 +30,7 @@ export const labelMock: Label = {
name: 'label',
label_id: {
messenger: 'none',
offline: 'none',
web: 'none',
dimelo: 'none',
twitter: 'none',
},
@ -43,7 +43,7 @@ export const customerLabelsMock: Label[] = [
name: 'client',
label_id: {
messenger: 'none',
offline: 'none',
web: 'none',
dimelo: 'none',
twitter: 'none',
},
@ -54,7 +54,7 @@ export const customerLabelsMock: Label[] = [
name: 'profressional',
label_id: {
messenger: 'none',
offline: 'none',
web: 'none',
dimelo: 'none',
twitter: 'none',
},

View File

@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
lastvisit: new Date(),
retainedFrom: new Date(),
channel: {
name: 'offline-channel',
name: 'web-channel',
},
labels: [],
...modelInstance,

View File

@ -173,7 +173,7 @@ describe('LabelController', () => {
name: 'LABEL_2',
label_id: {
messenger: 'messenger',
offline: 'offline',
web: 'web',
twitter: 'twitter',
dimelo: 'dimelo',
},

View File

@ -24,10 +24,10 @@ import { ContentTypeModel } from '@/cms/schemas/content-type.schema';
import { Content, ContentModel } from '@/cms/schemas/content.schema';
import { ContentTypeService } from '@/cms/services/content-type.service';
import { ContentService } from '@/cms/services/content.service';
import OfflineHandler from '@/extensions/channels/offline/index.channel';
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
import { Offline } from '@/extensions/channels/offline/types';
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
import WebChannelHandler from '@/extensions/channels/web/index.channel';
import { WEB_CHANNEL_NAME } from '@/extensions/channels/web/settings';
import { Web } from '@/extensions/channels/web/types';
import WebEventWrapper from '@/extensions/channels/web/wrapper';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
@ -222,22 +222,22 @@ describe('BlockService', () => {
describe('match', () => {
const handlerMock = {
getName: jest.fn(() => OFFLINE_CHANNEL_NAME),
} as any as OfflineHandler;
const offlineEventGreeting = new OfflineEventWrapper(
getName: jest.fn(() => WEB_CHANNEL_NAME),
} as any as WebChannelHandler;
const webEventGreeting = new WebEventWrapper(
handlerMock,
{
type: Offline.IncomingMessageType.text,
type: Web.IncomingMessageType.text,
data: {
text: 'Hello',
},
},
{},
);
const offlineEventGetStarted = new OfflineEventWrapper(
const webEventGetStarted = new WebEventWrapper(
handlerMock,
{
type: Offline.IncomingMessageType.postback,
type: Web.IncomingMessageType.postback,
data: {
text: 'Get Started',
payload: 'GET_STARTED',
@ -247,40 +247,37 @@ describe('BlockService', () => {
);
it('should return undefined when no blocks are provided', async () => {
const result = await blockService.match([], offlineEventGreeting);
const result = await blockService.match([], webEventGreeting);
expect(result).toBe(undefined);
});
it('should return undefined for empty blocks', async () => {
const result = await blockService.match(
[blockEmpty],
offlineEventGreeting,
);
const result = await blockService.match([blockEmpty], webEventGreeting);
expect(result).toEqual(undefined);
});
it('should return undefined for no matching labels', async () => {
offlineEventGreeting.setSender(subscriberWithoutLabels);
const result = await blockService.match(blocks, offlineEventGreeting);
webEventGreeting.setSender(subscriberWithoutLabels);
const result = await blockService.match(blocks, webEventGreeting);
expect(result).toEqual(undefined);
});
it('should match block text and labels', async () => {
offlineEventGreeting.setSender(subscriberWithLabels);
const result = await blockService.match(blocks, offlineEventGreeting);
webEventGreeting.setSender(subscriberWithLabels);
const result = await blockService.match(blocks, webEventGreeting);
expect(result).toEqual(blockGetStarted);
});
it('should match block with payload', async () => {
offlineEventGetStarted.setSender(subscriberWithLabels);
const result = await blockService.match(blocks, offlineEventGetStarted);
webEventGetStarted.setSender(subscriberWithLabels);
const result = await blockService.match(blocks, webEventGetStarted);
expect(result).toEqual(blockGetStarted);
});
it('should match block with nlp', async () => {
offlineEventGreeting.setSender(subscriberWithLabels);
offlineEventGreeting.setNLP(nlpEntitiesGreeting);
const result = await blockService.match(blocks, offlineEventGreeting);
webEventGreeting.setSender(subscriberWithLabels);
webEventGreeting.setNLP(nlpEntitiesGreeting);
const result = await blockService.match(blocks, webEventGreeting);
expect(result).toEqual(blockGetStarted);
});
});
@ -502,7 +499,7 @@ describe('BlockService', () => {
describe('processText', () => {
const context: Context = {
...contextGetStartedInstance,
channel: 'offline-channel',
channel: 'web-channel',
text: '',
payload: undefined,
nlp: { entities: [] },

View File

@ -24,9 +24,9 @@ import { MenuModel } from '@/cms/schemas/menu.schema';
import { ContentTypeService } from '@/cms/services/content-type.service';
import { ContentService } from '@/cms/services/content.service';
import { MenuService } from '@/cms/services/menu.service';
import { offlineEventText } from '@/extensions/channels/offline/__test__/events.mock';
import OfflineHandler from '@/extensions/channels/offline/index.channel';
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
import { webEventText } from '@/extensions/channels/web/__test__/events.mock';
import WebChannelHandler from '@/extensions/channels/web/index.channel';
import WebEventWrapper from '@/extensions/channels/web/wrapper';
import { HelperService } from '@/helper/helper.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
@ -75,7 +75,7 @@ describe('BlockService', () => {
let blockService: BlockService;
let subscriberService: SubscriberService;
let botService: BotService;
let handler: OfflineHandler;
let handler: WebChannelHandler;
let eventEmitter: EventEmitter2;
beforeAll(async () => {
@ -126,7 +126,7 @@ describe('BlockService', () => {
ChannelService,
MessageService,
MenuService,
OfflineHandler,
WebChannelHandler,
ContextVarService,
ContextVarRepository,
LanguageService,
@ -170,7 +170,7 @@ describe('BlockService', () => {
botService = module.get<BotService>(BotService);
blockService = module.get<BlockService>(BlockService);
eventEmitter = module.get<EventEmitter2>(EventEmitter2);
handler = module.get<OfflineHandler>(OfflineHandler);
handler = module.get<WebChannelHandler>(WebChannelHandler);
});
afterEach(jest.clearAllMocks);
@ -183,38 +183,38 @@ describe('BlockService', () => {
triggeredEvents.push(args);
});
const event = new OfflineEventWrapper(handler, offlineEventText, {
const event = new WebEventWrapper(handler, webEventText, {
isSocket: false,
ipAddress: '1.1.1.1',
});
const [block] = await blockService.findAndPopulate({ patterns: ['Hi'] });
const offlineSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-offline-1',
const webSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-web-1',
});
event.setSender(offlineSubscriber);
event.setSender(webSubscriber);
let hasBotSpoken = false;
const clearMock = jest
.spyOn(botService, 'findBlockAndSendReply')
.mockImplementation(
(
actualEvent: OfflineEventWrapper,
actualEvent: WebEventWrapper,
actualConversation: Conversation,
actualBlock: BlockFull,
isFallback: boolean,
) => {
expect(actualConversation).toEqualPayload({
sender: offlineSubscriber.id,
sender: webSubscriber.id,
active: true,
next: [],
context: {
user: {
first_name: offlineSubscriber.first_name,
last_name: offlineSubscriber.last_name,
first_name: webSubscriber.first_name,
last_name: webSubscriber.last_name,
language: 'en',
id: offlineSubscriber.id,
id: webSubscriber.id,
},
user_location: {
lat: 0,
@ -224,8 +224,8 @@ describe('BlockService', () => {
nlp: null,
payload: null,
attempt: 0,
channel: 'offline-channel',
text: offlineEventText.data.text,
channel: 'web-channel',
text: webEventText.data.text,
},
});
expect(actualEvent).toEqual(event);
@ -251,40 +251,40 @@ describe('BlockService', () => {
triggeredEvents.push(args);
});
const event = new OfflineEventWrapper(handler, offlineEventText, {
const event = new WebEventWrapper(handler, webEventText, {
isSocket: false,
ipAddress: '1.1.1.1',
});
const offlineSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-offline-1',
const webSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-web-1',
});
event.setSender(offlineSubscriber);
event.setSender(webSubscriber);
const clearMock = jest
.spyOn(botService, 'handleIncomingMessage')
.mockImplementation(
async (
actualConversation: ConversationFull,
event: OfflineEventWrapper,
event: WebEventWrapper,
) => {
expect(actualConversation).toEqualPayload({
next: [],
sender: offlineSubscriber,
sender: webSubscriber,
active: true,
context: {
user: {
first_name: offlineSubscriber.first_name,
last_name: offlineSubscriber.last_name,
first_name: webSubscriber.first_name,
last_name: webSubscriber.last_name,
language: 'en',
id: offlineSubscriber.id,
id: webSubscriber.id,
},
user_location: { lat: 0, lon: 0 },
vars: {},
nlp: null,
payload: null,
attempt: 0,
channel: 'offline-channel',
text: offlineEventText.data.text,
channel: 'web-channel',
text: webEventText.data.text,
},
});
expect(event).toEqual(event);
@ -304,14 +304,14 @@ describe('BlockService', () => {
eventEmitter.on('hook:stats:entry', (...args) => {
triggeredEvents.push(args);
});
const event = new OfflineEventWrapper(handler, offlineEventText, {
const event = new WebEventWrapper(handler, webEventText, {
isSocket: false,
ipAddress: '1.1.1.1',
});
const offlineSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-offline-2',
const webSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-web-2',
});
event.setSender(offlineSubscriber);
event.setSender(webSubscriber);
const captured = await botService.processConversationMessage(event);
expect(captured).toBe(false);

View File

@ -19,7 +19,7 @@ import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from '../offline/base-web-channel';
import BaseWebChannelHandler from '../web/base-web-channel';
import { LIVE_CHAT_TEST_CHANNEL_NAME } from './settings';

View File

@ -10,7 +10,7 @@ import { ChannelSetting } from '@/channel/types';
import { config } from '@/config';
import { SettingType } from '@/setting/schemas/types';
import { Offline } from '../offline/types';
import { Web } from '../web/types';
export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester-channel';
@ -19,74 +19,74 @@ export const LIVE_CHAT_TEST_CHANNEL_NAMESPACE = 'live_chat_tester_channel';
export default [
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.verification_token,
label: Web.SettingLabel.verification_token,
value: 'test',
type: SettingType.text,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_domains,
label: Web.SettingLabel.allowed_domains,
value: config.frontendPath,
type: SettingType.text,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.start_button,
label: Web.SettingLabel.start_button,
value: true,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.input_disabled,
label: Web.SettingLabel.input_disabled,
value: false,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.persistent_menu,
label: Web.SettingLabel.persistent_menu,
value: true,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.greeting_message,
label: Web.SettingLabel.greeting_message,
value: 'Welcome! Ready to start a conversation with our chatbot?',
type: SettingType.textarea,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.theme_color,
label: Web.SettingLabel.theme_color,
value: 'teal',
type: SettingType.select,
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_emoji,
label: Web.SettingLabel.show_emoji,
value: true,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_file,
label: Web.SettingLabel.show_file,
value: true,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_location,
label: Web.SettingLabel.show_location,
value: true,
type: SettingType.checkbox,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_upload_size,
label: Web.SettingLabel.allowed_upload_size,
value: 2500000,
type: SettingType.number,
},
{
group: LIVE_CHAT_TEST_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_upload_types,
label: Web.SettingLabel.allowed_upload_types,
value:
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
type: SettingType.textarea,

View File

@ -1,3 +0,0 @@
{
"offline_channel": "Web Channel"
}

View File

@ -1,3 +0,0 @@
{
"offline_channel": "Canal Web"
}

View File

@ -1,16 +0,0 @@
import DEFAULT_OFFLINE_SETTINGS, {
OFFLINE_CHANNEL_NAMESPACE,
} from './settings';
declare global {
interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {}
}
declare module '@nestjs/event-emitter' {
interface IHookExtensionsOperationMap {
[OFFLINE_CHANNEL_NAMESPACE]: TDefinition<
object,
SettingMapByType<typeof DEFAULT_OFFLINE_SETTINGS>
>;
}
}

View File

@ -12,14 +12,14 @@ import { ButtonType } from '@/chat/schemas/types/button';
import { FileType } from '@/chat/schemas/types/message';
import { QuickReplyType } from '@/chat/schemas/types/quick-reply';
import { Offline } from '../types';
import { Web } from '../types';
export const offlineText: Offline.OutgoingMessageBase = {
type: Offline.OutgoingMessageType.text,
export const webText: Web.OutgoingMessageBase = {
type: Web.OutgoingMessageType.text,
data: textMessage,
};
export const offlineQuickReplies: Offline.OutgoingMessageBase = {
export const webQuickReplies: Web.OutgoingMessageBase = {
data: {
quick_replies: [
{
@ -35,10 +35,10 @@ export const offlineQuickReplies: Offline.OutgoingMessageBase = {
],
text: 'Choose one option',
},
type: Offline.OutgoingMessageType.quick_replies,
type: Web.OutgoingMessageType.quick_replies,
};
export const offlineButtons: Offline.OutgoingMessageBase = {
export const webButtons: Web.OutgoingMessageBase = {
data: {
buttons: [
{
@ -56,10 +56,10 @@ export const offlineButtons: Offline.OutgoingMessageBase = {
],
text: 'Hit one of these buttons :',
},
type: Offline.OutgoingMessageType.buttons,
type: Web.OutgoingMessageType.buttons,
};
export const offlineList: Offline.OutgoingMessageBase = {
export const webList: Web.OutgoingMessageBase = {
data: {
buttons: [
{
@ -95,10 +95,10 @@ export const offlineList: Offline.OutgoingMessageBase = {
},
],
},
type: Offline.OutgoingMessageType.list,
type: Web.OutgoingMessageType.list,
};
export const offlineCarousel: Offline.OutgoingMessageBase = {
export const webCarousel: Web.OutgoingMessageBase = {
data: {
elements: [
{
@ -127,10 +127,10 @@ export const offlineCarousel: Offline.OutgoingMessageBase = {
},
],
},
type: Offline.OutgoingMessageType.carousel,
type: Web.OutgoingMessageType.carousel,
};
export const offlineAttachment: Offline.OutgoingMessageBase = {
export const webAttachment: Web.OutgoingMessageBase = {
data: {
quick_replies: [
{
@ -142,5 +142,5 @@ export const offlineAttachment: Offline.OutgoingMessageBase = {
type: FileType.image,
url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
},
type: Offline.OutgoingMessageType.file,
type: Web.OutgoingMessageType.file,
};

View File

@ -12,56 +12,55 @@ import {
StdEventType,
} from '@/chat/schemas/types/message';
import { Offline } from '../types';
import { Web } from '../types';
const img_url =
'http://demo.hexabot.ai/attachment/download/5c334078e2c41d11206bd152/myimage.png';
// Offline events
const offlineEventPayload: Offline.Event = {
type: Offline.IncomingMessageType.postback,
// Web events
const webEventPayload: Web.Event = {
type: Web.IncomingMessageType.postback,
data: {
text: 'Get Started',
payload: 'GET_STARTED',
},
author: 'offline-9be7aq09-b45a-452q-bcs0-f145b9qce1cad',
mid: 'offline-event-payload',
author: 'web-9be7aq09-b45a-452q-bcs0-f145b9qce1cad',
mid: 'web-event-payload',
read: true,
};
export const offlineEventText: Offline.IncomingMessage<Offline.IncomingTextMessage> =
{
type: Offline.IncomingMessageType.text,
data: {
text: 'Hello',
},
author: 'offline-9qsdfgqxac09-f83a-452d-bca0-f1qsdqg457c1ad',
mid: 'offline-event-text',
read: true,
};
export const webEventText: Web.IncomingMessage<Web.IncomingTextMessage> = {
type: Web.IncomingMessageType.text,
data: {
text: 'Hello',
},
author: 'web-9qsdfgqxac09-f83a-452d-bca0-f1qsdqg457c1ad',
mid: 'web-event-text',
read: true,
};
const offlineEventLocation: Offline.IncomingMessage = {
type: Offline.IncomingMessageType.location,
const webEventLocation: Web.IncomingMessage = {
type: Web.IncomingMessageType.location,
data: {
coordinates: {
lat: 2.0545,
lng: 12.2558,
},
},
author: 'offline-9beqsdqa09-b489a-438c-bqd0-f11buykkhl851ad',
mid: 'offline-event-location',
author: 'web-9beqsdqa09-b489a-438c-bqd0-f11buykkhl851ad',
mid: 'web-event-location',
read: true,
};
const offlineEventFile: Offline.Event = {
type: Offline.IncomingMessageType.file,
const webEventFile: Web.Event = {
type: Web.IncomingMessageType.file,
data: {
type: FileType.image,
url: img_url,
size: 500,
},
author: 'offline-9be8ac09-b43a-432d-bca0-f11b98cec1ad',
mid: 'offline-event-file',
author: 'web-9be8ac09-b43a-432d-bca0-f11b98cec1ad',
mid: 'web-event-file',
read: true,
};
@ -85,66 +84,66 @@ const fileChannelData = {
ipAddress: '3.3.3.3',
};
export const offlineEvents: [string, Offline.IncomingMessage, any][] = [
export const webEvents: [string, Web.IncomingMessage, any][] = [
[
'Payload Event',
offlineEventPayload,
webEventPayload,
{
channelData: payloadChannelData,
id: offlineEventPayload.mid,
id: webEventPayload.mid,
eventType: StdEventType.message,
messageType: IncomingMessageType.postback,
payload: offlineEventPayload.data.payload,
payload: webEventPayload.data.payload,
message: {
postback: offlineEventPayload.data.payload,
text: offlineEventPayload.data.text,
postback: webEventPayload.data.payload,
text: webEventPayload.data.text,
},
},
],
[
'Text Event',
offlineEventText,
webEventText,
{
channelData: textChannelData,
id: offlineEventText.mid,
id: webEventText.mid,
eventType: StdEventType.message,
messageType: IncomingMessageType.message,
payload: undefined,
message: {
text: offlineEventText.data.text,
text: webEventText.data.text,
},
},
],
[
'Location Event',
offlineEventLocation,
webEventLocation,
{
channelData: locationChannelData,
id: offlineEventLocation.mid,
id: webEventLocation.mid,
eventType: StdEventType.message,
messageType: IncomingMessageType.location,
payload: {
type: Offline.IncomingMessageType.location,
type: Web.IncomingMessageType.location,
coordinates: {
lat: offlineEventLocation.data.coordinates.lat,
lon: offlineEventLocation.data.coordinates.lng,
lat: webEventLocation.data.coordinates.lat,
lon: webEventLocation.data.coordinates.lng,
},
},
message: {
type: Offline.IncomingMessageType.location,
type: Web.IncomingMessageType.location,
coordinates: {
lat: offlineEventLocation.data.coordinates.lat,
lon: offlineEventLocation.data.coordinates.lng,
lat: webEventLocation.data.coordinates.lat,
lon: webEventLocation.data.coordinates.lng,
},
},
},
],
[
'File Event',
offlineEventFile,
webEventFile,
{
channelData: fileChannelData,
id: offlineEventFile.mid,
id: webEventFile.mid,
eventType: StdEventType.message,
messageType: IncomingMessageType.attachments,
payload: {

View File

@ -48,21 +48,21 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import OfflineHandler from '../index.channel';
import WebChannelHandler from '../index.channel';
import {
offlineAttachment,
offlineButtons,
offlineCarousel,
offlineList,
offlineQuickReplies,
offlineText,
webAttachment,
webButtons,
webCarousel,
webList,
webQuickReplies,
webText,
} from './data.mock';
describe('Offline Handler', () => {
describe('WebChannelHandler', () => {
let subscriberService: SubscriberService;
let handler: OfflineHandler;
const offlineSettings = {};
let handler: WebChannelHandler;
const webSettings = {};
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
@ -87,7 +87,7 @@ describe('Offline Handler', () => {
chatbot: { lang: { default: 'fr' } },
})),
getSettings: jest.fn(() => ({
offline: offlineSettings,
web: webSettings,
})),
},
},
@ -102,7 +102,7 @@ describe('Offline Handler', () => {
MessageRepository,
MenuService,
MenuRepository,
OfflineHandler,
WebChannelHandler,
EventEmitter2,
LoggerService,
{
@ -122,7 +122,7 @@ describe('Offline Handler', () => {
],
}).compile();
subscriberService = module.get<SubscriberService>(SubscriberService);
handler = module.get<OfflineHandler>(OfflineHandler);
handler = module.get<WebChannelHandler>(WebChannelHandler);
});
afterAll(async () => {
@ -131,29 +131,29 @@ describe('Offline Handler', () => {
it('should have correct name', () => {
expect(handler).toBeDefined();
expect(handler.getName()).toEqual('offline-channel');
expect(handler.getName()).toEqual('web-channel');
});
it('should format text properly', () => {
const formatted = handler._textFormat(textMessage, {});
expect(formatted).toEqual(offlineText);
expect(formatted).toEqual(webText);
});
it('should format quick replies properly', () => {
const formatted = handler._quickRepliesFormat(quickRepliesMessage, {});
expect(formatted).toEqual(offlineQuickReplies);
expect(formatted).toEqual(webQuickReplies);
});
it('should format buttons properly', () => {
const formatted = handler._buttonsFormat(buttonsMessage, {});
expect(formatted).toEqual(offlineButtons);
expect(formatted).toEqual(webButtons);
});
it('should format list properly', () => {
const formatted = handler._listFormat(contentMessage, {
content: contentMessage.options,
});
expect(formatted).toEqual(offlineList);
expect(formatted).toEqual(webList);
});
it('should format carousel properly', () => {
@ -163,12 +163,12 @@ describe('Offline Handler', () => {
display: OutgoingMessageFormat.carousel,
},
});
expect(formatted).toEqual(offlineCarousel);
expect(formatted).toEqual(webCarousel);
});
it('should format attachment properly', () => {
const formatted = handler._attachmentFormat(attachmentMessage, {});
expect(formatted).toEqual(offlineAttachment);
expect(formatted).toEqual(webAttachment);
});
it('creates a new subscriber if needed + set a new session', async () => {
@ -180,7 +180,7 @@ describe('Offline Handler', () => {
user: {},
} as any as Request;
const generatedId = 'offline-test';
const generatedId = 'web-test';
const clearMock = jest
.spyOn(handler, 'generateId')
.mockImplementation(() => generatedId);
@ -192,7 +192,7 @@ describe('Offline Handler', () => {
agent: req.headers['user-agent'],
ipAddress: '0.0.0.0',
isSocket: false,
name: 'offline-channel',
name: 'web-channel',
},
country: '',
first_name: req.query.first_name,
@ -209,7 +209,7 @@ describe('Offline Handler', () => {
}, {});
expect(subscriberAttrs).toEqual(expectedAttrs);
expect(req.session).toEqual({
offline: {
web: {
isSocket: false,
messageQueue: [],
polling: false,
@ -222,7 +222,7 @@ describe('Offline Handler', () => {
const subscriber2nd = await handler['getOrCreateSession'](req);
expect(subscriber2nd.id).toBe(subscriber.id);
expect(req.session).toEqual({
offline: {
web: {
isSocket: false,
messageQueue: [],
polling: false,
@ -232,9 +232,8 @@ describe('Offline Handler', () => {
});
it('subscribes and returns the message history', async () => {
const subscriber = await subscriberService.findOneByForeignIdAndPopulate(
'foreign-id-offline-1',
);
const subscriber =
await subscriberService.findOneByForeignIdAndPopulate('foreign-id-web-1');
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const req = {
@ -257,7 +256,7 @@ describe('Offline Handler', () => {
} as any as SocketResponse;
req.session = {
cookie: { originalMaxAge: 0 },
offline: {
web: {
isSocket: true,
messageQueue: [],
polling: false,

View File

@ -36,14 +36,14 @@ import {
import { SocketEventDispatcherService } from '@/websocket/services/socket-event-dispatcher.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import OfflineHandler from '../index.channel';
import OfflineEventWrapper from '../wrapper';
import WebChannelHandler from '../index.channel';
import WebEventWrapper from '../wrapper';
import { offlineEvents } from './events.mock';
import { webEvents } from './events.mock';
describe(`Offline event wrapper`, () => {
let handler: OfflineHandler;
const offlineSettings = {};
describe(`Web event wrapper`, () => {
let handler: WebChannelHandler;
const webSettings = {};
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -65,7 +65,7 @@ describe(`Offline event wrapper`, () => {
chatbot: { lang: { default: 'fr' } },
})),
getSettings: jest.fn(() => ({
offline: offlineSettings,
web: webSettings,
})),
},
},
@ -86,7 +86,7 @@ describe(`Offline event wrapper`, () => {
MessageRepository,
MenuService,
MenuRepository,
OfflineHandler,
WebChannelHandler,
EventEmitter2,
LoggerService,
{
@ -105,7 +105,7 @@ describe(`Offline event wrapper`, () => {
},
],
}).compile();
handler = module.get<OfflineHandler>(OfflineHandler);
handler = module.get<WebChannelHandler>(WebChannelHandler);
});
afterAll(async () => {
@ -113,21 +113,18 @@ describe(`Offline event wrapper`, () => {
await closeInMongodConnection();
});
test.each(offlineEvents)(
'should wrap event : %s',
(_testCase, e, expected) => {
const event = new OfflineEventWrapper(
handler as unknown as OfflineHandler,
e,
expected.channelData,
);
expect(event.getChannelData()).toEqual(expected.channelData);
expect(event.getId()).toEqual(expected.id);
expect(event.getEventType()).toEqual(expected.eventType);
expect(event.getMessageType()).toEqual(expected.messageType);
expect(event.getPayload()).toEqual(expected.payload);
expect(event.getMessage()).toEqual(expected.message);
expect(event.getDeliveredMessages()).toEqual([]);
},
);
test.each(webEvents)('should wrap event : %s', (_testCase, e, expected) => {
const event = new WebEventWrapper(
handler as unknown as WebChannelHandler,
e,
expected.channelData,
);
expect(event.getChannelData()).toEqual(expected.channelData);
expect(event.getId()).toEqual(expected.id);
expect(event.getEventType()).toEqual(expected.eventType);
expect(event.getMessageType()).toEqual(expected.messageType);
expect(event.getPayload()).toEqual(expected.payload);
expect(event.getMessage()).toEqual(expected.message);
expect(event.getDeliveredMessages()).toEqual([]);
});
});

View File

@ -58,9 +58,9 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import { OFFLINE_CHANNEL_NAMESPACE } from './settings';
import { Offline } from './types';
import OfflineEventWrapper from './wrapper';
import { WEB_CHANNEL_NAMESPACE } from './settings';
import { Web } from './types';
import WebEventWrapper from './wrapper';
@Injectable()
export default abstract class BaseWebChannelHandler<
@ -92,7 +92,7 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Verify offline websocket connection and return settings
* Verify web websocket connection and return settings
*
* @param client - The socket client
*/
@ -138,20 +138,20 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Adapt incoming message structure for offline channel
* Adapt incoming message structure for web channel
*
* @param incoming - Incoming message
* @returns Formatted offline message
* @returns Formatted web message
*/
private formatIncomingHistoryMessage(
incoming: IncomingMessage,
): Offline.IncomingMessageBase {
): Web.IncomingMessageBase {
// Format incoming message
if ('type' in incoming.message) {
if (incoming.message.type === PayloadType.location) {
const coordinates = incoming.message.coordinates;
return {
type: Offline.IncomingMessageType.location,
type: Web.IncomingMessageType.location,
data: {
coordinates: {
lat: coordinates.lat,
@ -165,7 +165,7 @@ export default abstract class BaseWebChannelHandler<
? incoming.message.attachment[0]
: incoming.message.attachment;
return {
type: Offline.IncomingMessageType.file,
type: Web.IncomingMessageType.file,
data: {
type: attachment.type,
url: attachment.payload.url,
@ -174,21 +174,21 @@ export default abstract class BaseWebChannelHandler<
}
} else {
return {
type: Offline.IncomingMessageType.text,
type: Web.IncomingMessageType.text,
data: incoming.message,
};
}
}
/**
* Adapt the outgoing message structure for offline channel
* Adapt the outgoing message structure for web channel
*
* @param outgoing - The outgoing message
* @returns Formatted offline message
* @returns Formatted web message
*/
private formatOutgoingHistoryMessage(
outgoing: OutgoingMessage,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
// Format outgoing message
if ('buttons' in outgoing.message) {
return this._buttonsFormat(outgoing.message);
@ -212,13 +212,13 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Adapt the message structure for offline channel
* Adapt the message structure for web channel
*
* @param messages - The messages to be formatted
*
* @returns Formatted message
*/
private formatHistoryMessages(messages: AnyMessage[]): Offline.Message[] {
private formatHistoryMessages(messages: AnyMessage[]): Web.Message[] {
return messages.map((anyMessage: AnyMessage) => {
if ('sender' in anyMessage && anyMessage.sender) {
return {
@ -227,7 +227,7 @@ export default abstract class BaseWebChannelHandler<
read: true, // Temporary fix as read is false in the bd
mid: anyMessage.mid,
createdAt: anyMessage.createdAt,
} as Offline.IncomingMessage;
} as Web.IncomingMessage;
} else {
const outgoingMessage = anyMessage as OutgoingMessage;
return {
@ -237,7 +237,7 @@ export default abstract class BaseWebChannelHandler<
mid: outgoingMessage.mid,
handover: !!outgoingMessage.handover,
createdAt: outgoingMessage.createdAt,
} as Offline.OutgoingMessage;
} as Web.OutgoingMessage;
}
});
}
@ -254,8 +254,8 @@ export default abstract class BaseWebChannelHandler<
req: Request | SocketRequest,
until: Date = new Date(),
n: number = 30,
): Promise<Offline.Message[]> {
const profile = req.session?.offline?.profile;
): Promise<Web.Message[]> {
const profile = req.session?.web?.profile;
if (profile) {
const messages = await this.messageService.findHistoryUntilDate(
profile,
@ -279,8 +279,8 @@ export default abstract class BaseWebChannelHandler<
req: Request,
since: Date = new Date(10e14),
n: number = 30,
): Promise<Offline.Message[]> {
const profile = req.session?.offline?.profile;
): Promise<Web.Message[]> {
const profile = req.session?.web?.profile;
if (profile) {
const messages = await this.messageService.findHistorySinceDate(
profile,
@ -299,7 +299,7 @@ export default abstract class BaseWebChannelHandler<
*/
private async verifyToken(verificationToken: string) {
const settings =
(await this.getSettings()) as Settings[typeof OFFLINE_CHANNEL_NAMESPACE];
(await this.getSettings()) as Settings[typeof WEB_CHANNEL_NAMESPACE];
const verifyToken = settings.verification_token;
if (!verifyToken) {
@ -326,7 +326,7 @@ export default abstract class BaseWebChannelHandler<
req: Request | SocketRequest,
res: Response | SocketResponse,
) {
const settings = await this.getSettings<typeof OFFLINE_CHANNEL_NAMESPACE>();
const settings = await this.getSettings<typeof WEB_CHANNEL_NAMESPACE>();
// If we have an origin header...
if (req.headers && req.headers.origin) {
// Get the allowed origins
@ -376,7 +376,7 @@ export default abstract class BaseWebChannelHandler<
res: Response | SocketResponse,
next: (profile: Subscriber) => void,
) {
if (!req.session?.offline?.profile?.id) {
if (!req.session?.web?.profile?.id) {
this.logger.warn(
'Web Channel Handler : No session ID to be found!',
req.session,
@ -385,8 +385,8 @@ export default abstract class BaseWebChannelHandler<
.status(403)
.json({ err: 'Web Channel Handler : Unauthorized!' });
} else if (
('isSocket' in req && !!req.isSocket !== req.session.offline.isSocket) ||
!Array.isArray(req.session.offline.messageQueue)
('isSocket' in req && !!req.isSocket !== req.session.web.isSocket) ||
!Array.isArray(req.session.web.messageQueue)
) {
this.logger.warn(
'Web Channel Handler : Mixed channel request or invalid session data!',
@ -396,7 +396,7 @@ export default abstract class BaseWebChannelHandler<
.status(403)
.json({ err: 'Web Channel Handler : Unauthorized!' });
}
next(req.session?.offline?.profile);
next(req.session?.web?.profile);
}
/**
@ -440,15 +440,15 @@ export default abstract class BaseWebChannelHandler<
): Promise<SubscriberFull> {
const data = req.query;
// Subscriber has already a session
const sessionProfile = req.session?.offline?.profile;
const sessionProfile = req.session?.web?.profile;
if (sessionProfile) {
const subscriber = await this.subscriberService.findOneAndPopulate(
sessionProfile.id,
);
if (!subscriber || !req.session.offline) {
if (!subscriber || !req.session.web) {
throw new Error('Subscriber session was not persisted in DB');
}
req.session.offline.profile = subscriber;
req.session.web.profile = subscriber;
return subscriber;
}
@ -456,7 +456,7 @@ export default abstract class BaseWebChannelHandler<
const newProfile: SubscriberCreateDto = {
foreign_id: this.generateId(),
first_name: data.first_name ? data.first_name.toString() : 'Anon.',
last_name: data.last_name ? data.last_name.toString() : 'Offline User',
last_name: data.last_name ? data.last_name.toString() : 'Web User',
assignedTo: null,
assignedAt: null,
lastvisit: new Date(),
@ -481,7 +481,7 @@ export default abstract class BaseWebChannelHandler<
avatar: null,
};
req.session.offline = {
req.session.web = {
profile,
isSocket: 'isSocket' in req && !!req.isSocket,
messageQueue: [],
@ -507,9 +507,7 @@ export default abstract class BaseWebChannelHandler<
.json({ err: 'Polling not authorized when using websockets' });
}
// Session must be active
if (
!(req.session && req.session.offline && req.session.offline.profile.id)
) {
if (!(req.session && req.session.web && req.session.web.profile.id)) {
this.logger.warn(
'Web Channel Handler : Must be connected to poll messages',
);
@ -519,7 +517,7 @@ export default abstract class BaseWebChannelHandler<
}
// Can only request polling once at a time
if (req.session && req.session.offline && req.session.offline.polling) {
if (req.session && req.session.web && req.session.web.polling) {
this.logger.warn(
'Web Channel Handler : Poll rejected ... already requested',
);
@ -528,7 +526,7 @@ export default abstract class BaseWebChannelHandler<
.json({ err: 'Poll rejected ... already requested' });
}
req.session.offline.polling = true;
req.session.web.polling = true;
const fetchMessages = async (req: Request, res: Response, retrials = 1) => {
try {
@ -539,8 +537,8 @@ export default abstract class BaseWebChannelHandler<
setTimeout(async () => {
await fetchMessages(req, res, retrials * 2);
}, retrials * 1000);
} else if (req.session.offline) {
req.session.offline.polling = false;
} else if (req.session.web) {
req.session.web.polling = false;
return res.status(200).json(messages.map((msg) => ['message', msg]));
} else {
this.logger.error(
@ -549,8 +547,8 @@ export default abstract class BaseWebChannelHandler<
return res.status(500).json({ err: 'No session data' });
}
} catch (err) {
if (req.session.offline) {
req.session.offline.polling = false;
if (req.session.web) {
req.session.web.polling = false;
}
this.logger.error('Web Channel Handler : Polling failed', err);
return res.status(500).json({ err: 'Polling failed' });
@ -560,7 +558,7 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Allow the subscription to a offline's webhook after verification
* Allow the subscription to a web's webhook after verification
*
* @param req
* @param res
@ -608,7 +606,7 @@ export default abstract class BaseWebChannelHandler<
* @param filename
*/
private async storeAttachment(
upload: Omit<Offline.IncomingAttachmentMessageData, 'url' | 'file'>,
upload: Omit<Web.IncomingAttachmentMessageData, 'url' | 'file'>,
filename: string,
next: (
err: Error | null,
@ -623,7 +621,7 @@ export default abstract class BaseWebChannelHandler<
type: upload.type || 'text/txt',
size: upload.size || 0,
location: filename,
channel: { offline: {} },
channel: { web: {} },
});
this.logger.debug(
@ -657,9 +655,9 @@ export default abstract class BaseWebChannelHandler<
result: { type: string; url: string } | false,
) => void,
): Promise<void> {
const data: Offline.IncomingMessage = req.body;
const data: Web.IncomingMessage = req.body;
// Check if any file is provided
if (!req.session.offline) {
if (!req.session.web) {
this.logger.debug('Web Channel Handler : No session provided');
return next(null, false);
}
@ -682,7 +680,7 @@ export default abstract class BaseWebChannelHandler<
// Store file as attachment
const dirPath = path.join(config.parameters.uploadDir);
const sanitizedFilename = sanitize(
`${req.session.offline.profile.id}_${+new Date()}_${upload.name}`,
`${req.session.web.profile.id}_${+new Date()}_${upload.name}`,
);
const filePath = path.resolve(dirPath, sanitizedFilename);
@ -763,7 +761,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns The channel's data
*/
protected getChannelData(req: Request | SocketRequest): Offline.ChannelData {
protected getChannelData(req: Request | SocketRequest): Web.ChannelData {
return {
isSocket: 'isSocket' in req && !!req.isSocket,
ipAddress: this.getIpAddress(req),
@ -781,13 +779,13 @@ export default abstract class BaseWebChannelHandler<
req: Request | SocketRequest,
res: Response | SocketResponse,
): void {
const data: Offline.IncomingMessage = req.body;
const data: Web.IncomingMessage = req.body;
this.validateSession(req, res, (profile) => {
this.handleFilesUpload(
req,
res,
// @ts-expect-error @TODO : This needs to be fixed at a later point @TODO
(err: Error, upload: Offline.IncomingMessageData) => {
(err: Error, upload: Web.IncomingMessageData) => {
if (err) {
this.logger.warn(
'Web Channel Handler : Unable to upload file ',
@ -802,7 +800,7 @@ export default abstract class BaseWebChannelHandler<
data.data = upload;
}
const channelData = this.getChannelData(req);
const event: OfflineEventWrapper = new OfflineEventWrapper(
const event: WebEventWrapper = new WebEventWrapper(
this,
data,
channelData,
@ -844,14 +842,14 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Process incoming Offline data (finding out its type and assigning it to its proper handler)
* Process incoming Web Channel data (finding out its type and assigning it to its proper handler)
*
* @param req
* @param res
*/
async handle(req: Request | SocketRequest, res: Response | SocketResponse) {
const settings = await this.getSettings();
// Offline messaging can be done through websockets or long-polling
// Web Channel messaging can be done through websockets or long-polling
try {
await this.checkRequest(req, res);
if (req.method === 'GET') {
@ -887,7 +885,7 @@ export default abstract class BaseWebChannelHandler<
.json({ err: 'Webhook received unknown command' });
}
} else if (req.query._disconnect) {
req.session.offline = undefined;
req.session.web = undefined;
return res.status(200).json({ _disconnect: true });
} else {
// Handle webhook subscribe requests
@ -911,7 +909,7 @@ export default abstract class BaseWebChannelHandler<
* @returns UUID
*/
generateId(): string {
return 'offline-' + uuidv4();
return 'web-' + uuidv4();
}
/**
@ -925,9 +923,9 @@ export default abstract class BaseWebChannelHandler<
_textFormat(
message: StdOutgoingTextMessage,
_options?: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
return {
type: Offline.OutgoingMessageType.text,
type: Web.OutgoingMessageType.text,
data: message,
};
}
@ -943,9 +941,9 @@ export default abstract class BaseWebChannelHandler<
_quickRepliesFormat(
message: StdOutgoingQuickRepliesMessage,
_options?: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
return {
type: Offline.OutgoingMessageType.quick_replies,
type: Web.OutgoingMessageType.quick_replies,
data: {
text: message.text,
quick_replies: message.quickReplies,
@ -964,9 +962,9 @@ export default abstract class BaseWebChannelHandler<
_buttonsFormat(
message: StdOutgoingButtonsMessage,
_options?: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
return {
type: Offline.OutgoingMessageType.buttons,
type: Web.OutgoingMessageType.buttons,
data: {
text: message.text,
buttons: message.buttons,
@ -985,9 +983,9 @@ export default abstract class BaseWebChannelHandler<
_attachmentFormat(
message: StdOutgoingAttachmentMessage<WithUrl<Attachment>>,
_options?: BlockOptions,
): Offline.OutgoingMessageBase {
const payload: Offline.OutgoingMessageBase = {
type: Offline.OutgoingMessageType.file,
): Web.OutgoingMessageBase {
const payload: Web.OutgoingMessageBase = {
type: Web.OutgoingMessageType.file,
data: {
type: message.attachment.type,
url: message.attachment.payload.url,
@ -1007,10 +1005,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns An array of elements object
*/
_formatElements(
data: any[],
options: BlockOptions,
): Offline.MessageElement[] {
_formatElements(data: any[], options: BlockOptions): Web.MessageElement[] {
if (!options.content || !options.content.fields) {
throw new Error('Content options are missing the fields');
}
@ -1018,7 +1013,7 @@ export default abstract class BaseWebChannelHandler<
const fields = options.content.fields;
const buttons: Button[] = options.content.buttons;
return data.map((item) => {
const element: Offline.MessageElement = {
const element: Web.MessageElement = {
title: item[fields.title],
buttons: item.buttons || [],
};
@ -1031,7 +1026,7 @@ export default abstract class BaseWebChannelHandler<
if (!attachmentPayload.id) {
// @deprecated
this.logger.warn(
'Offline Channel Handler: Attachment remote url has been deprecated',
'Web Channel Handler: Attachment remote url has been deprecated',
item,
);
}
@ -1090,11 +1085,11 @@ export default abstract class BaseWebChannelHandler<
_listFormat(
message: StdOutgoingListMessage,
options: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
const data = message.elements || [];
const pagination = message.pagination;
let buttons: Button[] = [],
elements: Offline.MessageElement[] = [];
elements: Web.MessageElement[] = [];
// Items count min check
if (!data.length) {
@ -1123,7 +1118,7 @@ export default abstract class BaseWebChannelHandler<
}
: {};
return {
type: Offline.OutgoingMessageType.list,
type: Web.OutgoingMessageType.list,
data: {
elements,
buttons,
@ -1143,7 +1138,7 @@ export default abstract class BaseWebChannelHandler<
_carouselFormat(
message: StdOutgoingListMessage,
options: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
const data = message.elements || [];
// Items count min check
if (data.length === 0) {
@ -1156,7 +1151,7 @@ export default abstract class BaseWebChannelHandler<
// Populate items (elements/cards) with content
const elements = this._formatElements(data, options);
return {
type: Offline.OutgoingMessageType.carousel,
type: Web.OutgoingMessageType.carousel,
data: {
elements,
},
@ -1174,7 +1169,7 @@ export default abstract class BaseWebChannelHandler<
_formatMessage(
envelope: StdOutgoingEnvelope,
options: BlockOptions,
): Offline.OutgoingMessageBase {
): Web.OutgoingMessageBase {
switch (envelope.format) {
case OutgoingMessageFormat.attachment:
return this._attachmentFormat(envelope.message, options);
@ -1214,14 +1209,14 @@ export default abstract class BaseWebChannelHandler<
}
/**
* Send a Offline Message to the end-user
* Send a Web Channel Message to the end-user
*
* @param event - Incoming event/message being responded to
* @param envelope - The message to be sent {format, message}
* @param options - Might contain additional settings
* @param _context - Contextual data
*
* @returns The offline's response, otherwise an error
* @returns The web's response, otherwise an error
*/
async sendMessage(
event: EventWrapper<any, any>,
@ -1229,13 +1224,13 @@ export default abstract class BaseWebChannelHandler<
options: BlockOptions,
_context?: any,
): Promise<{ mid: string }> {
const messageBase: Offline.OutgoingMessageBase = this._formatMessage(
const messageBase: Web.OutgoingMessageBase = this._formatMessage(
envelope,
options,
);
const subscriber = event.getSender();
const message: Offline.OutgoingMessage = {
const message: Web.OutgoingMessage = {
...messageBase,
mid: this.generateId(),
author: 'chatbot',
@ -1296,9 +1291,9 @@ export default abstract class BaseWebChannelHandler<
*
* @param event - The message event received
*
* @returns The offline's response, otherwise an error
* @returns The web's response, otherwise an error
*/
async getUserData(event: OfflineEventWrapper): Promise<SubscriberCreateDto> {
async getUserData(event: WebEventWrapper): Promise<SubscriberCreateDto> {
return event.getSender() as SubscriberCreateDto;
}
}

View File

@ -0,0 +1,3 @@
{
"web_channel": "Web Channel"
}

View File

@ -0,0 +1,3 @@
{
"web_channel": "Canal Web"
}

View File

@ -20,11 +20,11 @@ import { SettingService } from '@/setting/services/setting.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from './base-web-channel';
import { OFFLINE_CHANNEL_NAME } from './settings';
import { WEB_CHANNEL_NAME } from './settings';
@Injectable()
export default class OfflineHandler extends BaseWebChannelHandler<
typeof OFFLINE_CHANNEL_NAME
export default class WebChannelHandler extends BaseWebChannelHandler<
typeof WEB_CHANNEL_NAME
> {
constructor(
settingService: SettingService,
@ -39,7 +39,7 @@ export default class OfflineHandler extends BaseWebChannelHandler<
websocketGateway: WebsocketGateway,
) {
super(
OFFLINE_CHANNEL_NAME,
WEB_CHANNEL_NAME,
settingService,
channelService,
logger,

View File

@ -0,0 +1,16 @@
import DEFAULT_WEB_CHANNEL_SETTINGS, {
WEB_CHANNEL_NAMESPACE,
} from './settings';
declare global {
interface Settings extends SettingTree<typeof DEFAULT_WEB_CHANNEL_SETTINGS> {}
}
declare module '@nestjs/event-emitter' {
interface IHookExtensionsOperationMap {
[WEB_CHANNEL_NAMESPACE]: TDefinition<
object,
SettingMapByType<typeof DEFAULT_WEB_CHANNEL_SETTINGS>
>;
}
}

View File

@ -1,7 +1,7 @@
{
"name": "hexabot-channel-offline",
"name": "hexabot-channel-web",
"version": "2.0.0",
"description": "The Web Channel Extension for Hexabot Chatbot / Agent Builder for website integration",
"author": "Hexastack",
"license": "AGPL-3.0-only"
}
}

View File

@ -9,97 +9,97 @@
import { ChannelSetting } from '@/channel/types';
import { SettingType } from '@/setting/schemas/types';
import { Offline } from './types';
import { Web } from './types';
export const OFFLINE_CHANNEL_NAME = 'offline-channel' as const;
export const WEB_CHANNEL_NAME = 'web-channel' as const;
export const OFFLINE_CHANNEL_NAMESPACE = 'offline_channel';
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
export default [
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.verification_token,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.verification_token,
value: 'token123',
type: SettingType.secret,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_domains,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_domains,
value: 'http://localhost:8080,http://localhost:4000',
type: SettingType.text,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.start_button,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.start_button,
value: true,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.input_disabled,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.input_disabled,
value: false,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.persistent_menu,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.persistent_menu,
value: true,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.greeting_message,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.greeting_message,
value: 'Welcome! Ready to start a conversation with our chatbot?',
type: SettingType.textarea,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.theme_color,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.theme_color,
value: 'teal',
type: SettingType.select,
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.window_title,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.window_title,
value: 'Widget Title',
type: SettingType.text,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.avatar_url,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.avatar_url,
value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64',
type: SettingType.text,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_emoji,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.show_emoji,
value: true,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_file,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.show_file,
value: true,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.show_location,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.show_location,
value: true,
type: SettingType.checkbox,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_upload_size,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_upload_size,
value: 2500000,
type: SettingType.number,
},
{
group: OFFLINE_CHANNEL_NAMESPACE,
label: Offline.SettingLabel.allowed_upload_types,
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_upload_types,
value:
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
type: SettingType.textarea,
},
] as const satisfies ChannelSetting<typeof OFFLINE_CHANNEL_NAME>[];
] as const satisfies ChannelSetting<typeof WEB_CHANNEL_NAME>[];

View File

@ -11,7 +11,7 @@ import { Button, WebUrlButton } from '@/chat/schemas/types/button';
import { FileType } from '@/chat/schemas/types/message';
import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
export namespace Offline {
export namespace Web {
export enum SettingLabel {
secret = 'secret',
verification_token = 'verification_token',
@ -39,7 +39,7 @@ export namespace Offline {
};
export type RequestSession = {
offline?: {
web?: {
profile: SubscriberFull;
isSocket: boolean;
messageQueue: any[];
@ -61,7 +61,7 @@ export namespace Offline {
file = 'file',
}
export type EventType = Offline.StatusEventType | Offline.IncomingMessageType;
export type EventType = Web.StatusEventType | Web.IncomingMessageType;
export enum OutgoingMessageType {
text = 'text',

View File

@ -21,56 +21,56 @@ import {
import { Payload } from '@/chat/schemas/types/quick-reply';
import BaseWebChannelHandler from './base-web-channel';
import { Offline } from './types';
import { Web } from './types';
type OfflineEventAdapter =
type WebEventAdapter =
| {
eventType: StdEventType.unknown;
messageType: never;
raw: Offline.Event;
raw: Web.Event;
}
| {
eventType: StdEventType.read;
messageType: never;
raw: Offline.StatusReadEvent;
raw: Web.StatusReadEvent;
}
| {
eventType: StdEventType.delivery;
messageType: never;
raw: Offline.StatusDeliveryEvent;
raw: Web.StatusDeliveryEvent;
}
| {
eventType: StdEventType.typing;
messageType: never;
raw: Offline.StatusTypingEvent;
raw: Web.StatusTypingEvent;
}
| {
eventType: StdEventType.message;
messageType: IncomingMessageType.message;
raw: Offline.IncomingMessage<Offline.IncomingTextMessage>;
raw: Web.IncomingMessage<Web.IncomingTextMessage>;
}
| {
eventType: StdEventType.message;
messageType:
| IncomingMessageType.postback
| IncomingMessageType.quick_reply;
raw: Offline.IncomingMessage<Offline.IncomingPayloadMessage>;
raw: Web.IncomingMessage<Web.IncomingPayloadMessage>;
}
| {
eventType: StdEventType.message;
messageType: IncomingMessageType.location;
raw: Offline.IncomingMessage<Offline.IncomingLocationMessage>;
raw: Web.IncomingMessage<Web.IncomingLocationMessage>;
}
| {
eventType: StdEventType.message;
messageType: IncomingMessageType.attachments;
raw: Offline.IncomingMessage<Offline.IncomingAttachmentMessage>;
raw: Web.IncomingMessage<Web.IncomingAttachmentMessage>;
};
export default class OfflineEventWrapper<
export default class WebEventWrapper<
T extends
BaseWebChannelHandler<ChannelName> = BaseWebChannelHandler<ChannelName>,
> extends EventWrapper<OfflineEventAdapter, Offline.Event> {
> extends EventWrapper<WebEventAdapter, Web.Event> {
/**
* Constructor : channel's event wrapper
*
@ -78,7 +78,7 @@ export default class OfflineEventWrapper<
* @param event - The message event received
* @param channelData - Channel's specific extra data {isSocket, ipAddress}
*/
constructor(handler: T, event: Offline.Event, channelData: any) {
constructor(handler: T, event: Web.Event, channelData: any) {
super(handler, event, channelData);
}
@ -90,34 +90,34 @@ export default class OfflineEventWrapper<
*
* @param event - The message event received
*/
_init(event: Offline.Event) {
_init(event: Web.Event) {
switch (event.type) {
case Offline.StatusEventType.delivery:
case Web.StatusEventType.delivery:
this._adapter.eventType = StdEventType.delivery;
break;
case Offline.StatusEventType.read:
case Web.StatusEventType.read:
this._adapter.eventType = StdEventType.read;
break;
case Offline.StatusEventType.typing:
case Web.StatusEventType.typing:
this._adapter.eventType = StdEventType.typing;
break;
case Offline.IncomingMessageType.text:
case Web.IncomingMessageType.text:
this._adapter.eventType = StdEventType.message;
this._adapter.messageType = IncomingMessageType.message;
break;
case Offline.IncomingMessageType.quick_reply:
case Web.IncomingMessageType.quick_reply:
this._adapter.eventType = StdEventType.message;
this._adapter.messageType = IncomingMessageType.quick_reply;
break;
case Offline.IncomingMessageType.postback:
case Web.IncomingMessageType.postback:
this._adapter.eventType = StdEventType.message;
this._adapter.messageType = IncomingMessageType.postback;
break;
case Offline.IncomingMessageType.location:
case Web.IncomingMessageType.location:
this._adapter.eventType = StdEventType.message;
this._adapter.messageType = IncomingMessageType.location;
break;
case Offline.IncomingMessageType.file:
case Web.IncomingMessageType.file:
this._adapter.eventType = StdEventType.message;
this._adapter.messageType = IncomingMessageType.attachments;
break;

4
api/src/index.d.ts vendored
View File

@ -27,7 +27,7 @@ declare module 'express-session' {
passport?: {
user?: SessionUser;
};
offline?: {
web?: {
profile?: T;
isSocket: boolean;
messageQueue: any[];
@ -40,7 +40,7 @@ declare module 'express-session' {
passport?: {
user?: SessionUser;
};
offline?: {
web?: {
profile?: SubscriberStub;
isSocket: boolean;
messageQueue: any[];

View File

@ -71,7 +71,7 @@ const conversations: ConversationCreateDto[] = [
{
sender: '1',
context: {
channel: 'offline-channel',
channel: 'web-channel',
text: 'Hello',
payload: '',
nlp: {
@ -106,7 +106,7 @@ const conversations: ConversationCreateDto[] = [
foreign_id: '',
labels: [],
assignedTo: null,
channel: { name: 'offline-channel' },
channel: { name: 'web-channel' },
},
skip: {},
attempt: 0,

View File

@ -9,7 +9,7 @@
import mongoose from 'mongoose';
import { LabelCreateDto } from '@/chat/dto/label.dto';
import { LabelModel, Label } from '@/chat/schemas/label.schema';
import { Label, LabelModel } from '@/chat/schemas/label.schema';
import { getFixturesWithDefaultValues } from '../defaultValues';
import { TFixturesDefaultValues } from '../types';
@ -19,7 +19,7 @@ export const labels: LabelCreateDto[] = [
description: 'test description 1',
label_id: {
messenger: 'messenger',
offline: 'offline',
web: 'web',
twitter: 'twitter',
dimelo: 'dimelo',
},
@ -30,7 +30,7 @@ export const labels: LabelCreateDto[] = [
description: 'test description 2',
label_id: {
messenger: 'messenger',
offline: 'offline',
web: 'web',
twitter: 'twitter',
dimelo: 'dimelo',
},

View File

@ -35,7 +35,7 @@ const subscribers: SubscriberCreateDto[] = [
retainedFrom: new Date('2020-01-01T20:40:03.249Z'),
},
{
foreign_id: 'foreign-id-offline-1',
foreign_id: 'foreign-id-web-1',
first_name: 'Maynard',
last_name: 'James Keenan',
language: 'en',
@ -43,7 +43,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline-channel',
name: 'web-channel',
},
labels: [],
assignedAt: null,
@ -51,7 +51,7 @@ const subscribers: SubscriberCreateDto[] = [
retainedFrom: new Date('2021-01-02T20:40:03.249Z'),
},
{
foreign_id: 'foreign-id-offline-2',
foreign_id: 'foreign-id-web-2',
first_name: 'Queen',
last_name: 'Elisabeth',
language: 'en',
@ -59,7 +59,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline-channel',
name: 'web-channel',
},
labels: [],
assignedAt: null,
@ -75,7 +75,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male',
country: 'US',
channel: {
name: 'offline-channel',
name: 'web-channel',
},
labels: [],
assignedAt: null,

View File

@ -16,7 +16,7 @@ import { modelInstance } from './misc';
import { subscriberInstance } from './subscriber';
export const contextBlankInstance: Context = {
channel: 'offline-channel',
channel: 'web-channel',
text: '',
payload: undefined,
nlp: { entities: [] },
@ -42,7 +42,7 @@ export const contextEmailVarInstance: Context = {
};
export const contextGetStartedInstance: Context = {
channel: 'offline-channel',
channel: 'web-channel',
text: 'Get Started',
payload: 'GET_STARTED',
nlp: { entities: [] },

View File

@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
lastvisit: new Date(),
retainedFrom: new Date(),
channel: {
name: 'offline-channel',
name: 'web-channel',
},
labels: [],
...modelInstance,

View File

@ -49,8 +49,8 @@ describe('WebsocketGateway', () => {
ioClient = io('http://localhost:3000', {
autoConnect: false,
transports: ['websocket', 'polling'],
// path: '/socket.io/?EIO=4&transport=websocket&channel=offline',
query: { EIO: '4', transport: 'websocket', channel: 'offline' },
// path: '/socket.io/?EIO=4&transport=websocket&channel=web',
query: { EIO: '4', transport: 'websocket', channel: 'web' },
});
app.listen(3000);

View File

@ -223,7 +223,7 @@ export class WebsocketGateway
'Unable to load session, creating a new one ...',
err,
);
if (searchParams.get('channel') === 'offline') {
if (searchParams.get('channel') === 'web') {
return this.createAndStoreSession(client, next);
} else {
return next(new Error('Unauthorized: Unknown session ID'));
@ -237,7 +237,7 @@ export class WebsocketGateway
return next(new Error('Unable to parse session ID from cookie'));
}
}
} else if (searchParams.get('channel') === 'offline') {
} else if (searchParams.get('channel') === 'web') {
return this.createAndStoreSession(client, next);
} else {
return next(new Error('Unauthorized to connect to WS'));

View File

@ -57,5 +57,5 @@ NEXT_PUBLIC_SSO_ENABLED=false
# Widget
APP_WIDGET_PORT=5173
REACT_APP_WIDGET_API_URL=http://${APP_DOMAIN}:${API_PORT}
REACT_APP_WIDGET_CHANNEL=offline
REACT_APP_WIDGET_CHANNEL=web
REACT_APP_WIDGET_TOKEN=token123

View File

@ -216,12 +216,6 @@
"handled_by_me": "Assigned to me",
"handled_by_chatbot": "Others",
"settings": "Settings",
"facebook_settings": "Facebook",
"messenger": "Facebook Messenger",
"msbot": "Microsoft Bot Connector",
"offline": "Web Channel",
"twitter": "Twitter",
"dimelo": "Dimelo",
"event_log": "Events Log",
"log_entry": "Log entry",
"dashboard": "Dashboard",
@ -304,11 +298,6 @@
"user_location_zipcode": "Zipcode",
"user_location_streetName": "Street Name",
"from_channels": "Target channels",
"messenger": "Facebook/Messenger",
"msbot": "Microsoft Bot Connector",
"offline": "Canal Web",
"twitter": "Twitter",
"dimelo": "Dimelo",
"simple_text": "Simple Text",
"quick_replies": "Quick Replies",
"buttons": "Buttons",

View File

@ -216,12 +216,6 @@
"handled_by_me": "Assignés à moi",
"handled_by_chatbot": "Autres",
"settings": "Paramètres",
"facebook_settings": "Paramètres Facebook",
"messenger": "Facebook Messenger",
"msbot": "Microsoft Bot Connector",
"offline": "Canal Web",
"twitter": "Twitter",
"dimelo": "Dimelo",
"event_log": "Journal des événements",
"log_entry": "Journal des entrées",
"dashboard": "Tableau de bord",
@ -304,11 +298,6 @@
"user_location_zipcode": "Code postal",
"user_location_streetName": "Adresse",
"from_channels": "Cibler les canaux",
"messenger": "Facebook/Messenger",
"msbot": "Microsoft Bot Connector",
"offline": "Canal Web",
"twitter": "Twitter",
"dimelo": "Dimelo",
"simple_text": "Texte simple",
"quick_replies": "Réponses rapides",
"buttons": "Boutons",

View File

@ -61,7 +61,7 @@ Once the widget is built, you can easily embed it into any webpage. Here's an ex
ReactDOM.render(
el(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline-channel',
channel: 'web-channel',
token: 'token123',
}),
domContainer,
@ -96,7 +96,7 @@ To prevent the website css from conflicting with the chat widget css, we can lev
ReactDOM.render(
React.createElement(HexabotWidget, {
apiUrl: 'https://api.yourdomain.com',
channel: 'offline-channel',
channel: 'web-channel',
token: 'token123',
}),
shadowContainer,

View File

@ -34,7 +34,7 @@
ReactDOM.render(
React.createElement(HexabotWidget, {
apiUrl: 'http://localhost:4000',
channel: 'offline-channel',
channel: 'web-channel',
token: 'token123',
}),
shadowContainer,

View File

@ -18,7 +18,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<ChatWidget
{...{
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'offline-channel',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'web-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'token123',
language: 'en',
}}

View File

@ -82,7 +82,7 @@ export type TChannelData = {
};
export type TRequestSession = {
offline?: {
web?: {
profile: ISubscriber;
isSocket: boolean;
// @TODO : not sure why we added messageQuery (long pooling ?)