Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@
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

Expand Down Expand Up @@ -148,7 +148,6 @@

init = async (session: SerializedSession, { dataDirPath }: ClientContext, prefs?: Record<string, any>) => {
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) {
Expand Down Expand Up @@ -224,7 +223,7 @@
])
return hashThread(mapThread(
chatRow,
{

Check failure on line 226 in src/api.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type '{ handleRowsMap: { [x: string]: MappedHandleRow[]; }; currentUserID: string; threadReadStore: ThreadReadStore | undefined; mapMessageArgsMap: { [x: string]: [...]; }; ... 5 more ...; lowPriorityStates: { ...; }; }' is not assignable to parameter of type 'Context'.
handleRowsMap: { [chatRow.guid]: handleRows },
currentUserID: this.currentUser!.id,
threadReadStore: this.threadReadStore,
Expand Down Expand Up @@ -254,7 +253,7 @@
if (handleRows.length < 1) throw new Error('newly created thread had no handles')
const thread = mapThread(
chatRow,
{

Check failure on line 256 in src/api.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type '{ handleRowsMap: { [x: string]: MappedHandleRow[]; }; currentUserID: string; threadReadStore: ThreadReadStore | undefined; mapMessageArgsMap: { [x: string]: [...]; }; unreadCounts: Map<...>; dndState: Set<...>; reminders: { ...; }; archivalStates: { ...; }; }' is not assignable to parameter of type 'Context'.
handleRowsMap: { [chatRow.guid]: handleRows },
currentUserID: this.currentUser!.id,
threadReadStore: this.threadReadStore,
Expand Down Expand Up @@ -359,6 +358,7 @@
texts.error(`imsg/getThreads: couldn't log hashed ids: ${err}`)
}
const items = mapThreads(chatRows, {
accountID: this.accountID,
mapMessageArgsMap,
handleRowsMap,
chatImagesMap,
Expand Down Expand Up @@ -393,7 +393,7 @@
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[],
Expand All @@ -411,7 +411,7 @@
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
Expand Down Expand Up @@ -440,7 +440,7 @@
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[],
Expand Down Expand Up @@ -682,11 +682,13 @@
}

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))
}
Expand Down Expand Up @@ -886,6 +888,16 @@
return url.pathToFileURL(filePath).href
}

case 'reaction-sticker': {
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")
return url.pathToFileURL(attachment.filePath).href
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run CGBI conversion when serving reaction sticker assets

The new reaction-sticker branch returns file://... directly and skips the existing convertCGBI fallback used by the default asset path. If the sticker attachment is an Apple CGBI PNG (a format this codebase already special-cases), the reaction image can be unreadable/corrupted when rendered.

Useful? React with 👍 / 👎.

}

default: {
const filePath = Buffer.from(pathHex, 'hex').toString()
const buffer = await fs.readFile(filePath)
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand All @@ -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',
Expand All @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions src/db-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 14 additions & 8 deletions src/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ 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}.heic`

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}/`))
Expand All @@ -75,6 +78,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)
Expand Down Expand Up @@ -241,7 +245,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)
Comment on lines 247 to 249
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapMessage now requires an accountID argument, which breaks existing test fixtures (src/tests/*.json) that serialize Parameters<typeof mapMessage> and currently only provide 4 parameters. To avoid churn and reduce the chance of runtime undefined account IDs, consider making accountID optional with a sensible default (e.g. $accountID) or updating the fixture format + tests in the same PR.

Copilot uses AI. Check for mistakes.
const isSMS = msgRow.service === 'SMS' || msgRow.service === 'RCS'
const isGroup = !!msgRow.room_name
Expand Down Expand Up @@ -615,9 +619,10 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac
type: reactionType,
messageID: m.linkedMessageID,
participantID: m.senderID,
imgURL: assocMsgType === 'reacted_sticker' ? reactionStickerAssetURL(accountID, msgRow.ROWID) : 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
Expand All @@ -629,7 +634,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
})
}
Expand Down Expand Up @@ -672,6 +677,7 @@ function mapParticipant({ participantID: id, uncanonicalized_id }: MappedHandleR
export const mapAccountLogin = (al: string) => al?.replace(/^(E|P):/, '')

type Context = {
accountID: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify every mapThread/mapMessages callsite threads accountID.
rg -n -C3 '\bmapThread\s*\(|\bmapMessages\s*\(' --type ts

Repository: beeper/platform-imessage

Length of output: 4617


🏁 Script executed:

# First, check what Context is being passed at the mapThread calls
cat -n src/api.ts | sed -n '220,240p'

Repository: beeper/platform-imessage

Length of output: 1200


🏁 Script executed:

# Also check the second mapThread call
cat -n src/api.ts | sed -n '250,270p'

Repository: beeper/platform-imessage

Length of output: 983


🏁 Script executed:

# Check the Context type definition
fd -t f -e ts | xargs rg -l 'type Context|interface Context' | head -5

Repository: beeper/platform-imessage

Length of output: 82


🏁 Script executed:

# Search for Context type/interface definition
rg -n 'type Context|interface Context' src/mappers.ts -A 15

Repository: beeper/platform-imessage

Length of output: 848


🏁 Script executed:

# Also check the mapThread function signature more completely
sed -n '705,720p' src/mappers.ts

Repository: beeper/platform-imessage

Length of output: 1145


🏁 Script executed:

# Check mapThreads function and its callers
rg -n 'mapThreads' src/ -B 2 -A 5

Repository: beeper/platform-imessage

Length of output: 2258


🏁 Script executed:

# Also verify TypeScript compilation would catch this type error
# by checking how accountID is passed in the correct calls
rg -n 'accountID:' src/ -B 2 -A 2 | head -40

Repository: beeper/platform-imessage

Length of output: 2518


Add accountID: this.accountID to Context objects in mapThread calls.

accountID is a required field in the Context type (line 680) and is used at lines 706 and 716 in mapThread. The calls at src/api.ts:224 and src/api.ts:254 do not provide it, causing a type error. The bulk call via mapThreads at src/api.ts:360 shows the correct pattern with accountID: this.accountID. Update both direct mapThread calls to include this field before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mappers.ts` at line 680, The Context objects passed into mapThread are
missing the required accountID property; update both direct mapThread calls to
include accountID: this.accountID (use the same pattern as the bulk mapThreads
call) so the Context passed to mapThread contains accountID, e.g., add
accountID: this.accountID to the objects fed into mapThread to match the Context
type and the existing mapThreads usage.

currentUserID: string
handleRowsMap: { [threadID: string]: MappedHandleRow[] }
mapMessageArgsMap: { [threadID: string]: [MappedMessageRow[], MappedAttachmentRow[], MappedReactionMessageRow[]] }
Expand All @@ -688,16 +694,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
Expand All @@ -707,7 +713,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 ],
Expand Down
3 changes: 2 additions & 1 deletion src/tests/fixture1.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,6 @@
"participantID": "+918470075752"
}
],
"kishan24x7@gmail.com"
"kishan24x7@gmail.com",
"imessage-test-account"
]
3 changes: 2 additions & 1 deletion src/tests/fixture2.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,5 +131,6 @@
"participantID": "+918470075752"
}
],
"kishan24x7@gmail.com"
"kishan24x7@gmail.com",
"imessage-test-account"
]
5 changes: 4 additions & 1 deletion src/tests/mappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import path from 'path'

import { mapMessage } from '../mappers'

type MapMessageFixture = Parameters<typeof mapMessage>

async function testMessageMapFixture(fixturePath: string) {
const pathRelativeToTests = path.join(__dirname, fixturePath)

test(path.basename(fixturePath), async () => {
const parameters: Parameters<typeof mapMessage> = 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]
Expand Down
3 changes: 2 additions & 1 deletion src/tests/partial_leading_unsends.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@
}
],
[],
"tinyslices@gmail.com"
"tinyslices@gmail.com",
"imessage-test-account"
]
3 changes: 2 additions & 1 deletion src/tests/partial_multiple_middle_adjacent_unsend.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@
}
],
[],
"tinyslices@gmail.com"
"tinyslices@gmail.com",
"imessage-test-account"
]
3 changes: 2 additions & 1 deletion src/tests/partial_trailing_unsends.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@
}
],
[],
"tinyslices@gmail.com"
"tinyslices@gmail.com",
"imessage-test-account"
]
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' |
Expand Down
Loading