From f14ad5e00e88309480834ed323f7fe7aa52de0ff Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:50:16 +0530 Subject: [PATCH 1/6] - --- src/api.ts | 21 +++++++++++++++------ src/constants.ts | 4 ++++ src/db-api.ts | 4 ++-- src/mappers.ts | 21 +++++++++++++-------- src/types.ts | 1 + 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/api.ts b/src/api.ts index 603dd4b1..642558f8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -47,9 +47,9 @@ function getDNDState() { return new Set(arr) } export default class AppleiMessage implements PlatformAPI { - currentUser: CurrentUser | undefined + constructor(public readonly accountID: string) {} - // private accountID: string + currentUser: CurrentUser | undefined private threadReadStore?: ThreadReadStore @@ -148,7 +148,6 @@ export default class AppleiMessage implements PlatformAPI { init = async (session: SerializedSession, { dataDirPath }: ClientContext, prefs?: Record) => { this.session = session || {} - // this.accountID = accountID const userDataDirPath = path.dirname(dataDirPath) this.experiments = await fs.readFile(path.join(userDataDirPath, 'imessage-enabled-experiments'), 'utf-8').catch(() => '') if (swiftServer) { @@ -359,6 +358,7 @@ export default class AppleiMessage implements PlatformAPI { texts.error(`imsg/getThreads: couldn't log hashed ids: ${err}`) } const items = mapThreads(chatRows, { + accountID: this.accountID, mapMessageArgsMap, handleRowsMap, chatImagesMap, @@ -393,7 +393,7 @@ export default class AppleiMessage implements PlatformAPI { db.getAttachments(msgRowIDs), db.getMessageReactions(msgGUIDs, { type: 'guid', guid: threadID }), ]) - const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id) + const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id, this.accountID) return { // NOTE(types): appease typescript, but we aren't actually using the texts SDK contract items: items.map(hashMessage) as Message[], @@ -411,7 +411,7 @@ export default class AppleiMessage implements PlatformAPI { db.getAttachments([msgRow.ROWID]), db.getMessageReactions([msgRow.guid], { type: 'guid', guid: threadID }), ]) - const items = mapMessages([msgRow], attachmentRows, reactionRows, this.currentUser!.id) + const items = mapMessages([msgRow], attachmentRows, reactionRows, this.currentUser!.id, this.accountID) const message = items.find(i => i.id === messageID) // NOTE(types): appease typescript, but we aren't actually using the texts SDK contract return (message ? hashMessage(message) : message) as Message @@ -440,7 +440,7 @@ export default class AppleiMessage implements PlatformAPI { db.getAttachments(msgRowIDs), threadID ? db.getMessageReactions(msgGUIDs, { type: 'guid', guid: threadID }) : [], ]) - const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id) + const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id, this.accountID) return { // NOTE(types): appease typescript, but we aren't actually using the texts SDK contract items: items.map(hashMessage) as Message[], @@ -886,6 +886,15 @@ export default class AppleiMessage implements PlatformAPI { return url.pathToFileURL(filePath).href } + case 'reaction-sticker': { + const reactionRowID = Number.parseInt(methodName, 10) + if (!Number.isFinite(reactionRowID)) throw new Error("invalid reaction sticker row ID") + const db = await this.ensureDB() + const attachment = (await db.getAttachments([reactionRowID])).find(a => a.filePath) + if (!attachment?.filePath) throw new Error("couldn't resolve sticker attachment for reaction row") + return url.pathToFileURL(attachment.filePath).href + } + default: { const filePath = Buffer.from(pathHex, 'hex').toString() const buffer = await fs.readFile(filePath) diff --git a/src/constants.ts b/src/constants.ts index 31c77dd1..07f2b914 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ export const ASSOC_MSG_TYPE = { 2004: 'reacted_emphasize', 2005: 'reacted_question', 2006: 'reacted_emoji', + 2007: 'reacted_sticker', 3000: 'unreacted_heart', 3001: 'unreacted_like', @@ -27,6 +28,7 @@ export const ASSOC_MSG_TYPE = { 3004: 'unreacted_emphasize', 3005: 'unreacted_question', 3006: 'unreacted_emoji', + 3007: 'unreacted_sticker', } as const export const REACTION_VERB_MAP = { @@ -37,6 +39,7 @@ export const REACTION_VERB_MAP = { reacted_emphasize: 'emphasized', reacted_question: 'questioned', reacted_emoji: 'reacted to', + reacted_sticker: 'reacted with a sticker to', unreacted_heart: 'removed a heart from', unreacted_like: 'removed a like from', @@ -45,6 +48,7 @@ export const REACTION_VERB_MAP = { unreacted_emphasize: 'removed an exclamation from', unreacted_question: 'removed a question mark from', unreacted_emoji: 'unreacted from', + unreacted_sticker: 'removed a sticker from', } as const export const EXPRESSIVE_MSGS = { diff --git a/src/db-api.ts b/src/db-api.ts index 88024485..133c64f4 100644 --- a/src/db-api.ts +++ b/src/db-api.ts @@ -80,7 +80,7 @@ FROM message AS m LEFT JOIN message_attachment_join AS maj ON maj.message_id = m.ROWID LEFT JOIN attachment AS a ON a.ROWID = maj.attachment_id WHERE m.ROWID IN (${new Array(msgIDs.length).fill('?').join(', ')})`, - getMessageReactions: (msgGUIDs: string[]) => `SELECT is_from_me, handle_id, associated_message_type, associated_message_guid, ${IS_SEQUOIA_OR_UP ? 'associated_message_emoji,' : ''} h.id AS participantID + getMessageReactions: (msgGUIDs: string[]) => `SELECT m.ROWID, is_from_me, handle_id, associated_message_type, associated_message_guid, ${IS_SEQUOIA_OR_UP ? 'associated_message_emoji,' : ''} h.id AS participantID FROM message AS m LEFT JOIN handle AS h ON m.handle_id = h.ROWID LEFT JOIN chat_message_join AS cmj ON cmj.message_id = m.ROWID @@ -411,7 +411,7 @@ WHERE m.ROWID = ?`, rowID) private getMappedMessagesWithoutExtraRows = async (chatGUID: string, pagination?: PaginationArg) => { const msgRows = await this.getMessages(chatGUID, pagination) if (pagination?.direction !== 'after') msgRows.reverse() - const items = mapMessages(msgRows, [], [], this.papi.currentUser!.id) + const items = mapMessages(msgRows, [], [], this.papi.currentUser!.id, this.papi.accountID) return { items, hasMore: msgRows.length === MESSAGES_LIMIT, diff --git a/src/mappers.ts b/src/mappers.ts index c178fd23..4af8ffa5 100644 --- a/src/mappers.ts +++ b/src/mappers.ts @@ -59,7 +59,9 @@ const removeObjReplacementChar = (text: string): string => { return text.replaceAll(OBJ_REPLACEMENT_CHAR, ' ').trim() } -function assignReactions(currentUserID: string, message: BeeperMessage, _reactionRows: MappedReactionMessageRow[] = [], filterIndex?: number) { +const reactionStickerAssetURL = (accountID: string, rowID: MappedReactionMessageRow['ROWID']) => `asset://${accountID}/reaction-sticker/${rowID}` + +function assignReactions(currentUserID: string, accountID: string, message: BeeperMessage, _reactionRows: MappedReactionMessageRow[] = [], filterIndex?: number) { const reactions: MessageReaction[] = [] const reactionRows = filterIndex != null ? _reactionRows.filter(r => r.associated_message_guid.startsWith(`p:${filterIndex}/`)) @@ -75,6 +77,7 @@ function assignReactions(currentUserID: string, message: BeeperMessage, _reactio id: participantID, reactionKey: actionKey === 'emoji' ? reaction.associated_message_emoji : actionKey, participantID, + imgURL: actionKey === 'sticker' ? reactionStickerAssetURL(accountID, reaction.ROWID) : undefined, }) } else if (actionType === 'unreacted') { const index = reactions.findIndex(r => r.id === participantID) @@ -241,7 +244,7 @@ const UUID_START = 11 const UUID_LENGTH = 36 const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i // eslint-disable-next-line @typescript-eslint/default-param-last -- FIXME(skip) -export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttachmentRow[] = [], reactionRows: MappedReactionMessageRow[], currentUserID: string): BeeperMessage[] { +export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttachmentRow[] = [], reactionRows: MappedReactionMessageRow[], currentUserID: string, accountID: string): BeeperMessage[] { const attachments = attachmentRows.map(a => mapAttachment(a, msgRow)).filter(attachment => attachment != null) const isSMS = msgRow.service === 'SMS' || msgRow.service === 'RCS' const isGroup = !!msgRow.room_name @@ -615,9 +618,10 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac type: reactionType, messageID: m.linkedMessageID, participantID: m.senderID, + imgURL: assocMsgType === 'reacted_sticker' ? attachmentRows[0]?.filename : undefined, reactionKey: actionKey === 'emoji' ? msgRow.associated_message_emoji : actionKey, } - if (actionKey === 'emoji' || actionKey in supportedReactions) { + if (actionKey === 'emoji' || actionKey === 'sticker' || actionKey in supportedReactions) { m.parseTemplate = true m.text = `${msgRow.is_from_me ? 'You' : '{{sender}}'} ${REACTION_VERB_MAP[assocMsgType]} ${msi?.ams ? `"${msi?.ams}"` : 'a message'}` m.isHidden = true @@ -629,7 +633,7 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac return messages.map(msg => { // texts.log('assigning reactions', msg.id, msg.index, reactionRows) - assignReactions(currentUserID, msg, reactionRows, messages.length === 1 ? undefined : msg.extra?.part) + assignReactions(currentUserID, accountID, msg, reactionRows, messages.length === 1 ? undefined : msg.extra?.part) return msg }) } @@ -672,6 +676,7 @@ function mapParticipant({ participantID: id, uncanonicalized_id }: MappedHandleR export const mapAccountLogin = (al: string) => al?.replace(/^(E|P):/, '') type Context = { + accountID: string currentUserID: string handleRowsMap: { [threadID: string]: MappedHandleRow[] } mapMessageArgsMap: { [threadID: string]: [MappedMessageRow[], MappedAttachmentRow[], MappedReactionMessageRow[]] } @@ -688,16 +693,16 @@ type Context = { // @ts-expect-error FIXME(skip): argument ordering // eslint-disable-next-line @typescript-eslint/default-param-last -export function mapMessages(messages: MappedMessageRow[], attachmentRows?: MappedAttachmentRow[], reactionRows?: MappedReactionMessageRow[], currentUserID: string): BeeperMessage[] { +export function mapMessages(messages: MappedMessageRow[], attachmentRows?: MappedAttachmentRow[], reactionRows?: MappedReactionMessageRow[], currentUserID: string, accountID: string): BeeperMessage[] { const groupedAttachmentRows = groupBy(attachmentRows, 'msgRowID') const groupedReactionRows = groupBy(reactionRows, r => r.associated_message_guid.replace(assocMsgGuidPrefix, '')) return messages - .flatMap(message => mapMessage(message, groupedAttachmentRows[message.ROWID], groupedReactionRows[message.guid], currentUserID)) + .flatMap(message => mapMessage(message, groupedAttachmentRows[message.ROWID], groupedReactionRows[message.guid], currentUserID, accountID)) .filter(Boolean) } export function mapThread(chat: MappedChatRow, context: Context): BeeperThread { - const { currentUserID } = context + const { currentUserID, accountID } = context const handleRows = context.handleRowsMap[chat.guid] const mapMessageArgs = context.mapMessageArgsMap?.[chat.guid] const selfID = chat.last_addressed_handle || mapAccountLogin(chat.account_login) || currentUserID @@ -707,7 +712,7 @@ export function mapThread(chat: MappedChatRow, context: Context): BeeperThread { const participants = [...handleRows.map(h => mapParticipant(h, chat.display_name)), selfParticipant].filter(participant => participant != null) const isGroup = !!chat.room_name const isReadOnly = chat.state === 0 && chat.properties != null - const messages = mapMessageArgs ? mapMessages(...mapMessageArgs, currentUserID) : [] + const messages = mapMessageArgs ? mapMessages(...mapMessageArgs, currentUserID, accountID) : [] /* props = { "com.apple.iChat.LastArchivedMessageID": [ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', 101010 ], diff --git a/src/types.ts b/src/types.ts index ae56ad04..6bb037ac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -175,6 +175,7 @@ export type MappedMessageRow = MessageRow & { // db-api.ts -> SQLS export type MappedReactionMessageRow = Pick< MappedMessageRow, +'ROWID' | 'is_from_me' | 'handle_id' | 'associated_message_type' | From f111e7cae474e940622103f0481db0a8a49a7df5 Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:47:11 +0530 Subject: [PATCH 2/6] Update mappers.ts --- src/mappers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mappers.ts b/src/mappers.ts index 4af8ffa5..4c448668 100644 --- a/src/mappers.ts +++ b/src/mappers.ts @@ -59,7 +59,8 @@ const removeObjReplacementChar = (text: string): string => { return text.replaceAll(OBJ_REPLACEMENT_CHAR, ' ').trim() } -const reactionStickerAssetURL = (accountID: string, rowID: MappedReactionMessageRow['ROWID']) => `asset://${accountID}/reaction-sticker/${rowID}` +const reactionStickerAssetURL = (accountID: string, rowID: MappedReactionMessageRow['ROWID']) => + `asset://${accountID}/reaction-sticker/${rowID}.heic` function assignReactions(currentUserID: string, accountID: string, message: BeeperMessage, _reactionRows: MappedReactionMessageRow[] = [], filterIndex?: number) { const reactions: MessageReaction[] = [] From 89484f031b5ce91839cac52d7ad349c649a46518 Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:48:34 +0530 Subject: [PATCH 3/6] Update api.ts --- src/api.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/api.ts b/src/api.ts index 642558f8..f1525a66 100644 --- a/src/api.ts +++ b/src/api.ts @@ -682,11 +682,13 @@ export default class AppleiMessage implements PlatformAPI { } addReaction = async (hashedThreadID: ThreadID, messageID: MessageID, reactionKey: string) => { + if (reactionKey === 'sticker') throw Error("Adding sticker reactions isn't supported") const threadID = originalThreadID(hashedThreadID) return this.threadPhaser.bracketed(hashedThreadID, this.setReaction(threadID, messageID, reactionKey, true)) } removeReaction = async (hashedThreadID: ThreadID, messageID: MessageID, reactionKey: string) => { + if (reactionKey === 'sticker') throw Error("Removing sticker reactions isn't supported") const threadID = originalThreadID(hashedThreadID) return this.threadPhaser.bracketed(hashedThreadID, this.setReaction(threadID, messageID, reactionKey, false)) } From 8295362d56624fcfece4a7c1acb59c1be1570336 Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:54:20 +0530 Subject: [PATCH 4/6] Update mappers.ts --- src/mappers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mappers.ts b/src/mappers.ts index 4c448668..a0194c74 100644 --- a/src/mappers.ts +++ b/src/mappers.ts @@ -619,7 +619,7 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac type: reactionType, messageID: m.linkedMessageID, participantID: m.senderID, - imgURL: assocMsgType === 'reacted_sticker' ? attachmentRows[0]?.filename : undefined, + imgURL: assocMsgType === 'reacted_sticker' ? reactionStickerAssetURL(accountID, msgRow.ROWID) : undefined, reactionKey: actionKey === 'emoji' ? msgRow.associated_message_emoji : actionKey, } if (actionKey === 'emoji' || actionKey === 'sticker' || actionKey in supportedReactions) { From e7ff95b90c41b7231b9312f0bee2070c51ea3eea Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:56:23 +0530 Subject: [PATCH 5/6] Update src/api.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api.ts b/src/api.ts index f1525a66..82df51f6 100644 --- a/src/api.ts +++ b/src/api.ts @@ -889,8 +889,9 @@ export default class AppleiMessage implements PlatformAPI { } case 'reaction-sticker': { - const reactionRowID = Number.parseInt(methodName, 10) - if (!Number.isFinite(reactionRowID)) throw new Error("invalid reaction sticker row ID") + if (!/^\d+$/.test(methodName)) throw new Error("invalid reaction sticker row ID") + const reactionRowID = Number(methodName) + if (!Number.isSafeInteger(reactionRowID)) throw new Error("invalid reaction sticker row ID") const db = await this.ensureDB() const attachment = (await db.getAttachments([reactionRowID])).find(a => a.filePath) if (!attachment?.filePath) throw new Error("couldn't resolve sticker attachment for reaction row") From a110d88e4106335b6f5e608d9827e85944b9e5b5 Mon Sep 17 00:00:00 2001 From: Kishan Bagaria <1093313+KishanBagaria@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:57:49 +0530 Subject: [PATCH 6/6] - --- src/tests/fixture1.json | 3 ++- src/tests/fixture2.json | 3 ++- src/tests/mappers.test.ts | 5 ++++- src/tests/partial_leading_unsends.json | 3 ++- src/tests/partial_multiple_middle_adjacent_unsend.json | 3 ++- src/tests/partial_trailing_unsends.json | 3 ++- 6 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/tests/fixture1.json b/src/tests/fixture1.json index 3c5501a3..ae525d95 100644 --- a/src/tests/fixture1.json +++ b/src/tests/fixture1.json @@ -145,5 +145,6 @@ "participantID": "+918470075752" } ], - "kishan24x7@gmail.com" + "kishan24x7@gmail.com", + "imessage-test-account" ] diff --git a/src/tests/fixture2.json b/src/tests/fixture2.json index f7a6c538..7a0df45d 100644 --- a/src/tests/fixture2.json +++ b/src/tests/fixture2.json @@ -131,5 +131,6 @@ "participantID": "+918470075752" } ], - "kishan24x7@gmail.com" + "kishan24x7@gmail.com", + "imessage-test-account" ] diff --git a/src/tests/mappers.test.ts b/src/tests/mappers.test.ts index 1d503b66..8c310946 100644 --- a/src/tests/mappers.test.ts +++ b/src/tests/mappers.test.ts @@ -4,11 +4,14 @@ import path from 'path' import { mapMessage } from '../mappers' +type MapMessageFixture = Parameters + async function testMessageMapFixture(fixturePath: string) { const pathRelativeToTests = path.join(__dirname, fixturePath) test(path.basename(fixturePath), async () => { - const parameters: Parameters = JSON.parse(await fs.readFile(pathRelativeToTests, 'utf8')) + const parameters = JSON.parse(await fs.readFile(pathRelativeToTests, 'utf8')) as MapMessageFixture + expect(parameters).toHaveLength(5) type Row = typeof parameters[0] type MessageRowBufferKeys = { [Key in keyof Row]: Row[Key] extends Buffer ? Key : never }[keyof Row] diff --git a/src/tests/partial_leading_unsends.json b/src/tests/partial_leading_unsends.json index 7d12b47e..dcd3809c 100644 --- a/src/tests/partial_leading_unsends.json +++ b/src/tests/partial_leading_unsends.json @@ -104,5 +104,6 @@ } ], [], - "tinyslices@gmail.com" + "tinyslices@gmail.com", + "imessage-test-account" ] diff --git a/src/tests/partial_multiple_middle_adjacent_unsend.json b/src/tests/partial_multiple_middle_adjacent_unsend.json index 4deb02fc..e8d774f8 100644 --- a/src/tests/partial_multiple_middle_adjacent_unsend.json +++ b/src/tests/partial_multiple_middle_adjacent_unsend.json @@ -104,5 +104,6 @@ } ], [], - "tinyslices@gmail.com" + "tinyslices@gmail.com", + "imessage-test-account" ] diff --git a/src/tests/partial_trailing_unsends.json b/src/tests/partial_trailing_unsends.json index fa816161..b9b51f82 100644 --- a/src/tests/partial_trailing_unsends.json +++ b/src/tests/partial_trailing_unsends.json @@ -104,5 +104,6 @@ } ], [], - "tinyslices@gmail.com" + "tinyslices@gmail.com", + "imessage-test-account" ]