mirror of
https://github.com/hexastack/hexabot
synced 2025-05-05 21:34:41 +00:00
feat: use attachment_id instead of url + in messages + webchannel and secure public urls
This commit is contained in:
parent
d48b88f41e
commit
0737cd99c6
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -126,6 +126,7 @@ export default abstract class EventWrapper<
|
||||
/**
|
||||
* Sets an event attribute value
|
||||
*
|
||||
* @deprecated
|
||||
* @param attr - Event attribute name
|
||||
* @param value - The value to set for the specified attribute.
|
||||
*/
|
||||
@ -136,6 +137,7 @@ export default abstract class EventWrapper<
|
||||
/**
|
||||
* Returns an event attribute value, default value if it does exist
|
||||
*
|
||||
* @deprecated
|
||||
* @param attr - Event attribute name
|
||||
* @param otherwise - Default value if attribute does not exist
|
||||
*
|
||||
|
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -234,7 +234,7 @@ export default abstract class ChannelHandler<
|
||||
* @param attachment The attachment ID or object to generate a signed URL for.
|
||||
* @return A signed URL string for downloading the specified attachment.
|
||||
*/
|
||||
protected async getPublicUrl(attachment: string | Attachment) {
|
||||
public async getPublicUrl(attachment: string | Attachment) {
|
||||
const resource =
|
||||
typeof attachment === 'string'
|
||||
? await this.attachmentService.findOne(attachment)
|
||||
|
@ -17,8 +17,9 @@ export enum FileType {
|
||||
}
|
||||
|
||||
export type AttachmentForeignKey = {
|
||||
url?: string;
|
||||
attachment_id: string;
|
||||
/** @deprecated use "attachment_id" instead */
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export interface AttachmentPayload<
|
||||
@ -30,7 +31,5 @@ export interface AttachmentPayload<
|
||||
|
||||
export interface IncomingAttachmentPayload {
|
||||
type: FileType;
|
||||
payload: {
|
||||
url: string;
|
||||
};
|
||||
payload: AttachmentForeignKey;
|
||||
}
|
||||
|
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -390,6 +390,7 @@ describe('BlockService', () => {
|
||||
attachments: {
|
||||
type: FileType.file,
|
||||
payload: {
|
||||
attachment_id: '9'.repeat(24),
|
||||
url: 'http://link.to/the/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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -77,7 +77,7 @@ export const webList: Web.OutgoingMessageBase = {
|
||||
type: ButtonType.postback,
|
||||
},
|
||||
],
|
||||
image_url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
|
||||
image_url: 'http://public.url/download/filename.extension?t=any',
|
||||
subtitle: 'About being first',
|
||||
title: 'First',
|
||||
},
|
||||
@ -89,7 +89,7 @@ export const webList: Web.OutgoingMessageBase = {
|
||||
type: ButtonType.postback,
|
||||
},
|
||||
],
|
||||
image_url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
|
||||
image_url: 'http://public.url/download/filename.extension?t=any',
|
||||
subtitle: 'About being second',
|
||||
title: 'Second',
|
||||
},
|
||||
@ -109,7 +109,7 @@ export const webCarousel: Web.OutgoingMessageBase = {
|
||||
type: ButtonType.postback,
|
||||
},
|
||||
],
|
||||
image_url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
|
||||
image_url: 'http://public.url/download/filename.extension?t=any',
|
||||
subtitle: 'About being first',
|
||||
title: 'First',
|
||||
},
|
||||
@ -121,7 +121,7 @@ export const webCarousel: Web.OutgoingMessageBase = {
|
||||
type: ButtonType.postback,
|
||||
},
|
||||
],
|
||||
image_url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
|
||||
image_url: 'http://public.url/download/filename.extension?t=any',
|
||||
subtitle: 'About being second',
|
||||
title: 'Second',
|
||||
},
|
||||
@ -140,7 +140,7 @@ export const webAttachment: Web.OutgoingMessageBase = {
|
||||
},
|
||||
],
|
||||
type: FileType.image,
|
||||
url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
|
||||
url: 'http://public.url/download/filename.extension?t=any',
|
||||
},
|
||||
type: Web.OutgoingMessageType.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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -14,9 +14,6 @@ import {
|
||||
|
||||
import { Web } from '../types';
|
||||
|
||||
const img_url =
|
||||
'http://demo.hexabot.ai/attachment/download/5c334078e2c41d11206bd152/myimage.png';
|
||||
|
||||
// Web events
|
||||
const webEventPayload: Web.Event = {
|
||||
type: Web.IncomingMessageType.postback,
|
||||
@ -55,9 +52,10 @@ const webEventLocation: Web.IncomingMessage = {
|
||||
const webEventFile: Web.Event = {
|
||||
type: Web.IncomingMessageType.file,
|
||||
data: {
|
||||
type: FileType.image,
|
||||
url: img_url,
|
||||
type: 'image/png',
|
||||
size: 500,
|
||||
name: 'filename.extension',
|
||||
file: Buffer.from('my-image', 'utf-8'),
|
||||
},
|
||||
author: 'web-9be8ac09-b43a-432d-bca0-f11b98cec1ad',
|
||||
mid: 'web-event-file',
|
||||
@ -151,18 +149,18 @@ export const webEvents: [string, Web.IncomingMessage, any][] = [
|
||||
attachments: {
|
||||
type: FileType.image,
|
||||
payload: {
|
||||
url: img_url,
|
||||
attachment_id: '9'.repeat(24),
|
||||
},
|
||||
},
|
||||
},
|
||||
message: {
|
||||
attachment: {
|
||||
payload: {
|
||||
url: img_url,
|
||||
attachment_id: '9'.repeat(24),
|
||||
},
|
||||
type: FileType.image,
|
||||
},
|
||||
serialized_text: `attachment:image:${img_url}`,
|
||||
serialized_text: 'attachment:image:filename.extension',
|
||||
type: IncomingMessageType.attachments,
|
||||
},
|
||||
},
|
||||
|
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -125,9 +125,14 @@ describe('WebChannelHandler', () => {
|
||||
}).compile();
|
||||
subscriberService = module.get<SubscriberService>(SubscriberService);
|
||||
handler = module.get<WebChannelHandler>(WebChannelHandler);
|
||||
|
||||
jest
|
||||
.spyOn(handler, 'getPublicUrl')
|
||||
.mockResolvedValue('http://public.url/download/filename.extension?t=any');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
jest.restoreAllMocks();
|
||||
await closeInMongodConnection();
|
||||
});
|
||||
|
||||
@ -204,15 +209,15 @@ describe('WebChannelHandler', () => {
|
||||
expect(formatted).toEqual(webButtons);
|
||||
});
|
||||
|
||||
it('should format list properly', () => {
|
||||
const formatted = handler._listFormat(contentMessage, {
|
||||
it('should format list properly', async () => {
|
||||
const formatted = await handler._listFormat(contentMessage, {
|
||||
content: contentMessage.options,
|
||||
});
|
||||
expect(formatted).toEqual(webList);
|
||||
});
|
||||
|
||||
it('should format carousel properly', () => {
|
||||
const formatted = handler._carouselFormat(contentMessage, {
|
||||
it('should format carousel properly', async () => {
|
||||
const formatted = await handler._carouselFormat(contentMessage, {
|
||||
content: {
|
||||
...contentMessage.options,
|
||||
display: OutgoingMessageFormat.carousel,
|
||||
@ -221,8 +226,8 @@ describe('WebChannelHandler', () => {
|
||||
expect(formatted).toEqual(webCarousel);
|
||||
});
|
||||
|
||||
it('should format attachment properly', () => {
|
||||
const formatted = handler._attachmentFormat(attachmentMessage, {});
|
||||
it('should format attachment properly', async () => {
|
||||
const formatted = await handler._attachmentFormat(attachmentMessage, {});
|
||||
expect(formatted).toEqual(webAttachment);
|
||||
});
|
||||
|
||||
|
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -13,13 +13,20 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
|
||||
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentModel,
|
||||
} from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { ChannelService } from '@/channel/channel.service';
|
||||
import { MessageRepository } from '@/chat/repositories/message.repository';
|
||||
import { SubscriberRepository } from '@/chat/repositories/subscriber.repository';
|
||||
import { MessageModel } from '@/chat/schemas/message.schema';
|
||||
import { SubscriberModel } from '@/chat/schemas/subscriber.schema';
|
||||
import {
|
||||
IncomingMessageType,
|
||||
StdEventType,
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { MessageService } from '@/chat/services/message.service';
|
||||
import { SubscriberService } from '@/chat/services/subscriber.service';
|
||||
import { MenuRepository } from '@/cms/repositories/menu.repository';
|
||||
@ -122,6 +129,18 @@ describe(`Web event wrapper`, () => {
|
||||
e,
|
||||
expected.channelData,
|
||||
);
|
||||
|
||||
if (
|
||||
event._adapter.eventType === StdEventType.message &&
|
||||
event._adapter.messageType === IncomingMessageType.attachments
|
||||
) {
|
||||
event._adapter.attachment = {
|
||||
id: '9'.repeat(24),
|
||||
type: 'image/png',
|
||||
name: 'filename.extension',
|
||||
} as Attachment;
|
||||
}
|
||||
|
||||
expect(event.getChannelData()).toEqual({
|
||||
...expected.channelData,
|
||||
name: WEB_CHANNEL_NAME,
|
||||
|
@ -22,11 +22,13 @@ import { MessageCreateDto } from '@/chat/dto/message.dto';
|
||||
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
||||
import { VIEW_MORE_PAYLOAD } from '@/chat/helpers/constants';
|
||||
import { Subscriber, SubscriberFull } from '@/chat/schemas/subscriber.schema';
|
||||
import { AttachmentForeignKey } from '@/chat/schemas/types/attachment';
|
||||
import { Button, ButtonType } from '@/chat/schemas/types/button';
|
||||
import {
|
||||
AnyMessage,
|
||||
ContentElement,
|
||||
IncomingMessage,
|
||||
IncomingMessageType,
|
||||
OutgoingMessage,
|
||||
OutgoingMessageFormat,
|
||||
PayloadType,
|
||||
@ -127,9 +129,9 @@ export default abstract class BaseWebChannelHandler<
|
||||
* @param incoming - Incoming message
|
||||
* @returns Formatted web message
|
||||
*/
|
||||
private formatIncomingHistoryMessage(
|
||||
private async formatIncomingHistoryMessage(
|
||||
incoming: IncomingMessage,
|
||||
): Web.IncomingMessageBase {
|
||||
): Promise<Web.IncomingMessageBase> {
|
||||
// Format incoming message
|
||||
if ('type' in incoming.message) {
|
||||
if (incoming.message.type === PayloadType.location) {
|
||||
@ -145,14 +147,17 @@ export default abstract class BaseWebChannelHandler<
|
||||
};
|
||||
} else {
|
||||
// @TODO : handle multiple files
|
||||
const attachment = Array.isArray(incoming.message.attachment)
|
||||
const attachmentPayload = Array.isArray(incoming.message.attachment)
|
||||
? incoming.message.attachment[0]
|
||||
: incoming.message.attachment;
|
||||
|
||||
return {
|
||||
type: Web.IncomingMessageType.file,
|
||||
data: {
|
||||
type: attachment.type,
|
||||
url: attachment.payload.url,
|
||||
type: attachmentPayload.type,
|
||||
url: await this.getPublicUrl(
|
||||
attachmentPayload.payload.attachment_id,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -170,9 +175,9 @@ export default abstract class BaseWebChannelHandler<
|
||||
* @param outgoing - The outgoing message
|
||||
* @returns Formatted web message
|
||||
*/
|
||||
private formatOutgoingHistoryMessage(
|
||||
private async formatOutgoingHistoryMessage(
|
||||
outgoing: OutgoingMessage,
|
||||
): Web.OutgoingMessageBase {
|
||||
): Promise<Web.OutgoingMessageBase> {
|
||||
// Format outgoing message
|
||||
if ('buttons' in outgoing.message) {
|
||||
return this._buttonsFormat(outgoing.message);
|
||||
@ -182,11 +187,11 @@ export default abstract class BaseWebChannelHandler<
|
||||
return this._quickRepliesFormat(outgoing.message);
|
||||
} else if ('options' in outgoing.message) {
|
||||
if (outgoing.message.options.display === 'carousel') {
|
||||
return this._carouselFormat(outgoing.message, {
|
||||
return await this._carouselFormat(outgoing.message, {
|
||||
content: outgoing.message.options,
|
||||
});
|
||||
} else {
|
||||
return this._listFormat(outgoing.message, {
|
||||
return await this._listFormat(outgoing.message, {
|
||||
content: outgoing.message.options,
|
||||
});
|
||||
}
|
||||
@ -212,12 +217,14 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns Formatted message
|
||||
*/
|
||||
protected formatMessages(messages: AnyMessage[]): Web.Message[] {
|
||||
protected async formatMessages(
|
||||
messages: AnyMessage[],
|
||||
): Promise<Web.Message[]> {
|
||||
const formattedMessages: Web.Message[] = [];
|
||||
|
||||
for (const anyMessage of messages) {
|
||||
if (this.isIncomingMessage(anyMessage)) {
|
||||
const message = this.formatIncomingHistoryMessage(anyMessage);
|
||||
const message = await this.formatIncomingHistoryMessage(anyMessage);
|
||||
formattedMessages.push({
|
||||
...message,
|
||||
author: anyMessage.sender,
|
||||
@ -226,7 +233,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
createdAt: anyMessage.createdAt,
|
||||
});
|
||||
} else {
|
||||
const message = this.formatOutgoingHistoryMessage(anyMessage);
|
||||
const message = await this.formatOutgoingHistoryMessage(anyMessage);
|
||||
formattedMessages.push({
|
||||
...message,
|
||||
author: 'chatbot',
|
||||
@ -261,7 +268,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
until,
|
||||
n,
|
||||
);
|
||||
return this.formatMessages(messages.reverse());
|
||||
return await this.formatMessages(messages.reverse());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@ -286,7 +293,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
since,
|
||||
n,
|
||||
);
|
||||
return this.formatMessages(messages);
|
||||
return await this.formatMessages(messages);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@ -579,9 +586,8 @@ export default abstract class BaseWebChannelHandler<
|
||||
'since' in req.query
|
||||
? req.query.since // Long polling case
|
||||
: req.body?.since || undefined; // Websocket case
|
||||
return this.fetchHistory(req, criteria).then((messages) => {
|
||||
const messages = await this.fetchHistory(req, criteria);
|
||||
return res.status(200).json({ profile, messages });
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.warn('Web Channel Handler : Unable to subscribe ', err);
|
||||
return res.status(500).json({ err: 'Unable to subscribe' });
|
||||
@ -589,40 +595,24 @@ export default abstract class BaseWebChannelHandler<
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file as attachment if provided
|
||||
* Handle upload via WebSocket
|
||||
*
|
||||
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||
* @param res Either a HTTP Express response or a WS response (Synthetic Object)
|
||||
* @param next Callback Function
|
||||
* @returns The stored attachment or null
|
||||
*/
|
||||
async handleFilesUpload(
|
||||
req: Request | SocketRequest,
|
||||
res: Response | SocketResponse,
|
||||
next: (
|
||||
err: null | Error,
|
||||
result?: Web.IncomingAttachmentMessageData,
|
||||
) => void,
|
||||
): Promise<void> {
|
||||
// Check if any file is provided
|
||||
if (!req.session.web) {
|
||||
this.logger.debug('Web Channel Handler : No session provided');
|
||||
return next(null);
|
||||
}
|
||||
|
||||
if (this.isSocketRequest(req)) {
|
||||
async handleWsUpload(req: SocketRequest): Promise<Attachment | null> {
|
||||
try {
|
||||
const { type, data } = req.body as Web.IncomingMessage;
|
||||
|
||||
// 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);
|
||||
return null;
|
||||
}
|
||||
|
||||
const size = Buffer.byteLength(data.file);
|
||||
|
||||
if (size > config.parameters.maxUploadSize) {
|
||||
return next(new Error('Max upload size has been exceeded'));
|
||||
throw new Error('Max upload size has been exceeded');
|
||||
}
|
||||
|
||||
const attachment = await this.attachmentService.store(data.file, {
|
||||
@ -630,18 +620,26 @@ export default abstract class BaseWebChannelHandler<
|
||||
size: Buffer.byteLength(data.file),
|
||||
type: data.type,
|
||||
});
|
||||
next(null, {
|
||||
type: Attachment.getTypeByMime(attachment.type),
|
||||
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
|
||||
});
|
||||
return attachment;
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
'Web Channel Handler : Unable to write uploaded file',
|
||||
'Web Channel Handler : Unable to store uploaded file',
|
||||
err,
|
||||
);
|
||||
return next(new Error('Unable to upload file!'));
|
||||
throw new Error('Unable to upload file!');
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle multipart/form-data upload
|
||||
*
|
||||
* @returns The stored attachment or null
|
||||
*/
|
||||
async handleWebUpload(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<Attachment | null> {
|
||||
try {
|
||||
const upload = multer({
|
||||
limits: {
|
||||
fileSize: config.parameters.maxUploadSize,
|
||||
@ -655,36 +653,66 @@ export default abstract class BaseWebChannelHandler<
|
||||
})(),
|
||||
}).single('file'); // 'file' is the field name in the form
|
||||
|
||||
const multerUpload = new Promise<Express.Multer.File | null>(
|
||||
(resolve, reject) => {
|
||||
upload(req as Request, res as Response, async (err?: any) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
'Web Channel Handler : Unable to write uploaded file',
|
||||
'Web Channel Handler : Unable to store uploaded file',
|
||||
err,
|
||||
);
|
||||
return next(new Error('Unable to upload file!'));
|
||||
reject(new Error('Unable to upload file!'));
|
||||
}
|
||||
|
||||
resolve(req.file);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const file = await multerUpload;
|
||||
|
||||
// Check if any file is provided
|
||||
if (!req.file) {
|
||||
this.logger.debug('Web Channel Handler : No files provided');
|
||||
return next(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = req.file;
|
||||
const attachment = await this.attachmentService.store(file, {
|
||||
name: file.originalname,
|
||||
size: file.size,
|
||||
type: file.mimetype,
|
||||
});
|
||||
next(null, {
|
||||
type: Attachment.getTypeByMime(attachment.type),
|
||||
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
|
||||
});
|
||||
return attachment;
|
||||
} catch (err) {
|
||||
next(err);
|
||||
this.logger.error(
|
||||
'Web Channel Handler : Unable to store uploaded file',
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file as attachment if provided
|
||||
*
|
||||
* @param req Either a HTTP Express request or a WS request (Synthetic Object)
|
||||
* @param res Either a HTTP Express response or a WS response (Synthetic Object)
|
||||
* @param next Callback Function
|
||||
*/
|
||||
async handleUpload(
|
||||
req: Request | SocketRequest,
|
||||
res: Response | SocketResponse,
|
||||
): Promise<Attachment | null> {
|
||||
// Check if any file is provided
|
||||
if (!req.session.web) {
|
||||
this.logger.debug('Web Channel Handler : No session provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.isSocketRequest(req)) {
|
||||
return this.handleWsUpload(req);
|
||||
} else {
|
||||
return this.handleWebUpload(req, res as Response);
|
||||
}
|
||||
}
|
||||
|
||||
@ -748,12 +776,21 @@ export default abstract class BaseWebChannelHandler<
|
||||
: req.body.data;
|
||||
}
|
||||
|
||||
this.validateSession(req, res, (profile) => {
|
||||
this.handleFilesUpload(
|
||||
req,
|
||||
res,
|
||||
(err: Error, data?: Web.IncomingAttachmentMessageData) => {
|
||||
if (err) {
|
||||
this.validateSession(req, res, async (profile) => {
|
||||
// Set data in file upload case
|
||||
const body: Web.IncomingMessage = req.body;
|
||||
|
||||
const channelAttrs = this.getChannelAttributes(req);
|
||||
const event = new WebEventWrapper<N>(this, body, channelAttrs);
|
||||
if (event._adapter.eventType === StdEventType.message) {
|
||||
// Handle upload when files are provided
|
||||
if (event._adapter.messageType === IncomingMessageType.attachments) {
|
||||
try {
|
||||
const attachment = await this.handleUpload(req, res);
|
||||
if (attachment) {
|
||||
event._adapter.attachment = attachment;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
'Web Channel Handler : Unable to upload file ',
|
||||
err,
|
||||
@ -762,17 +799,8 @@ export default abstract class BaseWebChannelHandler<
|
||||
.status(403)
|
||||
.json({ err: 'Web Channel Handler : File upload failed!' });
|
||||
}
|
||||
// Set data in file upload case
|
||||
const body: Web.IncomingMessage = data
|
||||
? {
|
||||
...req.body,
|
||||
data,
|
||||
}
|
||||
: req.body;
|
||||
|
||||
const channelAttrs = this.getChannelAttributes(req);
|
||||
const event = new WebEventWrapper<N>(this, body, channelAttrs);
|
||||
if (event.getEventType() === 'message') {
|
||||
// Handler sync message sent by chabbot
|
||||
if (body.sync && body.author === 'chatbot') {
|
||||
const sentMessage: MessageCreateDto = {
|
||||
@ -786,11 +814,12 @@ export default abstract class BaseWebChannelHandler<
|
||||
return res.status(200).json(event._adapter.raw);
|
||||
} else {
|
||||
// Generate unique ID and handle message
|
||||
event.set('mid', this.generateId());
|
||||
}
|
||||
}
|
||||
event._adapter.raw.mid = this.generateId();
|
||||
// Force author id from session
|
||||
event.set('author', profile.foreign_id);
|
||||
event._adapter.raw.author = profile.foreign_id;
|
||||
}
|
||||
}
|
||||
|
||||
event.setSender(profile);
|
||||
|
||||
const type = event.getEventType();
|
||||
@ -803,8 +832,6 @@ export default abstract class BaseWebChannelHandler<
|
||||
);
|
||||
}
|
||||
res.status(200).json(event._adapter.raw);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -957,15 +984,15 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns A ready to be sent attachment message
|
||||
*/
|
||||
_attachmentFormat(
|
||||
async _attachmentFormat(
|
||||
message: StdOutgoingAttachmentMessage<Attachment>,
|
||||
_options?: BlockOptions,
|
||||
): Web.OutgoingMessageBase {
|
||||
): Promise<Web.OutgoingMessageBase> {
|
||||
const payload: Web.OutgoingMessageBase = {
|
||||
type: Web.OutgoingMessageType.file,
|
||||
data: {
|
||||
type: message.attachment.type,
|
||||
url: message.attachment.payload.url,
|
||||
url: await this.getPublicUrl(message.attachment.payload.id),
|
||||
},
|
||||
};
|
||||
if (message.quickReplies && message.quickReplies.length > 0) {
|
||||
@ -982,35 +1009,42 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns An array of elements object
|
||||
*/
|
||||
_formatElements(
|
||||
async _formatElements(
|
||||
data: ContentElement[],
|
||||
options: BlockOptions,
|
||||
): Web.MessageElement[] {
|
||||
): Promise<Web.MessageElement[]> {
|
||||
if (!options.content || !options.content.fields) {
|
||||
throw new Error('Content options are missing the fields');
|
||||
}
|
||||
|
||||
const fields = options.content.fields;
|
||||
const buttons: Button[] = options.content.buttons;
|
||||
return data.map((item) => {
|
||||
const result: Web.MessageElement[] = [];
|
||||
|
||||
for (const item of data) {
|
||||
const element: Web.MessageElement = {
|
||||
title: item[fields.title],
|
||||
buttons: item.buttons || [],
|
||||
};
|
||||
|
||||
if (fields.subtitle && item[fields.subtitle]) {
|
||||
element.subtitle = item[fields.subtitle];
|
||||
}
|
||||
|
||||
if (fields.image_url && item[fields.image_url]) {
|
||||
const attachmentPayload = item[fields.image_url].payload;
|
||||
const attachmentPayload = item[fields.image_url]
|
||||
.payload as AttachmentForeignKey;
|
||||
if (attachmentPayload.url) {
|
||||
if (!attachmentPayload.id) {
|
||||
if (!attachmentPayload.attachment_id) {
|
||||
// @deprecated
|
||||
this.logger.warn(
|
||||
'Web Channel Handler: Attachment remote url has been deprecated',
|
||||
item,
|
||||
);
|
||||
}
|
||||
element.image_url = attachmentPayload.url;
|
||||
element.image_url = await this.getPublicUrl(
|
||||
attachmentPayload.attachment_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1047,11 +1081,15 @@ export default abstract class BaseWebChannelHandler<
|
||||
}
|
||||
element.buttons?.push(btn);
|
||||
});
|
||||
|
||||
if (Array.isArray(element.buttons) && element.buttons.length === 0) {
|
||||
delete element.buttons;
|
||||
}
|
||||
return element;
|
||||
});
|
||||
|
||||
result.push(element);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1062,10 +1100,10 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns A ready to be sent list template message
|
||||
*/
|
||||
_listFormat(
|
||||
async _listFormat(
|
||||
message: StdOutgoingListMessage,
|
||||
options: BlockOptions,
|
||||
): Web.OutgoingMessageBase {
|
||||
): Promise<Web.OutgoingMessageBase> {
|
||||
const data = message.elements || [];
|
||||
const pagination = message.pagination;
|
||||
let buttons: Button[] = [],
|
||||
@ -1091,7 +1129,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
}
|
||||
|
||||
// Populate items (elements/cards) with content
|
||||
elements = this._formatElements(data, options);
|
||||
elements = await this._formatElements(data, options);
|
||||
const topElementStyle = options.content?.top_element_style
|
||||
? {
|
||||
top_element_style: options.content?.top_element_style,
|
||||
@ -1115,10 +1153,10 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns A carousel ready to be sent as a message
|
||||
*/
|
||||
_carouselFormat(
|
||||
async _carouselFormat(
|
||||
message: StdOutgoingListMessage,
|
||||
options: BlockOptions,
|
||||
): Web.OutgoingMessageBase {
|
||||
): Promise<Web.OutgoingMessageBase> {
|
||||
const data = message.elements || [];
|
||||
// Items count min check
|
||||
if (data.length === 0) {
|
||||
@ -1129,7 +1167,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
}
|
||||
|
||||
// Populate items (elements/cards) with content
|
||||
const elements = this._formatElements(data, options);
|
||||
const elements = await this._formatElements(data, options);
|
||||
return {
|
||||
type: Web.OutgoingMessageType.carousel,
|
||||
data: {
|
||||
@ -1146,19 +1184,19 @@ export default abstract class BaseWebChannelHandler<
|
||||
*
|
||||
* @returns A template filled with its payload
|
||||
*/
|
||||
_formatMessage(
|
||||
async _formatMessage(
|
||||
envelope: StdOutgoingEnvelope,
|
||||
options: BlockOptions,
|
||||
): Web.OutgoingMessageBase {
|
||||
): Promise<Web.OutgoingMessageBase> {
|
||||
switch (envelope.format) {
|
||||
case OutgoingMessageFormat.attachment:
|
||||
return this._attachmentFormat(envelope.message, options);
|
||||
return await this._attachmentFormat(envelope.message, options);
|
||||
case OutgoingMessageFormat.buttons:
|
||||
return this._buttonsFormat(envelope.message, options);
|
||||
case OutgoingMessageFormat.carousel:
|
||||
return this._carouselFormat(envelope.message, options);
|
||||
return await this._carouselFormat(envelope.message, options);
|
||||
case OutgoingMessageFormat.list:
|
||||
return this._listFormat(envelope.message, options);
|
||||
return await this._listFormat(envelope.message, options);
|
||||
case OutgoingMessageFormat.quickReplies:
|
||||
return this._quickRepliesFormat(envelope.message, options);
|
||||
case OutgoingMessageFormat.text:
|
||||
@ -1206,7 +1244,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
options: BlockOptions,
|
||||
_context?: any,
|
||||
): Promise<{ mid: string }> {
|
||||
const messageBase: Web.OutgoingMessageBase = this._formatMessage(
|
||||
const messageBase: Web.OutgoingMessageBase = await this._formatMessage(
|
||||
envelope,
|
||||
options,
|
||||
);
|
||||
|
@ -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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
@ -59,13 +59,13 @@ export namespace Web {
|
||||
};
|
||||
};
|
||||
|
||||
// Depending if it's has been processed or not
|
||||
export type IncomingAttachmentMessageData =
|
||||
// After upload and attachment is processed
|
||||
// When it's a incoming history message
|
||||
| {
|
||||
type: FileType;
|
||||
url: string; // file download url
|
||||
} // Before upload and attachment is processed
|
||||
}
|
||||
// When it's a file upload message
|
||||
| {
|
||||
type: string; // mime type
|
||||
size: number; // file size
|
||||
|
@ -1,11 +1,12 @@
|
||||
/*
|
||||
* 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:
|
||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import EventWrapper from '@/channel/lib/EventWrapper';
|
||||
import { ChannelName } from '@/channel/types';
|
||||
import {
|
||||
@ -66,14 +67,13 @@ type WebEventAdapter =
|
||||
eventType: StdEventType.message;
|
||||
messageType: IncomingMessageType.attachments;
|
||||
raw: Web.IncomingMessage<Web.IncomingAttachmentMessage>;
|
||||
attachment: Attachment | null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
export default class WebEventWrapper<N extends ChannelName> extends EventWrapper<
|
||||
WebEventAdapter,
|
||||
Web.Event,
|
||||
N
|
||||
> {
|
||||
export default class WebEventWrapper<
|
||||
N extends ChannelName,
|
||||
> extends EventWrapper<WebEventAdapter, Web.Event, N> {
|
||||
/**
|
||||
* Constructor : channel's event wrapper
|
||||
*
|
||||
@ -216,16 +216,16 @@ export default class WebEventWrapper<N extends ChannelName> extends EventWrapper
|
||||
};
|
||||
}
|
||||
case IncomingMessageType.attachments:
|
||||
if (!('url' in this._adapter.raw.data)) {
|
||||
if (!this._adapter.attachment) {
|
||||
throw new Error('Attachment has not been processed');
|
||||
}
|
||||
|
||||
return {
|
||||
type: PayloadType.attachments,
|
||||
attachments: {
|
||||
type: this._adapter.raw.data.type,
|
||||
type: Attachment.getTypeByMime(this._adapter.raw.data.type),
|
||||
payload: {
|
||||
url: this._adapter.raw.data.url,
|
||||
attachment_id: this._adapter.attachment.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -266,19 +266,20 @@ export default class WebEventWrapper<N extends ChannelName> extends EventWrapper
|
||||
}
|
||||
|
||||
case IncomingMessageType.attachments: {
|
||||
const attachment = this._adapter.raw.data;
|
||||
|
||||
if (!('url' in attachment)) {
|
||||
if (!this._adapter.attachment) {
|
||||
throw new Error('Attachment has not been processed');
|
||||
}
|
||||
|
||||
const fileType = Attachment.getTypeByMime(
|
||||
this._adapter.attachment.type,
|
||||
);
|
||||
return {
|
||||
type: PayloadType.attachments,
|
||||
serialized_text: `attachment:${attachment.type}:${attachment.url}`,
|
||||
serialized_text: `attachment:${fileType}:${this._adapter.attachment.name}`,
|
||||
attachment: {
|
||||
type: attachment.type,
|
||||
type: fileType,
|
||||
payload: {
|
||||
url: attachment.url,
|
||||
attachment_id: this._adapter.attachment.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user