mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
Merge pull request #257 from Hexastack/feat/refactor-extension
feat: refactor extensions as npm packages (be brave 1)
This commit is contained in:
@@ -14,3 +14,4 @@ test
|
||||
*.mock.ts
|
||||
__mock__
|
||||
__test__
|
||||
.hexabot
|
||||
|
||||
1
api/.gitignore
vendored
1
api/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.hexabot/
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
|
||||
@@ -4,10 +4,6 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
COPY merge-extensions-deps.js ./
|
||||
|
||||
COPY src/extensions ./src/extensions
|
||||
|
||||
COPY patches ./patches
|
||||
|
||||
RUN npm ci
|
||||
@@ -22,8 +18,6 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
COPY --from=builder /app/merge-extensions-deps.js ./
|
||||
|
||||
COPY --from=builder /app/src/extensions ./src/extensions
|
||||
|
||||
COPY --from=builder /app/patches ./patches
|
||||
@@ -44,8 +38,6 @@ WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
COPY --from=builder /app/merge-extensions-deps.js ./
|
||||
|
||||
COPY --from=builder /app/src/extensions ./src/extensions
|
||||
|
||||
COPY --from=builder /app/patches ./patches
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path');
|
||||
|
||||
// Define the paths
|
||||
const rootPackageJsonPath = path.join(__dirname, 'package.json');
|
||||
const pluginsDir = path.join(__dirname, 'src', 'extensions', 'plugins');
|
||||
const channelsDir = path.join(__dirname, 'src', 'extensions', 'channels');
|
||||
const helpersDir = path.join(__dirname, 'src', 'extensions', 'helpers');
|
||||
|
||||
// Helper function to merge dependencies
|
||||
function mergeDependencies(rootDeps, pluginDeps) {
|
||||
return {
|
||||
...rootDeps,
|
||||
...Object.entries(pluginDeps).reduce((acc, [key, version]) => {
|
||||
if (!rootDeps[key]) {
|
||||
acc[key] = version;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Read the root package.json
|
||||
const rootPackageJson = JSON.parse(
|
||||
fs.readFileSync(rootPackageJsonPath, 'utf-8'),
|
||||
);
|
||||
|
||||
// Initialize dependencies if not already present
|
||||
if (!rootPackageJson.dependencies) {
|
||||
rootPackageJson.dependencies = {};
|
||||
}
|
||||
|
||||
// Iterate over extension directories
|
||||
[
|
||||
...fs.readdirSync(pluginsDir),
|
||||
...fs.readdirSync(helpersDir),
|
||||
...fs.readdirSync(channelsDir),
|
||||
].forEach((pluginFolder) => {
|
||||
const pluginPackageJsonPath = path.join(
|
||||
pluginsDir,
|
||||
pluginFolder,
|
||||
'package.json',
|
||||
);
|
||||
|
||||
if (fs.existsSync(pluginPackageJsonPath)) {
|
||||
const pluginPackageJson = JSON.parse(
|
||||
fs.readFileSync(pluginPackageJsonPath, 'utf-8'),
|
||||
);
|
||||
|
||||
// Merge extension dependencies into root dependencies
|
||||
if (pluginPackageJson.dependencies) {
|
||||
rootPackageJson.dependencies = mergeDependencies(
|
||||
rootPackageJson.dependencies,
|
||||
pluginPackageJson.dependencies,
|
||||
);
|
||||
}
|
||||
|
||||
// Merge extension devDependencies into root devDependencies
|
||||
if (pluginPackageJson.devDependencies) {
|
||||
rootPackageJson.devDependencies = mergeDependencies(
|
||||
rootPackageJson.devDependencies,
|
||||
pluginPackageJson.devDependencies,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Write the updated root package.json
|
||||
fs.writeFileSync(
|
||||
rootPackageJsonPath,
|
||||
JSON.stringify(rootPackageJson, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'Dependencies from extensions have been merged into the root package.json',
|
||||
);
|
||||
@@ -6,14 +6,19 @@
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only",
|
||||
"scripts": {
|
||||
"preinstall": "node merge-extensions-deps.js",
|
||||
"postinstall": "patch-package",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"build:clean": "rm -rf src/.hexabot",
|
||||
"build:channels": "mkdir -p src/.hexabot/channels && find node_modules/ -name 'hexabot-channel-*' -exec cp -R {} src/.hexabot/channels/ \\;",
|
||||
"build:helpers": "mkdir -p src/.hexabot/helpers && find node_modules/ -name 'hexabot-helper-*' -exec cp -R {} src/.hexabot/helpers/ \\;",
|
||||
"build:plugins": "mkdir -p src/.hexabot/plugins && find node_modules/ -name 'hexabot-plugin-*' -exec cp -R {} src/.hexabot/plugins/ \\;",
|
||||
"build:extensions": "npm run build:channels && npm run build:helpers && npm run build:plugins",
|
||||
"build:prepare": "npm run build:clean && npm run build:extensions",
|
||||
"build": "npm run build:prepare && nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug 0.0.0.0:9229 --watch",
|
||||
"start:dev": "npm run build:prepare && nest start --watch",
|
||||
"start:debug": "npm run build:prepare && nest start --debug 0.0.0.0:9229 --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
|
||||
@@ -108,13 +108,13 @@ export class AttachmentService extends BaseService<Attachment> {
|
||||
await this.getStoragePlugin().uploadAvatar(picture);
|
||||
this.logger.log(
|
||||
`Profile picture uploaded successfully to ${
|
||||
this.getStoragePlugin().id
|
||||
this.getStoragePlugin().name
|
||||
}`,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Error while uploading profile picture to ${
|
||||
this.getStoragePlugin().id
|
||||
this.getStoragePlugin().name
|
||||
}`,
|
||||
err,
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ChannelController {
|
||||
getChannels(): { name: string }[] {
|
||||
return this.channelService.getAll().map((handler) => {
|
||||
return {
|
||||
name: handler.getChannel(),
|
||||
name: handler.getName(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { ChannelService } from './channel.service';
|
||||
|
||||
@@ -20,7 +20,9 @@ export class ChannelMiddleware implements NestMiddleware {
|
||||
try {
|
||||
const [_, path, channelName] = req.path.split('/');
|
||||
if (path === 'webhook' && channelName) {
|
||||
const channel = this.channelService.getChannelHandler(channelName);
|
||||
const channel = this.channelService.getChannelHandler(
|
||||
`${channelName}-channel`,
|
||||
);
|
||||
if (channel) {
|
||||
return await channel.middleware(req, res, next);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
*/
|
||||
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||
import {
|
||||
Global,
|
||||
MiddlewareConsumer,
|
||||
Module,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
@@ -23,7 +28,13 @@ export interface ChannelModuleOptions {
|
||||
folder: string;
|
||||
}
|
||||
|
||||
@InjectDynamicProviders('dist/extensions/**/*.channel.js')
|
||||
@Global()
|
||||
@InjectDynamicProviders(
|
||||
// Core & under dev channels
|
||||
'dist/extensions/**/*.channel.js',
|
||||
// Installed channels via npm
|
||||
'dist/.hexabot/channels/**/*.channel.js',
|
||||
)
|
||||
@Module({
|
||||
controllers: [WebhookController, ChannelController],
|
||||
providers: [ChannelService],
|
||||
|
||||
@@ -10,8 +10,8 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
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 { CONSOLE_CHANNEL_NAME } from '@/extensions/channels/console/settings';
|
||||
import { WEB_CHANNEL_NAME } from '@/extensions/channels/web/settings';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
SocketGet,
|
||||
@@ -23,10 +23,11 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||
|
||||
import ChannelHandler from './lib/Handler';
|
||||
import { ChannelName } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelService {
|
||||
private registry: Map<string, ChannelHandler<string>> = new Map();
|
||||
private registry: Map<string, ChannelHandler<ChannelName>> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly logger: LoggerService,
|
||||
@@ -40,7 +41,7 @@ export class ChannelService {
|
||||
* @param channel - The channel handler associated with the channel name.
|
||||
* @typeParam C The channel handler's type that extends `ChannelHandler`.
|
||||
*/
|
||||
public setChannel<T extends string, C extends ChannelHandler<T>>(
|
||||
public setChannel<T extends ChannelName, C extends ChannelHandler<T>>(
|
||||
name: T,
|
||||
channel: C,
|
||||
) {
|
||||
@@ -62,19 +63,19 @@ export class ChannelService {
|
||||
* @param name - The name of the channel to find.
|
||||
* @returns The channel handler associated with the specified name, or undefined if the channel is not found.
|
||||
*/
|
||||
public findChannel(name: string) {
|
||||
public findChannel(name: ChannelName) {
|
||||
return this.getAll().find((c) => {
|
||||
return c.getChannel() === name;
|
||||
return c.getName() === name;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 string, C extends ChannelHandler<T>>(
|
||||
public getChannelHandler<T extends ChannelName, C extends ChannelHandler<T>>(
|
||||
name: T,
|
||||
): C {
|
||||
const handler = this.registry.get(name);
|
||||
@@ -93,42 +94,42 @@ export class ChannelService {
|
||||
* @returns A promise that resolves when the handler has processed the request.
|
||||
*/
|
||||
async handle(channel: string, req: Request, res: Response): Promise<void> {
|
||||
const handler = this.getChannelHandler(channel);
|
||||
const handler = this.getChannelHandler(`${channel}-channel`);
|
||||
handler.handle(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a websocket request for the live chat tester channel.
|
||||
* Handles a websocket request for the admin chat console channel.
|
||||
* It considers the user as a subscriber.
|
||||
*
|
||||
* @param req - The websocket request object.
|
||||
* @param res - The websocket response object.
|
||||
*/
|
||||
@SocketGet(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
|
||||
@SocketPost(`/webhook/${LIVE_CHAT_TEST_CHANNEL_NAME}/`)
|
||||
async handleWebsocketForLiveChatTester(
|
||||
@SocketGet(`/webhook/${CONSOLE_CHANNEL_NAME}/`)
|
||||
@SocketPost(`/webhook/${CONSOLE_CHANNEL_NAME}/`)
|
||||
async handleWebsocketForAdminChatConsole(
|
||||
@SocketReq() req: SocketRequest,
|
||||
@SocketRes() res: SocketResponse,
|
||||
) {
|
||||
this.logger.log(
|
||||
'Channel notification (Live Chat Tester Socket) : ',
|
||||
'Channel notification (Admin Chat Console Socket) : ',
|
||||
req.method,
|
||||
);
|
||||
|
||||
@@ -157,21 +158,21 @@ export class ChannelService {
|
||||
country: '',
|
||||
labels: [],
|
||||
channel: {
|
||||
name: LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
name: CONSOLE_CHANNEL_NAME,
|
||||
isSocket: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Update session (end user is both a user + subscriber)
|
||||
req.session.offline = {
|
||||
req.session.web = {
|
||||
profile: testSubscriber,
|
||||
isSocket: true,
|
||||
messageQueue: [],
|
||||
polling: false,
|
||||
};
|
||||
|
||||
const handler = this.getChannelHandler(LIVE_CHAT_TEST_CHANNEL_NAME);
|
||||
const handler = this.getChannelHandler(CONSOLE_CHANNEL_NAME);
|
||||
return handler.handle(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default abstract class EventWrapper<
|
||||
toString() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
handler: this._handler.getChannel(),
|
||||
handler: this._handler.getName(),
|
||||
channelData: this.getChannelData(),
|
||||
sender: this.getSender(),
|
||||
recipient: this.getRecipientForeignId(),
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import path from 'path';
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
@@ -17,35 +19,39 @@ import {
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { hyphenToUnderscore } from '@/utils/helpers/misc';
|
||||
import { Extension } from '@/utils/generics/extension';
|
||||
import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||
|
||||
import { ChannelService } from '../channel.service';
|
||||
import { ChannelSetting } from '../types';
|
||||
import { ChannelName, ChannelSetting } from '../types';
|
||||
|
||||
import EventWrapper from './EventWrapper';
|
||||
|
||||
@Injectable()
|
||||
export default abstract class ChannelHandler<N extends string = string> {
|
||||
private readonly name: N;
|
||||
|
||||
export default abstract class ChannelHandler<
|
||||
N extends ChannelName = ChannelName,
|
||||
>
|
||||
extends Extension
|
||||
implements OnModuleInit
|
||||
{
|
||||
private readonly settings: ChannelSetting<N>[];
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settings: ChannelSetting<N>[],
|
||||
protected readonly settingService: SettingService,
|
||||
private readonly channelService: ChannelService,
|
||||
protected readonly logger: LoggerService,
|
||||
) {
|
||||
this.name = name;
|
||||
this.settings = settings;
|
||||
super(name);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
this.settings = require(path.join(this.getPath(), 'settings')).default;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
async onModuleInit() {
|
||||
await super.onModuleInit();
|
||||
this.channelService.setChannel(
|
||||
this.getChannel(),
|
||||
this.getName() as ChannelName,
|
||||
this as unknown as ChannelHandler<N>,
|
||||
);
|
||||
this.setup();
|
||||
@@ -53,7 +59,7 @@ export default abstract class ChannelHandler<N extends string = string> {
|
||||
|
||||
async setup() {
|
||||
await this.settingService.seedIfNotExist(
|
||||
this.getChannel(),
|
||||
this.getName(),
|
||||
this.settings.map((s, i) => ({
|
||||
...s,
|
||||
weight: i + 1,
|
||||
@@ -62,22 +68,6 @@ export default abstract class ChannelHandler<N extends string = string> {
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel's name
|
||||
* @returns Channel's name
|
||||
*/
|
||||
getChannel() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel's group
|
||||
* @returns Channel's group
|
||||
*/
|
||||
protected getGroup() {
|
||||
return hyphenToUnderscore(this.getChannel()) as ChannelSetting<N>['group'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the channel's settings
|
||||
* @returns Channel's settings
|
||||
@@ -85,7 +75,7 @@ export default abstract class ChannelHandler<N extends string = string> {
|
||||
async getSettings<S extends string = HyphenToUnderscore<N>>() {
|
||||
const settings = await this.settingService.getSettings();
|
||||
// @ts-expect-error workaround typing
|
||||
return settings[this.getGroup() as keyof Settings] as Settings[S];
|
||||
return settings[this.getNamespace() as keyof Settings] as Settings[S];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
|
||||
lastvisit: new Date(),
|
||||
retainedFrom: new Date(),
|
||||
channel: {
|
||||
name: 'offline',
|
||||
name: 'web-channel',
|
||||
},
|
||||
labels: [],
|
||||
...modelInstance,
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
|
||||
export type ChannelName = `${string}-channel`;
|
||||
|
||||
export type ChannelSetting<N extends string = string> = Omit<
|
||||
SettingCreateDto,
|
||||
'group' | 'weight'
|
||||
|
||||
@@ -27,7 +27,7 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginType } from '@/plugins/types';
|
||||
import { PluginName, PluginType } from '@/plugins/types';
|
||||
import { UserService } from '@/user/services/user.service';
|
||||
import { BaseController } from '@/utils/generics/base-controller';
|
||||
import { DeleteResult } from '@/utils/generics/base-repository';
|
||||
@@ -85,20 +85,23 @@ export class BlockController extends BaseController<
|
||||
/**
|
||||
* Retrieves a custom block settings for a specific plugin.
|
||||
*
|
||||
* @param pluginId - The name of the plugin for which settings are to be retrieved.
|
||||
* @param pluginName - The name of the plugin for which settings are to be retrieved.
|
||||
*
|
||||
* @returns An array containing the settings of the specified plugin.
|
||||
*/
|
||||
@Get('customBlocks/settings')
|
||||
findSettings(@Query('plugin') pluginId: string) {
|
||||
findSettings(@Query('plugin') pluginName: PluginName) {
|
||||
try {
|
||||
if (!pluginId) {
|
||||
if (!pluginName) {
|
||||
throw new BadRequestException(
|
||||
'Plugin id must be supplied as a query param',
|
||||
'Plugin name must be supplied as a query param',
|
||||
);
|
||||
}
|
||||
|
||||
const plugin = this.pluginsService.getPlugin(PluginType.block, pluginId);
|
||||
const plugin = this.pluginsService.getPlugin(
|
||||
PluginType.block,
|
||||
pluginName,
|
||||
);
|
||||
|
||||
if (!plugin) {
|
||||
throw new NotFoundException('Plugin Not Found');
|
||||
@@ -122,11 +125,12 @@ export class BlockController extends BaseController<
|
||||
const plugins = this.pluginsService
|
||||
.getAllByType(PluginType.block)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
id: p.getName(),
|
||||
namespace: p.getNamespace(),
|
||||
template: {
|
||||
...p.template,
|
||||
message: {
|
||||
plugin: p.id,
|
||||
plugin: p.name,
|
||||
args: p.settings.reduce(
|
||||
(acc, setting) => {
|
||||
acc[setting.label] = setting.value;
|
||||
|
||||
@@ -173,7 +173,7 @@ describe('LabelController', () => {
|
||||
name: 'LABEL_2',
|
||||
label_id: {
|
||||
messenger: 'messenger',
|
||||
offline: 'offline',
|
||||
web: 'web',
|
||||
twitter: 'twitter',
|
||||
dimelo: 'dimelo',
|
||||
},
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { ChannelName } from '@/channel/types';
|
||||
|
||||
interface BaseChannelData {
|
||||
name: string; // channel name
|
||||
name: ChannelName; // channel name
|
||||
isSocket?: boolean;
|
||||
type?: any; //TODO: type has to be checked
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { Nlp } from '@/helper/types';
|
||||
|
||||
import { Subscriber } from '../subscriber.schema';
|
||||
@@ -13,7 +14,7 @@ import { Subscriber } from '../subscriber.schema';
|
||||
import { Payload } from './quick-reply';
|
||||
|
||||
export interface Context {
|
||||
channel?: string;
|
||||
channel?: ChannelName;
|
||||
text?: string;
|
||||
payload?: Payload | string;
|
||||
nlp?: Nlp.ParseEntities | null;
|
||||
|
||||
@@ -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 = {
|
||||
getChannel: 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: 'web-channel',
|
||||
text: '',
|
||||
payload: undefined,
|
||||
nlp: { entities: [] },
|
||||
|
||||
@@ -17,7 +17,7 @@ import { I18nService } from '@/i18n/services/i18n.service';
|
||||
import { LanguageService } from '@/i18n/services/language.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginType } from '@/plugins/types';
|
||||
import { PluginName, PluginType } from '@/plugins/types';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
import { getRandom } from '@/utils/helpers/safeRandom';
|
||||
@@ -71,7 +71,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
const payload = event.getPayload();
|
||||
|
||||
// Perform a filter on the specific channels
|
||||
const channel = event.getHandler().getChannel();
|
||||
const channel = event.getHandler().getName();
|
||||
blocks = blocks.filter((b) => {
|
||||
return (
|
||||
!b.trigger_channels ||
|
||||
@@ -593,7 +593,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
|
||||
} else if (blockMessage && 'plugin' in blockMessage) {
|
||||
const plugin = this.pluginService.findPlugin(
|
||||
PluginType.block,
|
||||
blockMessage.plugin,
|
||||
blockMessage.plugin as PluginName,
|
||||
);
|
||||
// Process custom plugin block
|
||||
try {
|
||||
|
||||
@@ -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',
|
||||
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',
|
||||
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);
|
||||
|
||||
@@ -246,7 +246,7 @@ export class ChatService {
|
||||
this.eventEmitter.emit('hook:stats:entry', 'new_users', 'New users');
|
||||
subscriberData.channel = {
|
||||
...event.getChannelData(),
|
||||
name: handler.getChannel(),
|
||||
name: handler.getName(),
|
||||
};
|
||||
subscriber = await this.subscriberService.create(subscriberData);
|
||||
} else {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
@@ -69,7 +70,7 @@ export class ConversationService extends BaseService<
|
||||
const msgType = event.getMessageType();
|
||||
const profile = event.getSender();
|
||||
// Capture channel specific context data
|
||||
convo.context.channel = event.getHandler().getChannel();
|
||||
convo.context.channel = event.getHandler().getName() as ChannelName;
|
||||
convo.context.text = event.getText();
|
||||
convo.context.payload = event.getPayload();
|
||||
convo.context.nlp = event.getNLP();
|
||||
|
||||
3
api/src/extensions/channels/console/i18n/en/title.json
Normal file
3
api/src/extensions/channels/console/i18n/en/title.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"console_channel": "Admin Chat Console"
|
||||
}
|
||||
3
api/src/extensions/channels/console/i18n/fr/title.json
Normal file
3
api/src/extensions/channels/console/i18n/fr/title.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"console_channel": "Testeur Live Chat"
|
||||
}
|
||||
@@ -19,16 +19,13 @@ 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 {
|
||||
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
|
||||
LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
} from './settings';
|
||||
import { CONSOLE_CHANNEL_NAME } from './settings';
|
||||
|
||||
@Injectable()
|
||||
export default class LiveChatTesterHandler extends BaseWebChannelHandler<
|
||||
typeof LIVE_CHAT_TEST_CHANNEL_NAME
|
||||
export default class ConsoleChannelHandler extends BaseWebChannelHandler<
|
||||
typeof CONSOLE_CHANNEL_NAME
|
||||
> {
|
||||
constructor(
|
||||
settingService: SettingService,
|
||||
@@ -43,8 +40,7 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
|
||||
websocketGateway: WebsocketGateway,
|
||||
) {
|
||||
super(
|
||||
LIVE_CHAT_TEST_CHANNEL_NAME,
|
||||
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
|
||||
CONSOLE_CHANNEL_NAME,
|
||||
settingService,
|
||||
channelService,
|
||||
logger,
|
||||
@@ -57,4 +53,8 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
|
||||
websocketGateway,
|
||||
);
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
return __dirname;
|
||||
}
|
||||
}
|
||||
24
api/src/extensions/channels/console/index.d.ts
vendored
Normal file
24
api/src/extensions/channels/console/index.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import CONSOLE_CHANNEL_SETTINGS, {
|
||||
CONSOLE_CHANNEL_NAMESPACE,
|
||||
} from './settings';
|
||||
|
||||
declare global {
|
||||
interface Settings extends SettingTree<typeof CONSOLE_CHANNEL_SETTINGS> {}
|
||||
}
|
||||
|
||||
declare module '@nestjs/event-emitter' {
|
||||
interface IHookExtensionsOperationMap {
|
||||
[CONSOLE_CHANNEL_NAMESPACE]: TDefinition<
|
||||
object,
|
||||
SettingMapByType<typeof CONSOLE_CHANNEL_SETTINGS>
|
||||
>;
|
||||
}
|
||||
}
|
||||
7
api/src/extensions/channels/console/package.json
Normal file
7
api/src/extensions/channels/console/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "hexabot-channel-console",
|
||||
"version": "2.0.0",
|
||||
"description": "The Admin Chat Console Channel Extension for Hexabot Chatbot / Agent Builder.",
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
@@ -10,85 +10,85 @@ 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';
|
||||
export const CONSOLE_CHANNEL_NAME = 'console-channel';
|
||||
|
||||
export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester';
|
||||
export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
|
||||
|
||||
export const DEFAULT_LIVE_CHAT_TEST_SETTINGS = [
|
||||
export default [
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.verification_token,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.verification_token,
|
||||
value: 'test',
|
||||
type: SettingType.text,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_domains,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.allowed_domains,
|
||||
value: config.frontendPath,
|
||||
type: SettingType.text,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.start_button,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.start_button,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.input_disabled,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.input_disabled,
|
||||
value: false,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.persistent_menu,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.persistent_menu,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.greeting_message,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.greeting_message,
|
||||
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
||||
type: SettingType.textarea,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.theme_color,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.theme_color,
|
||||
value: 'teal',
|
||||
type: SettingType.select,
|
||||
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_emoji,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_emoji,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_file,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_file,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_location,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_location,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_size,
|
||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.allowed_upload_size,
|
||||
value: 2500000,
|
||||
type: SettingType.number,
|
||||
},
|
||||
{
|
||||
group: LIVE_CHAT_TEST_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_types,
|
||||
group: CONSOLE_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 LIVE_CHAT_TEST_CHANNEL_NAME>[];
|
||||
] as const satisfies ChannelSetting<typeof CONSOLE_CHANNEL_NAME>[];
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"live_chat_tester": "Live Chat Tester"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"live_chat_tester": "Testeur Live Chat"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
|
||||
LIVE_CHAT_TEST_GROUP_NAME,
|
||||
} from './settings';
|
||||
|
||||
declare global {
|
||||
interface Settings
|
||||
extends SettingTree<typeof DEFAULT_LIVE_CHAT_TEST_SETTINGS> {}
|
||||
}
|
||||
|
||||
declare module '@nestjs/event-emitter' {
|
||||
interface IHookExtensionsOperationMap {
|
||||
[LIVE_CHAT_TEST_GROUP_NAME]: TDefinition<
|
||||
object,
|
||||
SettingMapByType<typeof DEFAULT_LIVE_CHAT_TEST_SETTINGS>
|
||||
>;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"offline": "Canal Web"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"live_chat_tester": "Testeur Live Chat"
|
||||
}
|
||||
14
api/src/extensions/channels/offline/index.d.ts
vendored
14
api/src/extensions/channels/offline/index.d.ts
vendored
@@ -1,14 +0,0 @@
|
||||
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_GROUP_NAME } from './settings';
|
||||
|
||||
declare global {
|
||||
interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {}
|
||||
}
|
||||
|
||||
declare module '@nestjs/event-emitter' {
|
||||
interface IHookExtensionsOperationMap {
|
||||
[OFFLINE_GROUP_NAME]: TDefinition<
|
||||
object,
|
||||
SettingMapByType<typeof DEFAULT_OFFLINE_SETTINGS>
|
||||
>;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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: {
|
||||
@@ -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.getChannel()).toEqual('offline');
|
||||
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',
|
||||
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,
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ChannelService } from '@/channel/channel.service';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import ChannelHandler from '@/channel/lib/Handler';
|
||||
import { ChannelSetting } from '@/channel/types';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { MessageCreateDto } from '@/chat/dto/message.dto';
|
||||
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
||||
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
|
||||
@@ -58,17 +58,16 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||
import { WebsocketGateway } from '@/websocket/websocket.gateway';
|
||||
|
||||
import { OFFLINE_GROUP_NAME } 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 class BaseWebChannelHandler<
|
||||
N extends string,
|
||||
export default abstract class BaseWebChannelHandler<
|
||||
N extends ChannelName,
|
||||
> extends ChannelHandler<N> {
|
||||
constructor(
|
||||
name: N,
|
||||
settings: ChannelSetting<N>[],
|
||||
settingService: SettingService,
|
||||
channelService: ChannelService,
|
||||
logger: LoggerService,
|
||||
@@ -80,7 +79,7 @@ export default class BaseWebChannelHandler<
|
||||
protected readonly menuService: MenuService,
|
||||
private readonly websocketGateway: WebsocketGateway,
|
||||
) {
|
||||
super(name, settings, settingService, channelService, logger);
|
||||
super(name, settingService, channelService, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +92,7 @@ export default class BaseWebChannelHandler<
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify offline websocket connection and return settings
|
||||
* Verify web websocket connection and return settings
|
||||
*
|
||||
* @param client - The socket client
|
||||
*/
|
||||
@@ -102,7 +101,7 @@ export default class BaseWebChannelHandler<
|
||||
const settings = await this.getSettings();
|
||||
const handshake = client.handshake;
|
||||
const { channel } = handshake.query;
|
||||
if (channel !== this.getChannel()) {
|
||||
if (channel !== this.getName()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -139,20 +138,20 @@ export default 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,
|
||||
@@ -166,7 +165,7 @@ export default 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,
|
||||
@@ -175,21 +174,21 @@ export default 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);
|
||||
@@ -213,13 +212,13 @@ export default 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 {
|
||||
@@ -228,7 +227,7 @@ export default 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 {
|
||||
@@ -238,7 +237,7 @@ export default class BaseWebChannelHandler<
|
||||
mid: outgoingMessage.mid,
|
||||
handover: !!outgoingMessage.handover,
|
||||
createdAt: outgoingMessage.createdAt,
|
||||
} as Offline.OutgoingMessage;
|
||||
} as Web.OutgoingMessage;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -255,8 +254,8 @@ export default 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,
|
||||
@@ -280,8 +279,8 @@ export default 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,
|
||||
@@ -300,7 +299,7 @@ export default class BaseWebChannelHandler<
|
||||
*/
|
||||
private async verifyToken(verificationToken: string) {
|
||||
const settings =
|
||||
(await this.getSettings()) as Settings[typeof OFFLINE_GROUP_NAME];
|
||||
(await this.getSettings()) as Settings[typeof WEB_CHANNEL_NAMESPACE];
|
||||
const verifyToken = settings.verification_token;
|
||||
|
||||
if (!verifyToken) {
|
||||
@@ -327,7 +326,7 @@ export default class BaseWebChannelHandler<
|
||||
req: Request | SocketRequest,
|
||||
res: Response | SocketResponse,
|
||||
) {
|
||||
const settings = await this.getSettings<typeof OFFLINE_GROUP_NAME>();
|
||||
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
|
||||
@@ -377,7 +376,7 @@ export default 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,
|
||||
@@ -386,8 +385,8 @@ export default 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!',
|
||||
@@ -397,7 +396,7 @@ export default class BaseWebChannelHandler<
|
||||
.status(403)
|
||||
.json({ err: 'Web Channel Handler : Unauthorized!' });
|
||||
}
|
||||
next(req.session?.offline?.profile);
|
||||
next(req.session?.web?.profile);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,15 +440,15 @@ export default 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;
|
||||
}
|
||||
|
||||
@@ -457,14 +456,14 @@ export default 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(),
|
||||
retainedFrom: new Date(),
|
||||
channel: {
|
||||
...channelData,
|
||||
name: this.getChannel(),
|
||||
name: this.getName() as ChannelName,
|
||||
},
|
||||
language: '',
|
||||
locale: '',
|
||||
@@ -482,7 +481,7 @@ export default class BaseWebChannelHandler<
|
||||
avatar: null,
|
||||
};
|
||||
|
||||
req.session.offline = {
|
||||
req.session.web = {
|
||||
profile,
|
||||
isSocket: 'isSocket' in req && !!req.isSocket,
|
||||
messageQueue: [],
|
||||
@@ -508,9 +507,7 @@ export default 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',
|
||||
);
|
||||
@@ -520,7 +517,7 @@ export default 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',
|
||||
);
|
||||
@@ -529,7 +526,7 @@ export default 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 {
|
||||
@@ -540,8 +537,8 @@ export default 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(
|
||||
@@ -550,8 +547,8 @@ export default 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' });
|
||||
@@ -561,7 +558,7 @@ export default 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
|
||||
@@ -609,7 +606,7 @@ export default 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,
|
||||
@@ -624,7 +621,7 @@ export default class BaseWebChannelHandler<
|
||||
type: upload.type || 'text/txt',
|
||||
size: upload.size || 0,
|
||||
location: filename,
|
||||
channel: { offline: {} },
|
||||
channel: { web: {} },
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
@@ -658,9 +655,9 @@ export default 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);
|
||||
}
|
||||
@@ -683,7 +680,7 @@ export default 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);
|
||||
|
||||
@@ -764,7 +761,7 @@ export default 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),
|
||||
@@ -782,13 +779,13 @@ export default 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 ',
|
||||
@@ -803,7 +800,7 @@ export default class BaseWebChannelHandler<
|
||||
data.data = upload;
|
||||
}
|
||||
const channelData = this.getChannelData(req);
|
||||
const event: OfflineEventWrapper = new OfflineEventWrapper(
|
||||
const event: WebEventWrapper = new WebEventWrapper(
|
||||
this,
|
||||
data,
|
||||
channelData,
|
||||
@@ -845,14 +842,14 @@ export default 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') {
|
||||
@@ -888,7 +885,7 @@ export default 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
|
||||
@@ -912,7 +909,7 @@ export default class BaseWebChannelHandler<
|
||||
* @returns UUID
|
||||
*/
|
||||
generateId(): string {
|
||||
return 'offline-' + uuidv4();
|
||||
return 'web-' + uuidv4();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -926,9 +923,9 @@ export default class BaseWebChannelHandler<
|
||||
_textFormat(
|
||||
message: StdOutgoingTextMessage,
|
||||
_options?: BlockOptions,
|
||||
): Offline.OutgoingMessageBase {
|
||||
): Web.OutgoingMessageBase {
|
||||
return {
|
||||
type: Offline.OutgoingMessageType.text,
|
||||
type: Web.OutgoingMessageType.text,
|
||||
data: message,
|
||||
};
|
||||
}
|
||||
@@ -944,9 +941,9 @@ export default 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,
|
||||
@@ -965,9 +962,9 @@ export default 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,
|
||||
@@ -986,9 +983,9 @@ export default 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,
|
||||
@@ -1008,10 +1005,7 @@ export default 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');
|
||||
}
|
||||
@@ -1019,7 +1013,7 @@ export default 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 || [],
|
||||
};
|
||||
@@ -1032,7 +1026,7 @@ export default 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,
|
||||
);
|
||||
}
|
||||
@@ -1091,11 +1085,11 @@ export default 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) {
|
||||
@@ -1124,7 +1118,7 @@ export default class BaseWebChannelHandler<
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
type: Offline.OutgoingMessageType.list,
|
||||
type: Web.OutgoingMessageType.list,
|
||||
data: {
|
||||
elements,
|
||||
buttons,
|
||||
@@ -1144,7 +1138,7 @@ export default class BaseWebChannelHandler<
|
||||
_carouselFormat(
|
||||
message: StdOutgoingListMessage,
|
||||
options: BlockOptions,
|
||||
): Offline.OutgoingMessageBase {
|
||||
): Web.OutgoingMessageBase {
|
||||
const data = message.elements || [];
|
||||
// Items count min check
|
||||
if (data.length === 0) {
|
||||
@@ -1157,7 +1151,7 @@ export default 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,
|
||||
},
|
||||
@@ -1175,7 +1169,7 @@ export default class BaseWebChannelHandler<
|
||||
_formatMessage(
|
||||
envelope: StdOutgoingEnvelope,
|
||||
options: BlockOptions,
|
||||
): Offline.OutgoingMessageBase {
|
||||
): Web.OutgoingMessageBase {
|
||||
switch (envelope.format) {
|
||||
case OutgoingMessageFormat.attachment:
|
||||
return this._attachmentFormat(envelope.message, options);
|
||||
@@ -1215,14 +1209,14 @@ export default 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>,
|
||||
@@ -1230,13 +1224,13 @@ export default 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',
|
||||
@@ -1297,9 +1291,9 @@ export default 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;
|
||||
}
|
||||
}
|
||||
3
api/src/extensions/channels/web/i18n/en/title.json
Normal file
3
api/src/extensions/channels/web/i18n/en/title.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"web_channel": "Web Channel"
|
||||
}
|
||||
3
api/src/extensions/channels/web/i18n/fr/title.json
Normal file
3
api/src/extensions/channels/web/i18n/fr/title.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"web_channel": "Canal Web"
|
||||
}
|
||||
@@ -20,11 +20,11 @@ import { SettingService } from '@/setting/services/setting.service';
|
||||
import { WebsocketGateway } from '@/websocket/websocket.gateway';
|
||||
|
||||
import BaseWebChannelHandler from './base-web-channel';
|
||||
import { DEFAULT_OFFLINE_SETTINGS, 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,8 +39,7 @@ export default class OfflineHandler extends BaseWebChannelHandler<
|
||||
websocketGateway: WebsocketGateway,
|
||||
) {
|
||||
super(
|
||||
OFFLINE_CHANNEL_NAME,
|
||||
DEFAULT_OFFLINE_SETTINGS,
|
||||
WEB_CHANNEL_NAME,
|
||||
settingService,
|
||||
channelService,
|
||||
logger,
|
||||
@@ -53,4 +52,8 @@ export default class OfflineHandler extends BaseWebChannelHandler<
|
||||
websocketGateway,
|
||||
);
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
return __dirname;
|
||||
}
|
||||
}
|
||||
24
api/src/extensions/channels/web/index.d.ts
vendored
Normal file
24
api/src/extensions/channels/web/index.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
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>
|
||||
>;
|
||||
}
|
||||
}
|
||||
7
api/src/extensions/channels/web/package.json
Normal file
7
api/src/extensions/channels/web/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -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' as const;
|
||||
export const WEB_CHANNEL_NAME = 'web-channel' as const;
|
||||
|
||||
export const OFFLINE_GROUP_NAME = OFFLINE_CHANNEL_NAME;
|
||||
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
|
||||
|
||||
export const DEFAULT_OFFLINE_SETTINGS = [
|
||||
export default [
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.verification_token,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.verification_token,
|
||||
value: 'token123',
|
||||
type: SettingType.secret,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
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_GROUP_NAME,
|
||||
label: Offline.SettingLabel.start_button,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.start_button,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.input_disabled,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.input_disabled,
|
||||
value: false,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.persistent_menu,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.persistent_menu,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
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_GROUP_NAME,
|
||||
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_GROUP_NAME,
|
||||
label: Offline.SettingLabel.window_title,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.window_title,
|
||||
value: 'Widget Title',
|
||||
type: SettingType.text,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
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_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_emoji,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_emoji,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_file,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_file,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.show_location,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.show_location,
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
label: Offline.SettingLabel.allowed_upload_size,
|
||||
group: WEB_CHANNEL_NAMESPACE,
|
||||
label: Web.SettingLabel.allowed_upload_size,
|
||||
value: 2500000,
|
||||
type: SettingType.number,
|
||||
},
|
||||
{
|
||||
group: OFFLINE_GROUP_NAME,
|
||||
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>[];
|
||||
@@ -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',
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import {
|
||||
AttachmentForeignKey,
|
||||
AttachmentPayload,
|
||||
@@ -20,55 +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<
|
||||
T extends BaseWebChannelHandler<string> = BaseWebChannelHandler<string>,
|
||||
> extends EventWrapper<OfflineEventAdapter, Offline.Event> {
|
||||
export default class WebEventWrapper<
|
||||
T extends
|
||||
BaseWebChannelHandler<ChannelName> = BaseWebChannelHandler<ChannelName>,
|
||||
> extends EventWrapper<WebEventAdapter, Web.Event> {
|
||||
/**
|
||||
* Constructor : channel's event wrapper
|
||||
*
|
||||
@@ -76,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);
|
||||
}
|
||||
|
||||
@@ -88,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;
|
||||
@@ -58,7 +58,7 @@ describe('Core NLU Helper', () => {
|
||||
provide: SettingService,
|
||||
useValue: {
|
||||
getSettings: jest.fn(() => ({
|
||||
core_nlu: {
|
||||
core_nlu_helper: {
|
||||
endpoint: 'path',
|
||||
token: 'token',
|
||||
threshold: '0.5',
|
||||
@@ -121,7 +121,7 @@ describe('Core NLU Helper', () => {
|
||||
true,
|
||||
);
|
||||
const settings = await settingService.getSettings();
|
||||
const threshold = settings.core_nlu.threshold;
|
||||
const threshold = settings.core_nlu_helper.threshold;
|
||||
const thresholdGuess = {
|
||||
entities: nlpBestGuess.entities.filter(
|
||||
(g) =>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"core_nlu": "Core NLU Engine"
|
||||
"core_nlu_helper": "Core NLU Engine"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"core_nlu": "Core NLU Engine"
|
||||
"core_nlu_helper": "Core NLU Engine"
|
||||
}
|
||||
|
||||
14
api/src/extensions/helpers/core-nlu/index.d.ts
vendored
14
api/src/extensions/helpers/core-nlu/index.d.ts
vendored
@@ -1,4 +1,14 @@
|
||||
import { CORE_NLU_HELPER_GROUP, CORE_NLU_HELPER_SETTINGS } from './settings';
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import CORE_NLU_HELPER_SETTINGS, {
|
||||
CORE_NLU_HELPER_NAMESPACE,
|
||||
} from './settings';
|
||||
|
||||
declare global {
|
||||
interface Settings extends SettingTree<typeof CORE_NLU_HELPER_SETTINGS> {}
|
||||
@@ -6,7 +16,7 @@ declare global {
|
||||
|
||||
declare module '@nestjs/event-emitter' {
|
||||
interface IHookExtensionsOperationMap {
|
||||
[CORE_NLU_HELPER_GROUP]: TDefinition<
|
||||
[CORE_NLU_HELPER_NAMESPACE]: TDefinition<
|
||||
object,
|
||||
SettingMapByType<typeof CORE_NLU_HELPER_SETTINGS>
|
||||
>;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { NlpValue } from '@/nlp/schemas/nlp-value.schema';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
|
||||
import { CORE_NLU_HELPER_NAME, CORE_NLU_HELPER_SETTINGS } from './settings';
|
||||
import { CORE_NLU_HELPER_NAME } from './settings';
|
||||
import { NlpParseResultType, RasaNlu } from './types';
|
||||
|
||||
@Injectable()
|
||||
@@ -34,13 +34,11 @@ export default class CoreNluHelper extends BaseNlpHelper<
|
||||
private readonly httpService: HttpService,
|
||||
private readonly languageService: LanguageService,
|
||||
) {
|
||||
super(
|
||||
CORE_NLU_HELPER_NAME,
|
||||
CORE_NLU_HELPER_SETTINGS,
|
||||
settingService,
|
||||
helperService,
|
||||
logger,
|
||||
);
|
||||
super(CORE_NLU_HELPER_NAME, settingService, helperService, logger);
|
||||
}
|
||||
|
||||
getPath() {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "hexabot-core-nlu",
|
||||
"name": "hexabot-helper-core-nlu",
|
||||
"version": "2.0.0",
|
||||
"description": "The Core NLU Helper Extension for Hexabot Chatbot / Agent Builder to enable the Intent Classification and Language Detection",
|
||||
"dependencies": {},
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { HelperSetting } from '@/helper/types';
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
export const CORE_NLU_HELPER_NAME = 'core-nlu';
|
||||
export const CORE_NLU_HELPER_NAME = 'core-nlu-helper';
|
||||
|
||||
export const CORE_NLU_HELPER_GROUP = 'core_nlu';
|
||||
export const CORE_NLU_HELPER_NAMESPACE = 'core_nlu_helper';
|
||||
|
||||
export const CORE_NLU_HELPER_SETTINGS = [
|
||||
export default [
|
||||
{
|
||||
group: CORE_NLU_HELPER_GROUP,
|
||||
group: CORE_NLU_HELPER_NAMESPACE,
|
||||
label: 'endpoint',
|
||||
value: 'http://nlu-api:5000/',
|
||||
type: SettingType.text,
|
||||
},
|
||||
{
|
||||
group: CORE_NLU_HELPER_GROUP,
|
||||
group: CORE_NLU_HELPER_NAMESPACE,
|
||||
label: 'token',
|
||||
value: 'token123',
|
||||
type: SettingType.text,
|
||||
},
|
||||
{
|
||||
group: CORE_NLU_HELPER_GROUP,
|
||||
group: CORE_NLU_HELPER_NAMESPACE,
|
||||
label: 'threshold',
|
||||
value: 0.1,
|
||||
type: SettingType.number,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ollama": "Ollama"
|
||||
"ollama_helper": "Ollama"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ollama": "Ollama"
|
||||
"ollama_helper": "Ollama"
|
||||
}
|
||||
|
||||
12
api/src/extensions/helpers/ollama/index.d.ts
vendored
12
api/src/extensions/helpers/ollama/index.d.ts
vendored
@@ -1,4 +1,12 @@
|
||||
import { OLLAMA_HELPER_GROUP, OLLAMA_HELPER_SETTINGS } from './settings';
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import OLLAMA_HELPER_SETTINGS, { OLLAMA_HELPER_NAMESPACE } from './settings';
|
||||
|
||||
declare global {
|
||||
interface Settings extends SettingTree<typeof OLLAMA_HELPER_SETTINGS> {}
|
||||
@@ -6,7 +14,7 @@ declare global {
|
||||
|
||||
declare module '@nestjs/event-emitter' {
|
||||
interface IHookExtensionsOperationMap {
|
||||
[OLLAMA_HELPER_GROUP]: TDefinition<
|
||||
[OLLAMA_HELPER_NAMESPACE]: TDefinition<
|
||||
object,
|
||||
SettingMapByType<typeof OLLAMA_HELPER_SETTINGS>
|
||||
>;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LoggerService } from '@/logger/logger.service';
|
||||
import { Setting } from '@/setting/schemas/setting.schema';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { OLLAMA_HELPER_NAME, OLLAMA_HELPER_SETTINGS } from './settings';
|
||||
import { OLLAMA_HELPER_NAME } from './settings';
|
||||
|
||||
@Injectable()
|
||||
export default class OllamaLlmHelper
|
||||
@@ -36,13 +36,11 @@ export default class OllamaLlmHelper
|
||||
helperService: HelperService,
|
||||
protected readonly logger: LoggerService,
|
||||
) {
|
||||
super(
|
||||
OLLAMA_HELPER_NAME,
|
||||
OLLAMA_HELPER_SETTINGS,
|
||||
settingService,
|
||||
helperService,
|
||||
logger,
|
||||
);
|
||||
super('ollama-helper', settingService, helperService, logger);
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
@@ -51,7 +49,7 @@ export default class OllamaLlmHelper
|
||||
this.client = new Ollama({ host: settings.api_url });
|
||||
}
|
||||
|
||||
@OnEvent('hook:ollama:api_url')
|
||||
@OnEvent('hook:ollama_helper:api_url')
|
||||
handleApiUrlChange(setting: Setting) {
|
||||
this.client = new Ollama({ host: setting.value });
|
||||
}
|
||||
|
||||
@@ -1,123 +1,133 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { HelperSetting } from '@/helper/types';
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
export const OLLAMA_HELPER_NAME = 'ollama';
|
||||
export const OLLAMA_HELPER_NAME = 'ollama-helper';
|
||||
|
||||
export const OLLAMA_HELPER_GROUP = 'ollama';
|
||||
export const OLLAMA_HELPER_NAMESPACE: HyphenToUnderscore<
|
||||
typeof OLLAMA_HELPER_NAME
|
||||
> = 'ollama_helper';
|
||||
|
||||
export const OLLAMA_HELPER_SETTINGS = [
|
||||
export default [
|
||||
{
|
||||
label: 'api_url',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
type: SettingType.text,
|
||||
value: 'http://ollama:11434', // Default value
|
||||
},
|
||||
{
|
||||
label: 'model',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
type: SettingType.text,
|
||||
value: 'llama3.2', // Default model
|
||||
},
|
||||
{
|
||||
label: 'keep_alive',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
type: SettingType.text,
|
||||
value: '5m', // Default value for keeping the model in memory
|
||||
},
|
||||
{
|
||||
label: 'mirostat',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0, // Default: disabled
|
||||
},
|
||||
{
|
||||
label: 'mirostat_eta',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0.1, // Default value
|
||||
},
|
||||
{
|
||||
label: 'mirostat_tau',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 5.0, // Default value
|
||||
},
|
||||
{
|
||||
label: 'num_ctx',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 2048, // Default value
|
||||
},
|
||||
{
|
||||
label: 'repeat_last_n',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 64, // Default value
|
||||
},
|
||||
{
|
||||
label: 'repeat_penalty',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 1.1, // Default value
|
||||
},
|
||||
{
|
||||
label: 'temperature',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0.8, // Default value
|
||||
},
|
||||
{
|
||||
label: 'seed',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0, // Default value
|
||||
},
|
||||
{
|
||||
label: 'stop',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.text,
|
||||
value: 'AI assistant:', // Default stop sequence
|
||||
},
|
||||
{
|
||||
label: 'tfs_z',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 1, // Default value, 1.0 means disabled
|
||||
},
|
||||
{
|
||||
label: 'num_predict',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 20, // Default value
|
||||
},
|
||||
{
|
||||
label: 'top_k',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 40, // Default value
|
||||
},
|
||||
{
|
||||
label: 'top_p',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0.9, // Default value
|
||||
},
|
||||
{
|
||||
label: 'min_p',
|
||||
group: OLLAMA_HELPER_GROUP,
|
||||
group: OLLAMA_HELPER_NAMESPACE,
|
||||
subgroup: 'options',
|
||||
type: SettingType.number,
|
||||
value: 0.0, // Default value
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ollama": "Ollama"
|
||||
"ollama_plugin": "Ollama Plugin"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"ollama": "Ollama"
|
||||
"ollama_plugin": "Ollama Plugin"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Block } from '@/chat/schemas/block.schema';
|
||||
@@ -14,14 +22,13 @@ import { HelperType } from '@/helper/types';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginBlockTemplate } from '@/plugins/types';
|
||||
|
||||
import { OLLAMA_PLUGIN_SETTINGS } from './settings';
|
||||
import SETTINGS from './settings';
|
||||
|
||||
@Injectable()
|
||||
export class OllamaPlugin extends BaseBlockPlugin<
|
||||
typeof OLLAMA_PLUGIN_SETTINGS
|
||||
> {
|
||||
public readonly settings = OLLAMA_PLUGIN_SETTINGS;
|
||||
export class OllamaPlugin extends BaseBlockPlugin<typeof SETTINGS> {
|
||||
template: PluginBlockTemplate = { name: 'Ollama Plugin' };
|
||||
|
||||
constructor(
|
||||
pluginService: PluginService,
|
||||
@@ -30,12 +37,11 @@ export class OllamaPlugin extends BaseBlockPlugin<
|
||||
private contentService: ContentService,
|
||||
private messageService: MessageService,
|
||||
) {
|
||||
super('ollama', OLLAMA_PLUGIN_SETTINGS, pluginService);
|
||||
super('ollama-plugin', pluginService);
|
||||
}
|
||||
|
||||
this.template = { name: 'Ollama Plugin' };
|
||||
this.effects = {
|
||||
onStoreContextData: () => {},
|
||||
};
|
||||
getPath(): string {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
async process(block: Block, context: Context, _convId: string) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"name": "hexabot-ollama",
|
||||
"name": "hexabot-plugin-ollama",
|
||||
"version": "2.0.0",
|
||||
"description": "The Ollama Plugin Extension for Hexabot Chatbot / Agent Builder that provides a custom block for Generative AI + RAG",
|
||||
"dependencies": {},
|
||||
"extensions": {
|
||||
"dependencies": {
|
||||
"hexabot-helper-ollama": "2.0.0"
|
||||
},
|
||||
"author": "Hexastack",
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { PluginSetting } from '@/plugins/types';
|
||||
import { SettingType } from '@/setting/schemas/types';
|
||||
|
||||
export const OLLAMA_PLUGIN_SETTINGS = [
|
||||
export default [
|
||||
{
|
||||
label: 'model',
|
||||
group: 'default',
|
||||
|
||||
@@ -14,7 +14,12 @@ import { HelperController } from './helper.controller';
|
||||
import { HelperService } from './helper.service';
|
||||
|
||||
@Global()
|
||||
@InjectDynamicProviders('dist/extensions/**/*.helper.js')
|
||||
@InjectDynamicProviders(
|
||||
// Core & under dev helpers
|
||||
'dist/extensions/**/*.helper.js',
|
||||
// Installed helpers via npm
|
||||
'dist/.hexabot/helpers/**/*.helper.js',
|
||||
)
|
||||
@Module({
|
||||
imports: [HttpModule],
|
||||
controllers: [HelperController],
|
||||
|
||||
@@ -12,7 +12,7 @@ import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import BaseHelper from './lib/base-helper';
|
||||
import { HelperRegistry, HelperType, TypeOfHelper } from './types';
|
||||
import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types';
|
||||
|
||||
@Injectable()
|
||||
export class HelperService {
|
||||
@@ -33,7 +33,7 @@ export class HelperService {
|
||||
*
|
||||
* @param name - The helper to be registered.
|
||||
*/
|
||||
public register<H extends BaseHelper<string>>(helper: H) {
|
||||
public register<H extends BaseHelper>(helper: H) {
|
||||
const helpers = this.registry.get(helper.getType());
|
||||
helpers.set(helper.getName(), helper);
|
||||
this.logger.log(`Helper "${helper.getName()}" has been registered!`);
|
||||
@@ -47,7 +47,7 @@ export class HelperService {
|
||||
*
|
||||
* @returns - The helper
|
||||
*/
|
||||
public get<T extends HelperType>(type: T, name: string) {
|
||||
public get<T extends HelperType>(type: T, name: HelperName) {
|
||||
const helpers = this.registry.get(type);
|
||||
|
||||
if (!helpers.has(name)) {
|
||||
@@ -67,6 +67,16 @@ export class HelperService {
|
||||
return Array.from(helpers.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered helpers as an array.
|
||||
*
|
||||
* @returns An array containing all the registered helpers.
|
||||
*/
|
||||
public getAll(): BaseHelper[] {
|
||||
return Array.from(this.registry.values()) // Get all the inner maps
|
||||
.flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a helper by class.
|
||||
*
|
||||
@@ -100,7 +110,7 @@ export class HelperService {
|
||||
|
||||
const defaultHelper = this.get(
|
||||
HelperType.NLU,
|
||||
settings.chatbot_settings.default_nlu_helper,
|
||||
settings.chatbot_settings.default_nlu_helper as HelperName,
|
||||
);
|
||||
|
||||
if (!defaultHelper) {
|
||||
@@ -120,7 +130,7 @@ export class HelperService {
|
||||
|
||||
const defaultHelper = this.get(
|
||||
HelperType.LLM,
|
||||
settings.chatbot_settings.default_llm_helper,
|
||||
settings.chatbot_settings.default_llm_helper as HelperName,
|
||||
);
|
||||
|
||||
if (!defaultHelper) {
|
||||
|
||||
@@ -6,33 +6,37 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import path from 'path';
|
||||
|
||||
import { LoggerService, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { hyphenToUnderscore } from '@/utils/helpers/misc';
|
||||
import { Extension } from '@/utils/generics/extension';
|
||||
|
||||
import { HelperService } from '../helper.service';
|
||||
import { HelperSetting, HelperType } from '../types';
|
||||
|
||||
export default abstract class BaseHelper<N extends string = string> {
|
||||
protected readonly name: N;
|
||||
import { HelperName, HelperSetting, HelperType } from '../types';
|
||||
|
||||
export default abstract class BaseHelper<N extends HelperName = HelperName>
|
||||
extends Extension
|
||||
implements OnModuleInit
|
||||
{
|
||||
protected readonly settings: HelperSetting<N>[] = [];
|
||||
|
||||
protected abstract type: HelperType;
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settings: HelperSetting<N>[],
|
||||
protected readonly settingService: SettingService,
|
||||
protected readonly helperService: HelperService,
|
||||
protected readonly logger: LoggerService,
|
||||
) {
|
||||
this.name = name;
|
||||
this.settings = settings;
|
||||
super(name);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
this.settings = require(path.join(this.getPath(), 'settings')).default;
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
async onModuleInit() {
|
||||
await super.onModuleInit();
|
||||
this.helperService.register(this);
|
||||
this.setup();
|
||||
}
|
||||
@@ -47,23 +51,6 @@ export default abstract class BaseHelper<N extends string = string> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the helper's name
|
||||
*
|
||||
* @returns Helper's name
|
||||
*/
|
||||
public getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the helper's group
|
||||
* @returns Helper's group
|
||||
*/
|
||||
protected getGroup() {
|
||||
return hyphenToUnderscore(this.getName()) as HelperSetting<N>['group'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the helper's type
|
||||
*
|
||||
@@ -81,6 +68,6 @@ export default abstract class BaseHelper<N extends string = string> {
|
||||
async getSettings<S extends string = HyphenToUnderscore<N>>() {
|
||||
const settings = await this.settingService.getSettings();
|
||||
// @ts-expect-error workaround typing
|
||||
return settings[this.getGroup() as keyof Settings] as Settings[S];
|
||||
return settings[this.getNamespace() as keyof Settings] as Settings[S];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,23 +11,22 @@ import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { HelperService } from '../helper.service';
|
||||
import { HelperSetting, HelperType } from '../types';
|
||||
import { HelperName, HelperType } from '../types';
|
||||
|
||||
import BaseHelper from './base-helper';
|
||||
|
||||
export default abstract class BaseLlmHelper<
|
||||
N extends string,
|
||||
N extends HelperName = HelperName,
|
||||
> extends BaseHelper<N> {
|
||||
protected readonly type: HelperType = HelperType.LLM;
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settings: HelperSetting<N>[],
|
||||
settingService: SettingService,
|
||||
helperService: HelperService,
|
||||
logger: LoggerService,
|
||||
) {
|
||||
super(name, settings, settingService, helperService, logger);
|
||||
super(name, settingService, helperService, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,23 +23,23 @@ import {
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
|
||||
import { HelperService } from '../helper.service';
|
||||
import { HelperSetting, HelperType, Nlp } from '../types';
|
||||
import { HelperName, HelperType, Nlp } from '../types';
|
||||
|
||||
import BaseHelper from './base-helper';
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
export default abstract class BaseNlpHelper<
|
||||
N extends string,
|
||||
N extends HelperName = HelperName,
|
||||
> extends BaseHelper<N> {
|
||||
protected readonly type: HelperType = HelperType.NLU;
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
settings: HelperSetting<N>[],
|
||||
settingService: SettingService,
|
||||
helperService: HelperService,
|
||||
logger: LoggerService,
|
||||
) {
|
||||
super(name, settings, settingService, helperService, logger);
|
||||
super(name, settingService, helperService, logger);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
|
||||
import BaseHelper from './lib/base-helper';
|
||||
@@ -29,10 +37,12 @@ export enum HelperType {
|
||||
UTIL = 'util',
|
||||
}
|
||||
|
||||
export type HelperName = `${string}-helper`;
|
||||
|
||||
export type TypeOfHelper<T extends HelperType> = T extends HelperType.LLM
|
||||
? BaseLlmHelper<string>
|
||||
? BaseLlmHelper<HelperName>
|
||||
: T extends HelperType.NLU
|
||||
? BaseNlpHelper<string>
|
||||
? BaseNlpHelper<HelperName>
|
||||
: BaseHelper;
|
||||
|
||||
export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
|
||||
@@ -40,7 +50,7 @@ export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
|
||||
Map<string, H>
|
||||
>;
|
||||
|
||||
export type HelperSetting<N extends string = string> = Omit<
|
||||
export type HelperSetting<N extends HelperName = HelperName> = Omit<
|
||||
SettingCreateDto,
|
||||
'group' | 'weight'
|
||||
> & {
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import { ChannelService } from '@/channel/channel.service';
|
||||
import { HelperService } from '@/helper/helper.service';
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
|
||||
import { I18nService } from '../services/i18n.service';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('i18n')
|
||||
export class I18nController {
|
||||
constructor(private readonly i18nService: I18nService) {}
|
||||
constructor(
|
||||
private readonly pluginService: PluginService,
|
||||
private readonly helperService: HelperService,
|
||||
private readonly channelService: ChannelService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves translations of all the installed extensions.
|
||||
@@ -23,6 +28,12 @@ export class I18nController {
|
||||
*/
|
||||
@Get()
|
||||
getTranslations() {
|
||||
return this.i18nService.getExtensionI18nTranslations();
|
||||
const plugins = this.pluginService.getAll();
|
||||
const helpers = this.helperService.getAll();
|
||||
const channels = this.channelService.getAll();
|
||||
return [...plugins, ...helpers, ...channels].reduce((acc, curr) => {
|
||||
acc[curr.getNamespace()] = curr.getTranslations();
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,8 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { existsSync, promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
I18nJsonLoader,
|
||||
I18nTranslation,
|
||||
I18nService as NativeI18nService,
|
||||
Path,
|
||||
PathValue,
|
||||
@@ -22,25 +17,13 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { Translation } from '@/i18n/schemas/translation.schema';
|
||||
import { hyphenToUnderscore } from '@/utils/helpers/misc';
|
||||
|
||||
@Injectable()
|
||||
export class I18nService<K = Record<string, unknown>>
|
||||
extends NativeI18nService<K>
|
||||
implements OnModuleInit
|
||||
{
|
||||
export class I18nService<
|
||||
K = Record<string, unknown>,
|
||||
> extends NativeI18nService<K> {
|
||||
private dynamicTranslations: Record<string, Record<string, string>> = {};
|
||||
|
||||
private extensionTranslations: I18nTranslation = {};
|
||||
|
||||
onModuleInit() {
|
||||
this.loadExtensionI18nTranslations();
|
||||
}
|
||||
|
||||
getExtensionI18nTranslations() {
|
||||
return this.extensionTranslations;
|
||||
}
|
||||
|
||||
t<P extends Path<K> = any, R = PathValue<K, P>>(
|
||||
key: P,
|
||||
options?: TranslateOptions,
|
||||
@@ -83,52 +66,4 @@ export class I18nService<K = Record<string, unknown>>
|
||||
return acc;
|
||||
}, this.dynamicTranslations);
|
||||
}
|
||||
|
||||
async loadExtensionI18nTranslations() {
|
||||
const baseDir = path.join(__dirname, '..', '..', 'extensions');
|
||||
const extensionTypes = ['channels', 'helpers', 'plugins'];
|
||||
|
||||
try {
|
||||
for (const type of extensionTypes) {
|
||||
const extensionsDir = path.join(baseDir, type);
|
||||
|
||||
if (!existsSync(extensionsDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionFolders = await fs.readdir(extensionsDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const folder of extensionFolders) {
|
||||
if (folder.isDirectory()) {
|
||||
const i18nPath = path.join(extensionsDir, folder.name, 'i18n');
|
||||
const namespace = hyphenToUnderscore(folder.name);
|
||||
try {
|
||||
// Check if the i18n directory exists
|
||||
await fs.access(i18nPath);
|
||||
|
||||
// Load and merge translations
|
||||
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
|
||||
const translations = await i18nLoader.load();
|
||||
for (const lang in translations) {
|
||||
if (!this.extensionTranslations[lang]) {
|
||||
this.extensionTranslations[lang] = {
|
||||
[namespace]: translations[lang],
|
||||
};
|
||||
} else {
|
||||
this.extensionTranslations[lang][namespace] =
|
||||
translations[lang];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If the i18n folder does not exist or error in reading, skip this folder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read extensions directory: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
api/src/index.d.ts
vendored
4
api/src/index.d.ts
vendored
@@ -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[];
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
|
||||
@@ -32,7 +31,6 @@ import { NlpSampleService } from './services/nlp-sample.service';
|
||||
import { NlpValueService } from './services/nlp-value.service';
|
||||
import { NlpService } from './services/nlp.service';
|
||||
|
||||
@InjectDynamicProviders('dist/extensions/**/*.nlp.helper.js')
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Block, BlockFull } from '@/chat/schemas/block.schema';
|
||||
@@ -17,6 +19,7 @@ import { PluginService } from './plugins.service';
|
||||
import {
|
||||
PluginBlockTemplate,
|
||||
PluginEffects,
|
||||
PluginName,
|
||||
PluginSetting,
|
||||
PluginType,
|
||||
} from './types';
|
||||
@@ -29,16 +32,13 @@ export abstract class BaseBlockPlugin<
|
||||
|
||||
public readonly settings: T;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
settings: T,
|
||||
pluginService: PluginService<BasePlugin>,
|
||||
) {
|
||||
super(id, pluginService);
|
||||
this.settings = settings;
|
||||
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
|
||||
super(name, pluginService);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
this.settings = require(path.join(this.getPath(), 'settings')).default;
|
||||
}
|
||||
|
||||
template: PluginBlockTemplate;
|
||||
abstract template: PluginBlockTemplate;
|
||||
|
||||
effects?: PluginEffects;
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BasePlugin } from './base-plugin.service';
|
||||
import { PluginService } from './plugins.service';
|
||||
import { PluginType } from './types';
|
||||
import { PluginName, PluginType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseEventPlugin extends BasePlugin {
|
||||
public readonly type: PluginType = PluginType.event;
|
||||
|
||||
constructor(id: string, pluginService: PluginService<BasePlugin>) {
|
||||
super(id, pluginService);
|
||||
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
|
||||
super(name, pluginService);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,24 @@
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { Extension } from '@/utils/generics/extension';
|
||||
|
||||
import { PluginService } from './plugins.service';
|
||||
import { PluginType } from './types';
|
||||
import { PluginName, PluginType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BasePlugin implements OnModuleInit {
|
||||
export abstract class BasePlugin extends Extension implements OnModuleInit {
|
||||
public readonly type: PluginType;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly name: PluginName,
|
||||
private pluginService: PluginService<BasePlugin>,
|
||||
) {}
|
||||
) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.pluginService.setPlugin(this.type, this.id, this);
|
||||
async onModuleInit() {
|
||||
await super.onModuleInit();
|
||||
this.pluginService.setPlugin(this.type, this.name, this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
|
||||
import { BasePlugin } from './base-plugin.service';
|
||||
import { PluginService } from './plugins.service';
|
||||
import { PluginType } from './types';
|
||||
import { PluginName, PluginType } from './types';
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseStoragePlugin extends BasePlugin {
|
||||
public readonly type: PluginType = PluginType.storage;
|
||||
|
||||
constructor(id: string, pluginService: PluginService<BasePlugin>) {
|
||||
super(id, pluginService);
|
||||
constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
|
||||
super(name, pluginService);
|
||||
}
|
||||
|
||||
abstract fileExists(attachment: Attachment): Promise<boolean>;
|
||||
|
||||
@@ -20,7 +20,12 @@ import { ContentModel } from '@/cms/schemas/content.schema';
|
||||
|
||||
import { PluginService } from './plugins.service';
|
||||
|
||||
@InjectDynamicProviders('dist/extensions/**/*.plugin.js')
|
||||
@InjectDynamicProviders(
|
||||
// Core & under dev plugins
|
||||
'dist/extensions/**/*.plugin.js',
|
||||
// Installed plugins via npm
|
||||
'dist/.hexabot/plugins/**/*.plugin.js',
|
||||
)
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('PluginsService', () => {
|
||||
imports: [LoggerModule],
|
||||
}).compile();
|
||||
pluginsService = module.get<PluginService>(PluginService);
|
||||
module.get<DummyPlugin>(DummyPlugin).onModuleInit();
|
||||
await module.get<DummyPlugin>(DummyPlugin).onModuleInit();
|
||||
});
|
||||
afterAll(async () => {
|
||||
jest.clearAllMocks();
|
||||
@@ -37,7 +37,7 @@ describe('PluginsService', () => {
|
||||
|
||||
describe('getPlugin', () => {
|
||||
it('should return the required plugin', () => {
|
||||
const result = pluginsService.getPlugin(PluginType.block, 'dummy');
|
||||
const result = pluginsService.getPlugin(PluginType.block, 'dummy-plugin');
|
||||
expect(result).toBeInstanceOf(DummyPlugin);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BasePlugin } from './base-plugin.service';
|
||||
import { PluginInstance } from './map-types';
|
||||
import { PluginType } from './types';
|
||||
import { PluginName, PluginType } from './types';
|
||||
|
||||
/**
|
||||
* @summary Service for managing and retrieving plugins.
|
||||
@@ -38,18 +38,18 @@ export class PluginService<T extends BasePlugin = BasePlugin> {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Registers a plugin with a given key.
|
||||
* Registers a plugin with a given name.
|
||||
*
|
||||
* @param key The unique identifier for the plugin.
|
||||
* @param name The unique identifier for the plugin.
|
||||
* @param plugin The plugin instance to register.
|
||||
*/
|
||||
public setPlugin(type: PluginType, key: string, plugin: T) {
|
||||
public setPlugin(type: PluginType, name: PluginName, plugin: T) {
|
||||
const registry = this.registry.get(type);
|
||||
registry.set(key, plugin);
|
||||
registry.set(name, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered plugins as an array.
|
||||
* Retrieves all registered plugins by as an array.
|
||||
*
|
||||
* @returns An array containing all the registered plugins.
|
||||
*/
|
||||
@@ -58,29 +58,39 @@ export class PluginService<T extends BasePlugin = BasePlugin> {
|
||||
return Array.from(registry.values()) as PluginInstance<PT>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all registered plugins as an array.
|
||||
*
|
||||
* @returns An array containing all the registered plugins.
|
||||
*/
|
||||
public getAll(): T[] {
|
||||
return Array.from(this.registry.values()) // Get all the inner maps
|
||||
.flatMap((innerMap) => Array.from(innerMap.values())); // Flatten and get the values from each inner map
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a plugin based on its key.
|
||||
*
|
||||
* @param id The key used to register the plugin.
|
||||
* @param name The key used to register the plugin.
|
||||
*
|
||||
* @returns The plugin associated with the given key, or `undefined` if not found.
|
||||
*/
|
||||
public getPlugin<PT extends PluginType>(type: PT, id: string) {
|
||||
public getPlugin<PT extends PluginType>(type: PT, name: PluginName) {
|
||||
const registry = this.registry.get(type);
|
||||
const plugin = registry.get(id);
|
||||
const plugin = registry.get(name);
|
||||
return plugin ? (plugin as PluginInstance<PT>) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a plugin by its internal `id` property.
|
||||
*
|
||||
* @param pluginId The unique `id` of the plugin to find.
|
||||
* @param name The unique `id` of the plugin to find.
|
||||
*
|
||||
* @returns The plugin with the matching `id`, or `undefined` if no plugin is found.
|
||||
*/
|
||||
public findPlugin<PT extends PluginType>(type: PT, pluginId: string) {
|
||||
public findPlugin<PT extends PluginType>(type: PT, name: PluginName) {
|
||||
return this.getAllByType(type).find((plugin) => {
|
||||
return plugin.id === pluginId;
|
||||
return plugin.name === name;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { ChannelEvent } from '@/channel/lib/EventWrapper';
|
||||
import { BlockCreateDto } from '@/chat/dto/block.dto';
|
||||
import { Block } from '@/chat/schemas/block.schema';
|
||||
import { Conversation } from '@/chat/schemas/conversation.schema';
|
||||
import { SettingCreateDto } from '@/setting/dto/setting.dto';
|
||||
|
||||
export type PluginName = `${string}-plugin`;
|
||||
|
||||
export enum PluginType {
|
||||
event = 'event',
|
||||
block = 'block',
|
||||
@@ -19,7 +22,6 @@ export enum PluginType {
|
||||
|
||||
export interface CustomBlocks {}
|
||||
|
||||
type ChannelEvent = any;
|
||||
type BlockAttrs = Partial<BlockCreateDto> & { name: string };
|
||||
|
||||
export type PluginSetting = Omit<SettingCreateDto, 'weight'>;
|
||||
|
||||
8
api/src/setting/index.d.ts
vendored
8
api/src/setting/index.d.ts
vendored
@@ -1,3 +1,11 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { SettingByType } from './schemas/types';
|
||||
import { DEFAULT_SETTINGS } from './seeds/setting.seed-model';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const DEFAULT_SETTINGS = [
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_nlu_helper',
|
||||
value: 'core-nlu',
|
||||
value: 'core-nlu-helper',
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: false,
|
||||
@@ -27,7 +27,7 @@ export const DEFAULT_SETTINGS = [
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'default_llm_helper',
|
||||
value: 'ollama',
|
||||
value: 'ollama-helper',
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: false,
|
||||
|
||||
51
api/src/utils/generics/extension.ts
Normal file
51
api/src/utils/generics/extension.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { OnModuleInit } from '@nestjs/common';
|
||||
import { I18nJsonLoader, I18nTranslation } from 'nestjs-i18n';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ExtensionName } from '../types/extension';
|
||||
|
||||
export abstract class Extension implements OnModuleInit {
|
||||
private translations: I18nTranslation | Observable<I18nTranslation>;
|
||||
|
||||
constructor(public readonly name: ExtensionName) {}
|
||||
|
||||
abstract getPath(): string;
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
getNamespace<N extends ExtensionName = ExtensionName>() {
|
||||
return this.name.replaceAll('-', '_') as HyphenToUnderscore<N>;
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
// Load i18n
|
||||
const i18nPath = path.join(this.getPath(), 'i18n');
|
||||
try {
|
||||
// Check if the i18n directory exists
|
||||
await fs.access(i18nPath);
|
||||
|
||||
// Load and merge translations
|
||||
const i18nLoader = new I18nJsonLoader({ path: i18nPath });
|
||||
this.translations = await i18nLoader.load();
|
||||
} catch (error) {
|
||||
// If the i18n folder does not exist or error in reading, skip this folder
|
||||
}
|
||||
}
|
||||
|
||||
getTranslations() {
|
||||
return this.translations;
|
||||
}
|
||||
}
|
||||
@@ -15,23 +15,27 @@ import {
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
|
||||
import { PluginService } from '@/plugins/plugins.service';
|
||||
import { PluginSetting } from '@/plugins/types';
|
||||
import { PluginBlockTemplate, PluginSetting } from '@/plugins/types';
|
||||
|
||||
@Injectable()
|
||||
export class DummyPlugin extends BaseBlockPlugin<PluginSetting[]> {
|
||||
template: PluginBlockTemplate = { name: 'Dummy Plugin' };
|
||||
|
||||
constructor(
|
||||
pluginService: PluginService,
|
||||
private logger: LoggerService,
|
||||
) {
|
||||
super('dummy', [], pluginService);
|
||||
|
||||
this.template = { name: 'Dummy Plugin' };
|
||||
super('dummy-plugin', pluginService);
|
||||
|
||||
this.effects = {
|
||||
onStoreContextData: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
async process() {
|
||||
const envelope: StdOutgoingTextEnvelope = {
|
||||
format: OutgoingMessageFormat.text,
|
||||
|
||||
9
api/src/utils/test/dummy/settings.ts
Normal file
9
api/src/utils/test/dummy/settings.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
export default [];
|
||||
10
api/src/utils/test/fixtures/conversation.ts
vendored
10
api/src/utils/test/fixtures/conversation.ts
vendored
@@ -10,8 +10,8 @@ import mongoose from 'mongoose';
|
||||
|
||||
import { ConversationCreateDto } from '@/chat/dto/conversation.dto';
|
||||
import {
|
||||
ConversationModel,
|
||||
Conversation,
|
||||
ConversationModel,
|
||||
} from '@/chat/schemas/conversation.schema';
|
||||
|
||||
import { getFixturesWithDefaultValues } from '../defaultValues';
|
||||
@@ -25,7 +25,7 @@ const conversations: ConversationCreateDto[] = [
|
||||
sender: '0',
|
||||
active: true,
|
||||
context: {
|
||||
channel: 'messenger',
|
||||
channel: 'messenger-channel',
|
||||
text: 'Hi',
|
||||
payload: '',
|
||||
nlp: {
|
||||
@@ -60,7 +60,7 @@ const conversations: ConversationCreateDto[] = [
|
||||
foreign_id: '',
|
||||
labels: [],
|
||||
assignedTo: null,
|
||||
channel: { name: 'messenger' },
|
||||
channel: { name: 'messenger-channel' },
|
||||
},
|
||||
skip: {},
|
||||
attempt: 0,
|
||||
@@ -71,7 +71,7 @@ const conversations: ConversationCreateDto[] = [
|
||||
{
|
||||
sender: '1',
|
||||
context: {
|
||||
channel: 'offline',
|
||||
channel: 'web-channel',
|
||||
text: 'Hello',
|
||||
payload: '',
|
||||
nlp: {
|
||||
@@ -106,7 +106,7 @@ const conversations: ConversationCreateDto[] = [
|
||||
foreign_id: '',
|
||||
labels: [],
|
||||
assignedTo: null,
|
||||
channel: { name: 'offline' },
|
||||
channel: { name: 'web-channel' },
|
||||
},
|
||||
skip: {},
|
||||
attempt: 0,
|
||||
|
||||
6
api/src/utils/test/fixtures/label.ts
vendored
6
api/src/utils/test/fixtures/label.ts
vendored
@@ -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',
|
||||
},
|
||||
|
||||
12
api/src/utils/test/fixtures/subscriber.ts
vendored
12
api/src/utils/test/fixtures/subscriber.ts
vendored
@@ -27,7 +27,7 @@ const subscribers: SubscriberCreateDto[] = [
|
||||
gender: 'male',
|
||||
country: 'FR',
|
||||
channel: {
|
||||
name: 'messenger',
|
||||
name: 'messenger-channel',
|
||||
},
|
||||
labels: [],
|
||||
assignedAt: null,
|
||||
@@ -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',
|
||||
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',
|
||||
name: 'web-channel',
|
||||
},
|
||||
labels: [],
|
||||
assignedAt: null,
|
||||
@@ -75,7 +75,7 @@ const subscribers: SubscriberCreateDto[] = [
|
||||
gender: 'male',
|
||||
country: 'US',
|
||||
channel: {
|
||||
name: 'offline',
|
||||
name: 'web-channel',
|
||||
},
|
||||
labels: [],
|
||||
assignedAt: null,
|
||||
|
||||
@@ -16,7 +16,7 @@ import { modelInstance } from './misc';
|
||||
import { subscriberInstance } from './subscriber';
|
||||
|
||||
export const contextBlankInstance: Context = {
|
||||
channel: 'offline',
|
||||
channel: 'web-channel',
|
||||
text: '',
|
||||
payload: undefined,
|
||||
nlp: { entities: [] },
|
||||
@@ -42,7 +42,7 @@ export const contextEmailVarInstance: Context = {
|
||||
};
|
||||
|
||||
export const contextGetStartedInstance: Context = {
|
||||
channel: 'offline',
|
||||
channel: 'web-channel',
|
||||
text: 'Get Started',
|
||||
payload: 'GET_STARTED',
|
||||
nlp: { entities: [] },
|
||||
|
||||
@@ -25,7 +25,7 @@ export const subscriberInstance: Subscriber = {
|
||||
lastvisit: new Date(),
|
||||
retainedFrom: new Date(),
|
||||
channel: {
|
||||
name: 'offline',
|
||||
name: 'web-channel',
|
||||
},
|
||||
labels: [],
|
||||
...modelInstance,
|
||||
|
||||
13
api/src/utils/types/extension.ts
Normal file
13
api/src/utils/types/extension.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright © 2024 Hexastack. All rights reserved.
|
||||
*
|
||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import { HelperName } from '@/helper/types';
|
||||
import { PluginName } from '@/plugins/types';
|
||||
|
||||
export type ExtensionName = ChannelName | HelperName | PluginName;
|
||||
@@ -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-channel',
|
||||
query: { EIO: '4', transport: 'websocket', channel: 'web-channel' },
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user