From 386c756a9711f907d307862d1976b5d621d2bb01 Mon Sep 17 00:00:00 2001 From: qayshp Date: Wed, 24 Jun 2026 22:14:25 -0700 Subject: [PATCH] Add missing message attachments from attributed body When attachments are requested, add any attachments referenced by file-transfer GUIDs in attributedBody that are not already present on the message. Resolve those GUIDs through the existing attachment lookup. This fixes affected group-chat image messages observed with BlueBubbles Server 1.9.9 backing a self-hosted Beeper iMessage bridge on macOS Sequoia 15.7.7, where the message/chat APIs returned attachments: [] even though the attachment GUID was present in attributedBody and resolvable by the attachment API. --- .../src/server/databases/imessage/index.ts | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/server/src/server/databases/imessage/index.ts b/packages/server/src/server/databases/imessage/index.ts index e811661c7..0bf6a0b5a 100644 --- a/packages/server/src/server/databases/imessage/index.ts +++ b/packages/server/src/server/databases/imessage/index.ts @@ -152,7 +152,7 @@ export class MessageRepository extends Loggable { * @param attachmentGuid A specific attachment identifier to get * @param withMessages Whether to include the participants or not */ - async getAttachment(attachmentGuid: string, withMessages = false) { + async getAttachment(attachmentGuid: string, withMessages = false): Promise { const query = this.db.getRepository(Attachment).createQueryBuilder("attachment"); if (withMessages) query.leftJoinAndSelect("attachment.messages", "message"); @@ -162,7 +162,7 @@ export class MessageRepository extends Loggable { // Original GUIDs can also be prefixed with at_x_ or p:/. const lookupGuids = [attachmentGuid]; if (attachmentGuid.length > 36) { - lookupGuids.push(attachmentGuid.substring(attachmentGuid.length - 36)); + lookupGuids.push(this.normalizeAttachmentGuid(attachmentGuid)); } for (const lookupGuid of lookupGuids) { @@ -202,6 +202,7 @@ export class MessageRepository extends Loggable { query.andWhere("message.guid = :guid", { guid }); const message = await query.getOne(); + if (withAttachments && message) await this.addMissingAttachmentsFromAttributedBody([message]); return message; } @@ -302,7 +303,9 @@ export class MessageRepository extends Loggable { query.skip(offset); query.take(limit); - return await query.getManyAndCount(); + const [messages, totalCount] = await query.getManyAndCount(); + if (withAttachments) await this.addMissingAttachmentsFromAttributedBody(messages); + return [messages, totalCount]; } /** @@ -459,7 +462,59 @@ export class MessageRepository extends Loggable { query.skip(offset); query.take(limit); - return await query.getMany(); + const messages = await query.getMany(); + if (withAttachments) await this.addMissingAttachmentsFromAttributedBody(messages); + return messages; + } + + private normalizeAttachmentGuid(guid: string): string { + return guid.slice(-36).toLowerCase(); + } + + private getAttributedBodyAttachmentGuids(message: Message): string[] { + const guids = new Set(); + for (const item of message.attributedBody ?? []) { + for (const run of item?.runs ?? []) { + const guid = run?.attributes?.__kIMFileTransferGUIDAttributeName; + if (typeof guid === "string" && guid.length > 0) guids.add(guid); + } + } + + return Array.from(guids); + } + + private async addMissingAttachmentsFromAttributedBody(messages: Message[]): Promise { + const attachmentCache = new Map(); + + for (const message of messages) { + const guids = this.getAttributedBodyAttachmentGuids(message); + if (guids.length === 0) continue; + + if (!message.attachments) message.attachments = []; + + for (const guid of guids) { + const normalizedGuid = this.normalizeAttachmentGuid(guid); + const existing = message.attachments.some(attachment => { + return [attachment.guid, attachment.originalGuid].some(attachmentGuid => { + return ( + typeof attachmentGuid === "string" && + this.normalizeAttachmentGuid(attachmentGuid) === normalizedGuid + ); + }); + }); + if (existing) continue; + + if (!attachmentCache.has(normalizedGuid)) { + attachmentCache.set(normalizedGuid, await this.getAttachment(guid)); + } + + const attachment = attachmentCache.get(normalizedGuid); + if (!attachment) continue; + + const alreadyAdded = message.attachments.some(item => item.ROWID === attachment.ROWID); + if (!alreadyAdded) message.attachments.push(attachment); + } + } } /**