Merge branch '634-issue---logout-is-not-shared-cross-tabs' into 612-disconnect-from-ws-on-logout

This commit is contained in:
yassinedorbozgithub 2025-01-29 08:18:20 +01:00
commit 559fcb2ef6
26 changed files with 167 additions and 163 deletions

View File

@ -4,6 +4,9 @@
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true, "deleteOutDir": true,
"assets": [{ "include": "config/i18n/**/*", "watchAssets": true }] "assets": [
{ "include": "config/i18n/**/*", "watchAssets": true },
{ "include": "templates/**/*.mjml", "watchAssets": true }
]
} }
} }

4
api/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "hexabot", "name": "hexabot",
"version": "2.2.2", "version": "2.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hexabot", "name": "hexabot",
"version": "2.2.2", "version": "2.2.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "hexabot", "name": "hexabot",
"version": "2.2.2", "version": "2.2.3",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",

View File

@ -1,3 +1,30 @@
diff --git a/node_modules/@nestjs-modules/mailer/dist/adapters/mjml.adapter.js b/node_modules/@nestjs-modules/mailer/dist/adapters/mjml.adapter.js
index 5eb7a4a..afaaaa9 100644
--- a/node_modules/@nestjs-modules/mailer/dist/adapters/mjml.adapter.js
+++ b/node_modules/@nestjs-modules/mailer/dist/adapters/mjml.adapter.js
@@ -26,8 +26,20 @@ class MjmlAdapter {
compile(mail, callback, mailerOptions) {
var _a;
(_a = this === null || this === void 0 ? void 0 : this.engine) === null || _a === void 0 ? void 0 : _a.compile(mail, () => {
- mail.data.html = mjml2html(mail.data.html).html;
- callback();
+ Promise.resolve(mail.data.html).then((html) => {
+ Promise.resolve(mjml2html(html)).then((result) => {
+ mail.data.html = result.html;
+ callback();
+ }).catch((err) => {
+ console.error('@nestjs-modules/mailer: Unable to convert mjml to html', err)
+ mail.data.html = '';
+ callback();
+ })
+ }).catch((err) => {
+ console.error('@nestjs-modules/mailer: Unable to compiling mjml', err)
+ mail.data.html = '';
+ callback();
+ })
}, mailerOptions);
}
}
diff --git a/node_modules/@nestjs-modules/mailer/dist/mailer.service.js b/node_modules/@nestjs-modules/mailer/dist/mailer.service.js diff --git a/node_modules/@nestjs-modules/mailer/dist/mailer.service.js b/node_modules/@nestjs-modules/mailer/dist/mailer.service.js
index 016055b..d534240 100644 index 016055b..d534240 100644
--- a/node_modules/@nestjs-modules/mailer/dist/mailer.service.js --- a/node_modules/@nestjs-modules/mailer/dist/mailer.service.js

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -127,10 +127,10 @@ export class BotStatsService extends BaseService<BotStats> {
try { try {
await this.updateOne(insight.id, { value: insight.value + 1 }); await this.updateOne(insight.id, { value: insight.value + 1 });
} catch (err) { } catch (err) {
this.logger.error('Stats hook : Unable to update insight', err); this.logger.error('Unable to update insight', err);
} }
} catch (err) { } catch (err) {
this.logger.error('Stats hook : Unable to find or create insight', err); this.logger.error('Unable to find or create insight', err);
} }
} }
} }

View File

@ -72,7 +72,7 @@ const i18nOptions: I18nOptions = {
}), }),
template: { template: {
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }), adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
dir: './src/templates', dir: path.join(process.cwd(), 'dist', 'templates'),
options: { options: {
context: { context: {
appName: config.parameters.appName, appName: config.parameters.appName,

View File

@ -108,7 +108,7 @@ describe('AttachmentController', () => {
helperService = module.get<HelperService>(HelperService); helperService = module.get<HelperService>(HelperService);
settingService = module.get<SettingService>(SettingService); settingService = module.get<SettingService>(SettingService);
loggerService = module.get<LoggerService>(LoggerService); loggerService = await module.resolve<LoggerService>(LoggerService);
helperService.register( helperService.register(
new LocalStorageHelper(settingService, helperService, loggerService), new LocalStorageHelper(settingService, helperService, loggerService),

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -173,7 +173,7 @@ export class MessageController extends BaseController<
success: true, success: true,
}; };
} catch (err) { } catch (err) {
this.logger.debug('MessageController send : Unable to send message', err); this.logger.debug('Unable to send message', err);
throw new BadRequestException( throw new BadRequestException(
'MessageController send : unable to send message', 'MessageController send : unable to send message',
); );

View File

@ -286,7 +286,7 @@ export class BlockService extends BaseService<
return e.entity === ev.entity; return e.entity === ev.entity;
}); });
} else { } else {
this.logger.warn('Block Service : Unknown NLP match type', ev); this.logger.warn('Unknown NLP match type', ev);
return false; return false;
} }
}); });

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -62,10 +62,7 @@ export class BotService {
context = context || getDefaultConversationContext(); context = context || getDefaultConversationContext();
fallback = typeof fallback !== 'undefined' ? fallback : false; fallback = typeof fallback !== 'undefined' ? fallback : false;
const options = block.options; const options = block.options;
this.logger.debug( this.logger.debug('Sending message ... ', event.getSenderForeignId());
'Bot service : Sending message ... ',
event.getSenderForeignId(),
);
// Process message : Replace tokens with context data and then send the message // Process message : Replace tokens with context data and then send the message
const recipient = event.getSender(); const recipient = event.getSender();
const envelope = await this.blockService.processMessage( const envelope = await this.blockService.processMessage(
@ -116,7 +113,7 @@ export class BotService {
assignTo, assignTo,
); );
this.logger.debug('Bot service : Assigned labels ', blockLabels); this.logger.debug('Assigned labels ', blockLabels);
return response; return response;
} }
@ -375,7 +372,7 @@ export class BotService {
); );
this.logger.debug( this.logger.debug(
'Bot service : Started a new conversation with ', 'Started a new conversation with ',
subscriber.id, subscriber.id,
block.name, block.name,
); );
@ -386,14 +383,11 @@ export class BotService {
false, false,
); );
} catch (err) { } catch (err) {
this.logger.error('Bot service : Unable to store context data!', err); this.logger.error('Unable to store context data!', err);
this.eventEmitter.emit('hook:conversation:end', convo, true); this.eventEmitter.emit('hook:conversation:end', convo, true);
} }
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Unable to start a new conversation with ', err);
'Botservice : Unable to start a new conversation with ',
err,
);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -65,10 +65,7 @@ export class MessageService extends BaseService<
success: true, success: true,
}); });
} catch (e) { } catch (e) {
this.logger.error( this.logger.error('Websocket subscription', e);
'MessageController subscribe : Websocket subscription',
e,
);
throw new InternalServerErrorException(e); throw new InternalServerErrorException(e);
} }
} }

View File

@ -72,9 +72,7 @@ export class SubscriberService extends BaseService<
subscribe: Room.SUBSCRIBER, subscribe: Room.SUBSCRIBER,
}); });
} catch (e) { } catch (e) {
this.logger.error( this.logger.error('Websocket subscription');
'SubscriberController subscribe : Websocket subscription',
);
throw new InternalServerErrorException(e); throw new InternalServerErrorException(e);
} }
} }

View File

@ -89,7 +89,7 @@ export default abstract class BaseWebChannelHandler<
* @returns - * @returns -
*/ */
init(): void { init(): void {
this.logger.debug('Web Channel Handler : initialization ...'); this.logger.debug('initialization ...');
} }
/** /**
@ -108,22 +108,17 @@ export default abstract class BaseWebChannelHandler<
return; return;
} }
this.logger.debug( this.logger.debug('WS connected .. sending settings');
'Web Channel Handler : WS connected .. sending settings',
);
try { try {
const menu = await this.menuService.getTree(); const menu = await this.menuService.getTree();
return client.emit('settings', { menu, ...settings }); return client.emit('settings', { menu, ...settings });
} catch (err) { } catch (err) {
this.logger.warn('Web Channel Handler : Unable to retrieve menu ', err); this.logger.warn('Unable to retrieve menu ', err);
return client.emit('settings', settings); return client.emit('settings', settings);
} }
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Unable to initiate websocket connection', err);
'Web Channel Handler : Unable to initiate websocket connection',
err,
);
client.disconnect(); client.disconnect();
} }
} }
@ -315,7 +310,7 @@ export default abstract class BaseWebChannelHandler<
// Check if we have an origin header... // Check if we have an origin header...
if (!req.headers?.origin) { if (!req.headers?.origin) {
this.logger.debug('Web Channel Handler : No origin ', req.headers); this.logger.debug('No origin ', req.headers);
throw new Error('CORS - No origin provided!'); throw new Error('CORS - No origin provided!');
} }
@ -332,10 +327,7 @@ export default abstract class BaseWebChannelHandler<
try { try {
return new URL(origin.trim()).origin; return new URL(origin.trim()).origin;
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(`Invalid URL in allowed domains: ${origin}`, error);
`Web Channel Handler : Invalid URL in allowed domains: ${origin}`,
error,
);
return null; return null;
} }
}) })
@ -353,10 +345,7 @@ export default abstract class BaseWebChannelHandler<
// For HTTP requests, set the Access-Control-Allow-Origin header to '', which the browser will // For HTTP requests, set the Access-Control-Allow-Origin header to '', which the browser will
// interpret as, 'no way Jose.' // interpret as, 'no way Jose.'
res.set('Access-Control-Allow-Origin', ''); res.set('Access-Control-Allow-Origin', '');
this.logger.debug( this.logger.debug('No origin found ', req.headers.origin);
'Web Channel Handler : No origin found ',
req.headers.origin,
);
throw new Error('CORS - Domain not allowed!'); throw new Error('CORS - Domain not allowed!');
} else { } else {
res.set('Access-Control-Allow-Origin', originUrl.origin); res.set('Access-Control-Allow-Origin', originUrl.origin);
@ -384,10 +373,7 @@ export default abstract class BaseWebChannelHandler<
next: (profile: Subscriber) => void, next: (profile: Subscriber) => void,
) { ) {
if (!req.session?.web?.profile?.id) { if (!req.session?.web?.profile?.id) {
this.logger.warn( this.logger.warn('No session ID to be found!', req.session);
'Web Channel Handler : No session ID to be found!',
req.session,
);
return res return res
.status(403) .status(403)
.json({ err: 'Web Channel Handler : Unauthorized!' }); .json({ err: 'Web Channel Handler : Unauthorized!' });
@ -397,7 +383,7 @@ export default abstract class BaseWebChannelHandler<
!Array.isArray(req.session.web.messageQueue) !Array.isArray(req.session.web.messageQueue)
) { ) {
this.logger.warn( this.logger.warn(
'Web Channel Handler : Mixed channel request or invalid session data!', 'Mixed channel request or invalid session data!',
req.session, req.session,
); );
return res return res
@ -420,10 +406,7 @@ export default abstract class BaseWebChannelHandler<
try { try {
await this.validateCors(req, res); await this.validateCors(req, res);
} catch (err) { } catch (err) {
this.logger.warn( this.logger.warn('Attempt to access from an unauthorized origin', err);
'Web Channel Handler : Attempt to access from an unauthorized origin',
err,
);
throw new Error('Unauthorized, invalid origin !'); throw new Error('Unauthorized, invalid origin !');
} }
} }
@ -498,18 +481,14 @@ export default abstract class BaseWebChannelHandler<
private getMessageQueue(req: Request, res: Response) { private getMessageQueue(req: Request, res: Response) {
// Polling not authorized when using websockets // Polling not authorized when using websockets
if (this.isSocketRequest(req)) { if (this.isSocketRequest(req)) {
this.logger.warn( this.logger.warn('Polling not authorized when using websockets');
'Web Channel Handler : Polling not authorized when using websockets',
);
return res return res
.status(403) .status(403)
.json({ err: 'Polling not authorized when using websockets' }); .json({ err: 'Polling not authorized when using websockets' });
} }
// Session must be active // Session must be active
if (!(req.session && req.session.web && req.session.web.profile.id)) { if (!(req.session && req.session.web && req.session.web.profile.id)) {
this.logger.warn( this.logger.warn('Must be connected to poll messages');
'Web Channel Handler : Must be connected to poll messages',
);
return res return res
.status(403) .status(403)
.json({ err: 'Polling not authorized : Must be connected' }); .json({ err: 'Polling not authorized : Must be connected' });
@ -517,9 +496,7 @@ export default abstract class BaseWebChannelHandler<
// Can only request polling once at a time // Can only request polling once at a time
if (req.session && req.session.web && req.session.web.polling) { if (req.session && req.session.web && req.session.web.polling) {
this.logger.warn( this.logger.warn('Poll rejected ... already requested');
'Web Channel Handler : Poll rejected ... already requested',
);
return res return res
.status(403) .status(403)
.json({ err: 'Poll rejected ... already requested' }); .json({ err: 'Poll rejected ... already requested' });
@ -543,16 +520,14 @@ export default abstract class BaseWebChannelHandler<
req.session.web.polling = false; req.session.web.polling = false;
return res.status(200).json(messages.map((msg) => ['message', msg])); return res.status(200).json(messages.map((msg) => ['message', msg]));
} else { } else {
this.logger.error( this.logger.error('Polling failed .. no session data');
'Web Channel Handler : Polling failed .. no session data',
);
return res.status(500).json({ err: 'No session data' }); return res.status(500).json({ err: 'No session data' });
} }
} catch (err) { } catch (err) {
if (req.session.web) { if (req.session.web) {
req.session.web.polling = false; req.session.web.polling = false;
} }
this.logger.error('Web Channel Handler : Polling failed', err); this.logger.error('Polling failed', err);
return res.status(500).json({ err: 'Polling failed' }); return res.status(500).json({ err: 'Polling failed' });
} }
}; };
@ -569,11 +544,7 @@ export default abstract class BaseWebChannelHandler<
req: Request | SocketRequest, req: Request | SocketRequest,
res: Response | SocketResponse, res: Response | SocketResponse,
) { ) {
this.logger.debug( this.logger.debug('subscribe (isSocket=' + this.isSocketRequest(req) + ')');
'Web Channel Handler : subscribe (isSocket=' +
this.isSocketRequest(req) +
')',
);
try { try {
const profile = await this.getOrCreateSession(req); const profile = await this.getOrCreateSession(req);
// Join socket room when using websocket // Join socket room when using websocket
@ -581,10 +552,7 @@ export default abstract class BaseWebChannelHandler<
try { try {
await req.socket.join(profile.foreign_id); await req.socket.join(profile.foreign_id);
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Unable to subscribe via websocket', err);
'Web Channel Handler : Unable to subscribe via websocket',
err,
);
} }
} }
// Fetch message history // Fetch message history
@ -595,7 +563,7 @@ export default abstract class BaseWebChannelHandler<
const messages = await this.fetchHistory(req, criteria); const messages = await this.fetchHistory(req, criteria);
return res.status(200).json({ profile, messages }); return res.status(200).json({ profile, messages });
} catch (err) { } catch (err) {
this.logger.warn('Web Channel Handler : Unable to subscribe ', err); this.logger.warn('Unable to subscribe ', err);
return res.status(500).json({ err: 'Unable to subscribe' }); return res.status(500).json({ err: 'Unable to subscribe' });
} }
} }
@ -610,13 +578,13 @@ export default abstract class BaseWebChannelHandler<
const { type, data } = req.body as Web.IncomingMessage; const { type, data } = req.body as Web.IncomingMessage;
if (!req.session?.web?.profile?.id) { if (!req.session?.web?.profile?.id) {
this.logger.debug('Web Channel Handler : No session'); this.logger.debug('No session');
return null; return null;
} }
// Check if any file is provided // Check if any file is provided
if (type !== 'file' || !('file' in data) || !data.file) { if (type !== 'file' || !('file' in data) || !data.file) {
this.logger.debug('Web Channel Handler : No files provided'); this.logger.debug('No files provided');
return null; return null;
} }
@ -636,10 +604,7 @@ export default abstract class BaseWebChannelHandler<
createdBy: req.session?.web?.profile?.id, createdBy: req.session?.web?.profile?.id,
}); });
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Unable to store uploaded file', err);
'Web Channel Handler : Unable to store uploaded file',
err,
);
throw new Error('Unable to upload file!'); throw new Error('Unable to upload file!');
} }
} }
@ -671,10 +636,7 @@ export default abstract class BaseWebChannelHandler<
(resolve, reject) => { (resolve, reject) => {
upload(req as Request, res as Response, async (err?: any) => { upload(req as Request, res as Response, async (err?: any) => {
if (err) { if (err) {
this.logger.error( this.logger.error('Unable to store uploaded file', err);
'Web Channel Handler : Unable to store uploaded file',
err,
);
reject(new Error('Unable to upload file!')); reject(new Error('Unable to upload file!'));
} }
@ -689,7 +651,7 @@ export default abstract class BaseWebChannelHandler<
// Check if any file is provided // Check if any file is provided
if (!file) { if (!file) {
this.logger.debug('Web Channel Handler : No files provided'); this.logger.debug('No files provided');
return null; return null;
} }
@ -703,10 +665,7 @@ export default abstract class BaseWebChannelHandler<
createdBy: req.session.web.profile?.id, createdBy: req.session.web.profile?.id,
}); });
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Unable to store uploaded file', err);
'Web Channel Handler : Unable to store uploaded file',
err,
);
throw err; throw err;
} }
} }
@ -724,7 +683,7 @@ export default abstract class BaseWebChannelHandler<
): Promise<Attachment | null | undefined> { ): Promise<Attachment | null | undefined> {
// Check if any file is provided // Check if any file is provided
if (!req.session.web) { if (!req.session.web) {
this.logger.debug('Web Channel Handler : No session provided'); this.logger.debug('No session provided');
return null; return null;
} }
@ -784,7 +743,7 @@ export default abstract class BaseWebChannelHandler<
): void { ): void {
// @TODO: perform payload validation // @TODO: perform payload validation
if (!req.body) { if (!req.body) {
this.logger.debug('Web Channel Handler : Empty body'); this.logger.debug('Empty body');
res.status(400).json({ err: 'Web Channel Handler : Bad Request!' }); res.status(400).json({ err: 'Web Channel Handler : Bad Request!' });
return; return;
} else { } else {
@ -814,10 +773,7 @@ export default abstract class BaseWebChannelHandler<
}; };
} }
} catch (err) { } catch (err) {
this.logger.warn( this.logger.warn('Unable to upload file ', err);
'Web Channel Handler : Unable to upload file ',
err,
);
return res return res
.status(403) .status(403)
.json({ err: 'Web Channel Handler : File upload failed!' }); .json({ err: 'Web Channel Handler : File upload failed!' });
@ -849,10 +805,7 @@ export default abstract class BaseWebChannelHandler<
if (type) { if (type) {
this.eventEmitter.emit(`hook:chatbot:${type}`, event); this.eventEmitter.emit(`hook:chatbot:${type}`, event);
} else { } else {
this.logger.error( this.logger.error('Webhook received unknown event ', event);
'Web Channel Handler : Webhook received unknown event ',
event,
);
} }
res.status(200).json(event._adapter.raw); res.status(200).json(event._adapter.raw);
}); });
@ -883,9 +836,7 @@ export default abstract class BaseWebChannelHandler<
if (!this.isSocketRequest(req) && req.query._get) { if (!this.isSocketRequest(req) && req.query._get) {
switch (req.query._get) { switch (req.query._get) {
case 'settings': case 'settings':
this.logger.debug( this.logger.debug('connected .. sending settings');
'Web Channel Handler : connected .. sending settings',
);
try { try {
const menu = await this.menuService.getTree(); const menu = await this.menuService.getTree();
return res.status(200).json({ return res.status(200).json({
@ -894,19 +845,14 @@ export default abstract class BaseWebChannelHandler<
...settings, ...settings,
}); });
} catch (err) { } catch (err) {
this.logger.warn( this.logger.warn('Unable to retrieve menu ', err);
'Web Channel Handler : Unable to retrieve menu ',
err,
);
return res.status(500).json({ err: 'Unable to retrieve menu' }); return res.status(500).json({ err: 'Unable to retrieve menu' });
} }
case 'polling': case 'polling':
// Handle polling when user is not connected via websocket // Handle polling when user is not connected via websocket
return this.getMessageQueue(req, res as Response); return this.getMessageQueue(req, res as Response);
default: default:
this.logger.error( this.logger.error('Webhook received unknown command');
'Web Channel Handler : Webhook received unknown command',
);
return res return res
.status(500) .status(500)
.json({ err: 'Webhook received unknown command' }); .json({ err: 'Webhook received unknown command' });
@ -923,7 +869,7 @@ export default abstract class BaseWebChannelHandler<
return this._handleEvent(req, res); return this._handleEvent(req, res);
} }
} catch (err) { } catch (err) {
this.logger.warn('Web Channel Handler : Request check failed', err); this.logger.warn('Request check failed', err);
return res return res
.status(403) .status(403)
.json({ err: 'Web Channel Handler : Unauthorized!' }); .json({ err: 'Web Channel Handler : Unauthorized!' });
@ -1131,9 +1077,7 @@ export default abstract class BaseWebChannelHandler<
// Items count min check // Items count min check
if (!data.length) { if (!data.length) {
this.logger.error( this.logger.error('Unsufficient content count (must be >= 0 for list)');
'Web Channel Handler : Unsufficient content count (must be >= 0 for list)',
);
throw new Error('Unsufficient content count (list >= 0)'); throw new Error('Unsufficient content count (list >= 0)');
} }
@ -1181,7 +1125,7 @@ export default abstract class BaseWebChannelHandler<
// Items count min check // Items count min check
if (data.length === 0) { if (data.length === 0) {
this.logger.error( this.logger.error(
'Web Channel Handler : Unsufficient content count (must be > 0 for carousel)', 'Unsufficient content count (must be > 0 for carousel)',
); );
throw new Error('Unsufficient content count (carousel > 0)'); throw new Error('Unsufficient content count (carousel > 0)');
} }
@ -1293,10 +1237,7 @@ export default abstract class BaseWebChannelHandler<
await this.sendTypingIndicator(subscriber, timeout); await this.sendTypingIndicator(subscriber, timeout);
return next(); return next();
} catch (err) { } catch (err) {
this.logger.error( this.logger.error('Failed in sending typing indicator ', err);
'Web Channel Handler : Failed in sending typing indicator ',
err,
);
} }
} }

View File

@ -8,8 +8,9 @@
import path from 'path'; import path from 'path';
import { LoggerService, OnModuleInit } from '@nestjs/common'; import { OnModuleInit } from '@nestjs/common';
import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service'; import { SettingService } from '@/setting/services/setting.service';
import { Extension } from '@/utils/generics/extension'; import { Extension } from '@/utils/generics/extension';
import { HyphenToUnderscore } from '@/utils/types/extension'; import { HyphenToUnderscore } from '@/utils/types/extension';

View File

@ -1,11 +1,58 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 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). * 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 { ConsoleLogger } from '@nestjs/common'; import {
ConsoleLogger,
Inject,
Injectable,
LogLevel,
Scope,
} from '@nestjs/common';
import { INQUIRER } from '@nestjs/core';
export class LoggerService extends ConsoleLogger {} @Injectable({ scope: Scope.TRANSIENT })
export class LoggerService extends ConsoleLogger {
constructor(@Inject(INQUIRER) private parentClass: object) {
super(parentClass.constructor.name, {
logLevels: process.env.NODE_ENV?.includes('dev')
? ['log', 'debug', 'error', 'verbose', 'fatal', 'warn']
: ['log', 'warn', 'error'],
});
}
log(message: string, ...args: any[]) {
this.logArguments('log', message, args);
}
error(message: string, ...args: any[]) {
this.logArguments('error', message, args);
}
warn(message: string, ...args: any[]) {
this.logArguments('warn', message, args);
}
debug(message: string, ...args: any[]) {
this.logArguments('debug', message, args);
}
verbose(message: string, ...args: any[]) {
this.logArguments('verbose', message, args);
}
fatal(message: string, ...args: any[]) {
this.logArguments('fatal', message, args);
}
private logArguments(type: LogLevel, message: string, args: any[]) {
super[type](message);
args.forEach((arg) => {
super[type](arg);
});
}
}

View File

@ -101,10 +101,11 @@ async function bootstrap() {
app.useWebSocketAdapter(redisIoAdapter); app.useWebSocketAdapter(redisIoAdapter);
} }
process.on('uncaughtException', (error) => { process.on('uncaughtException', async (error) => {
if (error.stack?.toLowerCase().includes('smtp')) if (error.stack?.toLowerCase().includes('smtp')) {
app.get(LoggerService).error('SMTP error', error.stack); const logger = await app.resolve(LoggerService);
else throw error; logger.error('SMTP error', error.stack);
} else throw error;
}); });
if (!isProduction) { if (!isProduction) {

View File

@ -77,7 +77,7 @@ describe('MigrationService', () => {
}).compile(); }).compile();
service = module.get<MigrationService>(MigrationService); service = module.get<MigrationService>(MigrationService);
loggerService = module.get<LoggerService>(LoggerService); loggerService = await module.resolve<LoggerService>(LoggerService);
metadataService = module.get<MetadataService>(MetadataService); metadataService = module.get<MetadataService>(MetadataService);
}); });

View File

@ -37,7 +37,7 @@ import { UserSeeder } from './user/seeds/user.seed';
import { userModels } from './user/seeds/user.seed-model'; import { userModels } from './user/seeds/user.seed-model';
export async function seedDatabase(app: INestApplicationContext) { export async function seedDatabase(app: INestApplicationContext) {
const logger = app.get(LoggerService); const logger = await app.resolve(LoggerService);
const modelSeeder = app.get(ModelSeeder); const modelSeeder = app.get(ModelSeeder);
const categorySeeder = app.get(CategorySeeder); const categorySeeder = app.get(CategorySeeder);
const contextVarSeeder = app.get(ContextVarSeeder); const contextVarSeeder = app.get(ContextVarSeeder);

View File

@ -2,8 +2,8 @@
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-image width="100px" src="/assets/logo.png"></mj-image> <mj-image width="186px" src="https://hexabot.ai/assets/images/logo.png"></mj-image>
<mj-divider border-color="#048BA8"></mj-divider> <mj-divider border-color="#000"></mj-divider>
<mj-text font-size="16px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('hi') %> <%= first_name %>,</mj-text ><%= t('hi') %> <%= first_name %>,</mj-text
> >
@ -17,7 +17,7 @@
<mj-button <mj-button
href="<%= this.appUrl %>/login/<%= token %>" href="<%= this.appUrl %>/login/<%= token %>"
font-size="16px" font-size="16px"
background-color="#F45E43" background-color="#000"
><%= t('confirm') %></mj-button ><%= t('confirm') %></mj-button
> >
</mj-column> </mj-column>

View File

@ -2,8 +2,8 @@
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-image width="100px" src="/assets/logo.png"></mj-image> <mj-image width="186px" src="https://hexabot.ai/assets/images/logo.png"></mj-image>
<mj-divider border-color="#048BA8"></mj-divider> <mj-divider border-color="#000"></mj-divider>
<mj-text font-size="16px" color="#000" font-family="helvetica"> <mj-text font-size="16px" color="#000" font-family="helvetica">
<%= t('welcome') %>, <%= t('welcome') %>,
</mj-text> </mj-text>
@ -19,7 +19,7 @@
<mj-button <mj-button
href="<%= this.appUrl %>/register/<%= token %>" href="<%= this.appUrl %>/register/<%= token %>"
font-size="16px" font-size="16px"
background-color="#F45E43" background-color="#000"
><%= t('join') %></mj-button ><%= t('join') %></mj-button
> >
</mj-column> </mj-column>

View File

@ -2,8 +2,8 @@
<mj-body> <mj-body>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-image width="100px" src="/assets/logo.png"></mj-image> <mj-image width="186px" src="https://hexabot.ai/assets/images/logo.png"></mj-image>
<mj-divider border-color="#048BA8"></mj-divider> <mj-divider border-color="#000"></mj-divider>
<mj-text font-size="16px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('hi') %> <%= first_name %>,</mj-text ><%= t('hi') %> <%= first_name %>,</mj-text
> >
@ -16,7 +16,7 @@
<mj-button <mj-button
href="<%= this.appUrl %>/reset/<%= token %>" href="<%= this.appUrl %>/reset/<%= token %>"
font-size="16px" font-size="16px"
background-color="#F45E43" background-color="#000"
><%= t('reset') %></mj-button ><%= t('reset') %></mj-button
> >
</mj-column> </mj-column>

View File

@ -1,7 +1,7 @@
{ {
"name": "hexabot-ui", "name": "hexabot-ui",
"private": true, "private": true,
"version": "2.2.2", "version": "2.2.3",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",

View File

@ -41,13 +41,11 @@ export function useBroadcastChannel<T extends BroadcastChannelData = string>(
handleMessage?: (event: MessageEvent) => void, handleMessage?: (event: MessageEvent) => void,
handleMessageError?: (event: MessageEvent) => void, handleMessageError?: (event: MessageEvent) => void,
): (data: T) => void { ): (data: T) => void {
const channelRef = React.useRef<BroadcastChannel | null>(null); const channelRef = React.useRef<BroadcastChannel | null>(
typeof window !== "undefined" && "BroadcastChannel" in window
React.useEffect(() => { ? new BroadcastChannel(channelName + "-channel")
if (typeof window !== "undefined" && "BroadcastChannel" in window) { : null,
channelRef.current = new BroadcastChannel(channelName + "-channel"); );
}
}, [channelName]);
useChannelEventListener(channelRef.current, "message", handleMessage); useChannelEventListener(channelRef.current, "message", handleMessage);
useChannelEventListener( useChannelEventListener(
@ -56,10 +54,7 @@ export function useBroadcastChannel<T extends BroadcastChannelData = string>(
handleMessageError, handleMessageError,
); );
return React.useCallback( return (data: T) => channelRef.current?.postMessage(data);
(data: T) => channelRef.current?.postMessage(data),
[channelRef.current],
);
} }
/** /**

8
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "hexabot", "name": "hexabot",
"version": "2.2.1", "version": "2.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "hexabot", "name": "hexabot",
"version": "2.2.1", "version": "2.2.2",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"workspaces": [ "workspaces": [
"frontend", "frontend",
@ -45,7 +45,7 @@
}, },
"frontend": { "frontend": {
"name": "hexabot-ui", "name": "hexabot-ui",
"version": "2.2.2", "version": "2.2.3",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@chatscope/chat-ui-kit-react": "^2.0.3", "@chatscope/chat-ui-kit-react": "^2.0.3",
@ -10304,7 +10304,7 @@
}, },
"widget": { "widget": {
"name": "hexabot-chat-widget", "name": "hexabot-chat-widget",
"version": "2.2.2", "version": "2.2.3",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@types/emoji-js": "^3.5.2", "@types/emoji-js": "^3.5.2",

View File

@ -5,7 +5,7 @@
"frontend", "frontend",
"widget" "widget"
], ],
"version": "2.2.2", "version": "2.2.3",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",

View File

@ -1,6 +1,6 @@
{ {
"name": "hexabot-chat-widget", "name": "hexabot-chat-widget",
"version": "2.2.2", "version": "2.2.3",
"description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.", "description": "Hexabot is a solution for creating and managing chatbots across multiple channels, leveraging AI for advanced conversational capabilities. It provides a user-friendly interface for building, training, and deploying chatbots with integrated support for various messaging platforms.",
"author": "Hexastack", "author": "Hexastack",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",