feat: refactor extensions as npm packages (be brave 1)

This commit is contained in:
Mohamed Marrouchi 2024-10-22 11:01:44 +01:00
parent 4cf5c57053
commit 35cf78c523
87 changed files with 394 additions and 331 deletions

View File

@ -14,3 +14,4 @@ test
*.mock.ts *.mock.ts
__mock__ __mock__
__test__ __test__
.hexabot

1
api/.gitignore vendored
View File

@ -1,3 +1,4 @@
.hexabot/
node_modules/ node_modules/
dist/ dist/
coverage/ coverage/

5
api/package-lock.json generated
View File

@ -11355,6 +11355,11 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/hexabot-plugin-medmar": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/hexabot-plugin-medmar/-/hexabot-plugin-medmar-2.0.1.tgz",
"integrity": "sha512-qT2wG+vPjgBD9PZYl1EqzZPqVpoOrpldRUEDiwuzIKkIHEdfL6Q5Tr3rcgnYqFIPimVQhn/0tMgd1yQLD1dCPQ=="
},
"node_modules/hexoid": { "node_modules/hexoid": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",

View File

@ -8,12 +8,18 @@
"scripts": { "scripts": {
"preinstall": "node merge-extensions-deps.js", "preinstall": "node merge-extensions-deps.js",
"postinstall": "patch-package", "postinstall": "patch-package",
"build": "nest build", "build:clean": "rm -rf src/.hexabot",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "build:channels": "mkdir -p src/.hexabot/channels && cp -R node_modules/hexabot-channel-* src/.hexabot/channels/",
"build:helpers": "mkdir -p src/.hexabot/helpers && cp -R node_modules/hexabot-helper-* src/.hexabot/helpers/",
"build:plugins": "mkdir -p src/.hexabot/plugins && cp -R node_modules/hexabot-plugin-* 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", "start": "nest start",
"doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w", "doc": "npx @compodoc/compodoc --hideGenerator -p tsconfig.doc.json -s -r 9003 -w",
"start:dev": "nest start --watch", "start:dev": "npm run build:prepare && nest start --watch",
"start:debug": "nest start --debug 0.0.0.0:9229 --watch", "start:debug": "npm run build:prepare && nest start --debug 0.0.0.0:9229 --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",

View File

@ -108,13 +108,13 @@ export class AttachmentService extends BaseService<Attachment> {
await this.getStoragePlugin().uploadAvatar(picture); await this.getStoragePlugin().uploadAvatar(picture);
this.logger.log( this.logger.log(
`Profile picture uploaded successfully to ${ `Profile picture uploaded successfully to ${
this.getStoragePlugin().id this.getStoragePlugin().name
}`, }`,
); );
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
`Error while uploading profile picture to ${ `Error while uploading profile picture to ${
this.getStoragePlugin().id this.getStoragePlugin().name
}`, }`,
err, err,
); );

View File

@ -23,7 +23,7 @@ export class ChannelController {
getChannels(): { name: string }[] { getChannels(): { name: string }[] {
return this.channelService.getAll().map((handler) => { return this.channelService.getAll().map((handler) => {
return { return {
name: handler.getChannel(), name: handler.getName(),
}; };
}); });
} }

View File

@ -7,7 +7,7 @@
*/ */
import { Injectable, NestMiddleware } from '@nestjs/common'; import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express'; import { NextFunction, Request, Response } from 'express';
import { ChannelService } from './channel.service'; import { ChannelService } from './channel.service';
@ -20,7 +20,9 @@ export class ChannelMiddleware implements NestMiddleware {
try { try {
const [_, path, channelName] = req.path.split('/'); const [_, path, channelName] = req.path.split('/');
if (path === 'webhook' && channelName) { if (path === 'webhook' && channelName) {
const channel = this.channelService.getChannelHandler(channelName); const channel = this.channelService.getChannelHandler(
`${channelName}-channel`,
);
if (channel) { if (channel) {
return await channel.middleware(req, res, next); return await channel.middleware(req, res, next);
} }

View File

@ -7,7 +7,12 @@
*/ */
import { HttpModule } from '@nestjs/axios'; 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 { InjectDynamicProviders } from 'nestjs-dynamic-providers';
import { AttachmentModule } from '@/attachment/attachment.module'; import { AttachmentModule } from '@/attachment/attachment.module';
@ -23,6 +28,7 @@ export interface ChannelModuleOptions {
folder: string; folder: string;
} }
@Global()
@InjectDynamicProviders('dist/extensions/**/*.channel.js') @InjectDynamicProviders('dist/extensions/**/*.channel.js')
@Module({ @Module({
controllers: [WebhookController, ChannelController], controllers: [WebhookController, ChannelController],

View File

@ -23,10 +23,11 @@ import { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response'; import { SocketResponse } from '@/websocket/utils/socket-response';
import ChannelHandler from './lib/Handler'; import ChannelHandler from './lib/Handler';
import { ChannelName } from './types';
@Injectable() @Injectable()
export class ChannelService { export class ChannelService {
private registry: Map<string, ChannelHandler<string>> = new Map(); private registry: Map<string, ChannelHandler<ChannelName>> = new Map();
constructor( constructor(
private readonly logger: LoggerService, private readonly logger: LoggerService,
@ -40,7 +41,7 @@ export class ChannelService {
* @param channel - The channel handler associated with the channel name. * @param channel - The channel handler associated with the channel name.
* @typeParam C The channel handler's type that extends `ChannelHandler`. * @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, name: T,
channel: C, channel: C,
) { ) {
@ -62,9 +63,9 @@ export class ChannelService {
* @param name - The name of the channel to find. * @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. * @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 this.getAll().find((c) => {
return c.getChannel() === name; return c.getName() === name;
}); });
} }
@ -74,7 +75,7 @@ export class ChannelService {
* @param channelName - The name of the channel (messenger, offline, ...). * @param channelName - The name of the channel (messenger, offline, ...).
* @returns The handler for the specified channel. * @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, name: T,
): C { ): C {
const handler = this.registry.get(name); const handler = this.registry.get(name);
@ -93,7 +94,7 @@ export class ChannelService {
* @returns A promise that resolves when the handler has processed the request. * @returns A promise that resolves when the handler has processed the request.
*/ */
async handle(channel: string, req: Request, res: Response): Promise<void> { 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); handler.handle(req, res);
} }

View File

@ -55,7 +55,7 @@ export default abstract class EventWrapper<
toString() { toString() {
return JSON.stringify( return JSON.stringify(
{ {
handler: this._handler.getChannel(), handler: this._handler.getName(),
channelData: this.getChannelData(), channelData: this.getChannelData(),
sender: this.getSender(), sender: this.getSender(),
recipient: this.getRecipientForeignId(), recipient: this.getRecipientForeignId(),

View File

@ -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). * 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 { NextFunction, Request, Response } from 'express';
import { Attachment } from '@/attachment/schemas/attachment.schema'; import { Attachment } from '@/attachment/schemas/attachment.schema';
@ -17,35 +19,39 @@ import {
} from '@/chat/schemas/types/message'; } from '@/chat/schemas/types/message';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.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 { SocketRequest } from '@/websocket/utils/socket-request';
import { SocketResponse } from '@/websocket/utils/socket-response'; import { SocketResponse } from '@/websocket/utils/socket-response';
import { ChannelService } from '../channel.service'; import { ChannelService } from '../channel.service';
import { ChannelSetting } from '../types'; import { ChannelName, ChannelSetting } from '../types';
import EventWrapper from './EventWrapper'; import EventWrapper from './EventWrapper';
@Injectable() @Injectable()
export default abstract class ChannelHandler<N extends string = string> { export default abstract class ChannelHandler<
private readonly name: N; N extends ChannelName = ChannelName,
>
extends Extension
implements OnModuleInit
{
private readonly settings: ChannelSetting<N>[]; private readonly settings: ChannelSetting<N>[];
constructor( constructor(
name: N, name: N,
settings: ChannelSetting<N>[],
protected readonly settingService: SettingService, protected readonly settingService: SettingService,
private readonly channelService: ChannelService, private readonly channelService: ChannelService,
protected readonly logger: LoggerService, protected readonly logger: LoggerService,
) { ) {
this.name = name; super(name);
this.settings = settings; // 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.channelService.setChannel(
this.getChannel(), this.getName() as ChannelName,
this as unknown as ChannelHandler<N>, this as unknown as ChannelHandler<N>,
); );
this.setup(); this.setup();
@ -53,7 +59,7 @@ export default abstract class ChannelHandler<N extends string = string> {
async setup() { async setup() {
await this.settingService.seedIfNotExist( await this.settingService.seedIfNotExist(
this.getChannel(), this.getName(),
this.settings.map((s, i) => ({ this.settings.map((s, i) => ({
...s, ...s,
weight: i + 1, weight: i + 1,
@ -62,22 +68,6 @@ export default abstract class ChannelHandler<N extends string = string> {
this.init(); 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 the channel's settings
* @returns 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>>() { async getSettings<S extends string = HyphenToUnderscore<N>>() {
const settings = await this.settingService.getSettings(); const settings = await this.settingService.getSettings();
// @ts-expect-error workaround typing // @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];
} }
/** /**

View File

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

View File

@ -1,5 +1,7 @@
import { SettingCreateDto } from '@/setting/dto/setting.dto'; import { SettingCreateDto } from '@/setting/dto/setting.dto';
export type ChannelName = `${string}-channel`;
export type ChannelSetting<N extends string = string> = Omit< export type ChannelSetting<N extends string = string> = Omit<
SettingCreateDto, SettingCreateDto,
'group' | 'weight' 'group' | 'weight'

View File

@ -27,7 +27,7 @@ import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service'; 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 { UserService } from '@/user/services/user.service';
import { BaseController } from '@/utils/generics/base-controller'; import { BaseController } from '@/utils/generics/base-controller';
import { DeleteResult } from '@/utils/generics/base-repository'; 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. * 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. * @returns An array containing the settings of the specified plugin.
*/ */
@Get('customBlocks/settings') @Get('customBlocks/settings')
findSettings(@Query('plugin') pluginId: string) { findSettings(@Query('plugin') pluginName: PluginName) {
try { try {
if (!pluginId) { if (!pluginName) {
throw new BadRequestException( throw new BadRequestException(
'Plugin id must be supplied as a query param', 'Plugin id 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) { if (!plugin) {
throw new NotFoundException('Plugin Not Found'); throw new NotFoundException('Plugin Not Found');
@ -122,11 +125,12 @@ export class BlockController extends BaseController<
const plugins = this.pluginsService const plugins = this.pluginsService
.getAllByType(PluginType.block) .getAllByType(PluginType.block)
.map((p) => ({ .map((p) => ({
id: p.id, id: p.getName(),
namespace: p.getNamespace(),
template: { template: {
...p.template, ...p.template,
message: { message: {
plugin: p.id, plugin: p.name,
args: p.settings.reduce( args: p.settings.reduce(
(acc, setting) => { (acc, setting) => {
acc[setting.label] = setting.value; acc[setting.label] = setting.value;

View File

@ -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). * 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 { interface BaseChannelData {
name: string; // channel name name: ChannelName; // channel name
isSocket?: boolean; isSocket?: boolean;
type?: any; //TODO: type has to be checked type?: any; //TODO: type has to be checked
} }

View File

@ -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). * 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 { Nlp } from '@/helper/types';
import { Subscriber } from '../subscriber.schema'; import { Subscriber } from '../subscriber.schema';
@ -13,7 +14,7 @@ import { Subscriber } from '../subscriber.schema';
import { Payload } from './quick-reply'; import { Payload } from './quick-reply';
export interface Context { export interface Context {
channel?: string; channel?: ChannelName;
text?: string; text?: string;
payload?: Payload | string; payload?: Payload | string;
nlp?: Nlp.ParseEntities | null; nlp?: Nlp.ParseEntities | null;

View File

@ -222,7 +222,7 @@ describe('BlockService', () => {
describe('match', () => { describe('match', () => {
const handlerMock = { const handlerMock = {
getChannel: jest.fn(() => OFFLINE_CHANNEL_NAME), getName: jest.fn(() => OFFLINE_CHANNEL_NAME),
} as any as OfflineHandler; } as any as OfflineHandler;
const offlineEventGreeting = new OfflineEventWrapper( const offlineEventGreeting = new OfflineEventWrapper(
handlerMock, handlerMock,
@ -502,7 +502,7 @@ describe('BlockService', () => {
describe('processText', () => { describe('processText', () => {
const context: Context = { const context: Context = {
...contextGetStartedInstance, ...contextGetStartedInstance,
channel: 'offline', channel: 'offline-channel',
text: '', text: '',
payload: undefined, payload: undefined,
nlp: { entities: [] }, nlp: { entities: [] },

View File

@ -17,7 +17,7 @@ import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service'; import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.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 { SettingService } from '@/setting/services/setting.service';
import { BaseService } from '@/utils/generics/base-service'; import { BaseService } from '@/utils/generics/base-service';
import { getRandom } from '@/utils/helpers/safeRandom'; import { getRandom } from '@/utils/helpers/safeRandom';
@ -71,7 +71,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
const payload = event.getPayload(); const payload = event.getPayload();
// Perform a filter on the specific channels // Perform a filter on the specific channels
const channel = event.getHandler().getChannel(); const channel = event.getHandler().getName();
blocks = blocks.filter((b) => { blocks = blocks.filter((b) => {
return ( return (
!b.trigger_channels || !b.trigger_channels ||
@ -593,7 +593,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
} else if (blockMessage && 'plugin' in blockMessage) { } else if (blockMessage && 'plugin' in blockMessage) {
const plugin = this.pluginService.findPlugin( const plugin = this.pluginService.findPlugin(
PluginType.block, PluginType.block,
blockMessage.plugin, blockMessage.plugin as PluginName,
); );
// Process custom plugin block // Process custom plugin block
try { try {

View File

@ -224,7 +224,7 @@ describe('BlockService', () => {
nlp: null, nlp: null,
payload: null, payload: null,
attempt: 0, attempt: 0,
channel: 'offline', channel: 'offline-channel',
text: offlineEventText.data.text, text: offlineEventText.data.text,
}, },
}); });
@ -283,7 +283,7 @@ describe('BlockService', () => {
nlp: null, nlp: null,
payload: null, payload: null,
attempt: 0, attempt: 0,
channel: 'offline', channel: 'offline-channel',
text: offlineEventText.data.text, text: offlineEventText.data.text,
}, },
}); });

View File

@ -246,7 +246,7 @@ export class ChatService {
this.eventEmitter.emit('hook:stats:entry', 'new_users', 'New users'); this.eventEmitter.emit('hook:stats:entry', 'new_users', 'New users');
subscriberData.channel = { subscriberData.channel = {
...event.getChannelData(), ...event.getChannelData(),
name: handler.getChannel(), name: handler.getName(),
}; };
subscriber = await this.subscriberService.create(subscriberData); subscriber = await this.subscriberService.create(subscriberData);
} else { } else {

View File

@ -9,6 +9,7 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import EventWrapper from '@/channel/lib/EventWrapper'; import EventWrapper from '@/channel/lib/EventWrapper';
import { ChannelName } from '@/channel/types';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { BaseService } from '@/utils/generics/base-service'; import { BaseService } from '@/utils/generics/base-service';
@ -69,7 +70,7 @@ export class ConversationService extends BaseService<
const msgType = event.getMessageType(); const msgType = event.getMessageType();
const profile = event.getSender(); const profile = event.getSender();
// Capture channel specific context data // 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.text = event.getText();
convo.context.payload = event.getPayload(); convo.context.payload = event.getPayload();
convo.context.nlp = event.getNLP(); convo.context.nlp = event.getNLP();

View File

@ -1,3 +1,3 @@
{ {
"live_chat_tester": "Live Chat Tester" "live_chat_tester_channel": "Live Chat Tester"
} }

View File

@ -1,3 +1,3 @@
{ {
"live_chat_tester": "Testeur Live Chat" "live_chat_tester_channel": "Testeur Live Chat"
} }

View File

@ -21,10 +21,7 @@ import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from '../offline/base-web-channel'; import BaseWebChannelHandler from '../offline/base-web-channel';
import { import { LIVE_CHAT_TEST_CHANNEL_NAME } from './settings';
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
LIVE_CHAT_TEST_CHANNEL_NAME,
} from './settings';
@Injectable() @Injectable()
export default class LiveChatTesterHandler extends BaseWebChannelHandler< export default class LiveChatTesterHandler extends BaseWebChannelHandler<
@ -44,7 +41,6 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
) { ) {
super( super(
LIVE_CHAT_TEST_CHANNEL_NAME, LIVE_CHAT_TEST_CHANNEL_NAME,
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
settingService, settingService,
channelService, channelService,
logger, logger,
@ -57,4 +53,8 @@ export default class LiveChatTesterHandler extends BaseWebChannelHandler<
websocketGateway, websocketGateway,
); );
} }
getPath(): string {
return __dirname;
}
} }

View File

@ -1,5 +1,4 @@
import { import DEFAULT_LIVE_CHAT_TEST_SETTINGS, {
DEFAULT_LIVE_CHAT_TEST_SETTINGS,
LIVE_CHAT_TEST_GROUP_NAME, LIVE_CHAT_TEST_GROUP_NAME,
} from './settings'; } from './settings';

View File

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

View File

@ -12,11 +12,11 @@ import { SettingType } from '@/setting/schemas/types';
import { Offline } from '../offline/types'; import { Offline } from '../offline/types';
export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester'; export const LIVE_CHAT_TEST_CHANNEL_NAME = 'live-chat-tester-channel';
export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester'; export const LIVE_CHAT_TEST_GROUP_NAME = 'live_chat_tester_channel';
export const DEFAULT_LIVE_CHAT_TEST_SETTINGS = [ export default [
{ {
group: LIVE_CHAT_TEST_GROUP_NAME, group: LIVE_CHAT_TEST_GROUP_NAME,
label: Offline.SettingLabel.verification_token, label: Offline.SettingLabel.verification_token,

View File

@ -131,7 +131,7 @@ describe('Offline Handler', () => {
it('should have correct name', () => { it('should have correct name', () => {
expect(handler).toBeDefined(); expect(handler).toBeDefined();
expect(handler.getChannel()).toEqual('offline'); expect(handler.getName()).toEqual('offline-channel');
}); });
it('should format text properly', () => { it('should format text properly', () => {
@ -192,7 +192,7 @@ describe('Offline Handler', () => {
agent: req.headers['user-agent'], agent: req.headers['user-agent'],
ipAddress: '0.0.0.0', ipAddress: '0.0.0.0',
isSocket: false, isSocket: false,
name: 'offline', name: 'offline-channel',
}, },
country: '', country: '',
first_name: req.query.first_name, first_name: req.query.first_name,

View File

@ -22,7 +22,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { ChannelService } from '@/channel/channel.service'; import { ChannelService } from '@/channel/channel.service';
import EventWrapper from '@/channel/lib/EventWrapper'; import EventWrapper from '@/channel/lib/EventWrapper';
import ChannelHandler from '@/channel/lib/Handler'; import ChannelHandler from '@/channel/lib/Handler';
import { ChannelSetting } from '@/channel/types'; import { ChannelName } from '@/channel/types';
import { MessageCreateDto } from '@/chat/dto/message.dto'; import { MessageCreateDto } from '@/chat/dto/message.dto';
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto'; import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants'; import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
@ -63,12 +63,11 @@ import { Offline } from './types';
import OfflineEventWrapper from './wrapper'; import OfflineEventWrapper from './wrapper';
@Injectable() @Injectable()
export default class BaseWebChannelHandler< export default abstract class BaseWebChannelHandler<
N extends string, N extends ChannelName,
> extends ChannelHandler<N> { > extends ChannelHandler<N> {
constructor( constructor(
name: N, name: N,
settings: ChannelSetting<N>[],
settingService: SettingService, settingService: SettingService,
channelService: ChannelService, channelService: ChannelService,
logger: LoggerService, logger: LoggerService,
@ -80,7 +79,7 @@ export default class BaseWebChannelHandler<
protected readonly menuService: MenuService, protected readonly menuService: MenuService,
private readonly websocketGateway: WebsocketGateway, private readonly websocketGateway: WebsocketGateway,
) { ) {
super(name, settings, settingService, channelService, logger); super(name, settingService, channelService, logger);
} }
/** /**
@ -102,7 +101,7 @@ export default class BaseWebChannelHandler<
const settings = await this.getSettings(); const settings = await this.getSettings();
const handshake = client.handshake; const handshake = client.handshake;
const { channel } = handshake.query; const { channel } = handshake.query;
if (channel !== this.getChannel()) { if (channel !== this.getName()) {
return; return;
} }
try { try {
@ -464,7 +463,7 @@ export default class BaseWebChannelHandler<
retainedFrom: new Date(), retainedFrom: new Date(),
channel: { channel: {
...channelData, ...channelData,
name: this.getChannel(), name: this.getName() as ChannelName,
}, },
language: '', language: '',
locale: '', locale: '',

View File

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

View File

@ -1,3 +1,3 @@
{ {
"live_chat_tester": "Testeur Live Chat" "offline_channel": "Canal Web"
} }

View File

@ -20,7 +20,7 @@ import { SettingService } from '@/setting/services/setting.service';
import { WebsocketGateway } from '@/websocket/websocket.gateway'; import { WebsocketGateway } from '@/websocket/websocket.gateway';
import BaseWebChannelHandler from './base-web-channel'; import BaseWebChannelHandler from './base-web-channel';
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_CHANNEL_NAME } from './settings'; import { OFFLINE_CHANNEL_NAME } from './settings';
@Injectable() @Injectable()
export default class OfflineHandler extends BaseWebChannelHandler< export default class OfflineHandler extends BaseWebChannelHandler<
@ -40,7 +40,6 @@ export default class OfflineHandler extends BaseWebChannelHandler<
) { ) {
super( super(
OFFLINE_CHANNEL_NAME, OFFLINE_CHANNEL_NAME,
DEFAULT_OFFLINE_SETTINGS,
settingService, settingService,
channelService, channelService,
logger, logger,
@ -53,4 +52,8 @@ export default class OfflineHandler extends BaseWebChannelHandler<
websocketGateway, websocketGateway,
); );
} }
getPath(): string {
return __dirname;
}
} }

View File

@ -1,4 +1,4 @@
import { DEFAULT_OFFLINE_SETTINGS, OFFLINE_GROUP_NAME } from './settings'; import DEFAULT_OFFLINE_SETTINGS, { OFFLINE_GROUP_NAME } from './settings';
declare global { declare global {
interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {} interface Settings extends SettingTree<typeof DEFAULT_OFFLINE_SETTINGS> {}

View File

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

View File

@ -11,11 +11,11 @@ import { SettingType } from '@/setting/schemas/types';
import { Offline } from './types'; import { Offline } from './types';
export const OFFLINE_CHANNEL_NAME = 'offline' as const; export const OFFLINE_CHANNEL_NAME = 'offline-channel' as const;
export const OFFLINE_GROUP_NAME = OFFLINE_CHANNEL_NAME; export const OFFLINE_GROUP_NAME = 'offline_channel';
export const DEFAULT_OFFLINE_SETTINGS = [ export default [
{ {
group: OFFLINE_GROUP_NAME, group: OFFLINE_GROUP_NAME,
label: Offline.SettingLabel.verification_token, label: Offline.SettingLabel.verification_token,

View File

@ -7,6 +7,7 @@
*/ */
import EventWrapper from '@/channel/lib/EventWrapper'; import EventWrapper from '@/channel/lib/EventWrapper';
import { ChannelName } from '@/channel/types';
import { import {
AttachmentForeignKey, AttachmentForeignKey,
AttachmentPayload, AttachmentPayload,
@ -67,7 +68,8 @@ type OfflineEventAdapter =
}; };
export default class OfflineEventWrapper< export default class OfflineEventWrapper<
T extends BaseWebChannelHandler<string> = BaseWebChannelHandler<string>, T extends
BaseWebChannelHandler<ChannelName> = BaseWebChannelHandler<ChannelName>,
> extends EventWrapper<OfflineEventAdapter, Offline.Event> { > extends EventWrapper<OfflineEventAdapter, Offline.Event> {
/** /**
* Constructor : channel's event wrapper * Constructor : channel's event wrapper

View File

@ -58,7 +58,7 @@ describe('Core NLU Helper', () => {
provide: SettingService, provide: SettingService,
useValue: { useValue: {
getSettings: jest.fn(() => ({ getSettings: jest.fn(() => ({
core_nlu: { core_nlu_helper: {
endpoint: 'path', endpoint: 'path',
token: 'token', token: 'token',
threshold: '0.5', threshold: '0.5',
@ -121,7 +121,7 @@ describe('Core NLU Helper', () => {
true, true,
); );
const settings = await settingService.getSettings(); const settings = await settingService.getSettings();
const threshold = settings.core_nlu.threshold; const threshold = settings.core_nlu_helper.threshold;
const thresholdGuess = { const thresholdGuess = {
entities: nlpBestGuess.entities.filter( entities: nlpBestGuess.entities.filter(
(g) => (g) =>

View File

@ -1,3 +1,3 @@
{ {
"core_nlu": "Core NLU Engine" "core_nlu_helper": "Core NLU Engine"
} }

View File

@ -1,3 +1,3 @@
{ {
"core_nlu": "Core NLU Engine" "core_nlu_helper": "Core NLU Engine"
} }

View File

@ -1,4 +1,4 @@
import { CORE_NLU_HELPER_GROUP, CORE_NLU_HELPER_SETTINGS } from './settings'; import CORE_NLU_HELPER_SETTINGS, { CORE_NLU_HELPER_GROUP } from './settings';
declare global { declare global {
interface Settings extends SettingTree<typeof CORE_NLU_HELPER_SETTINGS> {} interface Settings extends SettingTree<typeof CORE_NLU_HELPER_SETTINGS> {}

View File

@ -20,7 +20,7 @@ import { NlpValue } from '@/nlp/schemas/nlp-value.schema';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { buildURL } from '@/utils/helpers/URL'; 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'; import { NlpParseResultType, RasaNlu } from './types';
@Injectable() @Injectable()
@ -34,13 +34,11 @@ export default class CoreNluHelper extends BaseNlpHelper<
private readonly httpService: HttpService, private readonly httpService: HttpService,
private readonly languageService: LanguageService, private readonly languageService: LanguageService,
) { ) {
super( super(CORE_NLU_HELPER_NAME, settingService, helperService, logger);
CORE_NLU_HELPER_NAME, }
CORE_NLU_HELPER_SETTINGS,
settingService, getPath() {
helperService, return __dirname;
logger,
);
} }
/** /**

View File

@ -1,5 +1,5 @@
{ {
"name": "hexabot-core-nlu", "name": "hexabot-helper-core-nlu",
"version": "2.0.0", "version": "2.0.0",
"description": "The Core NLU Helper Extension for Hexabot Chatbot / Agent Builder to enable the Intent Classification and Language Detection", "description": "The Core NLU Helper Extension for Hexabot Chatbot / Agent Builder to enable the Intent Classification and Language Detection",
"dependencies": {}, "dependencies": {},

View File

@ -1,11 +1,11 @@
import { HelperSetting } from '@/helper/types'; import { HelperSetting } from '@/helper/types';
import { SettingType } from '@/setting/schemas/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_GROUP = 'core_nlu_helper';
export const CORE_NLU_HELPER_SETTINGS = [ export default [
{ {
group: CORE_NLU_HELPER_GROUP, group: CORE_NLU_HELPER_GROUP,
label: 'endpoint', label: 'endpoint',

View File

@ -1,3 +1,3 @@
{ {
"ollama": "Ollama" "ollama_helper": "Ollama"
} }

View File

@ -1,3 +1,3 @@
{ {
"ollama": "Ollama" "ollama_helper": "Ollama"
} }

View File

@ -1,4 +1,4 @@
import { OLLAMA_HELPER_GROUP, OLLAMA_HELPER_SETTINGS } from './settings'; import OLLAMA_HELPER_SETTINGS, { OLLAMA_HELPER_GROUP } from './settings';
declare global { declare global {
interface Settings extends SettingTree<typeof OLLAMA_HELPER_SETTINGS> {} interface Settings extends SettingTree<typeof OLLAMA_HELPER_SETTINGS> {}

View File

@ -17,7 +17,7 @@ import { LoggerService } from '@/logger/logger.service';
import { Setting } from '@/setting/schemas/setting.schema'; import { Setting } from '@/setting/schemas/setting.schema';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { OLLAMA_HELPER_NAME, OLLAMA_HELPER_SETTINGS } from './settings'; import { OLLAMA_HELPER_NAME } from './settings';
@Injectable() @Injectable()
export default class OllamaLlmHelper export default class OllamaLlmHelper
@ -36,13 +36,11 @@ export default class OllamaLlmHelper
helperService: HelperService, helperService: HelperService,
protected readonly logger: LoggerService, protected readonly logger: LoggerService,
) { ) {
super( super('ollama-helper', settingService, helperService, logger);
OLLAMA_HELPER_NAME, }
OLLAMA_HELPER_SETTINGS,
settingService, getPath(): string {
helperService, return __dirname;
logger,
);
} }
async onApplicationBootstrap() { async onApplicationBootstrap() {
@ -51,7 +49,7 @@ export default class OllamaLlmHelper
this.client = new Ollama({ host: settings.api_url }); this.client = new Ollama({ host: settings.api_url });
} }
@OnEvent('hook:ollama:api_url') @OnEvent('hook:ollama_helper:api_url')
handleApiUrlChange(setting: Setting) { handleApiUrlChange(setting: Setting) {
this.client = new Ollama({ host: setting.value }); this.client = new Ollama({ host: setting.value });
} }

View File

@ -1,11 +1,13 @@
import { HelperSetting } from '@/helper/types'; import { HelperSetting } from '@/helper/types';
import { SettingType } from '@/setting/schemas/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_GROUP: HyphenToUnderscore<
typeof OLLAMA_HELPER_NAME
> = 'ollama_helper';
export const OLLAMA_HELPER_SETTINGS = [ export default [
{ {
label: 'api_url', label: 'api_url',
group: OLLAMA_HELPER_GROUP, group: OLLAMA_HELPER_GROUP,

View File

@ -1,3 +1,3 @@
{ {
"ollama": "Ollama" "ollama_plugin": "Ollama Plugin"
} }

View File

@ -1,3 +1,3 @@
{ {
"ollama": "Ollama" "ollama_plugin": "Ollama Plugin"
} }

View File

@ -14,14 +14,13 @@ import { HelperType } from '@/helper/types';
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service'; import { PluginService } from '@/plugins/plugins.service';
import { PluginBlockTemplate } from '@/plugins/types';
import { OLLAMA_PLUGIN_SETTINGS } from './settings'; import SETTINGS from './settings';
@Injectable() @Injectable()
export class OllamaPlugin extends BaseBlockPlugin< export class OllamaPlugin extends BaseBlockPlugin<typeof SETTINGS> {
typeof OLLAMA_PLUGIN_SETTINGS template: PluginBlockTemplate = { name: 'Ollama Plugin' };
> {
public readonly settings = OLLAMA_PLUGIN_SETTINGS;
constructor( constructor(
pluginService: PluginService, pluginService: PluginService,
@ -30,12 +29,11 @@ export class OllamaPlugin extends BaseBlockPlugin<
private contentService: ContentService, private contentService: ContentService,
private messageService: MessageService, private messageService: MessageService,
) { ) {
super('ollama', OLLAMA_PLUGIN_SETTINGS, pluginService); super('ollama-plugin', pluginService);
}
this.template = { name: 'Ollama Plugin' }; getPath(): string {
this.effects = { return __dirname;
onStoreContextData: () => {},
};
} }
async process(block: Block, context: Context, _convId: string) { async process(block: Block, context: Context, _convId: string) {

View File

@ -1,9 +1,8 @@
{ {
"name": "hexabot-ollama", "name": "hexabot-plugin-ollama",
"version": "2.0.0", "version": "2.0.0",
"description": "The Ollama Plugin Extension for Hexabot Chatbot / Agent Builder that provides a custom block for Generative AI + RAG", "description": "The Ollama Plugin Extension for Hexabot Chatbot / Agent Builder that provides a custom block for Generative AI + RAG",
"dependencies": {}, "dependencies": {
"extensions": {
"hexabot-helper-ollama": "2.0.0" "hexabot-helper-ollama": "2.0.0"
}, },
"author": "Hexastack", "author": "Hexastack",

View File

@ -1,7 +1,7 @@
import { PluginSetting } from '@/plugins/types'; import { PluginSetting } from '@/plugins/types';
import { SettingType } from '@/setting/schemas/types'; import { SettingType } from '@/setting/schemas/types';
export const OLLAMA_PLUGIN_SETTINGS = [ export default [
{ {
label: 'model', label: 'model',
group: 'default', group: 'default',

View File

@ -12,7 +12,7 @@ import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import BaseHelper from './lib/base-helper'; import BaseHelper from './lib/base-helper';
import { HelperRegistry, HelperType, TypeOfHelper } from './types'; import { HelperName, HelperRegistry, HelperType, TypeOfHelper } from './types';
@Injectable() @Injectable()
export class HelperService { export class HelperService {
@ -33,7 +33,7 @@ export class HelperService {
* *
* @param name - The helper to be registered. * @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()); const helpers = this.registry.get(helper.getType());
helpers.set(helper.getName(), helper); helpers.set(helper.getName(), helper);
this.logger.log(`Helper "${helper.getName()}" has been registered!`); this.logger.log(`Helper "${helper.getName()}" has been registered!`);
@ -47,7 +47,7 @@ export class HelperService {
* *
* @returns - The helper * @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); const helpers = this.registry.get(type);
if (!helpers.has(name)) { if (!helpers.has(name)) {
@ -67,6 +67,16 @@ export class HelperService {
return Array.from(helpers.values()); 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. * Get a helper by class.
* *
@ -100,7 +110,7 @@ export class HelperService {
const defaultHelper = this.get( const defaultHelper = this.get(
HelperType.NLU, HelperType.NLU,
settings.chatbot_settings.default_nlu_helper, settings.chatbot_settings.default_nlu_helper as HelperName,
); );
if (!defaultHelper) { if (!defaultHelper) {
@ -120,7 +130,7 @@ export class HelperService {
const defaultHelper = this.get( const defaultHelper = this.get(
HelperType.LLM, HelperType.LLM,
settings.chatbot_settings.default_llm_helper, settings.chatbot_settings.default_llm_helper as HelperName,
); );
if (!defaultHelper) { if (!defaultHelper) {

View File

@ -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). * 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 { SettingService } from '@/setting/services/setting.service';
import { hyphenToUnderscore } from '@/utils/helpers/misc'; import { Extension } from '@/utils/generics/extension';
import { HelperService } from '../helper.service'; import { HelperService } from '../helper.service';
import { HelperSetting, HelperType } from '../types'; import { HelperName, HelperSetting, HelperType } from '../types';
export default abstract class BaseHelper<N extends string = string> {
protected readonly name: N;
export default abstract class BaseHelper<N extends HelperName = HelperName>
extends Extension
implements OnModuleInit
{
protected readonly settings: HelperSetting<N>[] = []; protected readonly settings: HelperSetting<N>[] = [];
protected abstract type: HelperType; protected abstract type: HelperType;
constructor( constructor(
name: N, name: N,
settings: HelperSetting<N>[],
protected readonly settingService: SettingService, protected readonly settingService: SettingService,
protected readonly helperService: HelperService, protected readonly helperService: HelperService,
protected readonly logger: LoggerService, protected readonly logger: LoggerService,
) { ) {
this.name = name; super(name);
this.settings = settings; // 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.helperService.register(this);
this.setup(); 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 * 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>>() { async getSettings<S extends string = HyphenToUnderscore<N>>() {
const settings = await this.settingService.getSettings(); const settings = await this.settingService.getSettings();
// @ts-expect-error workaround typing // @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];
} }
} }

View File

@ -11,23 +11,22 @@ import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { HelperService } from '../helper.service'; import { HelperService } from '../helper.service';
import { HelperSetting, HelperType } from '../types'; import { HelperName, HelperType } from '../types';
import BaseHelper from './base-helper'; import BaseHelper from './base-helper';
export default abstract class BaseLlmHelper< export default abstract class BaseLlmHelper<
N extends string, N extends HelperName = HelperName,
> extends BaseHelper<N> { > extends BaseHelper<N> {
protected readonly type: HelperType = HelperType.LLM; protected readonly type: HelperType = HelperType.LLM;
constructor( constructor(
name: N, name: N,
settings: HelperSetting<N>[],
settingService: SettingService, settingService: SettingService,
helperService: HelperService, helperService: HelperService,
logger: LoggerService, logger: LoggerService,
) { ) {
super(name, settings, settingService, helperService, logger); super(name, settingService, helperService, logger);
} }
/** /**

View File

@ -23,23 +23,23 @@ import {
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { HelperService } from '../helper.service'; import { HelperService } from '../helper.service';
import { HelperSetting, HelperType, Nlp } from '../types'; import { HelperName, HelperType, Nlp } from '../types';
import BaseHelper from './base-helper'; import BaseHelper from './base-helper';
// eslint-disable-next-line prettier/prettier
export default abstract class BaseNlpHelper< export default abstract class BaseNlpHelper<
N extends string, N extends HelperName = HelperName,
> extends BaseHelper<N> { > extends BaseHelper<N> {
protected readonly type: HelperType = HelperType.NLU; protected readonly type: HelperType = HelperType.NLU;
constructor( constructor(
name: N, name: N,
settings: HelperSetting<N>[],
settingService: SettingService, settingService: SettingService,
helperService: HelperService, helperService: HelperService,
logger: LoggerService, logger: LoggerService,
) { ) {
super(name, settings, settingService, helperService, logger); super(name, settingService, helperService, logger);
} }
/** /**

View File

@ -29,10 +29,12 @@ export enum HelperType {
UTIL = 'util', UTIL = 'util',
} }
export type HelperName = `${string}-helper`;
export type TypeOfHelper<T extends HelperType> = T extends HelperType.LLM export type TypeOfHelper<T extends HelperType> = T extends HelperType.LLM
? BaseLlmHelper<string> ? BaseLlmHelper<HelperName>
: T extends HelperType.NLU : T extends HelperType.NLU
? BaseNlpHelper<string> ? BaseNlpHelper<HelperName>
: BaseHelper; : BaseHelper;
export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map< export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
@ -40,7 +42,7 @@ export type HelperRegistry<H extends BaseHelper = BaseHelper> = Map<
Map<string, H> Map<string, H>
>; >;
export type HelperSetting<N extends string = string> = Omit< export type HelperSetting<N extends HelperName = HelperName> = Omit<
SettingCreateDto, SettingCreateDto,
'group' | 'weight' 'group' | 'weight'
> & { > & {

View File

@ -8,14 +8,19 @@
import { Controller, Get, UseInterceptors } from '@nestjs/common'; 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 { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
import { PluginService } from '@/plugins/plugins.service';
import { I18nService } from '../services/i18n.service';
@UseInterceptors(CsrfInterceptor) @UseInterceptors(CsrfInterceptor)
@Controller('i18n') @Controller('i18n')
export class I18nController { 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. * Retrieves translations of all the installed extensions.
@ -23,6 +28,12 @@ export class I18nController {
*/ */
@Get() @Get()
getTranslations() { 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;
}, {});
} }
} }

View File

@ -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). * 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 { Injectable } from '@nestjs/common';
import * as path from 'path';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { import {
I18nJsonLoader,
I18nTranslation,
I18nService as NativeI18nService, I18nService as NativeI18nService,
Path, Path,
PathValue, PathValue,
@ -22,25 +17,13 @@ import { IfAnyOrNever } from 'nestjs-i18n/dist/types';
import { config } from '@/config'; import { config } from '@/config';
import { Translation } from '@/i18n/schemas/translation.schema'; import { Translation } from '@/i18n/schemas/translation.schema';
import { hyphenToUnderscore } from '@/utils/helpers/misc';
@Injectable() @Injectable()
export class I18nService<K = Record<string, unknown>> export class I18nService<
extends NativeI18nService<K> K = Record<string, unknown>,
implements OnModuleInit > extends NativeI18nService<K> {
{
private dynamicTranslations: Record<string, Record<string, string>> = {}; 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>>( t<P extends Path<K> = any, R = PathValue<K, P>>(
key: P, key: P,
options?: TranslateOptions, options?: TranslateOptions,
@ -83,52 +66,4 @@ export class I18nService<K = Record<string, unknown>>
return acc; return acc;
}, this.dynamicTranslations); }, 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}`);
}
}
} }

View File

@ -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). * 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 { Injectable } from '@nestjs/common';
import { Block, BlockFull } from '@/chat/schemas/block.schema'; import { Block, BlockFull } from '@/chat/schemas/block.schema';
@ -17,6 +19,7 @@ import { PluginService } from './plugins.service';
import { import {
PluginBlockTemplate, PluginBlockTemplate,
PluginEffects, PluginEffects,
PluginName,
PluginSetting, PluginSetting,
PluginType, PluginType,
} from './types'; } from './types';
@ -29,16 +32,13 @@ export abstract class BaseBlockPlugin<
public readonly settings: T; public readonly settings: T;
constructor( constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
id: string, super(name, pluginService);
settings: T, // eslint-disable-next-line @typescript-eslint/no-var-requires
pluginService: PluginService<BasePlugin>, this.settings = require(path.join(this.getPath(), 'settings')).default;
) {
super(id, pluginService);
this.settings = settings;
} }
template: PluginBlockTemplate; abstract template: PluginBlockTemplate;
effects?: PluginEffects; effects?: PluginEffects;

View File

@ -10,13 +10,13 @@ import { Injectable } from '@nestjs/common';
import { BasePlugin } from './base-plugin.service'; import { BasePlugin } from './base-plugin.service';
import { PluginService } from './plugins.service'; import { PluginService } from './plugins.service';
import { PluginType } from './types'; import { PluginName, PluginType } from './types';
@Injectable() @Injectable()
export abstract class BaseEventPlugin extends BasePlugin { export abstract class BaseEventPlugin extends BasePlugin {
public readonly type: PluginType = PluginType.event; public readonly type: PluginType = PluginType.event;
constructor(id: string, pluginService: PluginService<BasePlugin>) { constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
super(id, pluginService); super(name, pluginService);
} }
} }

View File

@ -8,19 +8,24 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { Extension } from '@/utils/generics/extension';
import { PluginService } from './plugins.service'; import { PluginService } from './plugins.service';
import { PluginType } from './types'; import { PluginName, PluginType } from './types';
@Injectable() @Injectable()
export abstract class BasePlugin implements OnModuleInit { export abstract class BasePlugin extends Extension implements OnModuleInit {
public readonly type: PluginType; public readonly type: PluginType;
constructor( constructor(
public readonly id: string, public readonly name: PluginName,
private pluginService: PluginService<BasePlugin>, private pluginService: PluginService<BasePlugin>,
) {} ) {
super(name);
}
onModuleInit() { async onModuleInit() {
this.pluginService.setPlugin(this.type, this.id, this); await super.onModuleInit();
this.pluginService.setPlugin(this.type, this.name, this);
} }
} }

View File

@ -13,14 +13,14 @@ import { Attachment } from '@/attachment/schemas/attachment.schema';
import { BasePlugin } from './base-plugin.service'; import { BasePlugin } from './base-plugin.service';
import { PluginService } from './plugins.service'; import { PluginService } from './plugins.service';
import { PluginType } from './types'; import { PluginName, PluginType } from './types';
@Injectable() @Injectable()
export abstract class BaseStoragePlugin extends BasePlugin { export abstract class BaseStoragePlugin extends BasePlugin {
public readonly type: PluginType = PluginType.storage; public readonly type: PluginType = PluginType.storage;
constructor(id: string, pluginService: PluginService<BasePlugin>) { constructor(name: PluginName, pluginService: PluginService<BasePlugin>) {
super(id, pluginService); super(name, pluginService);
} }
abstract fileExists(attachment: Attachment): Promise<boolean>; abstract fileExists(attachment: Attachment): Promise<boolean>;

View File

@ -20,7 +20,12 @@ import { ContentModel } from '@/cms/schemas/content.schema';
import { PluginService } from './plugins.service'; 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() @Global()
@Module({ @Module({
imports: [ imports: [

View File

@ -23,7 +23,7 @@ describe('PluginsService', () => {
imports: [LoggerModule], imports: [LoggerModule],
}).compile(); }).compile();
pluginsService = module.get<PluginService>(PluginService); pluginsService = module.get<PluginService>(PluginService);
module.get<DummyPlugin>(DummyPlugin).onModuleInit(); await module.get<DummyPlugin>(DummyPlugin).onModuleInit();
}); });
afterAll(async () => { afterAll(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
@ -37,7 +37,7 @@ describe('PluginsService', () => {
describe('getPlugin', () => { describe('getPlugin', () => {
it('should return the required plugin', () => { 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); expect(result).toBeInstanceOf(DummyPlugin);
}); });
}); });

View File

@ -10,7 +10,7 @@ import { Injectable } from '@nestjs/common';
import { BasePlugin } from './base-plugin.service'; import { BasePlugin } from './base-plugin.service';
import { PluginInstance } from './map-types'; import { PluginInstance } from './map-types';
import { PluginType } from './types'; import { PluginName, PluginType } from './types';
/** /**
* @summary Service for managing and retrieving plugins. * @summary Service for managing and retrieving plugins.
@ -38,18 +38,18 @@ export class PluginService<T extends BasePlugin = BasePlugin> {
constructor() {} 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. * @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); 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. * @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>[]; 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. * 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. * @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 registry = this.registry.get(type);
const plugin = registry.get(id); const plugin = registry.get(name);
return plugin ? (plugin as PluginInstance<PT>) : undefined; return plugin ? (plugin as PluginInstance<PT>) : undefined;
} }
/** /**
* Finds a plugin by its internal `id` property. * 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. * @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 this.getAllByType(type).find((plugin) => {
return plugin.id === pluginId; return plugin.name === name;
}); });
} }
} }

View File

@ -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). * 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 { BlockCreateDto } from '@/chat/dto/block.dto';
import { Block } from '@/chat/schemas/block.schema'; import { Block } from '@/chat/schemas/block.schema';
import { Conversation } from '@/chat/schemas/conversation.schema'; import { Conversation } from '@/chat/schemas/conversation.schema';
import { SettingCreateDto } from '@/setting/dto/setting.dto'; import { SettingCreateDto } from '@/setting/dto/setting.dto';
export type PluginName = `${string}-plugin`;
export enum PluginType { export enum PluginType {
event = 'event', event = 'event',
block = 'block', block = 'block',
@ -19,7 +22,6 @@ export enum PluginType {
export interface CustomBlocks {} export interface CustomBlocks {}
type ChannelEvent = any;
type BlockAttrs = Partial<BlockCreateDto> & { name: string }; type BlockAttrs = Partial<BlockCreateDto> & { name: string };
export type PluginSetting = Omit<SettingCreateDto, 'weight'>; export type PluginSetting = Omit<SettingCreateDto, 'weight'>;

View File

@ -13,7 +13,7 @@ export const DEFAULT_SETTINGS = [
{ {
group: 'chatbot_settings', group: 'chatbot_settings',
label: 'default_nlu_helper', label: 'default_nlu_helper',
value: 'core-nlu', value: 'core-nlu-helper',
type: SettingType.select, type: SettingType.select,
config: { config: {
multiple: false, multiple: false,
@ -27,7 +27,7 @@ export const DEFAULT_SETTINGS = [
{ {
group: 'chatbot_settings', group: 'chatbot_settings',
label: 'default_llm_helper', label: 'default_llm_helper',
value: 'ollama', value: 'ollama-helper',
type: SettingType.select, type: SettingType.select,
config: { config: {
multiple: false, multiple: false,

View File

@ -0,0 +1,43 @@
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;
}
}

View File

@ -15,23 +15,27 @@ import {
import { LoggerService } from '@/logger/logger.service'; import { LoggerService } from '@/logger/logger.service';
import { BaseBlockPlugin } from '@/plugins/base-block-plugin'; import { BaseBlockPlugin } from '@/plugins/base-block-plugin';
import { PluginService } from '@/plugins/plugins.service'; import { PluginService } from '@/plugins/plugins.service';
import { PluginSetting } from '@/plugins/types'; import { PluginBlockTemplate, PluginSetting } from '@/plugins/types';
@Injectable() @Injectable()
export class DummyPlugin extends BaseBlockPlugin<PluginSetting[]> { export class DummyPlugin extends BaseBlockPlugin<PluginSetting[]> {
template: PluginBlockTemplate = { name: 'Dummy Plugin' };
constructor( constructor(
pluginService: PluginService, pluginService: PluginService,
private logger: LoggerService, private logger: LoggerService,
) { ) {
super('dummy', [], pluginService); super('dummy-plugin', pluginService);
this.template = { name: 'Dummy Plugin' };
this.effects = { this.effects = {
onStoreContextData: () => {}, onStoreContextData: () => {},
}; };
} }
getPath(): string {
return __dirname;
}
async process() { async process() {
const envelope: StdOutgoingTextEnvelope = { const envelope: StdOutgoingTextEnvelope = {
format: OutgoingMessageFormat.text, format: OutgoingMessageFormat.text,

View File

@ -0,0 +1 @@
export default [];

View File

@ -10,8 +10,8 @@ import mongoose from 'mongoose';
import { ConversationCreateDto } from '@/chat/dto/conversation.dto'; import { ConversationCreateDto } from '@/chat/dto/conversation.dto';
import { import {
ConversationModel,
Conversation, Conversation,
ConversationModel,
} from '@/chat/schemas/conversation.schema'; } from '@/chat/schemas/conversation.schema';
import { getFixturesWithDefaultValues } from '../defaultValues'; import { getFixturesWithDefaultValues } from '../defaultValues';
@ -25,7 +25,7 @@ const conversations: ConversationCreateDto[] = [
sender: '0', sender: '0',
active: true, active: true,
context: { context: {
channel: 'messenger', channel: 'messenger-channel',
text: 'Hi', text: 'Hi',
payload: '', payload: '',
nlp: { nlp: {
@ -60,7 +60,7 @@ const conversations: ConversationCreateDto[] = [
foreign_id: '', foreign_id: '',
labels: [], labels: [],
assignedTo: null, assignedTo: null,
channel: { name: 'messenger' }, channel: { name: 'messenger-channel' },
}, },
skip: {}, skip: {},
attempt: 0, attempt: 0,
@ -71,7 +71,7 @@ const conversations: ConversationCreateDto[] = [
{ {
sender: '1', sender: '1',
context: { context: {
channel: 'offline', channel: 'offline-channel',
text: 'Hello', text: 'Hello',
payload: '', payload: '',
nlp: { nlp: {
@ -106,7 +106,7 @@ const conversations: ConversationCreateDto[] = [
foreign_id: '', foreign_id: '',
labels: [], labels: [],
assignedTo: null, assignedTo: null,
channel: { name: 'offline' }, channel: { name: 'offline-channel' },
}, },
skip: {}, skip: {},
attempt: 0, attempt: 0,

View File

@ -27,7 +27,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male', gender: 'male',
country: 'FR', country: 'FR',
channel: { channel: {
name: 'messenger', name: 'messenger-channel',
}, },
labels: [], labels: [],
assignedAt: null, assignedAt: null,
@ -43,7 +43,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male', gender: 'male',
country: 'US', country: 'US',
channel: { channel: {
name: 'offline', name: 'offline-channel',
}, },
labels: [], labels: [],
assignedAt: null, assignedAt: null,
@ -59,7 +59,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male', gender: 'male',
country: 'US', country: 'US',
channel: { channel: {
name: 'offline', name: 'offline-channel',
}, },
labels: [], labels: [],
assignedAt: null, assignedAt: null,
@ -75,7 +75,7 @@ const subscribers: SubscriberCreateDto[] = [
gender: 'male', gender: 'male',
country: 'US', country: 'US',
channel: { channel: {
name: 'offline', name: 'offline-channel',
}, },
labels: [], labels: [],
assignedAt: null, assignedAt: null,

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import { ChannelName } from '@/channel/types';
import { HelperName } from '@/helper/types';
import { PluginName } from '@/plugins/types';
export type ExtensionName = ChannelName | HelperName | PluginName;

View File

@ -23,5 +23,11 @@
"@/*": ["src/*"] "@/*": ["src/*"]
} }
}, },
"include": ["src/**/*.ts", "src/**/*.json", "test/**/*.ts"] "include": [
"src/**/*.ts",
"src/**/*.json",
"test/**/*.ts",
"src/.hexabot/**/*.ts",
"src/.hexabot/**/*.json"
]
} }

View File

@ -33,7 +33,7 @@ export const ChatWidget = () => {
<UiChatWidget <UiChatWidget
config={{ config={{
apiUrl, apiUrl,
channel: "live-chat-tester", channel: "live-chat-tester-channel",
token: "test", token: "test",
language: i18n.language, language: i18n.language,
}} }}

View File

@ -31,7 +31,9 @@ export const CustomBlocks = () => {
{customBlocks?.map((customBlock) => ( {customBlocks?.map((customBlock) => (
<Block <Block
key={customBlock.id} key={customBlock.id}
title={t(`title.${customBlock.id}`, { ns: customBlock.id })} title={t(`title.${customBlock.namespace}`, {
ns: customBlock.namespace,
})}
Icon={PluginIcon} Icon={PluginIcon}
blockTemplate={customBlock.template} blockTemplate={customBlock.template}
name={customBlock.template.name} name={customBlock.template.name}

View File

@ -63,7 +63,8 @@ const PluginMessageForm = () => {
<SettingInput <SettingInput
setting={setting} setting={setting}
field={field} field={field}
ns={message.plugin} // @TODO : clean this later
ns={message.plugin.replaceAll("-", "_")}
/> />
</FormControl> </FormControl>
)} )}

View File

@ -22,14 +22,13 @@ export const useRemoteI18n = () => {
const fetchRemoteI18n = async () => { const fetchRemoteI18n = async () => {
try { try {
const additionalTranslations = await apiClient.fetchRemoteI18n(); const additionalTranslations = await apiClient.fetchRemoteI18n();
// Assuming additionalTranslations is an object like { en: { translation: { key: 'value' } } }
Object.keys(additionalTranslations).forEach((lang) => { Object.keys(additionalTranslations).forEach((namespace) => {
Object.keys(additionalTranslations[lang]).forEach((namespace) => { Object.keys(additionalTranslations[namespace]).forEach((lang) => {
i18n.addResourceBundle( i18n.addResourceBundle(
lang, lang,
namespace, namespace,
additionalTranslations[lang][namespace], additionalTranslations[namespace][lang],
true, true,
true, true,
); );

View File

@ -139,4 +139,6 @@ export interface ICustomBlockTemplateAttributes {
// @TODO : templates doe not contain base schema attributes // @TODO : templates doe not contain base schema attributes
export interface ICustomBlockTemplate export interface ICustomBlockTemplate
extends IBaseSchema, extends IBaseSchema,
OmitPopulate<ICustomBlockTemplateAttributes, EntityType.CUSTOM_BLOCK> {} OmitPopulate<ICustomBlockTemplateAttributes, EntityType.CUSTOM_BLOCK> {
namespace: string;
}

View File

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

View File

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

View File

@ -8,7 +8,7 @@
export const DEFAULT_CONFIG = { export const DEFAULT_CONFIG = {
apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000', apiUrl: process.env.REACT_APP_WIDGET_API_URL || 'http://localhost:4000',
channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester', channel: process.env.REACT_APP_WIDGET_CHANNEL || 'live-chat-tester-channel',
token: process.env.REACT_APP_WIDGET_TOKEN || 'test', token: process.env.REACT_APP_WIDGET_TOKEN || 'test',
language: 'en', language: 'en',
}; };

View File

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