Merge branch 'main' of https://github.com/Hexastack/Hexabot into 256-request-move-blocks-between-categories

This commit is contained in:
hexastack
2024-11-14 17:22:42 +01:00
57 changed files with 333 additions and 255 deletions

View File

@@ -6,21 +6,13 @@
* 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 {
BadRequestException,
Controller,
Get,
Req,
Res,
Session,
} from '@nestjs/common';
import { Controller, Get, Req, Session } from '@nestjs/common';
import { CsrfCheck, CsrfGenAuth } from '@tekuconcept/nestjs-csrf';
import { CsrfGenerator } from '@tekuconcept/nestjs-csrf/dist/csrf.generator';
import { Request, Response } from 'express';
import { Request } from 'express';
import { Session as ExpressSession } from 'express-session';
import { AppService } from './app.service';
import { config } from './config';
import { LoggerService } from './logger/logger.service';
import { Roles } from './utils/decorators/roles.decorator';
@@ -53,20 +45,6 @@ export class AppController {
@Get('__getcookie')
cookies(@Req() req: Request): string {
req.session.anonymous = true;
return '_sailsIoJSConnect();';
}
// @TODO : remove once old frontend is abandoned
@Get('logout')
logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
res.clearCookie(config.session.name);
req.session.destroy((error) => {
if (error) {
this.logger.error(error);
throw new BadRequestException();
}
});
return { status: 'ok' };
return '';
}
}

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",

View File

@@ -17,12 +17,6 @@ export const CONSOLE_CHANNEL_NAME = 'console-channel';
export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
export default [
{
group: CONSOLE_CHANNEL_NAMESPACE,
label: Web.SettingLabel.verification_token,
value: 'test',
type: SettingType.text,
},
{
group: CONSOLE_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_domains,

View File

@@ -77,7 +77,7 @@ export default abstract class BaseWebChannelHandler<
protected readonly attachmentService: AttachmentService,
protected readonly messageService: MessageService,
protected readonly menuService: MenuService,
private readonly websocketGateway: WebsocketGateway,
protected readonly websocketGateway: WebsocketGateway,
) {
super(name, settingService, channelService, logger);
}
@@ -98,42 +98,32 @@ export default abstract class BaseWebChannelHandler<
*/
@OnEvent('hook:websocket:connection', { async: true })
async onWebSocketConnection(client: Socket) {
const settings = await this.getSettings();
const handshake = client.handshake;
const { channel } = handshake.query;
if (channel !== this.getName()) {
return;
}
try {
const { verification_token } = client.handshake.query;
await this.verifyToken(verification_token.toString());
try {
this.logger.debug(
'Web Channel Handler : WS connected .. sending settings',
);
try {
const menu = await this.menuService.getTree();
return client.emit('settings', { menu, ...settings });
} catch (err) {
this.logger.warn(
'Web Channel Handler : Unable to retrieve menu ',
err,
);
return client.emit('settings', settings);
}
} catch (err) {
this.logger.warn(
'Web Channel Handler : Unable to verify token, disconnecting ...',
err,
);
client.disconnect();
const settings = await this.getSettings();
const handshake = client.handshake;
const { channel } = handshake.query;
if (channel !== this.getName()) {
return;
}
this.logger.debug(
'Web Channel Handler : WS connected .. sending settings',
);
try {
const menu = await this.menuService.getTree();
return client.emit('settings', { menu, ...settings });
} catch (err) {
this.logger.warn('Web Channel Handler : Unable to retrieve menu ', err);
return client.emit('settings', settings);
}
} catch (err) {
this.logger.error(
'Web Channel Handler : Unable to initiate websocket connection',
err,
);
client.disconnect();
}
}
@@ -218,7 +208,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns Formatted message
*/
private formatHistoryMessages(messages: AnyMessage[]): Web.Message[] {
protected formatMessages(messages: AnyMessage[]): Web.Message[] {
return messages.map((anyMessage: AnyMessage) => {
if ('sender' in anyMessage && anyMessage.sender) {
return {
@@ -262,7 +252,7 @@ export default abstract class BaseWebChannelHandler<
until,
n,
);
return this.formatHistoryMessages(messages.reverse());
return this.formatMessages(messages.reverse());
}
return [];
}
@@ -287,35 +277,11 @@ export default abstract class BaseWebChannelHandler<
since,
n,
);
return this.formatHistoryMessages(messages);
return this.formatMessages(messages);
}
return [];
}
/**
* Verify the received token.
*
* @param verificationToken - Verification Token
*/
private async verifyToken(verificationToken: string) {
const settings =
(await this.getSettings()) as unknown as Settings[typeof WEB_CHANNEL_NAMESPACE];
const verifyToken = settings.verification_token;
if (!verifyToken) {
throw new Error('You need to specify a verifyToken in your config.');
}
if (!verificationToken) {
throw new Error('Did not recieve any verification token.');
}
if (verificationToken !== verifyToken) {
throw new Error('Make sure the validation tokens match.');
}
this.logger.log(
'Web Channel Handler : Token has been verified successfully!',
);
}
/**
* Verify the origin against whitelisted domains.
*
@@ -405,20 +371,12 @@ export default abstract class BaseWebChannelHandler<
* @param req
* @param res
*/
private async checkRequest(
protected async checkRequest(
req: Request | SocketRequest,
res: Response | SocketResponse,
) {
try {
await this.validateCors(req, res);
try {
const { verification_token } =
'verification_token' in req.query ? req.query : req.body;
await this.verifyToken(verification_token);
} catch (err) {
this.logger.warn('Web Channel Handler : Unable to verify token ', err);
throw new Error('Unauthorized, invalid token!');
}
} catch (err) {
this.logger.warn(
'Web Channel Handler : Attempt to access from an unauthorized origin',
@@ -741,7 +699,7 @@ export default abstract class BaseWebChannelHandler<
*
* @returns IP Address
*/
private getIpAddress(req: Request | SocketRequest): string {
protected getIpAddress(req: Request | SocketRequest): string {
if ('isSocket' in req && req.isSocket) {
return req.socket.handshake.address;
} else if (Array.isArray(req.ips) && req.ips.length > 0) {

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Verification Token",
"allowed_domains": "Allowed Domains",
"start_button": "Enable `Get Started`",
"input_disabled": "Disable Input",

View File

@@ -1,5 +1,4 @@
{
"verification_token": "Jeton de vérification",
"allowed_domains": "Domaines autorisés",
"start_button": "Activer `Démarrer`",
"input_disabled": "Désactiver la saisie",

View File

@@ -16,12 +16,6 @@ export const WEB_CHANNEL_NAME = 'web-channel' as const;
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
export default [
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.verification_token,
value: 'token123',
type: SettingType.secret,
},
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.allowed_domains,
@@ -68,7 +62,7 @@ export default [
{
group: WEB_CHANNEL_NAMESPACE,
label: Web.SettingLabel.avatar_url,
value: 'https://eu.ui-avatars.com/api/?name=Hexa+Bot&size=64',
value: '',
type: SettingType.text,
},
{

View File

@@ -13,8 +13,6 @@ import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
export namespace Web {
export enum SettingLabel {
secret = 'secret',
verification_token = 'verification_token',
allowed_domains = 'allowed_domains',
start_button = 'start_button',
input_disabled = 'input_disabled',

5
api/src/global.d.ts vendored
View File

@@ -6,6 +6,9 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import '../types/event-emitter';
import '../types/express-session';
declare global {
type HyphenToUnderscore<S extends string> = S extends `${infer P}-${infer Q}`
? `${P}_${HyphenToUnderscore<Q>}`
@@ -13,4 +16,4 @@ declare global {
}
// eslint-disable-next-line prettier/prettier
export { };
export {};

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).
*/
import { HttpModule } from '@nestjs/axios';
import { Global, Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
@@ -40,6 +41,7 @@ import { PluginService } from './plugins.service';
CmsModule,
AttachmentModule,
ChatModule,
HttpModule,
],
providers: [PluginService],
exports: [PluginService],

View File

@@ -83,8 +83,8 @@ export class ReadOnlyUserController extends BaseController<
*/
@Roles('public')
@Get('bot/profile_pic')
async botProfilePic() {
return getBotAvatar();
async botProfilePic(@Query('color') color: string) {
return getBotAvatar(color);
}
/**

View File

@@ -39,8 +39,8 @@ export const generateInitialsAvatar = async (name: {
return await generateAvatarSvg(svg);
};
export const getBotAvatar = async () => {
const svg = generateBotAvatarSvg({});
export const getBotAvatar = async (color: string) => {
const svg = generateBotAvatarSvg({ bgColor: color });
return await generateAvatarSvg(svg);
};

View File

@@ -43,17 +43,17 @@ export function generateUIAvatarSvg({
export function generateBotAvatarSvg({
size = 64,
bgColor = '#d1d1d1',
bgColor = '#000',
textColor = '#fff',
}: UIAvatarSvgParams): string {
return `
<svg width="${size}px" height="${size}px" viewBox="-3.36 -3.36 30.72 30.72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#4f4f4f" stroke="#4f4f4f" transform="rotate(0)">
<svg width="${size}px" height="${size}px" viewBox="-3.36 -3.36 30.72 30.72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke-width="0" transform="translate(0,0), scale(1)">
<rect x="-3.36" y="-3.36" width="30.72" height="30.72" rx="15.36" fill="${bgColor}" strokewidth="0"></rect>
</g>
<g stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.336"></g>
<g>
<g stroke-width="0.00024000000000000003" fill="none" fill-rule="evenodd">
<g fill="#000000" fill-rule="nonzero">
<g stroke-width="0.00024000000000000003" fill="${textColor}" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M17.7530511,13.999921 C18.9956918,13.999921 20.0030511,15.0072804 20.0030511,16.249921 L20.0030511,17.1550008 C20.0030511,18.2486786 19.5255957,19.2878579 18.6957793,20.0002733 C17.1303315,21.344244 14.8899962,22.0010712 12,22.0010712 C9.11050247,22.0010712 6.87168436,21.3444691 5.30881727,20.0007885 C4.48019625,19.2883988 4.00354153,18.2500002 4.00354153,17.1572408 L4.00354153,16.249921 C4.00354153,15.0072804 5.01090084,13.999921 6.25354153,13.999921 L17.7530511,13.999921 Z M17.7530511,15.499921 L6.25354153,15.499921 C5.83932796,15.499921 5.50354153,15.8357075 5.50354153,16.249921 L5.50354153,17.1572408 C5.50354153,17.8128951 5.78953221,18.4359296 6.28670709,18.8633654 C7.5447918,19.9450082 9.44080155,20.5010712 12,20.5010712 C14.5599799,20.5010712 16.4578003,19.9446634 17.7186879,18.8621641 C18.2165778,18.4347149 18.5030511,17.8112072 18.5030511,17.1550005 L18.5030511,16.249921 C18.5030511,15.8357075 18.1672647,15.499921 17.7530511,15.499921 Z M11.8985607,2.00734093 L12.0003312,2.00049432 C12.380027,2.00049432 12.6938222,2.2826482 12.7434846,2.64872376 L12.7503312,2.75049432 L12.7495415,3.49949432 L16.25,3.5 C17.4926407,3.5 18.5,4.50735931 18.5,5.75 L18.5,10.254591 C18.5,11.4972317 17.4926407,12.504591 16.25,12.504591 L7.75,12.504591 C6.50735931,12.504591 5.5,11.4972317 5.5,10.254591 L5.5,5.75 C5.5,4.50735931 6.50735931,3.5 7.75,3.5 L11.2495415,3.49949432 L11.2503312,2.75049432 C11.2503312,2.37079855 11.5324851,2.05700336 11.8985607,2.00734093 L12.0003312,2.00049432 L11.8985607,2.00734093 Z M16.25,5 L7.75,5 C7.33578644,5 7,5.33578644 7,5.75 L7,10.254591 C7,10.6688046 7.33578644,11.004591 7.75,11.004591 L16.25,11.004591 C16.6642136,11.004591 17,10.6688046 17,10.254591 L17,5.75 C17,5.33578644 16.6642136,5 16.25,5 Z M9.74928905,6.5 C10.4392523,6.5 10.9985781,7.05932576 10.9985781,7.74928905 C10.9985781,8.43925235 10.4392523,8.99857811 9.74928905,8.99857811 C9.05932576,8.99857811 8.5,8.43925235 8.5,7.74928905 C8.5,7.05932576 9.05932576,6.5 9.74928905,6.5 Z M14.2420255,6.5 C14.9319888,6.5 15.4913145,7.05932576 15.4913145,7.74928905 C15.4913145,8.43925235 14.9319888,8.99857811 14.2420255,8.99857811 C13.5520622,8.99857811 12.9927364,8.43925235 12.9927364,7.74928905 C12.9927364,7.05932576 13.5520622,6.5 14.2420255,6.5 Z"></path>
</g>
</g>

View File

@@ -16,11 +16,14 @@ export type TFilterKeysOfNeverType<T> = Omit<T, TFilterKeysOfType<T, []>>;
export type NestedKeys<T> = T extends object
? {
[K in keyof T]: Array<any> extends T[K]
? Exclude<K, symbol>
: K extends symbol
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T]: T[K] extends Function
? never
: Array<any> extends T[K]
? Exclude<K, symbol>
: `${Exclude<K, symbol>}${'' | `.${NestedKeys<T[K]>}`}`;
: K extends symbol
? Exclude<K, symbol>
: `${Exclude<K, symbol>}${'' | `.${NestedKeys<T[K]>}`}`;
}[keyof T]
: never;

View File

@@ -224,7 +224,7 @@ export class WebsocketGateway
'Unable to load session, creating a new one ...',
err,
);
if (searchParams.get('channel') === 'web-channel') {
if (searchParams.get('channel') !== 'console-channel') {
return this.createAndStoreSession(client, next);
} else {
return next(new Error('Unauthorized: Unknown session ID'));