Use the C2C roam cursor and canonical NT sequence for bidirectional history. Persist direction-aware recall tombstones so recalled messages cannot reappear.
Distinguish QQ-authoritative message sequences from local message IDs so group files and reply placeholders cannot become history anchors or be reused by protocol actions.
Filter QQ null roam records and migrate previously stored synthetic rows.
Fixes#254
Add reverse_order to group and private history actions, preserve latest-page behavior without an anchor, and keep local fallback results chronologically ordered.
Key self-sent private messages and reply targets by the conversation peer, reject cross-conversation anchors, and include request context when a server fetch falls back to local history.
Refs #229
core's typecheck covers its own tests (protocol/onebot typecheck src
only), so the newly-required GroupMessage.groupName field failed the
Dev Build typecheck on test fixtures that construct group_message
events. Add groupName to every such fixture across core/onebot/protocol
tests.
Harvests the sound ideas from PR #128 while KEEPING origSeqs[0] as the c2c
reply sequence (on-target capture in #114/#124 proved origSeqs[0] == the quoted
message's head.sequence; pbReserve.friendSequence is a small unrelated counter
that makes reply.id miss). The PR's friendSequence override is intentionally
NOT taken.
- rich-body-decoder: enrich the reply element with replySenderUin/replyTime and
the quoted message's own decoded elements (SrcMsg.elems, field 5); recover a
c2c quoted FILE from sourceMsg (field 9 -> RichText.notOnlineFile) when elems
carried none, so a quoted file survives into get_msg (#124).
- backfillReplyTarget: tiered fallback — Tier 0 local store, Tier 1 server fetch
(correct seq = origSeqs[0]), Tier 2 reconstruct from the embedded quoted
elements (no round-trip), Tier 3 minimal '[引用消息]' placeholder so get_msg
never returns 'message not found'.
- to-segment: per-element try/catch (logged, not silent) so one bad element
doesn't drop the whole message.
- actions/message: log get_msg misses.
- tests: fix request-actions getGroupInviteCardSequence mock (gap from #125);
cover backfill Tier 2 (reconstruct) + Tier 3 (placeholder); flip the stale
c2c reply test to assert origSeqs[0] (was asserting the disproven
friendSequence).
Validated on-target (Docker): get_msg on group reply, private text reply, and a
private quoted FILE all return correct content (file with name/size/id); full
onebot + protocol suites green.
When an incoming reply quotes a message SnowLuma's store doesn't have (e.g. one
from before it was running, or never observed), the receive pipeline now fetches
just that message from the server and persists it before dispatch — so a
consumer's get_msg on the quote resolves instead of returning "message not
found". Works for both group and private (c2c).
- core: MessageApi.getGroupMessageBySeq / getC2cMessageBySeq — fetch a single
message over a 1-wide [seq,seq] range, through the shared history throttle
gate (same rate-limit as the bulk history APIs — no flood / kick risk).
- onebot: backfillReplyTarget — on a group/friend reply whose target isn't in
the store, fetch (group SsoGetGroupMsg / c2c SsoGetC2cMsg, uid resolved for
c2c) and persist under the EXACT id the reply resolves to, so get_msg hits
regardless of who sent the quoted message (a bot self-sent c2c message
converts with user_id=self but is keyed under the peer session). Awaited
before dispatch so the consumer sees it; best-effort — a back-fill failure
never blocks delivery of the live message.
- tests: gating (already-stored / no-reply / nothing-on-server) + keying
(group, c2c, c2c self-sent under peer session).
Only fires on a store miss, so the common path (no reply, or quote already
stored) stays zero-cost.