diff --git a/api/src/channel/lib/EventWrapper.ts b/api/src/channel/lib/EventWrapper.ts index fcfde39b..1351a384 100644 --- a/api/src/channel/lib/EventWrapper.ts +++ b/api/src/channel/lib/EventWrapper.ts @@ -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 * diff --git a/api/src/channel/lib/Handler.ts b/api/src/channel/lib/Handler.ts index 733a8ae8..10578b14 100644 --- a/api/src/channel/lib/Handler.ts +++ b/api/src/channel/lib/Handler.ts @@ -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); - 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)}`, - ); + 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.'); + } } /** diff --git a/api/src/channel/lib/__test__/common.mock.ts b/api/src/channel/lib/__test__/common.mock.ts index 9bceceae..ebc85516 100644 --- a/api/src/channel/lib/__test__/common.mock.ts +++ b/api/src/channel/lib/__test__/common.mock.ts @@ -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(), diff --git a/api/src/chat/dto/subscriber.dto.ts b/api/src/chat/dto/subscriber.dto.ts index 82148f96..cf6ffc15 100644 --- a/api/src/chat/dto/subscriber.dto.ts +++ b/api/src/chat/dto/subscriber.dto.ts @@ -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; + + @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) {} diff --git a/api/src/chat/schemas/types/attachment.ts b/api/src/chat/schemas/types/attachment.ts index 0a3afe28..274d5926 100644 --- a/api/src/chat/schemas/types/attachment.ts +++ b/api/src/chat/schemas/types/attachment.ts @@ -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; } diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 3e4c0418..8fb8619c 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -411,7 +411,7 @@ export class BlockService extends BaseService { '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 { } } 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 = { diff --git a/api/src/chat/services/chat.service.ts b/api/src/chat/services/chat.service.ts index 89db5fd2..acd34223 100644 --- a/api/src/chat/services/chat.service.ts +++ b/api/src/chat/services/chat.service.ts @@ -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); diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index 2568ca53..83fb9e3a 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -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) => {