Skip to content

Commit f23461f

Browse files
parse sticker reactions (#57)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fd133ff commit f23461f

11 files changed

Lines changed: 53 additions & 22 deletions

src/api.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ function getDNDState() {
4747
return new Set(arr)
4848
}
4949
export default class AppleiMessage implements PlatformAPI {
50-
currentUser: CurrentUser | undefined
50+
constructor(public readonly accountID: string) {}
5151

52-
// private accountID: string
52+
currentUser: CurrentUser | undefined
5353

5454
private threadReadStore?: ThreadReadStore
5555

@@ -148,7 +148,6 @@ export default class AppleiMessage implements PlatformAPI {
148148

149149
init = async (session: SerializedSession, { dataDirPath }: ClientContext, prefs?: Record<string, any>) => {
150150
this.session = session || {}
151-
// this.accountID = accountID
152151
const userDataDirPath = path.dirname(dataDirPath)
153152
this.experiments = await fs.readFile(path.join(userDataDirPath, 'imessage-enabled-experiments'), 'utf-8').catch(() => '')
154153
if (swiftServer) {
@@ -359,6 +358,7 @@ export default class AppleiMessage implements PlatformAPI {
359358
texts.error(`imsg/getThreads: couldn't log hashed ids: ${err}`)
360359
}
361360
const items = mapThreads(chatRows, {
361+
accountID: this.accountID,
362362
mapMessageArgsMap,
363363
handleRowsMap,
364364
chatImagesMap,
@@ -393,7 +393,7 @@ export default class AppleiMessage implements PlatformAPI {
393393
db.getAttachments(msgRowIDs),
394394
db.getMessageReactions(msgGUIDs, { type: 'guid', guid: threadID }),
395395
])
396-
const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id)
396+
const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id, this.accountID)
397397
return {
398398
// NOTE(types): appease typescript, but we aren't actually using the texts SDK contract
399399
items: items.map(hashMessage) as Message[],
@@ -411,7 +411,7 @@ export default class AppleiMessage implements PlatformAPI {
411411
db.getAttachments([msgRow.ROWID]),
412412
db.getMessageReactions([msgRow.guid], { type: 'guid', guid: threadID }),
413413
])
414-
const items = mapMessages([msgRow], attachmentRows, reactionRows, this.currentUser!.id)
414+
const items = mapMessages([msgRow], attachmentRows, reactionRows, this.currentUser!.id, this.accountID)
415415
const message = items.find(i => i.id === messageID)
416416
// NOTE(types): appease typescript, but we aren't actually using the texts SDK contract
417417
return (message ? hashMessage(message) : message) as Message
@@ -440,7 +440,7 @@ export default class AppleiMessage implements PlatformAPI {
440440
db.getAttachments(msgRowIDs),
441441
threadID ? db.getMessageReactions(msgGUIDs, { type: 'guid', guid: threadID }) : [],
442442
])
443-
const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id)
443+
const items = mapMessages(msgRows, attachmentRows, reactionRows, this.currentUser!.id, this.accountID)
444444
return {
445445
// NOTE(types): appease typescript, but we aren't actually using the texts SDK contract
446446
items: items.map(hashMessage) as Message[],
@@ -682,11 +682,13 @@ export default class AppleiMessage implements PlatformAPI {
682682
}
683683

684684
addReaction = async (hashedThreadID: ThreadID, messageID: MessageID, reactionKey: string) => {
685+
if (reactionKey === 'sticker') throw Error("Adding sticker reactions isn't supported")
685686
const threadID = originalThreadID(hashedThreadID)
686687
return this.threadPhaser.bracketed(hashedThreadID, this.setReaction(threadID, messageID, reactionKey, true))
687688
}
688689

689690
removeReaction = async (hashedThreadID: ThreadID, messageID: MessageID, reactionKey: string) => {
691+
if (reactionKey === 'sticker') throw Error("Removing sticker reactions isn't supported")
690692
const threadID = originalThreadID(hashedThreadID)
691693
return this.threadPhaser.bracketed(hashedThreadID, this.setReaction(threadID, messageID, reactionKey, false))
692694
}
@@ -886,6 +888,16 @@ export default class AppleiMessage implements PlatformAPI {
886888
return url.pathToFileURL(filePath).href
887889
}
888890

891+
case 'reaction-sticker': {
892+
if (!/^\d+$/.test(methodName)) throw new Error("invalid reaction sticker row ID")
893+
const reactionRowID = Number(methodName)
894+
if (!Number.isSafeInteger(reactionRowID)) throw new Error("invalid reaction sticker row ID")
895+
const db = await this.ensureDB()
896+
const attachment = (await db.getAttachments([reactionRowID])).find(a => a.filePath)
897+
if (!attachment?.filePath) throw new Error("couldn't resolve sticker attachment for reaction row")
898+
return url.pathToFileURL(attachment.filePath).href
899+
}
900+
889901
default: {
890902
const filePath = Buffer.from(pathHex, 'hex').toString()
891903
const buffer = await fs.readFile(filePath)

src/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const ASSOC_MSG_TYPE = {
1919
2004: 'reacted_emphasize',
2020
2005: 'reacted_question',
2121
2006: 'reacted_emoji',
22+
2007: 'reacted_sticker',
2223

2324
3000: 'unreacted_heart',
2425
3001: 'unreacted_like',
@@ -27,6 +28,7 @@ export const ASSOC_MSG_TYPE = {
2728
3004: 'unreacted_emphasize',
2829
3005: 'unreacted_question',
2930
3006: 'unreacted_emoji',
31+
3007: 'unreacted_sticker',
3032
} as const
3133

3234
export const REACTION_VERB_MAP = {
@@ -37,6 +39,7 @@ export const REACTION_VERB_MAP = {
3739
reacted_emphasize: 'emphasized',
3840
reacted_question: 'questioned',
3941
reacted_emoji: 'reacted to',
42+
reacted_sticker: 'reacted with a sticker to',
4043

4144
unreacted_heart: 'removed a heart from',
4245
unreacted_like: 'removed a like from',
@@ -45,6 +48,7 @@ export const REACTION_VERB_MAP = {
4548
unreacted_emphasize: 'removed an exclamation from',
4649
unreacted_question: 'removed a question mark from',
4750
unreacted_emoji: 'unreacted from',
51+
unreacted_sticker: 'removed a sticker from',
4852
} as const
4953

5054
export const EXPRESSIVE_MSGS = {

src/db-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ FROM message AS m
8080
LEFT JOIN message_attachment_join AS maj ON maj.message_id = m.ROWID
8181
LEFT JOIN attachment AS a ON a.ROWID = maj.attachment_id
8282
WHERE m.ROWID IN (${new Array(msgIDs.length).fill('?').join(', ')})`,
83-
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
83+
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
8484
FROM message AS m
8585
LEFT JOIN handle AS h ON m.handle_id = h.ROWID
8686
LEFT JOIN chat_message_join AS cmj ON cmj.message_id = m.ROWID
@@ -411,7 +411,7 @@ WHERE m.ROWID = ?`, rowID)
411411
private getMappedMessagesWithoutExtraRows = async (chatGUID: string, pagination?: PaginationArg) => {
412412
const msgRows = await this.getMessages(chatGUID, pagination)
413413
if (pagination?.direction !== 'after') msgRows.reverse()
414-
const items = mapMessages(msgRows, [], [], this.papi.currentUser!.id)
414+
const items = mapMessages(msgRows, [], [], this.papi.currentUser!.id, this.papi.accountID)
415415
return {
416416
items,
417417
hasMore: msgRows.length === MESSAGES_LIMIT,

src/mappers.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ const removeObjReplacementChar = (text: string): string => {
5959
return text.replaceAll(OBJ_REPLACEMENT_CHAR, ' ').trim()
6060
}
6161

62-
function assignReactions(currentUserID: string, message: BeeperMessage, _reactionRows: MappedReactionMessageRow[] = [], filterIndex?: number) {
62+
const reactionStickerAssetURL = (accountID: string, rowID: MappedReactionMessageRow['ROWID']) =>
63+
`asset://${accountID}/reaction-sticker/${rowID}.heic`
64+
65+
function assignReactions(currentUserID: string, accountID: string, message: BeeperMessage, _reactionRows: MappedReactionMessageRow[] = [], filterIndex?: number) {
6366
const reactions: MessageReaction[] = []
6467
const reactionRows = filterIndex != null
6568
? _reactionRows.filter(r => r.associated_message_guid.startsWith(`p:${filterIndex}/`))
@@ -75,6 +78,7 @@ function assignReactions(currentUserID: string, message: BeeperMessage, _reactio
7578
id: participantID,
7679
reactionKey: actionKey === 'emoji' ? reaction.associated_message_emoji : actionKey,
7780
participantID,
81+
imgURL: actionKey === 'sticker' ? reactionStickerAssetURL(accountID, reaction.ROWID) : undefined,
7882
})
7983
} else if (actionType === 'unreacted') {
8084
const index = reactions.findIndex(r => r.id === participantID)
@@ -241,7 +245,7 @@ const UUID_START = 11
241245
const UUID_LENGTH = 36
242246
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
243247
// eslint-disable-next-line @typescript-eslint/default-param-last -- FIXME(skip)
244-
export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttachmentRow[] = [], reactionRows: MappedReactionMessageRow[], currentUserID: string): BeeperMessage[] {
248+
export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttachmentRow[] = [], reactionRows: MappedReactionMessageRow[], currentUserID: string, accountID: string): BeeperMessage[] {
245249
const attachments = attachmentRows.map(a => mapAttachment(a, msgRow)).filter(attachment => attachment != null)
246250
const isSMS = msgRow.service === 'SMS' || msgRow.service === 'RCS'
247251
const isGroup = !!msgRow.room_name
@@ -615,9 +619,10 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac
615619
type: reactionType,
616620
messageID: m.linkedMessageID,
617621
participantID: m.senderID,
622+
imgURL: assocMsgType === 'reacted_sticker' ? reactionStickerAssetURL(accountID, msgRow.ROWID) : undefined,
618623
reactionKey: actionKey === 'emoji' ? msgRow.associated_message_emoji : actionKey,
619624
}
620-
if (actionKey === 'emoji' || actionKey in supportedReactions) {
625+
if (actionKey === 'emoji' || actionKey === 'sticker' || actionKey in supportedReactions) {
621626
m.parseTemplate = true
622627
m.text = `${msgRow.is_from_me ? 'You' : '{{sender}}'} ${REACTION_VERB_MAP[assocMsgType]} ${msi?.ams ? `"${msi?.ams}"` : 'a message'}`
623628
m.isHidden = true
@@ -629,7 +634,7 @@ export function mapMessage(msgRow: MappedMessageRow, attachmentRows: MappedAttac
629634

630635
return messages.map(msg => {
631636
// texts.log('assigning reactions', msg.id, msg.index, reactionRows)
632-
assignReactions(currentUserID, msg, reactionRows, messages.length === 1 ? undefined : msg.extra?.part)
637+
assignReactions(currentUserID, accountID, msg, reactionRows, messages.length === 1 ? undefined : msg.extra?.part)
633638
return msg
634639
})
635640
}
@@ -672,6 +677,7 @@ function mapParticipant({ participantID: id, uncanonicalized_id }: MappedHandleR
672677
export const mapAccountLogin = (al: string) => al?.replace(/^(E|P):/, '')
673678

674679
type Context = {
680+
accountID: string
675681
currentUserID: string
676682
handleRowsMap: { [threadID: string]: MappedHandleRow[] }
677683
mapMessageArgsMap: { [threadID: string]: [MappedMessageRow[], MappedAttachmentRow[], MappedReactionMessageRow[]] }
@@ -688,16 +694,16 @@ type Context = {
688694

689695
// @ts-expect-error FIXME(skip): argument ordering
690696
// eslint-disable-next-line @typescript-eslint/default-param-last
691-
export function mapMessages(messages: MappedMessageRow[], attachmentRows?: MappedAttachmentRow[], reactionRows?: MappedReactionMessageRow[], currentUserID: string): BeeperMessage[] {
697+
export function mapMessages(messages: MappedMessageRow[], attachmentRows?: MappedAttachmentRow[], reactionRows?: MappedReactionMessageRow[], currentUserID: string, accountID: string): BeeperMessage[] {
692698
const groupedAttachmentRows = groupBy(attachmentRows, 'msgRowID')
693699
const groupedReactionRows = groupBy(reactionRows, r => r.associated_message_guid.replace(assocMsgGuidPrefix, ''))
694700
return messages
695-
.flatMap(message => mapMessage(message, groupedAttachmentRows[message.ROWID], groupedReactionRows[message.guid], currentUserID))
701+
.flatMap(message => mapMessage(message, groupedAttachmentRows[message.ROWID], groupedReactionRows[message.guid], currentUserID, accountID))
696702
.filter(Boolean)
697703
}
698704

699705
export function mapThread(chat: MappedChatRow, context: Context): BeeperThread {
700-
const { currentUserID } = context
706+
const { currentUserID, accountID } = context
701707
const handleRows = context.handleRowsMap[chat.guid]
702708
const mapMessageArgs = context.mapMessageArgsMap?.[chat.guid]
703709
const selfID = chat.last_addressed_handle || mapAccountLogin(chat.account_login) || currentUserID
@@ -707,7 +713,7 @@ export function mapThread(chat: MappedChatRow, context: Context): BeeperThread {
707713
const participants = [...handleRows.map(h => mapParticipant(h, chat.display_name)), selfParticipant].filter(participant => participant != null)
708714
const isGroup = !!chat.room_name
709715
const isReadOnly = chat.state === 0 && chat.properties != null
710-
const messages = mapMessageArgs ? mapMessages(...mapMessageArgs, currentUserID) : []
716+
const messages = mapMessageArgs ? mapMessages(...mapMessageArgs, currentUserID, accountID) : []
711717
/*
712718
props = {
713719
"com.apple.iChat.LastArchivedMessageID": [ 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX', 101010 ],

src/tests/fixture1.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,6 @@
145145
"participantID": "+918470075752"
146146
}
147147
],
148-
"kishan24x7@gmail.com"
148+
"kishan24x7@gmail.com",
149+
"imessage-test-account"
149150
]

src/tests/fixture2.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,6 @@
131131
"participantID": "+918470075752"
132132
}
133133
],
134-
"kishan24x7@gmail.com"
134+
"kishan24x7@gmail.com",
135+
"imessage-test-account"
135136
]

src/tests/mappers.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import path from 'path'
44

55
import { mapMessage } from '../mappers'
66

7+
type MapMessageFixture = Parameters<typeof mapMessage>
8+
79
async function testMessageMapFixture(fixturePath: string) {
810
const pathRelativeToTests = path.join(__dirname, fixturePath)
911

1012
test(path.basename(fixturePath), async () => {
11-
const parameters: Parameters<typeof mapMessage> = JSON.parse(await fs.readFile(pathRelativeToTests, 'utf8'))
13+
const parameters = JSON.parse(await fs.readFile(pathRelativeToTests, 'utf8')) as MapMessageFixture
14+
expect(parameters).toHaveLength(5)
1215

1316
type Row = typeof parameters[0]
1417
type MessageRowBufferKeys = { [Key in keyof Row]: Row[Key] extends Buffer ? Key : never }[keyof Row]

src/tests/partial_leading_unsends.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,6 @@
104104
}
105105
],
106106
[],
107-
"tinyslices@gmail.com"
107+
"tinyslices@gmail.com",
108+
"imessage-test-account"
108109
]

src/tests/partial_multiple_middle_adjacent_unsend.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,6 @@
104104
}
105105
],
106106
[],
107-
"tinyslices@gmail.com"
107+
"tinyslices@gmail.com",
108+
"imessage-test-account"
108109
]

src/tests/partial_trailing_unsends.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,6 @@
104104
}
105105
],
106106
[],
107-
"tinyslices@gmail.com"
107+
"tinyslices@gmail.com",
108+
"imessage-test-account"
108109
]

0 commit comments

Comments
 (0)