import fs from 'fs'; import os from 'os'; import path from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { convertFriendMessage } from '../src/event-converter/to-message'; import { hashMessageIdInt32, PRIVATE_NT_MESSAGE_EVENT, PRIVATE_SENT_MESSAGE_EVENT, } from '../src/message-id'; import { MessageStore } from '../src/message-store'; import { createMessageIdResolver as productionResolver } from '../src/message-id-resolver'; import type { ConverterContext } from '../src/event-converter'; import type { FriendMessage } from '@snowluma/protocol/events'; const SELF_ID = 3961840894; const PEER_ID = 2705892349; describe('private reply id matches send receipt (#417, #433)', () => { let dir: string; let store: MessageStore; beforeEach(async () => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sl-reply-id-')); store = new MessageStore(path.join(dir, 'messages.db')); await store.initialization; }); afterEach(async () => { await store.close(); fs.rmSync(dir, { recursive: true, force: true }); }); it('matches quoted text instead of a later voice message in the same second (#502)', async () => { const sentAt = 1_790_000_000; for (const [id, message] of [ [101, [{ type: 'text', data: { text: 'the quoted text' } }]], [102, [{ type: 'record', data: { file: 'voice.amr' } }]], ] as const) { await store.storeMeta(id, { isGroup: false, targetId: PEER_ID, sequence: id, sequenceAuthoritative: true, eventName: PRIVATE_NT_MESSAGE_EVENT, clientSequence: id + 1000, privateDirection: 'outgoing', random: id, timestamp: sentAt, }); await store.storeEvent(id, false, PEER_ID, id, PRIVATE_NT_MESSAGE_EVENT, { time: sentAt, post_type: 'message_sent', message_type: 'private', sub_type: 'friend', message: JSON.parse(JSON.stringify(message)), }); } await store.close(); store = new MessageStore(path.join(dir, 'messages.db')); await store.initialization; const ctx: ConverterContext = { selfId: SELF_ID, imageUrlResolver: null, mediaUrlResolver: null, mediaSegmentSink: null, messageIdResolver: await productionResolver(store), }; const event: FriendMessage = { kind: 'friend_message', time: sentAt + 2, selfUin: SELF_ID, senderUin: PEER_ID, peerUin: PEER_ID, senderUid: 'u_peer', senderNick: 'peer', msgSeq: 2773, ntMsgSeq: 103, clientSeq: 2773, sequenceAuthoritative: true, msgId: 2, elements: [ { type: 'reply', replySeq: 9999, replySenderUin: SELF_ID, replyTime: sentAt, replyElements: [{ type: 'text', text: 'the quoted text' }], }, ], }; const json = await convertFriendMessage(ctx, event); expect(json.message).toEqual([{ type: 'reply', data: { id: '101' } }]); }); it.each([ { name: '#433', ntSeq: 682, localClientSeq: 201_616_628, quoteSeq: 27_892, sentAt: 1_788_102_428, sendId: -12_721_351, }, { name: '#417', ntSeq: 655, localClientSeq: 509_147_526, quoteSeq: 63_878, sentAt: 1_787_409_444, sendId: 866_508_396, }, ])( '$name resolves a quote of a bot-sent private message to the send receipt id', async (fixture) => { const sendId = hashMessageIdInt32(fixture.ntSeq, PEER_ID, PRIVATE_NT_MESSAGE_EVENT); expect(sendId).toBe(fixture.sendId); expect(sendId).not.toBe( hashMessageIdInt32(fixture.quoteSeq, PEER_ID, PRIVATE_SENT_MESSAGE_EVENT), ); await store.storeMeta(sendId, { isGroup: false, targetId: PEER_ID, sequence: fixture.ntSeq, sequenceAuthoritative: true, eventName: PRIVATE_NT_MESSAGE_EVENT, clientSequence: fixture.localClientSeq, privateDirection: 'outgoing', random: 1, timestamp: fixture.sentAt, }); const ctx: ConverterContext = { selfId: SELF_ID, imageUrlResolver: null, mediaUrlResolver: null, mediaSegmentSink: null, messageIdResolver: await productionResolver(store), }; const event: FriendMessage = { kind: 'friend_message', time: fixture.sentAt + 2, selfUin: SELF_ID, senderUin: PEER_ID, peerUin: PEER_ID, senderUid: 'u_peer', senderNick: 'peer', msgSeq: 2773, ntMsgSeq: fixture.ntSeq + 1, clientSeq: 2773, sequenceAuthoritative: true, msgId: 2, elements: [ { type: 'reply', replySeq: fixture.quoteSeq, replySenderUin: SELF_ID, replyTime: fixture.sentAt, }, { type: 'text', text: '这是对bot消息的回复消息' }, ], }; const json = await convertFriendMessage(ctx, event); const reply = (json.message as Array<{ type: string; data: { id: string } }>)[0]; expect(reply).toEqual({ type: 'reply', data: { id: String(sendId) } }); }, ); it('resolves a bot quote when the quote time drifts from the send receipt (#465)', async () => { const ntSeq = 6008; const localClientSeq = 567_154_552; const quoteSeq = 6008; const sentAt = 1_789_720_450; const quoteTime = sentAt + 2; const sendId = hashMessageIdInt32(ntSeq, PEER_ID, PRIVATE_NT_MESSAGE_EVENT); expect(sendId).not.toBe(hashMessageIdInt32(quoteSeq, PEER_ID, PRIVATE_SENT_MESSAGE_EVENT)); await store.storeMeta(sendId, { isGroup: false, targetId: PEER_ID, sequence: ntSeq, sequenceAuthoritative: true, eventName: PRIVATE_NT_MESSAGE_EVENT, clientSequence: localClientSeq, privateDirection: 'outgoing', random: 1, timestamp: sentAt, }); const ctx: ConverterContext = { selfId: SELF_ID, imageUrlResolver: null, mediaUrlResolver: null, mediaSegmentSink: null, messageIdResolver: await productionResolver(store), }; const event: FriendMessage = { kind: 'friend_message', time: quoteTime + 1, selfUin: SELF_ID, senderUin: PEER_ID, peerUin: PEER_ID, senderUid: 'u_peer', senderNick: 'peer', msgSeq: 6010, ntMsgSeq: ntSeq + 1, clientSeq: 6010, sequenceAuthoritative: true, msgId: 2, elements: [ { type: 'reply', replySeq: quoteSeq, replySenderUin: SELF_ID, replyTime: quoteTime, }, { type: 'text', text: '111' }, ], }; const json = await convertFriendMessage(ctx, event); const reply = (json.message as Array<{ type: string; data: { id: string } }>)[0]; expect(reply).toEqual({ type: 'reply', data: { id: String(sendId) } }); }); it('resolves a bot quote by nearby send time when the quote sequence is unrelated', async () => { const ntSeq = 682; const localClientSeq = 201_616_628; const quoteSeq = 27_892; const sentAt = 1_788_102_428; const quoteTime = sentAt + 2; const sendId = hashMessageIdInt32(ntSeq, PEER_ID, PRIVATE_NT_MESSAGE_EVENT); await store.storeMeta(sendId, { isGroup: false, targetId: PEER_ID, sequence: ntSeq, sequenceAuthoritative: true, eventName: PRIVATE_NT_MESSAGE_EVENT, clientSequence: localClientSeq, privateDirection: 'outgoing', random: 1, timestamp: sentAt, }); const ctx: ConverterContext = { selfId: SELF_ID, imageUrlResolver: null, mediaUrlResolver: null, mediaSegmentSink: null, messageIdResolver: await productionResolver(store), }; const event: FriendMessage = { kind: 'friend_message', time: quoteTime + 1, selfUin: SELF_ID, senderUin: PEER_ID, peerUin: PEER_ID, senderUid: 'u_peer', senderNick: 'peer', msgSeq: 2773, ntMsgSeq: ntSeq + 1, clientSeq: 2773, sequenceAuthoritative: true, msgId: 2, elements: [ { type: 'reply', replySeq: quoteSeq, replySenderUin: SELF_ID, replyTime: quoteTime, }, { type: 'text', text: '这是对bot消息的回复消息' }, ], }; const json = await convertFriendMessage(ctx, event); const reply = (json.message as Array<{ type: string; data: { id: string } }>)[0]; expect(reply).toEqual({ type: 'reply', data: { id: String(sendId) } }); }); });