diff --git a/api/src/channel/lib/EventWrapper.ts b/api/src/channel/lib/EventWrapper.ts index daa8861b..fcfde39b 100644 --- a/api/src/channel/lib/EventWrapper.ts +++ b/api/src/channel/lib/EventWrapper.ts @@ -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, @@ -251,7 +248,7 @@ export default abstract class EventWrapper< * * @returns Received attachments message */ - abstract getAttachments(): AttachmentPayload[]; + abstract getAttachments(): AttachmentPayload[]; /** * Returns the list of delivered messages @@ -380,7 +377,7 @@ export class GenericEventWrapper extends EventWrapper< * @returns A list of received attachments * @deprecated - This method is deprecated */ - getAttachments(): AttachmentPayload[] { + getAttachments(): AttachmentPayload[] { return []; } diff --git a/api/src/channel/lib/__test__/common.mock.ts b/api/src/channel/lib/__test__/common.mock.ts index e85a9542..a45304b5 100644 --- a/api/src/channel/lib/__test__/common.mock.ts +++ b/api/src/channel/lib/__test__/common.mock.ts @@ -92,11 +92,6 @@ const attachment: Attachment = { 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: { attachment_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: { attachment_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 = { +export const attachmentMessage: StdOutgoingAttachmentMessage = { attachment: { type: FileType.image, - payload: attachmentWithUrl, + payload: { attachment_id: attachment.id }, }, quickReplies: [ { diff --git a/api/src/chat/schemas/types/attachment.ts b/api/src/chat/schemas/types/attachment.ts index e5e0eaf2..7c5ea4c4 100644 --- a/api/src/chat/schemas/types/attachment.ts +++ b/api/src/chat/schemas/types/attachment.ts @@ -6,8 +6,6 @@ * 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'; - export enum FileType { image = 'image', video = 'video', @@ -17,14 +15,12 @@ export enum FileType { } export type AttachmentForeignKey = { - attachment_id: string; + attachment_id: string | null; /** @deprecated use "attachment_id" instead */ url?: string; }; -export interface AttachmentPayload< - A extends Attachment | AttachmentForeignKey, -> { +export interface AttachmentPayload { type: FileType; - payload: A; + payload: AttachmentForeignKey; } diff --git a/api/src/chat/schemas/types/message.ts b/api/src/chat/schemas/types/message.ts index dbe53b05..c8c8e14f 100644 --- a/api/src/chat/schemas/types/message.ts +++ b/api/src/chat/schemas/types/message.ts @@ -6,12 +6,11 @@ * 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 { PluginName } from '@/plugins/types'; import { Message } from '../message.schema'; -import { AttachmentForeignKey, AttachmentPayload } from './attachment'; +import { AttachmentPayload } from './attachment'; import { Button } from './button'; import { ContentOptions } from './options'; import { StdQuickReply } from './quick-reply'; @@ -96,11 +95,9 @@ export type StdOutgoingListMessage = { }; }; -export type StdOutgoingAttachmentMessage< - A extends Attachment | AttachmentForeignKey, -> = { +export type StdOutgoingAttachmentMessage = { // Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying - attachment: AttachmentPayload; + attachment: AttachmentPayload; quickReplies?: StdQuickReply[]; }; @@ -115,7 +112,7 @@ export type BlockMessage = | StdOutgoingQuickRepliesMessage | StdOutgoingButtonsMessage | StdOutgoingListMessage - | StdOutgoingAttachmentMessage + | StdOutgoingAttachmentMessage | StdPluginMessage; export type StdOutgoingMessage = @@ -123,7 +120,7 @@ export type StdOutgoingMessage = | StdOutgoingQuickRepliesMessage | StdOutgoingButtonsMessage | StdOutgoingListMessage - | StdOutgoingAttachmentMessage; + | StdOutgoingAttachmentMessage; type StdIncomingTextMessage = { text: string }; @@ -142,9 +139,7 @@ export type StdIncomingLocationMessage = { export type StdIncomingAttachmentMessage = { type: PayloadType.attachments; serialized_text: string; - attachment: - | AttachmentPayload - | AttachmentPayload[]; + attachment: AttachmentPayload | AttachmentPayload[]; }; export type StdIncomingMessage = @@ -189,7 +184,7 @@ export interface StdOutgoingListEnvelope { export interface StdOutgoingAttachmentEnvelope { format: OutgoingMessageFormat.attachment; - message: StdOutgoingAttachmentMessage; + message: StdOutgoingAttachmentMessage; } export type StdOutgoingEnvelope = diff --git a/api/src/chat/schemas/types/quick-reply.ts b/api/src/chat/schemas/types/quick-reply.ts index f1e2c541..18ae2b37 100644 --- a/api/src/chat/schemas/types/quick-reply.ts +++ b/api/src/chat/schemas/types/quick-reply.ts @@ -6,7 +6,7 @@ * 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 { AttachmentForeignKey, AttachmentPayload } from './attachment'; +import { AttachmentPayload } from './attachment'; import { PayloadType } from './message'; export type Payload = @@ -19,7 +19,7 @@ export type Payload = } | { type: PayloadType.attachments; - attachments: AttachmentPayload; + attachments: AttachmentPayload; }; export enum QuickReplyType { diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index 131f45f5..9c778e1a 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -526,24 +526,12 @@ export class BlockService extends BaseService { throw new Error('Remote attachments are no longer supported!'); } - const attachment = await this.attachmentService.findOne( - attachmentPayload.attachment_id, - ); - - if (!attachment) { - this.logger.debug( - 'Unable to locate the attachment for the given block', - block, - ); - throw new Error('Unable to find attachment.'); - } - const envelope: StdOutgoingEnvelope = { format: OutgoingMessageFormat.attachment, message: { attachment: { type: blockMessage.attachment.type, - payload: attachment, + payload: blockMessage.attachment.payload, }, quickReplies: blockMessage.quickReplies ? [...blockMessage.quickReplies] diff --git a/api/src/cms/services/content.service.spec.ts b/api/src/cms/services/content.service.spec.ts index 696732cd..7edd605c 100644 --- a/api/src/cms/services/content.service.spec.ts +++ b/api/src/cms/services/content.service.spec.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. @@ -13,11 +13,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AttachmentRepository } from '@/attachment/repositories/attachment.repository'; import { AttachmentModel } from '@/attachment/schemas/attachment.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; -import { FileType } from '@/chat/schemas/types/attachment'; -import { - ContentElement, - OutgoingMessageFormat, -} from '@/chat/schemas/types/message'; +import { OutgoingMessageFormat } from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; import { IGNORED_TEST_FIELDS } from '@/utils/test/constants'; @@ -43,7 +39,6 @@ describe('ContentService', () => { let contentService: ContentService; let contentTypeService: ContentTypeService; let contentRepository: ContentRepository; - let attachmentService: AttachmentService; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -69,7 +64,6 @@ describe('ContentService', () => { contentService = module.get(ContentService); contentTypeService = module.get(ContentTypeService); contentRepository = module.get(ContentRepository); - attachmentService = module.get(AttachmentService); }); afterAll(async () => { @@ -111,103 +105,6 @@ describe('ContentService', () => { }); }); - describe('getAttachmentIds', () => { - const contents: Content[] = [ - { - id: '1', - title: 'store 1', - entity: 'stores', - status: true, - dynamicFields: { - image: { - type: FileType.image, - payload: { - attachment_id: '123', - }, - }, - }, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: '2', - title: 'store 2', - entity: 'stores', - status: true, - dynamicFields: { - image: { - type: FileType.image, - payload: { - attachment_id: '456', - }, - }, - }, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: '3', - title: 'store 3', - entity: 'stores', - status: true, - dynamicFields: { - image: { - type: FileType.image, - payload: { - url: 'https://remote.file/image.jpg', - }, - }, - }, - createdAt: new Date(), - updatedAt: new Date(), - }, - ]; - it('should return all content attachment ids', () => { - const result = contentService.getAttachmentIds( - contents.map(Content.toElement), - 'image', - ); - expect(result).toEqual(['123', '456']); - }); - - it('should not return any of the attachment ids', () => { - const result = contentService.getAttachmentIds(contents, 'file'); - expect(result).toEqual([]); - }); - }); - - describe('populateAttachments', () => { - it('should return populated content', async () => { - const storeContents = await contentService.find({ title: /^store/ }); - const elements: ContentElement[] = await Promise.all( - storeContents.map(Content.toElement).map(async (store) => { - const attachmentId = store.image.payload.attachment_id; - if (attachmentId) { - const attachment = await attachmentService.findOne(attachmentId); - if (attachment) { - return { - ...store, - image: { - type: 'image', - payload: { - ...attachment, - url: `http://localhost:4000/attachment/download/${attachment.id}/${attachment.name}`, - }, - }, - }; - } - } - return store; - }), - ); - const result = await contentService.populateAttachments( - storeContents.map(Content.toElement), - 'image', - ); - expect(result).toEqualPayload(elements); - }); - }); - describe('getContent', () => { const contentOptions: ContentOptions = { display: OutgoingMessageFormat.list, diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index 2a491a2b..51a60a86 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -8,12 +8,8 @@ import { Injectable } from '@nestjs/common'; -import { Attachment } from '@/attachment/schemas/attachment.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; -import { - ContentElement, - StdOutgoingListMessage, -} from '@/chat/schemas/types/message'; +import { StdOutgoingListMessage } from '@/chat/schemas/types/message'; import { ContentOptions } from '@/chat/schemas/types/options'; import { LoggerService } from '@/logger/logger.service'; import { BaseService } from '@/utils/generics/base-service'; @@ -51,93 +47,6 @@ export class ContentService extends BaseService< return await this.repository.textSearch(query); } - /** - * Extracts attachment IDs from content entities, issuing warnings for any issues. - * - * @param contents - An array of content entities. - * @param attachmentFieldName - The name of the attachment field to check for. - * - * @return A list of attachment IDs. - */ - getAttachmentIds(contents: ContentElement[], attachmentFieldName: string) { - return contents.reduce((acc, content) => { - if (attachmentFieldName in content) { - const attachment = content[attachmentFieldName]; - - if ( - typeof attachment === 'object' && - 'attachment_id' in attachment.payload - ) { - acc.push(attachment.payload.attachment_id); - } else { - this.logger.error( - `Remote attachments have been deprecated, content "${content.title}" is missing the "attachment_id"`, - ); - } - } else { - this.logger.warn( - `Field "${attachmentFieldName}" not found in content "${content.title}"`, - ); - } - return acc; - }, [] as string[]); - } - - /** - * Populates attachment fields within content entities with detailed attachment information. - * - * @param elements - An array of content entities. - * @param attachmentFieldName - The name of the attachment field to populate. - * - * @return A list of content with populated attachment data. - */ - async populateAttachments( - elements: ContentElement[], - attachmentFieldName: string, - ): Promise { - const attachmentIds = this.getAttachmentIds(elements, attachmentFieldName); - - if (attachmentIds.length > 0) { - const attachments = await this.attachmentService.find({ - _id: { $in: attachmentIds }, - }); - - const attachmentsById = attachments.reduce( - (acc, curr) => { - acc[curr.id] = curr; - return acc; - }, - {} as { [key: string]: Attachment }, - ); - const populatedContents = elements.map((content) => { - const attachmentField = content[attachmentFieldName]; - if ( - typeof attachmentField === 'object' && - 'attachment_id' in attachmentField.payload - ) { - const attachmentId = attachmentField?.payload?.attachment_id; - return { - ...content, - [attachmentFieldName]: { - type: attachmentField.type, - payload: { - ...(attachmentsById[attachmentId] || attachmentField.payload), - url: Attachment.getAttachmentUrl( - attachmentId, - attachmentsById[attachmentId].name, - ), - }, - }, - }; - } else { - return content; - } - }); - return populatedContents; - } - return elements; - } - /** * Retrieves content based on the provided options and pagination settings. * @@ -175,21 +84,6 @@ export class ContentService extends BaseService< sort: ['createdAt', 'desc'], }); const elements = contents.map(Content.toElement); - const attachmentFieldName = options.fields.image_url; - if (attachmentFieldName) { - // Populate attachment when there's an image field - return { - elements: await this.populateAttachments( - elements, - attachmentFieldName, - ), - pagination: { - total, - skip, - limit, - }, - }; - } return { elements, pagination: { diff --git a/api/src/extensions/channels/web/base-web-channel.ts b/api/src/extensions/channels/web/base-web-channel.ts index 84216a0e..45a9a466 100644 --- a/api/src/extensions/channels/web/base-web-channel.ts +++ b/api/src/extensions/channels/web/base-web-channel.ts @@ -985,14 +985,14 @@ export default abstract class BaseWebChannelHandler< * @returns A ready to be sent attachment message */ async _attachmentFormat( - message: StdOutgoingAttachmentMessage, + message: StdOutgoingAttachmentMessage, _options?: BlockOptions, ): Promise { const payload: Web.OutgoingMessageBase = { type: Web.OutgoingMessageType.file, data: { type: message.attachment.type, - url: await this.getPublicUrl(message.attachment.payload.id), + url: await this.getPublicUrl(message.attachment.payload.attachment_id), }, }; if (message.quickReplies && message.quickReplies.length > 0) { @@ -1034,14 +1034,7 @@ export default abstract class BaseWebChannelHandler< if (fields.image_url && item[fields.image_url]) { const attachmentPayload = item[fields.image_url] .payload as AttachmentForeignKey; - if (attachmentPayload.url) { - if (!attachmentPayload.attachment_id) { - // @deprecated - this.logger.warn( - 'Web Channel Handler: Attachment remote url has been deprecated', - item, - ); - } + if (attachmentPayload.attachment_id) { element.image_url = await this.getPublicUrl( attachmentPayload.attachment_id, ); diff --git a/api/src/extensions/channels/web/wrapper.ts b/api/src/extensions/channels/web/wrapper.ts index ea6515e0..03f737c6 100644 --- a/api/src/extensions/channels/web/wrapper.ts +++ b/api/src/extensions/channels/web/wrapper.ts @@ -9,10 +9,7 @@ import { Attachment } from '@/attachment/schemas/attachment.schema'; import EventWrapper from '@/channel/lib/EventWrapper'; import { ChannelName } from '@/channel/types'; -import { - AttachmentForeignKey, - AttachmentPayload, -} from '@/chat/schemas/types/attachment'; +import { AttachmentPayload } from '@/chat/schemas/types/attachment'; import { IncomingMessageType, PayloadType, @@ -71,9 +68,11 @@ type WebEventAdapter = }; // eslint-disable-next-line prettier/prettier -export default class WebEventWrapper< - N extends ChannelName, -> extends EventWrapper { +export default class WebEventWrapper extends EventWrapper< + WebEventAdapter, + Web.Event, + N +> { /** * Constructor : channel's event wrapper * @@ -298,7 +297,7 @@ export default class WebEventWrapper< * @deprecated * @returns Received attachments message */ - getAttachments(): AttachmentPayload[] { + getAttachments(): AttachmentPayload[] { const message = this.getMessage() as any; return 'attachment' in message ? [].concat(message.attachment) : []; } diff --git a/frontend/src/components/inbox/components/AttachmentViewer.tsx b/frontend/src/components/inbox/components/AttachmentViewer.tsx index f8ba00b6..04e0a765 100644 --- a/frontend/src/components/inbox/components/AttachmentViewer.tsx +++ b/frontend/src/components/inbox/components/AttachmentViewer.tsx @@ -15,7 +15,6 @@ import { DialogTitle } from "@/app-components/dialogs"; import { useDialog } from "@/hooks/useDialog"; import { useTranslate } from "@/hooks/useTranslate"; import { - AttachmentAttrs, FileType, StdIncomingAttachmentMessage, StdOutgoingAttachmentMessage, @@ -93,9 +92,7 @@ const componentMap: { [key in FileType]: FC } = { }; export const AttachmentViewer = (props: { - message: - | StdIncomingAttachmentMessage - | StdOutgoingAttachmentMessage; + message: StdIncomingAttachmentMessage | StdOutgoingAttachmentMessage; }) => { const message = props.message; diff --git a/frontend/src/components/visual-editor/constants.ts b/frontend/src/components/visual-editor/constants.ts index 380ad827..c84bf8b8 100644 --- a/frontend/src/components/visual-editor/constants.ts +++ b/frontend/src/components/visual-editor/constants.ts @@ -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 { IBlockAttributes } from "@/types/block.types"; import { ButtonType, @@ -35,7 +36,7 @@ export const ATTACHMENT_BLOCK_TEMPLATE: Partial = { message: { attachment: { type: FileType.unknown, - payload: { attachment_id: undefined }, + payload: { attachment_id: null }, }, quickReplies: [], }, diff --git a/frontend/src/types/block.types.ts b/frontend/src/types/block.types.ts index 3822cf18..2b40d51f 100644 --- a/frontend/src/types/block.types.ts +++ b/frontend/src/types/block.types.ts @@ -1,17 +1,17 @@ /* - * 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 { EntityType, Format } from "@/services/types"; import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; import { ILabel } from "./label.types"; import { - AttachmentForeignKey, ContentOptions, PayloadType, StdOutgoingAttachmentMessage, @@ -57,7 +57,7 @@ export type BlockMessage = | StdOutgoingQuickRepliesMessage | StdOutgoingButtonsMessage | StdOutgoingListMessage - | StdOutgoingAttachmentMessage + | StdOutgoingAttachmentMessage | StdPluginMessage; export interface PayloadPattern { diff --git a/frontend/src/types/message.types.ts b/frontend/src/types/message.types.ts index b1433934..60d63b7d 100644 --- a/frontend/src/types/message.types.ts +++ b/frontend/src/types/message.types.ts @@ -52,15 +52,14 @@ export interface AttachmentAttrs { } export type AttachmentForeignKey = { + attachment_id: string | null; + /** @deprecated use attachment_id instead */ url?: string; - attachment_id: string | undefined; }; -export interface AttachmentPayload< - A extends AttachmentAttrs | AttachmentForeignKey, -> { +export interface AttachmentPayload { type: FileType; - payload?: A; + payload: AttachmentForeignKey; } // Content @@ -96,7 +95,7 @@ export type Payload = } | { type: PayloadType.attachments; - attachments: AttachmentPayload; + attachments: AttachmentPayload; }; export enum QuickReplyType { @@ -163,11 +162,9 @@ export type StdOutgoingListMessage = { limit: number; }; }; -export type StdOutgoingAttachmentMessage< - A extends AttachmentAttrs | AttachmentForeignKey, -> = { +export type StdOutgoingAttachmentMessage = { // Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying - attachment: AttachmentPayload; + attachment: AttachmentPayload; quickReplies?: StdQuickReply[]; }; @@ -190,9 +187,7 @@ export type StdIncomingLocationMessage = { export type StdIncomingAttachmentMessage = { type: PayloadType.attachments; serialized_text: string; - attachment: - | AttachmentPayload - | AttachmentPayload[]; + attachment: AttachmentPayload | AttachmentPayload[]; }; export type StdPluginMessage = { @@ -211,7 +206,7 @@ export type StdOutgoingMessage = | StdOutgoingQuickRepliesMessage | StdOutgoingButtonsMessage | StdOutgoingListMessage - | StdOutgoingAttachmentMessage; + | StdOutgoingAttachmentMessage; export interface IMessageAttributes { mid?: string;