Merge pull request #546 from Hexastack/refactor/attachment-payload
Some checks are pending
Build and Push Docker API Image / build-and-push (push) Waiting to run
Build and Push Docker Base Image / build-and-push (push) Waiting to run
Build and Push Docker UI Image / build-and-push (push) Waiting to run

feat: Refactor attachment payload + use public signed urls in web channel
This commit is contained in:
Med Marrouchi
2025-01-14 19:12:00 +01:00
committed by GitHub
38 changed files with 692 additions and 576 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@@ -7,10 +7,7 @@
*/
import { Subscriber } from '@/chat/schemas/subscriber.schema';
import {
AttachmentForeignKey,
AttachmentPayload,
} from '@/chat/schemas/types/attachment';
import { AttachmentPayload } from '@/chat/schemas/types/attachment';
import { SubscriberChannelData } from '@/chat/schemas/types/channel';
import {
IncomingMessageType,
@@ -101,7 +98,7 @@ export default abstract class EventWrapper<
*
* @returns The current instance of the channel handler.
*/
getHandler(): ChannelHandler {
getHandler(): C {
return this._handler;
}
@@ -126,6 +123,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 +134,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
*
@@ -190,6 +189,16 @@ export default abstract class EventWrapper<
this._profile = profile;
}
/**
* Pre-Process the message event
*
* Child class can perform operations such as storing files as attachments.
*/
preprocess() {
// Nothing ...
return Promise.resolve();
}
/**
* Returns event recipient id
*
@@ -249,7 +258,7 @@ export default abstract class EventWrapper<
*
* @returns Received attachments message
*/
abstract getAttachments(): AttachmentPayload<AttachmentForeignKey>[];
abstract getAttachments(): AttachmentPayload[];
/**
* Returns the list of delivered messages
@@ -378,7 +387,7 @@ export class GenericEventWrapper extends EventWrapper<
* @returns A list of received attachments
* @deprecated - This method is deprecated
*/
getAttachments(): AttachmentPayload<AttachmentForeignKey>[] {
getAttachments(): AttachmentPayload[] {
return [];
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@@ -21,6 +21,7 @@ import { NextFunction, Request, Response } from 'express';
import { Attachment } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
import { AttachmentRef } from '@/chat/schemas/types/attachment';
import {
StdOutgoingEnvelope,
StdOutgoingMessage,
@@ -234,22 +235,32 @@ 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) {
const resource =
typeof attachment === 'string'
? await this.attachmentService.findOne(attachment)
: attachment;
public async getPublicUrl(attachment: AttachmentRef | Attachment) {
if ('id' in attachment) {
if (!attachment.id) {
throw new TypeError(
'Attachment ID is empty, unable to generate public URL.',
);
}
if (!resource) {
throw new NotFoundException('Unable to find attachment');
const resource = await this.attachmentService.findOne(attachment.id);
if (!resource) {
throw new NotFoundException('Unable to find attachment');
}
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
const [name, _suffix] = this.getName().split('-');
return buildURL(
config.apiBaseUrl,
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
);
} else if ('url' in attachment && attachment.url) {
// In case the url is external
return attachment.url;
} else {
throw new TypeError('Unable to resolve the attachment public URL.');
}
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
const [name, _suffix] = this.getName().split('-');
return buildURL(
config.apiBaseUrl,
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
);
}
/**
@@ -266,7 +277,11 @@ export default abstract class ChannelHandler<
*/
public async download(token: string, _req: Request) {
try {
const result = this.jwtService.verify(token, this.jwtSignOptions);
const {
exp: _exp,
iat: _iat,
...result
} = this.jwtService.verify(token, this.jwtSignOptions);
const attachment = plainToClass(Attachment, result);
return await this.attachmentService.download(attachment);
} catch (err) {

View File

@@ -78,25 +78,20 @@ export const urlButtonsMessage: StdOutgoingButtonsMessage = {
};
const attachment: Attachment = {
id: '1',
id: '1'.repeat(24),
name: 'attachment.jpg',
type: 'image/jpeg',
size: 3539,
location: '39991e51-55c6-4a26-9176-b6ba04f180dc.jpg',
channel: {
['dimelo']: {
id: 'attachment-id-dimelo',
['any-channel']: {
id: 'any-channel-attachment-id',
},
},
createdAt: new Date(),
updatedAt: new Date(),
};
const attachmentWithUrl: Attachment = {
...attachment,
url: 'http://localhost:4000/attachment/download/1/attachment.jpg',
};
export const contentMessage: StdOutgoingListMessage = {
options: {
display: OutgoingMessageFormat.list,
@@ -121,7 +116,8 @@ export const contentMessage: StdOutgoingListMessage = {
title: 'First',
desc: 'About being first',
thumbnail: {
payload: attachmentWithUrl,
type: 'image',
payload: { id: attachment.id },
},
getPayload() {
return this.title;
@@ -136,7 +132,8 @@ export const contentMessage: StdOutgoingListMessage = {
title: 'Second',
desc: 'About being second',
thumbnail: {
payload: attachmentWithUrl,
type: 'image',
payload: { id: attachment.id },
},
getPayload() {
return this.title;
@@ -149,14 +146,14 @@ export const contentMessage: StdOutgoingListMessage = {
pagination: {
total: 3,
skip: 0,
limit: 1,
limit: 2,
},
};
export const attachmentMessage: StdOutgoingAttachmentMessage<Attachment> = {
export const attachmentMessage: StdOutgoingAttachmentMessage = {
attachment: {
type: FileType.image,
payload: attachmentWithUrl,
payload: { id: attachment.id },
},
quickReplies: [
{