mirror of
https://github.com/hexastack/hexabot
synced 2025-05-08 22:59:39 +00:00
Merge pull request #486 from Hexastack/fix/webchannel-updates
Fix/webchannel updates
This commit is contained in:
commit
039ce0e1bc
@ -9,54 +9,57 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
|
IsNotEmpty,
|
||||||
IsObject,
|
IsObject,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
MaxLength,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsString,
|
IsString,
|
||||||
|
MaxLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { ChannelName } from '@/channel/types';
|
||||||
import { ObjectIdDto } from '@/utils/dto/object-id.dto';
|
import { ObjectIdDto } from '@/utils/dto/object-id.dto';
|
||||||
|
|
||||||
export class AttachmentCreateDto {
|
export class AttachmentMetadataDto {
|
||||||
/**
|
/**
|
||||||
* Attachment channel
|
* Attachment original file name
|
||||||
*/
|
*/
|
||||||
@ApiPropertyOptional({ description: 'Attachment channel', type: Object })
|
@ApiProperty({ description: 'Attachment original file name', type: String })
|
||||||
@IsNotEmpty()
|
|
||||||
@IsObject()
|
|
||||||
channel?: Partial<Record<string, any>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attachment location
|
|
||||||
*/
|
|
||||||
@ApiProperty({ description: 'Attachment location', type: String })
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString()
|
|
||||||
location: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attachment name
|
|
||||||
*/
|
|
||||||
@ApiProperty({ description: 'Attachment name', type: String })
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attachment size
|
* Attachment size in bytes
|
||||||
*/
|
*/
|
||||||
@ApiProperty({ description: 'Attachment size', type: Number })
|
@ApiProperty({ description: 'Attachment size in bytes', type: Number })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
size: number;
|
size: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attachment type
|
* Attachment MIME type
|
||||||
*/
|
*/
|
||||||
@ApiProperty({ description: 'Attachment type', type: String })
|
@ApiProperty({ description: 'Attachment MIME type', type: String })
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attachment specia channel(s) metadata
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({ description: 'Attachment channel', type: Object })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsObject()
|
||||||
|
channel?: Partial<Record<ChannelName, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AttachmentCreateDto extends AttachmentMetadataDto {
|
||||||
|
/**
|
||||||
|
* Attachment location (file would/should be stored under a unique name)
|
||||||
|
*/
|
||||||
|
@ApiProperty({ description: 'Attachment location', type: String })
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
location: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AttachmentDownloadDto extends ObjectIdDto {
|
export class AttachmentDownloadDto extends ObjectIdDto {
|
||||||
|
@ -42,7 +42,7 @@ export const attachments: Attachment[] = [
|
|||||||
size: 343370,
|
size: 343370,
|
||||||
location:
|
location:
|
||||||
'/app/src/attachment/uploads/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
|
'/app/src/attachment/uploads/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
|
||||||
channel: { dimelo: {} },
|
channel: { 'web-channel': {} },
|
||||||
id: '65940d115178607da65c82b7',
|
id: '65940d115178607da65c82b7',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
@ -53,7 +53,7 @@ export const attachments: Attachment[] = [
|
|||||||
size: 33829,
|
size: 33829,
|
||||||
location:
|
location:
|
||||||
'/app/src/attachment/uploads/Screenshot from 2022-03-18 08-58-15-af61e7f71281f9fd3f1ad7ad10107741c.png',
|
'/app/src/attachment/uploads/Screenshot from 2022-03-18 08-58-15-af61e7f71281f9fd3f1ad7ad10107741c.png',
|
||||||
channel: { dimelo: {} },
|
channel: { 'web-channel': {} },
|
||||||
id: '65940d115178607da65c82b8',
|
id: '65940d115178607da65c82b8',
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
@ -6,8 +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 fs, { createReadStream } from 'fs';
|
import fs, { createReadStream, promises as fsPromises } from 'fs';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
@ -16,6 +17,7 @@ import {
|
|||||||
StreamableFile,
|
StreamableFile,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
@ -24,9 +26,14 @@ import { PluginService } from '@/plugins/plugins.service';
|
|||||||
import { PluginType } from '@/plugins/types';
|
import { PluginType } from '@/plugins/types';
|
||||||
import { BaseService } from '@/utils/generics/base-service';
|
import { BaseService } from '@/utils/generics/base-service';
|
||||||
|
|
||||||
|
import { AttachmentMetadataDto } from '../dto/attachment.dto';
|
||||||
import { AttachmentRepository } from '../repositories/attachment.repository';
|
import { AttachmentRepository } from '../repositories/attachment.repository';
|
||||||
import { Attachment } from '../schemas/attachment.schema';
|
import { Attachment } from '../schemas/attachment.schema';
|
||||||
import { fileExists, getStreamableFile } from '../utilities';
|
import {
|
||||||
|
fileExists,
|
||||||
|
generateUniqueFilename,
|
||||||
|
getStreamableFile,
|
||||||
|
} from '../utilities';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttachmentService extends BaseService<Attachment> {
|
export class AttachmentService extends BaseService<Attachment> {
|
||||||
@ -180,6 +187,57 @@ export class AttachmentService extends BaseService<Attachment> {
|
|||||||
return uploadedFiles;
|
return uploadedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads files to the server. If a storage plugin is configured it uploads files accordingly.
|
||||||
|
* Otherwise, uploads files to the local directory.
|
||||||
|
*
|
||||||
|
* @param file - The file
|
||||||
|
* @returns A promise that resolves to an array of uploaded attachments.
|
||||||
|
*/
|
||||||
|
async store(
|
||||||
|
file: Buffer | Readable | Express.Multer.File,
|
||||||
|
metadata: AttachmentMetadataDto,
|
||||||
|
): Promise<Attachment> {
|
||||||
|
if (this.getStoragePlugin()) {
|
||||||
|
const storedDto = await this.getStoragePlugin().store(file, metadata);
|
||||||
|
return await this.create(storedDto);
|
||||||
|
} else {
|
||||||
|
const dirPath = path.join(config.parameters.uploadDir);
|
||||||
|
const uniqueFilename = generateUniqueFilename(metadata.name);
|
||||||
|
const filePath = path.resolve(dirPath, sanitizeFilename(uniqueFilename));
|
||||||
|
|
||||||
|
if (!filePath.startsWith(dirPath)) {
|
||||||
|
throw new Error('Invalid file path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(file)) {
|
||||||
|
await fsPromises.writeFile(filePath, file);
|
||||||
|
} else if (file instanceof Readable) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const writeStream = fs.createWriteStream(filePath);
|
||||||
|
file.pipe(writeStream);
|
||||||
|
writeStream.on('finish', resolve);
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (file.path) {
|
||||||
|
// For example, if the file is an instance of `Express.Multer.File` (diskStorage case)
|
||||||
|
const srcFilePath = path.resolve(file.path);
|
||||||
|
await fsPromises.copyFile(srcFilePath, filePath);
|
||||||
|
await fsPromises.unlink(srcFilePath);
|
||||||
|
} else {
|
||||||
|
await fsPromises.writeFile(filePath, file.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const location = filePath.replace(dirPath, '');
|
||||||
|
return await this.create({
|
||||||
|
...metadata,
|
||||||
|
location,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloads an attachment identified by the provided parameters.
|
* Downloads an attachment identified by the provided parameters.
|
||||||
*
|
*
|
||||||
|
@ -7,19 +7,31 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { createReadStream, existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { extname, join } from 'path';
|
||||||
|
|
||||||
import { Logger, StreamableFile } from '@nestjs/common';
|
import { Logger, StreamableFile } from '@nestjs/common';
|
||||||
import { StreamableFileOptions } from '@nestjs/common/file-stream/interfaces/streamable-options.interface';
|
import { StreamableFileOptions } from '@nestjs/common/file-stream/interfaces/streamable-options.interface';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
|
|
||||||
export const MIME_REGEX = /^[a-z-]+\/[0-9a-z\-.]+$/gm;
|
export const MIME_REGEX = /^[a-z-]+\/[0-9a-z\-.]+$/gm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a given string matches the MIME type format.
|
||||||
|
*
|
||||||
|
* @param type The string to validate.
|
||||||
|
* @returns Whether the string is a valid MIME type.
|
||||||
|
*/
|
||||||
export const isMime = (type: string): boolean => {
|
export const isMime = (type: string): boolean => {
|
||||||
return MIME_REGEX.test(type);
|
return MIME_REGEX.test(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a file exists in the specified upload directory.
|
||||||
|
* @param location The relative location of the file.
|
||||||
|
* @returns Whether the file exists.
|
||||||
|
*/
|
||||||
export const fileExists = (location: string): boolean => {
|
export const fileExists = (location: string): boolean => {
|
||||||
// bypass test env
|
// bypass test env
|
||||||
if (config.env === 'test') {
|
if (config.env === 'test') {
|
||||||
@ -35,6 +47,12 @@ export const fileExists = (location: string): boolean => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a streamable file from a given file path and options.
|
||||||
|
*
|
||||||
|
* @param options The object containing the file path and optional settings.
|
||||||
|
* @returns A streamable file object.
|
||||||
|
*/
|
||||||
export const getStreamableFile = ({
|
export const getStreamableFile = ({
|
||||||
path,
|
path,
|
||||||
options,
|
options,
|
||||||
@ -50,3 +68,15 @@ export const getStreamableFile = ({
|
|||||||
|
|
||||||
return new StreamableFile(fileReadStream, options);
|
return new StreamableFile(fileReadStream, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique filename by appending a UUID to the original name.
|
||||||
|
*
|
||||||
|
* @param originalname The original filename.
|
||||||
|
* @returns A unique filename.
|
||||||
|
*/
|
||||||
|
export const generateUniqueFilename = (originalname: string) => {
|
||||||
|
const extension = extname(originalname);
|
||||||
|
const name = originalname.slice(0, -extension.length);
|
||||||
|
return `${name}-${uuidv4()}${extension}`;
|
||||||
|
};
|
||||||
|
@ -17,6 +17,5 @@
|
|||||||
"show_emoji": "Enable Emoji Picker",
|
"show_emoji": "Enable Emoji Picker",
|
||||||
"show_file": "Enable Attachment Uploader",
|
"show_file": "Enable Attachment Uploader",
|
||||||
"show_location": "Enable Geolocation Share",
|
"show_location": "Enable Geolocation Share",
|
||||||
"allowed_upload_size": "Max Upload Size (in bytes)",
|
|
||||||
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
|
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,5 @@
|
|||||||
"show_emoji": "Activer le sélecteur d'Emojis",
|
"show_emoji": "Activer le sélecteur d'Emojis",
|
||||||
"show_file": "Activer l'upload de fichiers",
|
"show_file": "Activer l'upload de fichiers",
|
||||||
"show_location": "Activer le partage de géolocalisation",
|
"show_location": "Activer le partage de géolocalisation",
|
||||||
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
|
|
||||||
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
|
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,6 @@ import { ChannelSetting } from '@/channel/types';
|
|||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { SettingType } from '@/setting/schemas/types';
|
import { SettingType } from '@/setting/schemas/types';
|
||||||
|
|
||||||
import { Web } from '../web/types';
|
|
||||||
|
|
||||||
export const CONSOLE_CHANNEL_NAME = 'console-channel';
|
export const CONSOLE_CHANNEL_NAME = 'console-channel';
|
||||||
|
|
||||||
export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
|
export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
|
||||||
@ -19,68 +17,62 @@ export const CONSOLE_CHANNEL_NAMESPACE = 'console_channel';
|
|||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.allowed_domains,
|
label: 'allowed_domains',
|
||||||
value: config.security.cors.allowOrigins.join(','),
|
value: config.security.cors.allowOrigins.join(','),
|
||||||
type: SettingType.text,
|
type: SettingType.text,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.start_button,
|
label: 'start_button',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.input_disabled,
|
label: 'input_disabled',
|
||||||
value: false,
|
value: false,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.persistent_menu,
|
label: 'persistent_menu',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.greeting_message,
|
label: 'greeting_message',
|
||||||
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
||||||
type: SettingType.textarea,
|
type: SettingType.textarea,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.theme_color,
|
label: 'theme_color',
|
||||||
value: 'teal',
|
value: 'teal',
|
||||||
type: SettingType.select,
|
type: SettingType.select,
|
||||||
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_emoji,
|
label: 'show_emoji',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_file,
|
label: 'show_file',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_location,
|
label: 'show_location',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
group: CONSOLE_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.allowed_upload_size,
|
label: 'allowed_upload_types',
|
||||||
value: 2500000,
|
|
||||||
type: SettingType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: CONSOLE_CHANNEL_NAMESPACE,
|
|
||||||
label: Web.SettingLabel.allowed_upload_types,
|
|
||||||
value:
|
value:
|
||||||
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
type: SettingType.textarea,
|
type: SettingType.textarea,
|
||||||
|
@ -6,14 +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 { promises as fsPromises } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import multer, { diskStorage } from 'multer';
|
import multer, { diskStorage, memoryStorage } from 'multer';
|
||||||
import sanitize from 'sanitize-filename';
|
|
||||||
import { Socket } from 'socket.io';
|
import { Socket } from 'socket.io';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -31,7 +27,6 @@ import { Button, ButtonType } from '@/chat/schemas/types/button';
|
|||||||
import {
|
import {
|
||||||
AnyMessage,
|
AnyMessage,
|
||||||
ContentElement,
|
ContentElement,
|
||||||
FileType,
|
|
||||||
IncomingMessage,
|
IncomingMessage,
|
||||||
OutgoingMessage,
|
OutgoingMessage,
|
||||||
OutgoingMessageFormat,
|
OutgoingMessageFormat,
|
||||||
@ -201,6 +196,16 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given message is an IncomingMessage
|
||||||
|
*
|
||||||
|
* @param message Any type of message
|
||||||
|
* @returns True, if it's a incoming message
|
||||||
|
*/
|
||||||
|
private isIncomingMessage(message: AnyMessage): message is IncomingMessage {
|
||||||
|
return 'sender' in message && !!message.sender;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapt the message structure for web channel
|
* Adapt the message structure for web channel
|
||||||
*
|
*
|
||||||
@ -209,27 +214,32 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
* @returns Formatted message
|
* @returns Formatted message
|
||||||
*/
|
*/
|
||||||
protected formatMessages(messages: AnyMessage[]): Web.Message[] {
|
protected formatMessages(messages: AnyMessage[]): Web.Message[] {
|
||||||
return messages.map((anyMessage: AnyMessage) => {
|
const formattedMessages: Web.Message[] = [];
|
||||||
if ('sender' in anyMessage && anyMessage.sender) {
|
|
||||||
return {
|
for (const anyMessage of messages) {
|
||||||
...this.formatIncomingHistoryMessage(anyMessage as IncomingMessage),
|
if (this.isIncomingMessage(anyMessage)) {
|
||||||
|
const message = this.formatIncomingHistoryMessage(anyMessage);
|
||||||
|
formattedMessages.push({
|
||||||
|
...message,
|
||||||
author: anyMessage.sender,
|
author: anyMessage.sender,
|
||||||
read: true, // Temporary fix as read is false in the bd
|
read: true, // Temporary fix as read is false in the bd
|
||||||
mid: anyMessage.mid,
|
mid: anyMessage.mid,
|
||||||
createdAt: anyMessage.createdAt,
|
createdAt: anyMessage.createdAt,
|
||||||
} as Web.IncomingMessage;
|
});
|
||||||
} else {
|
} else {
|
||||||
const outgoingMessage = anyMessage as OutgoingMessage;
|
const message = this.formatOutgoingHistoryMessage(anyMessage);
|
||||||
return {
|
formattedMessages.push({
|
||||||
...this.formatOutgoingHistoryMessage(outgoingMessage),
|
...message,
|
||||||
author: 'chatbot',
|
author: 'chatbot',
|
||||||
read: true, // Temporary fix as read is false in the bd
|
read: true, // Temporary fix as read is false in the bd
|
||||||
mid: outgoingMessage.mid,
|
mid: anyMessage.mid,
|
||||||
handover: !!outgoingMessage.handover,
|
handover: !!anyMessage.handover,
|
||||||
createdAt: outgoingMessage.createdAt,
|
createdAt: anyMessage.createdAt,
|
||||||
} as Web.OutgoingMessage;
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return formattedMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,7 +383,8 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
.status(403)
|
.status(403)
|
||||||
.json({ err: 'Web Channel Handler : Unauthorized!' });
|
.json({ err: 'Web Channel Handler : Unauthorized!' });
|
||||||
} else if (
|
} else if (
|
||||||
('isSocket' in req && !!req.isSocket !== req.session.web.isSocket) ||
|
(this.isSocketRequest(req) &&
|
||||||
|
!!req.isSocket !== req.session.web.isSocket) ||
|
||||||
!Array.isArray(req.session.web.messageQueue)
|
!Array.isArray(req.session.web.messageQueue)
|
||||||
) {
|
) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
@ -462,7 +473,7 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
|
|
||||||
req.session.web = {
|
req.session.web = {
|
||||||
profile,
|
profile,
|
||||||
isSocket: 'isSocket' in req && !!req.isSocket,
|
isSocket: this.isSocketRequest(req),
|
||||||
messageQueue: [],
|
messageQueue: [],
|
||||||
polling: false,
|
polling: false,
|
||||||
};
|
};
|
||||||
@ -472,12 +483,12 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
/**
|
/**
|
||||||
* Return message queue (using by long polling case only)
|
* Return message queue (using by long polling case only)
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req HTTP Express Request
|
||||||
* @param res
|
* @param res HTTP Express Response
|
||||||
*/
|
*/
|
||||||
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 ('isSocket' in req && req.isSocket) {
|
if (this.isSocketRequest(req)) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'Web Channel Handler : Polling not authorized when using websockets',
|
'Web Channel Handler : Polling not authorized when using websockets',
|
||||||
);
|
);
|
||||||
@ -548,13 +559,13 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
) {
|
) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Web Channel Handler : subscribe (isSocket=' +
|
'Web Channel Handler : subscribe (isSocket=' +
|
||||||
('isSocket' in req && !!req.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
|
||||||
if ('isSocket' in req && !!req.isSocket) {
|
if (this.isSocketRequest(req)) {
|
||||||
try {
|
try {
|
||||||
await req.socket.join(profile.foreign_id);
|
await req.socket.join(profile.foreign_id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -581,134 +592,99 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
/**
|
/**
|
||||||
* Upload file as attachment if provided
|
* Upload file as attachment if provided
|
||||||
*
|
*
|
||||||
* @param upload
|
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||||
* @param filename
|
* @param res Either a HTTP Express response or a WS response (Synthetic Object)
|
||||||
*/
|
* @param next Callback Function
|
||||||
private async storeAttachment(
|
|
||||||
upload: Omit<Web.IncomingAttachmentMessageData, 'url' | 'file'>,
|
|
||||||
filename: string,
|
|
||||||
next: (
|
|
||||||
err: Error | null,
|
|
||||||
payload: { type: string; url: string } | false,
|
|
||||||
) => void,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.debug('Web Channel Handler : Successfully uploaded file');
|
|
||||||
|
|
||||||
const attachment = await this.attachmentService.create({
|
|
||||||
name: upload.name || '',
|
|
||||||
type: upload.type || 'text/txt',
|
|
||||||
size: upload.size || 0,
|
|
||||||
location: filename,
|
|
||||||
channel: { web: {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
'Web Channel Handler : Successfully stored file as attachment',
|
|
||||||
);
|
|
||||||
|
|
||||||
next(null, {
|
|
||||||
type: Attachment.getTypeByMime(attachment.type),
|
|
||||||
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'Web Channel Handler : Unable to store uploaded file as attachment',
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
next(err, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload file as attachment if provided
|
|
||||||
*
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
*/
|
||||||
async handleFilesUpload(
|
async handleFilesUpload(
|
||||||
req: Request | SocketRequest,
|
req: Request | SocketRequest,
|
||||||
res: Response | SocketResponse,
|
res: Response | SocketResponse,
|
||||||
next: (
|
next: (
|
||||||
err: null | Error,
|
err: null | Error,
|
||||||
result: { type: string; url: string } | false,
|
result?: Web.IncomingAttachmentMessageData,
|
||||||
) => void,
|
) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const data: Web.IncomingMessage = req.body;
|
|
||||||
// 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('Web Channel Handler : No session provided');
|
||||||
return next(null, false);
|
return next(null);
|
||||||
}
|
|
||||||
// Check if any file is provided
|
|
||||||
if (!data || !data.type || data.type !== 'file') {
|
|
||||||
this.logger.debug('Web Channel Handler : No files provided');
|
|
||||||
return next(null, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse json form data (in case of content-type multipart/form-data)
|
if (this.isSocketRequest(req)) {
|
||||||
data.data =
|
|
||||||
typeof data.data === 'string' ? JSON.parse(data.data) : data.data;
|
|
||||||
|
|
||||||
// Check max size upload
|
|
||||||
const upload = data.data;
|
|
||||||
if (typeof upload.size === 'undefined') {
|
|
||||||
return next(new Error('File upload probably failed.'), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store file as attachment
|
|
||||||
const dirPath = path.join(config.parameters.uploadDir);
|
|
||||||
const sanitizedFilename = sanitize(
|
|
||||||
`${req.session.web.profile.id}_${+new Date()}_${upload.name}`,
|
|
||||||
);
|
|
||||||
const filePath = path.resolve(dirPath, sanitizedFilename);
|
|
||||||
|
|
||||||
if (!filePath.startsWith(dirPath)) {
|
|
||||||
return next(new Error('Invalid file path!'), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('isSocket' in req && req.isSocket) {
|
|
||||||
// @TODO : test this
|
|
||||||
try {
|
try {
|
||||||
await fsPromises.writeFile(filePath, upload.file);
|
const { type, data } = req.body as Web.IncomingMessage;
|
||||||
this.storeAttachment(upload, sanitizedFilename, next);
|
|
||||||
|
// Check if any file is provided
|
||||||
|
if (type !== 'file' || !('file' in data) || !data.file) {
|
||||||
|
this.logger.debug('Web Channel Handler : No files provided');
|
||||||
|
return next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = Buffer.byteLength(data.file);
|
||||||
|
|
||||||
|
if (size > config.parameters.maxUploadSize) {
|
||||||
|
return next(new Error('Max upload size has been exceeded'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = await this.attachmentService.store(data.file, {
|
||||||
|
name: data.name,
|
||||||
|
size: Buffer.byteLength(data.file),
|
||||||
|
type: data.type,
|
||||||
|
});
|
||||||
|
next(null, {
|
||||||
|
type: Attachment.getTypeByMime(attachment.type),
|
||||||
|
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'Web Channel Handler : Unable to write uploaded file',
|
'Web Channel Handler : Unable to write uploaded file',
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
return next(new Error('Unable to upload file!'), false);
|
return next(new Error('Unable to upload file!'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: diskStorage({
|
limits: {
|
||||||
destination: dirPath, // Set the destination directory for file storage
|
fileSize: config.parameters.maxUploadSize,
|
||||||
filename: (_req, _file, cb) => {
|
},
|
||||||
cb(null, sanitizedFilename); // Set the file name
|
storage: (() => {
|
||||||
},
|
if (config.parameters.storageMode === 'memory') {
|
||||||
}),
|
return memoryStorage();
|
||||||
|
} else {
|
||||||
|
return diskStorage({});
|
||||||
|
}
|
||||||
|
})(),
|
||||||
}).single('file'); // 'file' is the field name in the form
|
}).single('file'); // 'file' is the field name in the form
|
||||||
|
|
||||||
upload(req as Request, res as Response, (err) => {
|
upload(req as Request, res as Response, async (err?: any) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
'Web Channel Handler : Unable to write uploaded file',
|
'Web Channel Handler : Unable to write uploaded file',
|
||||||
err,
|
err,
|
||||||
);
|
);
|
||||||
return next(new Error('Unable to upload file!'), false);
|
return next(new Error('Unable to upload file!'));
|
||||||
}
|
}
|
||||||
// @TODO : test upload
|
|
||||||
// @ts-expect-error @TODO : This needs to be fixed at a later point
|
// Check if any file is provided
|
||||||
const file = req.file;
|
if (!req.file) {
|
||||||
this.storeAttachment(
|
this.logger.debug('Web Channel Handler : No files provided');
|
||||||
{
|
return next(null);
|
||||||
name: file.filename,
|
}
|
||||||
type: file.mimetype as FileType, // @Todo : test this
|
|
||||||
|
try {
|
||||||
|
const file = req.file;
|
||||||
|
const attachment = await this.attachmentService.store(file, {
|
||||||
|
name: file.originalname,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
},
|
type: file.mimetype,
|
||||||
file.path.replace(dirPath, ''),
|
});
|
||||||
next,
|
next(null, {
|
||||||
);
|
type: Attachment.getTypeByMime(attachment.type),
|
||||||
|
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -716,12 +692,12 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
/**
|
/**
|
||||||
* Returns the request client IP address
|
* Returns the request client IP address
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req Either a HTTP request or a WS Request (Synthetic object)
|
||||||
*
|
*
|
||||||
* @returns IP Address
|
* @returns IP Address
|
||||||
*/
|
*/
|
||||||
protected getIpAddress(req: Request | SocketRequest): string {
|
protected getIpAddress(req: Request | SocketRequest): string {
|
||||||
if ('isSocket' in req && req.isSocket) {
|
if (this.isSocketRequest(req)) {
|
||||||
return req.socket.handshake.address;
|
return req.socket.handshake.address;
|
||||||
} else if (Array.isArray(req.ips) && req.ips.length > 0) {
|
} else if (Array.isArray(req.ips) && req.ips.length > 0) {
|
||||||
// If config.http.trustProxy is enabled, this variable contains the IP addresses
|
// If config.http.trustProxy is enabled, this variable contains the IP addresses
|
||||||
@ -736,7 +712,7 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
/**
|
/**
|
||||||
* Return subscriber channel specific attributes
|
* Return subscriber channel specific attributes
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||||
*
|
*
|
||||||
* @returns The subscriber channel's attributes
|
* @returns The subscriber channel's attributes
|
||||||
*/
|
*/
|
||||||
@ -744,7 +720,7 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
req: Request | SocketRequest,
|
req: Request | SocketRequest,
|
||||||
): SubscriberChannelDict[typeof WEB_CHANNEL_NAME] {
|
): SubscriberChannelDict[typeof WEB_CHANNEL_NAME] {
|
||||||
return {
|
return {
|
||||||
isSocket: 'isSocket' in req && !!req.isSocket,
|
isSocket: this.isSocketRequest(req),
|
||||||
ipAddress: this.getIpAddress(req),
|
ipAddress: this.getIpAddress(req),
|
||||||
agent: req.headers['user-agent'],
|
agent: req.headers['user-agent'],
|
||||||
};
|
};
|
||||||
@ -753,20 +729,31 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
/**
|
/**
|
||||||
* Handle channel event (probably a message)
|
* Handle channel event (probably a message)
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||||
* @param res
|
* @param res Either a HTTP Express response or a WS response (Synthetic Object)
|
||||||
*/
|
*/
|
||||||
_handleEvent(
|
_handleEvent(
|
||||||
req: Request | SocketRequest,
|
req: Request | SocketRequest,
|
||||||
res: Response | SocketResponse,
|
res: Response | SocketResponse,
|
||||||
): void {
|
): void {
|
||||||
const data: Web.IncomingMessage = req.body;
|
// @TODO: perform payload validation
|
||||||
|
if (!req.body) {
|
||||||
|
this.logger.debug('Web Channel Handler : Empty body');
|
||||||
|
res.status(400).json({ err: 'Web Channel Handler : Bad Request!' });
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Parse json form data (in case of content-type multipart/form-data)
|
||||||
|
req.body.data =
|
||||||
|
typeof req.body.data === 'string'
|
||||||
|
? JSON.parse(req.body.data)
|
||||||
|
: req.body.data;
|
||||||
|
}
|
||||||
|
|
||||||
this.validateSession(req, res, (profile) => {
|
this.validateSession(req, res, (profile) => {
|
||||||
this.handleFilesUpload(
|
this.handleFilesUpload(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
// @ts-expect-error @TODO : This needs to be fixed at a later point @TODO
|
(err: Error, data?: Web.IncomingAttachmentMessageData) => {
|
||||||
(err: Error, upload: Web.IncomingMessageData) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'Web Channel Handler : Unable to upload file ',
|
'Web Channel Handler : Unable to upload file ',
|
||||||
@ -777,14 +764,18 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
.json({ err: 'Web Channel Handler : File upload failed!' });
|
.json({ err: 'Web Channel Handler : File upload failed!' });
|
||||||
}
|
}
|
||||||
// Set data in file upload case
|
// Set data in file upload case
|
||||||
if (upload) {
|
const body: Web.IncomingMessage = data
|
||||||
data.data = upload;
|
? {
|
||||||
}
|
...req.body,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
: req.body;
|
||||||
|
|
||||||
const channelAttrs = this.getChannelAttributes(req);
|
const channelAttrs = this.getChannelAttributes(req);
|
||||||
const event = new WebEventWrapper<N>(this, data, channelAttrs);
|
const event = new WebEventWrapper<N>(this, body, channelAttrs);
|
||||||
if (event.getEventType() === 'message') {
|
if (event.getEventType() === 'message') {
|
||||||
// Handler sync message sent by chabbot
|
// Handler sync message sent by chabbot
|
||||||
if (data.sync && data.author === 'chatbot') {
|
if (body.sync && body.author === 'chatbot') {
|
||||||
const sentMessage: MessageCreateDto = {
|
const sentMessage: MessageCreateDto = {
|
||||||
mid: event.getId(),
|
mid: event.getId(),
|
||||||
message: event.getMessage() as StdOutgoingMessage,
|
message: event.getMessage() as StdOutgoingMessage,
|
||||||
@ -818,11 +809,21 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given request is a socket request
|
||||||
|
*
|
||||||
|
* @param req Either a HTTP express request or a WS request
|
||||||
|
* @returns True if it's a WS request
|
||||||
|
*/
|
||||||
|
isSocketRequest(req: Request | SocketRequest): req is SocketRequest {
|
||||||
|
return 'isSocket' in req && req.isSocket;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process incoming Web Channel data (finding out its type and assigning it to its proper handler)
|
* Process incoming Web Channel data (finding out its type and assigning it to its proper handler)
|
||||||
*
|
*
|
||||||
* @param req
|
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||||
* @param res
|
* @param res Either a HTTP Express response or a WS response (Synthetic Object)
|
||||||
*/
|
*/
|
||||||
async handle(req: Request | SocketRequest, res: Response | SocketResponse) {
|
async handle(req: Request | SocketRequest, res: Response | SocketResponse) {
|
||||||
const settings = await this.getSettings();
|
const settings = await this.getSettings();
|
||||||
@ -830,7 +831,7 @@ export default abstract class BaseWebChannelHandler<
|
|||||||
try {
|
try {
|
||||||
await this.checkRequest(req, res);
|
await this.checkRequest(req, res);
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (!('isSocket' in 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(
|
||||||
|
@ -17,6 +17,5 @@
|
|||||||
"show_emoji": "Enable Emoji Picker",
|
"show_emoji": "Enable Emoji Picker",
|
||||||
"show_file": "Enable Attachment Uploader",
|
"show_file": "Enable Attachment Uploader",
|
||||||
"show_location": "Enable Geolocation Share",
|
"show_location": "Enable Geolocation Share",
|
||||||
"allowed_upload_size": "Max Upload Size (in bytes)",
|
|
||||||
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
|
"allowed_upload_types": "Allowed Upload Mime Types (comma separated)"
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,5 @@
|
|||||||
"show_emoji": "Activer le sélecteur d'Emojis",
|
"show_emoji": "Activer le sélecteur d'Emojis",
|
||||||
"show_file": "Activer l'upload de fichiers",
|
"show_file": "Activer l'upload de fichiers",
|
||||||
"show_location": "Activer le partage de géolocalisation",
|
"show_location": "Activer le partage de géolocalisation",
|
||||||
"allowed_upload_size": "Taille maximale de téléchargement (en octets)",
|
|
||||||
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
|
"allowed_upload_types": "Types MIME autorisés pour le téléchargement (séparés par des virgules)"
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,6 @@
|
|||||||
import { ChannelSetting } from '@/channel/types';
|
import { ChannelSetting } from '@/channel/types';
|
||||||
import { SettingType } from '@/setting/schemas/types';
|
import { SettingType } from '@/setting/schemas/types';
|
||||||
|
|
||||||
import { Web } from './types';
|
|
||||||
|
|
||||||
export const WEB_CHANNEL_NAME = 'web-channel' as const;
|
export const WEB_CHANNEL_NAME = 'web-channel' as const;
|
||||||
|
|
||||||
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
|
export const WEB_CHANNEL_NAMESPACE = 'web_channel';
|
||||||
@ -18,82 +16,76 @@ export const WEB_CHANNEL_NAMESPACE = 'web_channel';
|
|||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.allowed_domains,
|
label: 'allowed_domains',
|
||||||
value: 'http://localhost:8080,http://localhost:4000',
|
value: 'http://localhost:8080,http://localhost:4000',
|
||||||
type: SettingType.text,
|
type: SettingType.text,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.start_button,
|
label: 'start_button',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.input_disabled,
|
label: 'input_disabled',
|
||||||
value: false,
|
value: false,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.persistent_menu,
|
label: 'persistent_menu',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.greeting_message,
|
label: 'greeting_message',
|
||||||
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
value: 'Welcome! Ready to start a conversation with our chatbot?',
|
||||||
type: SettingType.textarea,
|
type: SettingType.textarea,
|
||||||
translatable: true,
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.theme_color,
|
label: 'theme_color',
|
||||||
value: 'teal',
|
value: 'teal',
|
||||||
type: SettingType.select,
|
type: SettingType.select,
|
||||||
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
options: ['teal', 'orange', 'red', 'green', 'blue', 'dark'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.window_title,
|
label: 'window_title',
|
||||||
value: 'Widget Title',
|
value: 'Widget Title',
|
||||||
type: SettingType.text,
|
type: SettingType.text,
|
||||||
translatable: true,
|
translatable: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.avatar_url,
|
label: 'avatar_url',
|
||||||
value: '',
|
value: '',
|
||||||
type: SettingType.text,
|
type: SettingType.text,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_emoji,
|
label: 'show_emoji',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_file,
|
label: 'show_file',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.show_location,
|
label: 'show_location',
|
||||||
value: true,
|
value: true,
|
||||||
type: SettingType.checkbox,
|
type: SettingType.checkbox,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
group: WEB_CHANNEL_NAMESPACE,
|
||||||
label: Web.SettingLabel.allowed_upload_size,
|
label: 'allowed_upload_types',
|
||||||
value: 2500000,
|
|
||||||
type: SettingType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: WEB_CHANNEL_NAMESPACE,
|
|
||||||
label: Web.SettingLabel.allowed_upload_types,
|
|
||||||
value:
|
value:
|
||||||
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
'audio/mpeg,audio/x-ms-wma,audio/vnd.rn-realaudio,audio/x-wav,image/gif,image/jpeg,image/png,image/tiff,image/vnd.microsoft.icon,image/vnd.djvu,image/svg+xml,text/css,text/csv,text/html,text/plain,text/xml,video/mpeg,video/mp4,video/quicktime,video/x-ms-wmv,video/x-msvideo,video/x-flv,video/web,application/msword,application/vnd.ms-powerpoint,application/pdf,application/vnd.ms-excel,application/vnd.oasis.opendocument.presentation,application/vnd.oasis.opendocument.tex,application/vnd.oasis.opendocument.spreadsheet,application/vnd.oasis.opendocument.graphics,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
type: SettingType.textarea,
|
type: SettingType.textarea,
|
||||||
|
@ -12,24 +12,6 @@ import { FileType } from '@/chat/schemas/types/message';
|
|||||||
import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
|
import { StdQuickReply } from '@/chat/schemas/types/quick-reply';
|
||||||
|
|
||||||
export namespace Web {
|
export namespace Web {
|
||||||
export enum SettingLabel {
|
|
||||||
allowed_domains = 'allowed_domains',
|
|
||||||
start_button = 'start_button',
|
|
||||||
input_disabled = 'input_disabled',
|
|
||||||
persistent_menu = 'persistent_menu',
|
|
||||||
greeting_message = 'greeting_message',
|
|
||||||
theme_color = 'theme_color',
|
|
||||||
window_title = 'window_title',
|
|
||||||
avatar_url = 'avatar_url',
|
|
||||||
show_emoji = 'show_emoji',
|
|
||||||
show_file = 'show_file',
|
|
||||||
show_location = 'show_location',
|
|
||||||
allowed_upload_size = 'allowed_upload_size',
|
|
||||||
allowed_upload_types = 'allowed_upload_types',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Settings = Record<SettingLabel, any>;
|
|
||||||
|
|
||||||
export type RequestSession = {
|
export type RequestSession = {
|
||||||
web?: {
|
web?: {
|
||||||
profile: SubscriberFull;
|
profile: SubscriberFull;
|
||||||
@ -77,14 +59,19 @@ export namespace Web {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IncomingAttachmentMessageData = {
|
// Depending if it's has been processed or not
|
||||||
type: FileType; // mime type in a file case
|
export type IncomingAttachmentMessageData =
|
||||||
url: string; // file url
|
// After upload and attachment is processed
|
||||||
// Only when uploaded
|
| {
|
||||||
size?: number; // file size
|
type: FileType;
|
||||||
name?: string;
|
url: string; // file download url
|
||||||
file?: any;
|
} // Before upload and attachment is processed
|
||||||
};
|
| {
|
||||||
|
type: string; // mime type
|
||||||
|
size: number; // file size
|
||||||
|
name: string;
|
||||||
|
file: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
export type IncomingMessageData =
|
export type IncomingMessageData =
|
||||||
| IncomingTextMessageData
|
| IncomingTextMessageData
|
||||||
|
@ -216,6 +216,10 @@ export default class WebEventWrapper<N extends ChannelName> extends EventWrapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case IncomingMessageType.attachments:
|
case IncomingMessageType.attachments:
|
||||||
|
if (!('url' in this._adapter.raw.data)) {
|
||||||
|
throw new Error('Attachment has not been processed');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: PayloadType.attachments,
|
type: PayloadType.attachments,
|
||||||
attachments: {
|
attachments: {
|
||||||
@ -263,6 +267,11 @@ export default class WebEventWrapper<N extends ChannelName> extends EventWrapper
|
|||||||
|
|
||||||
case IncomingMessageType.attachments: {
|
case IncomingMessageType.attachments: {
|
||||||
const attachment = this._adapter.raw.data;
|
const attachment = this._adapter.raw.data;
|
||||||
|
|
||||||
|
if (!('url' in attachment)) {
|
||||||
|
throw new Error('Attachment has not been processed');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: PayloadType.attachments,
|
type: PayloadType.attachments,
|
||||||
serialized_text: `attachment:${attachment.type}:${attachment.url}`,
|
serialized_text: `attachment:${attachment.type}:${attachment.url}`,
|
||||||
|
@ -6,9 +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 { Readable } from 'stream';
|
||||||
|
|
||||||
import { Injectable, StreamableFile } from '@nestjs/common';
|
import { Injectable, StreamableFile } from '@nestjs/common';
|
||||||
|
|
||||||
import { AttachmentCreateDto } from '@/attachment/dto/attachment.dto';
|
import {
|
||||||
|
AttachmentCreateDto,
|
||||||
|
AttachmentMetadataDto,
|
||||||
|
} from '@/attachment/dto/attachment.dto';
|
||||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||||
|
|
||||||
import { BasePlugin } from './base-plugin.service';
|
import { BasePlugin } from './base-plugin.service';
|
||||||
@ -34,4 +39,9 @@ export abstract class BaseStoragePlugin extends BasePlugin {
|
|||||||
abstract downloadProfilePic(name: string): Promise<StreamableFile>;
|
abstract downloadProfilePic(name: string): Promise<StreamableFile>;
|
||||||
|
|
||||||
readAsBuffer?(attachment: Attachment): Promise<Buffer>;
|
readAsBuffer?(attachment: Attachment): Promise<Buffer>;
|
||||||
|
|
||||||
|
store?(
|
||||||
|
file: Buffer | Readable | Express.Multer.File,
|
||||||
|
metadata: AttachmentMetadataDto,
|
||||||
|
): Promise<Attachment>;
|
||||||
}
|
}
|
||||||
|
4
api/src/utils/test/fixtures/attachment.ts
vendored
4
api/src/utils/test/fixtures/attachment.ts
vendored
@ -18,7 +18,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
|
|||||||
size: 3539,
|
size: 3539,
|
||||||
location: '39991e51-55c6-4a26-9176-b6ba04f180dc.jpg',
|
location: '39991e51-55c6-4a26-9176-b6ba04f180dc.jpg',
|
||||||
channel: {
|
channel: {
|
||||||
dimelo: {
|
'web-channel': {
|
||||||
id: '1',
|
id: '1',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -30,7 +30,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
|
|||||||
size: 3539,
|
size: 3539,
|
||||||
location: '39991e51-55c6-4a26-9176-b6ba04f180dd.jpg',
|
location: '39991e51-55c6-4a26-9176-b6ba04f180dd.jpg',
|
||||||
channel: {
|
channel: {
|
||||||
dimelo: {
|
'web-channel': {
|
||||||
id: '2',
|
id: '2',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -16,7 +16,7 @@ SESSION_SECRET=f661ff500fff6b0c8f91310b6fff6b0c
|
|||||||
SESSION_NAME=s.id
|
SESSION_NAME=s.id
|
||||||
UPLOAD_DIR=/uploads
|
UPLOAD_DIR=/uploads
|
||||||
AVATAR_DIR=/avatars
|
AVATAR_DIR=/avatars
|
||||||
UPLOAD_MAX_SIZE_IN_BYTES=2000000
|
UPLOAD_MAX_SIZE_IN_BYTES=20971520
|
||||||
INVITATION_JWT_SECRET=dev_only
|
INVITATION_JWT_SECRET=dev_only
|
||||||
INVITATION_EXPIRES_IN=24h
|
INVITATION_EXPIRES_IN=24h
|
||||||
PASSWORD_RESET_JWT_SECRET=dev_only
|
PASSWORD_RESET_JWT_SECRET=dev_only
|
||||||
|
@ -23,6 +23,6 @@ export default function handler(
|
|||||||
ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false,
|
ssoEnabled: process.env.NEXT_PUBLIC_SSO_ENABLED === "true" || false,
|
||||||
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||||
: 50 * 1024 * 1024, // 50 MB in bytes
|
: 20 * 1024 * 1024, // 20 MB in bytes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,20 +6,21 @@
|
|||||||
* 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 "normalize.css";
|
||||||
|
import "./ChatWidget.css";
|
||||||
import Launcher from "./components/Launcher";
|
import Launcher from "./components/Launcher";
|
||||||
import UserSubscription from "./components/UserSubscription";
|
import UserSubscription from "./components/UserSubscription";
|
||||||
import ChatProvider from "./providers/ChatProvider";
|
import ChatProvider from "./providers/ChatProvider";
|
||||||
import { ColorProvider } from "./providers/ColorProvider";
|
import { ColorProvider } from "./providers/ColorProvider";
|
||||||
import { Config, ConfigProvider } from "./providers/ConfigProvider";
|
import { ConfigProvider } from "./providers/ConfigProvider";
|
||||||
import { CookieProvider } from "./providers/CookieProvider";
|
import { CookieProvider } from "./providers/CookieProvider";
|
||||||
import { SettingsProvider } from "./providers/SettingsProvider";
|
import { SettingsProvider } from "./providers/SettingsProvider";
|
||||||
import { SocketProvider } from "./providers/SocketProvider";
|
import { SocketProvider } from "./providers/SocketProvider";
|
||||||
import { TranslationProvider } from "./providers/TranslationProvider";
|
import { TranslationProvider } from "./providers/TranslationProvider";
|
||||||
import WidgetProvider from "./providers/WidgetProvider";
|
import WidgetProvider from "./providers/WidgetProvider";
|
||||||
import "normalize.css";
|
import { Config } from "./types/config.types";
|
||||||
import "./ChatWidget.css";
|
|
||||||
|
|
||||||
function ChatWidget(props: Config) {
|
function ChatWidget(props: Partial<Config>) {
|
||||||
return (
|
return (
|
||||||
<ConfigProvider {...props}>
|
<ConfigProvider {...props}>
|
||||||
<TranslationProvider>
|
<TranslationProvider>
|
||||||
|
@ -12,13 +12,14 @@ import Launcher from "./components/Launcher";
|
|||||||
import UserSubscription from "./components/UserSubscription";
|
import UserSubscription from "./components/UserSubscription";
|
||||||
import ChatProvider from "./providers/ChatProvider";
|
import ChatProvider from "./providers/ChatProvider";
|
||||||
import { ColorProvider } from "./providers/ColorProvider";
|
import { ColorProvider } from "./providers/ColorProvider";
|
||||||
import { Config, ConfigProvider } from "./providers/ConfigProvider";
|
import { ConfigProvider } from "./providers/ConfigProvider";
|
||||||
import { SettingsProvider } from "./providers/SettingsProvider";
|
import { SettingsProvider } from "./providers/SettingsProvider";
|
||||||
import { SocketProvider } from "./providers/SocketProvider";
|
import { SocketProvider } from "./providers/SocketProvider";
|
||||||
import { TranslationProvider } from "./providers/TranslationProvider";
|
import { TranslationProvider } from "./providers/TranslationProvider";
|
||||||
import WidgetProvider, { WidgetContextType } from "./providers/WidgetProvider";
|
import WidgetProvider, { WidgetContextType } from "./providers/WidgetProvider";
|
||||||
import "./UiChatWidget.css";
|
import { Config } from "./types/config.types";
|
||||||
import { ConnectionState } from "./types/state.types";
|
import { ConnectionState } from "./types/state.types";
|
||||||
|
import "./UiChatWidget.css";
|
||||||
|
|
||||||
type UiChatWidgetProps = PropsWithChildren<{
|
type UiChatWidgetProps = PropsWithChildren<{
|
||||||
CustomLauncher?: (props: { widget: WidgetContextType }) => JSX.Element;
|
CustomLauncher?: (props: { widget: WidgetContextType }) => JSX.Element;
|
||||||
@ -26,7 +27,7 @@ type UiChatWidgetProps = PropsWithChildren<{
|
|||||||
CustomAvatar?: () => JSX.Element;
|
CustomAvatar?: () => JSX.Element;
|
||||||
PreChat?: React.FC;
|
PreChat?: React.FC;
|
||||||
PostChat?: React.FC;
|
PostChat?: React.FC;
|
||||||
config: Config;
|
config: Partial<Config>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
function UiChatWidget({
|
function UiChatWidget({
|
||||||
|
@ -11,6 +11,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "../hooks/useTranslation";
|
import { useTranslation } from "../hooks/useTranslation";
|
||||||
import { useChat } from "../providers/ChatProvider";
|
import { useChat } from "../providers/ChatProvider";
|
||||||
import { useColors } from "../providers/ColorProvider";
|
import { useColors } from "../providers/ColorProvider";
|
||||||
|
import { useConfig } from "../providers/ConfigProvider";
|
||||||
import { useSettings } from "../providers/SettingsProvider";
|
import { useSettings } from "../providers/SettingsProvider";
|
||||||
import { TOutgoingMessageType } from "../types/message.types";
|
import { TOutgoingMessageType } from "../types/message.types";
|
||||||
import { OutgoingMessageState } from "../types/state.types";
|
import { OutgoingMessageState } from "../types/state.types";
|
||||||
@ -27,6 +28,7 @@ import Suggestions from "./Suggestions";
|
|||||||
import "./UserInput.scss";
|
import "./UserInput.scss";
|
||||||
|
|
||||||
const UserInput: React.FC = () => {
|
const UserInput: React.FC = () => {
|
||||||
|
const { maxUploadSize } = useConfig();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colors } = useColors();
|
const { colors } = useColors();
|
||||||
const {
|
const {
|
||||||
@ -43,7 +45,6 @@ const UserInput: React.FC = () => {
|
|||||||
focusOnOpen,
|
focusOnOpen,
|
||||||
autoFlush,
|
autoFlush,
|
||||||
allowedUploadTypes,
|
allowedUploadTypes,
|
||||||
allowedUploadSize,
|
|
||||||
showEmoji,
|
showEmoji,
|
||||||
showLocation,
|
showLocation,
|
||||||
showFile,
|
showFile,
|
||||||
@ -106,7 +107,7 @@ const UserInput: React.FC = () => {
|
|||||||
|
|
||||||
if (!typeCheck) {
|
if (!typeCheck) {
|
||||||
setFileError(t("messages.file_message.unsupported_file_type"));
|
setFileError(t("messages.file_message.unsupported_file_type"));
|
||||||
} else if (file.size > (allowedUploadSize || 0)) {
|
} else if (file.size > maxUploadSize) {
|
||||||
setFileError(t("messages.file_message.unsupported_file_size"));
|
setFileError(t("messages.file_message.unsupported_file_size"));
|
||||||
} else {
|
} else {
|
||||||
send({
|
send({
|
||||||
|
@ -6,8 +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).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
import { Config } from "../types/config.types";
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG: 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 || "console-channel",
|
channel: process.env.REACT_APP_WIDGET_CHANNEL || "console-channel",
|
||||||
language: "en",
|
language: "en",
|
||||||
|
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
|
||||||
|
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||||
|
: 20 * 1024 * 1024, // 20 MB in bytes
|
||||||
};
|
};
|
||||||
|
@ -9,23 +9,15 @@
|
|||||||
import React, { createContext, ReactNode, useContext, useRef } from "react";
|
import React, { createContext, ReactNode, useContext, useRef } from "react";
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "../constants/defaultConfig";
|
import { DEFAULT_CONFIG } from "../constants/defaultConfig";
|
||||||
|
import { Config } from "../types/config.types";
|
||||||
|
|
||||||
// Define the type for your config, including all possible properties
|
|
||||||
export type Config = {
|
|
||||||
apiUrl: string;
|
|
||||||
channel: string;
|
|
||||||
language: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a context with a specific type, providing better type-checking
|
|
||||||
const ConfigContext = createContext<Config>(DEFAULT_CONFIG);
|
const ConfigContext = createContext<Config>(DEFAULT_CONFIG);
|
||||||
|
|
||||||
export const ConfigProvider: React.FC<{
|
export const ConfigProvider: React.FC<
|
||||||
apiUrl?: string;
|
Partial<Config> & {
|
||||||
channel?: string;
|
children: ReactNode;
|
||||||
language?: string;
|
}
|
||||||
children: ReactNode;
|
> = ({ children, ...providedConfig }) => {
|
||||||
}> = ({ children, ...providedConfig }) => {
|
|
||||||
const config = useRef<Config>({
|
const config = useRef<Config>({
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
...providedConfig,
|
...providedConfig,
|
||||||
|
@ -35,7 +35,6 @@ type ChannelSettings = {
|
|||||||
show_location: boolean;
|
show_location: boolean;
|
||||||
allowed_upload_types: string;
|
allowed_upload_types: string;
|
||||||
greeting_message: string;
|
greeting_message: string;
|
||||||
allowed_upload_size: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChatSettings = {
|
type ChatSettings = {
|
||||||
@ -52,7 +51,6 @@ type ChatSettings = {
|
|||||||
menu: IMenuNode[];
|
menu: IMenuNode[];
|
||||||
autoFlush: boolean;
|
autoFlush: boolean;
|
||||||
allowedUploadTypes: string[];
|
allowedUploadTypes: string[];
|
||||||
allowedUploadSize: number;
|
|
||||||
color: string;
|
color: string;
|
||||||
greetingMessage: string;
|
greetingMessage: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
@ -72,7 +70,6 @@ const defaultSettings: ChatSettings = {
|
|||||||
menu: [],
|
menu: [],
|
||||||
autoFlush: true,
|
autoFlush: true,
|
||||||
allowedUploadTypes: ["image/gif", "image/png", "image/jpeg"],
|
allowedUploadTypes: ["image/gif", "image/png", "image/jpeg"],
|
||||||
allowedUploadSize: 2500000,
|
|
||||||
color: "blue",
|
color: "blue",
|
||||||
greetingMessage: "Welcome !",
|
greetingMessage: "Welcome !",
|
||||||
avatarUrl: "",
|
avatarUrl: "",
|
||||||
@ -106,7 +103,6 @@ export const SettingsProvider: React.FC<ChatSettingsProviderProps> = ({
|
|||||||
titleImageUrl: settings.avatar_url,
|
titleImageUrl: settings.avatar_url,
|
||||||
menu: settings.menu,
|
menu: settings.menu,
|
||||||
allowedUploadTypes: settings.allowed_upload_types.split(","),
|
allowedUploadTypes: settings.allowed_upload_types.split(","),
|
||||||
allowedUploadSize: settings.allowed_upload_size,
|
|
||||||
inputDisabled: settings.input_disabled,
|
inputDisabled: settings.input_disabled,
|
||||||
color: settings.theme_color,
|
color: settings.theme_color,
|
||||||
greetingMessage: settings.greeting_message,
|
greetingMessage: settings.greeting_message,
|
||||||
|
14
widget/src/types/config.types.ts
Normal file
14
widget/src/types/config.types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2024 Hexastack. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
apiUrl: string;
|
||||||
|
channel: string;
|
||||||
|
language: string;
|
||||||
|
maxUploadSize: number;
|
||||||
|
};
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { io, ManagerOptions, Socket, SocketOptions } from "socket.io-client";
|
import { io, ManagerOptions, Socket, SocketOptions } from "socket.io-client";
|
||||||
|
|
||||||
import { Config } from "../providers/ConfigProvider";
|
import { Config } from "../types/config.types";
|
||||||
import {
|
import {
|
||||||
IOIncomingMessage,
|
IOIncomingMessage,
|
||||||
IOOutgoingMessage,
|
IOOutgoingMessage,
|
||||||
|
Loading…
Reference in New Issue
Block a user