fix: leave support for url in attachment payload (case for external urls)

This commit is contained in:
Mohamed Marrouchi 2025-01-13 16:20:01 +01:00
parent d7cb39f9f4
commit f399416553
8 changed files with 83 additions and 37 deletions

View File

@ -98,7 +98,7 @@ export default abstract class EventWrapper<
*
* @returns The current instance of the channel handler.
*/
getHandler(): ChannelHandler {
getHandler(): C {
return this._handler;
}
@ -189,6 +189,16 @@ export default abstract class EventWrapper<
this._profile = profile;
}
/**
* Pre-Process messageevent
*
* Child class can perform operations such as storing files as attachments.
*/
preprocess() {
// Nothing ...
return Promise.resolve();
}
/**
* Returns event recipient id
*

View File

@ -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.
*/
public 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)}`,
);
}
/**

View File

@ -78,14 +78,14 @@ 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(),

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.
@ -111,6 +111,16 @@ export class SubscriberCreateDto {
@IsNotEmpty()
@IsChannelData()
channel: SubscriberChannelData<ChannelName>;
@ApiPropertyOptional({
description: 'Subscriber Avatar',
type: String,
default: null,
})
@IsOptional()
@IsString()
@IsObjectId({ message: 'Avatar Attachment ID must be a valid ObjectId' })
avatar?: string | null = null;
}
export class SubscriberUpdateDto extends PartialType(SubscriberCreateDto) {}

View File

@ -14,13 +14,24 @@ export enum FileType {
unknown = 'unknown',
}
export type AttachmentForeignKey = {
id: string | null;
/** @deprecated use "id" instead */
url?: string;
};
/**
* The `AttachmentRef` type defines two possible ways to reference an attachment:
* 1. By `id`: This is used when the attachment is uploaded and stored in the Hexabot system.
* The `id` field represents the unique identifier of the uploaded attachment in the system.
* 2. By `url`: This is used when the attachment is externally hosted, especially when
* the content is generated or retrieved by a plugin that consumes a third-party API.
* In this case, the `url` field contains the direct link to the external resource.
*/
export type AttachmentRef =
| {
id: string | null;
}
| {
/** To be used only for external URLs (plugins), for attachments use "id" instead */
url: string;
};
export interface AttachmentPayload {
type: FileType;
payload: AttachmentForeignKey;
payload: AttachmentRef;
}

View File

@ -411,7 +411,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
'url' in block.message.attachment.payload
) {
this.logger.error(
'Attachment Model : `url` payload has been deprecated in favor of `id`',
'Attachment Block : `url` payload has been deprecated in favor of `id`',
block.id,
block.message,
);
@ -521,9 +521,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
}
} else if (blockMessage && 'attachment' in blockMessage) {
const attachmentPayload = blockMessage.attachment.payload;
if (!attachmentPayload.id) {
if (!('id' in attachmentPayload)) {
this.checkDeprecatedAttachmentUrl(block);
throw new Error('Remote attachments are no longer supported!');
throw new Error(
'Remote attachments in blocks are no longer supported!',
);
}
const envelope: StdOutgoingEnvelope = {

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.
@ -256,6 +256,8 @@ export class ChatService {
event.setSender(subscriber);
await event.preprocess();
// Trigger message received event
this.eventEmitter.emit('hook:chatbot:received', event);

View File

@ -22,7 +22,7 @@ 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 { AttachmentRef } from '@/chat/schemas/types/attachment';
import { Button, ButtonType } from '@/chat/schemas/types/button';
import {
AnyMessage,
@ -155,7 +155,7 @@ export default abstract class BaseWebChannelHandler<
type: Web.IncomingMessageType.file,
data: {
type: attachmentPayload.type,
url: await this.getPublicUrl(attachmentPayload.payload.id),
url: await this.getPublicUrl(attachmentPayload.payload),
},
};
}
@ -994,7 +994,7 @@ export default abstract class BaseWebChannelHandler<
type: Web.OutgoingMessageType.file,
data: {
type: message.attachment.type,
url: await this.getPublicUrl(message.attachment.payload.id),
url: await this.getPublicUrl(message.attachment.payload),
},
};
if (message.quickReplies && message.quickReplies.length > 0) {
@ -1034,11 +1034,11 @@ export default abstract class BaseWebChannelHandler<
}
if (fields.image_url && item[fields.image_url]) {
const attachmentPayload = item[fields.image_url]
.payload as AttachmentForeignKey;
if (attachmentPayload.id) {
element.image_url = await this.getPublicUrl(attachmentPayload.id);
}
const attachmentRef =
typeof item[fields.image_url] === 'string'
? { url: item[fields.image_url] }
: (item[fields.image_url].payload as AttachmentRef);
element.image_url = await this.getPublicUrl(attachmentRef);
}
buttons.forEach((button: Button, index) => {