Commit Graph
414 Commits
Author SHA1 Message Date
DrSmoothl b4f5e9b71b fix(media): support base64 data URLs for uploads
Parse RFC 2397 base64 data URLs through the shared binary source classifier so inline media cannot fall through to local file handling.

Reuse the same parsing path for buffered and disk-staged uploads, reject malformed or oversized inputs explicitly, and add regression coverage for both paths.

Refs #236
2026-07-16 10:05:56 +08:00
DrSmoothl ebd02c3f3d chore(release): v1.12.3 2026-07-15 21:31:59 +08:00
DrSmoothl 6492500158 fix(core): handle omitted group album success codes
Treat an absent media-list return code as the protobuf zero default while preserving failures for explicit non-zero codes.

Add a regression test that exercises the encoded response path with media data and no status field.

Refs #234
2026-07-15 17:30:29 +08:00
DrSmoothl da26d6796f feat(core): aggregate receive health by account
Track receive health for every process attached to an account while treating unarmed monitoring as compatible rather than degraded.

Report the account as healthy while any attached process remains healthy, and mark it degraded only after every attached process has been confirmed stale. Keep health ownership synchronized with process attach, migration, and detach transitions.

Fail fast when process ownership and health state diverge instead of silently presenting a healthy status. Add multi-process regression coverage for partial degradation, full degradation, and recovery.

Refs #233.
2026-07-15 16:47:26 +08:00
DrSmoothl bd9236e3fa feat(webui): expose transactional backup restore
Replace route-local restore writes with the core transaction and map preflight, rollback, and post-commit cleanup failures to precise HTTP responses. Treat only ENOENT as a missing export file and return transaction, restored, skipped, migrated, and restart facts.

Update the settings panel to describe the full sensitive configuration group, avoid restart guidance for empty plans, and reload settings after both successful and failed imports so the UI reflects committed disk state. Add HTTP regression coverage for export and restore error semantics.
2026-07-15 16:12:51 +08:00
DrSmoothl ccbee78cc7 feat(core): execute config restores as a transaction
Move restore execution into a dedicated module with explicit preflight, snapshot, stage, commit, rollback, and cleanup phases. Keep live files untouched until staging completes, roll back partial commits, and retain the transaction directory when rollback or cleanup cannot finish.

Report the transaction ID, active phase, failed files, commit state, rollback result, and recovery path without logging configuration contents. Cover every filesystem boundary with a focused fault matrix, including unreadable current files and cleanup failures.
2026-07-15 16:12:03 +08:00
DrSmoothl 2fea3ca4d9 feat(core): validate backup restores before mutation
Route selected backup entries through owner-owned config parsers before any filesystem write. Reject unsupported paths, mismatched encodings, malformed base64, lossy normalization, invalid effective overlays, and decoded payloads larger than 32 MiB.

Treat WebUI state, private keys, and every OneBot config as a single sensitive group. Skip unselected sensitive entries without inspecting their contents, preserve recognized migrations, and assign restrictive modes to credential files.
2026-07-15 16:11:44 +08:00
DrSmoothl f9d251d1fa fix(protocol): decode group member operators correctly
Handle member joins and ordinary removals using their direct operator identity while preserving the special nested form used when the bot is removed.

Reject malformed values with bounded diagnostic context and add regression coverage for #228 and related member-decrease events.
2026-07-14 20:08:29 +08:00
DrSmoothl a3467a8d89 chore(release): v1.12.2 2026-07-14 07:57:08 +08:00
DrSmoothl 5885187e7d test(group-notice): cover publishing options and readback
Verify endpoint selection, option validation, image metadata parsing, response merging, and OneBot forwarding. Cover the type-20 new-member path and fail-fast conflict behavior.
2026-07-14 03:39:27 +08:00
DrSmoothl 51eb1f733d feat(group-notice): support advanced announcement controls
Route regular and new-member announcements through their correct web endpoints. Validate option conflicts, preserve existing defaults, merge returned notice collections, and surface transport and upload failures.
2026-07-14 03:39:11 +08:00
DrSmoothl 216213e102 ci(release): redesign cross-platform release notes
Present Windows x64, Linux x64, and Linux arm64 as fully supported release targets in a concise download matrix.

Add quick-start and unattended-consent guidance, link commits and version comparisons, and filter merge and release housekeeping from the change list.

Refresh the adjacent packaging documentation so it matches current platform support.
2026-07-13 23:15:20 +08:00
DrSmoothl 74939da3d7 feat(webui): support environment agreement consent
Allow unattended deployments to accept the EULA and Privacy Notice through SNOWLUMA_ACCEPT_EULA and SNOWLUMA_ACCEPT_PRIVACY.

Require both values, reject invalid configuration, keep environment consent non-persistent, and log whether acceptance is complete or partial.

Document the deployment contract and add regression coverage for parsing, persistence isolation, and stored-consent compatibility.
2026-07-13 23:15:10 +08:00
DrSmoothl 03aec3b052 fix(core): verify conversation read updates
Replace message-header sequence forwarding with a server-guided read flow for group and private conversations.

Batch read reports conservatively, validate per-conversation results and cursor advancement, and fail with actionable errors when the server does not apply an update.

Add regression coverage for mixed batches, pagination, already-current sessions, malformed responses, and private sequence-domain mismatches.
2026-07-13 23:01:39 +08:00
DrSmoothl 8cd7d51d80 fix(protocol): preserve friend category metadata 2026-07-13 18:51:11 +08:00
DrSmoothl c04864dd21 chore(release): v1.12.1 2026-07-10 14:00:40 +08:00
DrSmoothl 852004291b feat(onebot): reconcile and drain network adapters
Serialize desired network plans, isolate per-adapter apply failures, restore prior bindings when possible, and report saved versus applied state. Quiesce Action and event ingress, await in-flight dispatches and transport closes, retain failed generations for retry, and propagate shutdown failures.
2026-07-10 13:49:17 +08:00
DrSmoothl 01688c7567 fix(protocol): enforce message element contracts end to end
Replace the open message bag with a validated discriminated union and preflight complete messages before uploads or sends. Return BAD_REQUEST for invalid or unsendable segments instead of dropping fields, sending partial content, or silently changing private messages.
2026-07-10 13:47:06 +08:00
DrSmoothl 9f7f34cd8f fix(core): bound merged-forward decompression
Cap long-message gunzip output at 32 MiB and fail before an oversized QQ response can exhaust process memory. Include the resource id, compressed size, and configured limit in observable errors.
2026-07-10 13:42:34 +08:00
DrSmoothl b7d648ea0f fix(proton): harden generated protobuf decoders
Reject malformed varints, out-of-bounds fields, and invalid groups before they escape their parent frame. Aggregate unknown-field metadata and decode signed int32 values while preserving SnowLuma's existing outbound encoding bytes.
2026-07-10 13:40:53 +08:00
DrSmoothl 4425bee612 feat(protocol): make identity persistence availability-first 2026-07-10 03:06:00 +08:00
DrSmoothl 369a82b5e4 fix(core): enforce bridge PID ownership 2026-07-10 03:05:50 +08:00
DrSmoothl 35958a7fc2 fix(onebot): use canonical group request flags 2026-07-10 02:42:07 +08:00
DrSmoothl 3c73d1d5b0 feat(onebot): reply into group temp sessions (临时会话) (#212)
send_private_msg / send_msg can now reply to a group temp-session message
by passing the source group_id. Inbound temp messages surface their
source group on sender.group_id; a reply is only made into a temp session
the bot has an inbound record of. Group temp replies route through
RoutingHead.grpTmp (field 3).
2026-07-10 02:29:18 +08:00
DrSmoothl 5e73d69663 fix(test): add groupName to GroupMessage test fixtures (#212 CI)
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.
2026-07-09 19:18:09 +08:00
DrSmoothl 7ab91c81e9 perf(highway): single-connection pipelined upload — supersede #211 concurrency
The #211 concurrency attempt (c17fff6) was disproven on a live endpoint.
QQ's highway server requires blocks to arrive in offset order and rejects
an out-of-order PUT with error_code=102902 — ~20% failure under a real
account even after deferring the finalize block. Parallel connections
can't guarantee arrival order, so that approach is unshippable.

Replace it with a single persistent connection that PUTs blocks in strict
offset order, reusing the socket across the whole file via HTTP keep-alive:
- In order: the server never sees an out-of-order block, so no 102902.
- One warm connection: the TCP congestion window stays open across the
  file instead of paying slow-start on a fresh connection per 1 MiB block.
  That per-block cold start was the real throughput cap behind #211.

Adaptive to QQ's edge nodes FIN'ing between chunks (the reason #118 went
one-connection-per-chunk): on a transport failure, or a peer half-close
after a response, drop the socket, reconnect, and re-send the SAME block
(re-PUTting an offset is idempotent). Worst case degrades to one
connection per block — no worse than #118, and still strictly in order.

Also fixes a latent bug on the reconnect path, surfaced by review: a
response truncated mid-stream (header arrived, Content-Length unmet, then
FIN) was resolved as a partial frame and then fatally rejected by
unpackHighwayFrame OUTSIDE the retry loop, turning a recoverable mid-body
FIN into a failed upload. A truncated response now rejects as a retryable
transport failure so the block is re-sent. Added an idle read timeout so
a silent peer can't hang the upload forever (the persistent socket widens
that exposure).

Verified live (tempserver, repeated 48 MiB group-file uploads): 1
connection per file, 0 failures across dense repeats, ~1.4x throughput
over the old per-chunk-connection serial path in a low-RTT datacenter —
the win is larger on high-RTT home links where per-block slow-start hurt
most (the original #211 report). Unit tests cover keep-alive reuse,
FIN-after-each-response degradation, mid-stream reconnect+resume, body-
truncation retry, in-order exactly-once coverage, and close-once.
2026-07-09 13:55:25 +08:00
DrSmoothl c17fff6e2f perf(core): parallelize highway chunk uploads (#211)
Group/private file uploads (and all highway media) topped out around
500 KB/s regardless of link speed. Two costs stacked up in
uploadHighwayHttp: chunks were sent strictly one at a time (throughput
capped at one 1 MiB block per round-trip), and each block opened a fresh
connection that paid TCP slow-start from a cold congestion window — so a
1 MiB block finished before the window ramped and never reached line
rate.

Drive the (already offset-addressed, self-contained) blocks through a
small worker pool instead of a sequential loop. Highway frames carry
their own dataOffset/chunkMd5/fileMd5, so blocks are independent and the
server reassembles by offset; a handful of connections overlap the
round-trip stalls and ramp their windows together. Concurrency is kept
modest (4) to stay friendly to the edge nodes' rate limiting, and the
one-connection-per-block invariant from #118 is preserved — parallelism
just puts several of those short-lived connections in flight at once.

Correctness preserved and covered by new tests:
- offset claim is synchronous (no await between read and advance of
  nextOffset), so every byte range is uploaded exactly once — no skip,
  no duplicate, ragged final block correct;
- concurrent ChunkSource reads are race-free (positional FileHandle
  reads into per-call buffers; read-only Buffer subarrays);
- first permanent block failure aborts the whole upload and rejects;
  peers stop claiming new blocks and drain the one in flight;
- workers never reject, so Promise.all settles all in-flight
  reads/sockets before the source is closed exactly once — no
  close-during-read, no masked upload error;
- 0-byte source still opens zero connections; peak memory bounded to
  concurrency x ~1 MiB, never the whole file.
2026-07-09 01:20:26 +08:00
DrSmoothl b6fb5bd530 chore(release): v1.12.0 2026-07-07 22:38:02 +08:00
DrSmoothl 8fbcbd29ff feat(onebot): honour a custom per-node time in merged forward (#209)
Merged-forward nodes always stamped the current time. A custom forward can now
set each node's displayed timestamp via the OneBot `data.time` field (unix
seconds), matching the field the wire already carries per node.

parseForwardNodes reads `data.time` on custom/fake nodes and threads it onto the
node payload (the `time?` field already existed for the receive side);
buildForwardPushBody uses it for `contentHead.timestamp`, falling back to now
when absent. The value is validated to a plausible unix-seconds range — the wire
field is uint32, so a millisecond or out-of-range input is rejected (would
overflow) and defaults to now rather than corrupting the timestamp. The
message_id-reference branch (time from the quoted message) and the c2c-file
expireTime (real now) are unchanged. Nested inner nodes honour their own time.
2026-07-07 21:35:33 +08:00
DrSmoothl d9dfe513ee feat(onebot): report forced offline as notice/bot_offline
Register a handler for the StatusService.KickNT push (QQ NT's unified
forced-offline notification — kick / login-elsewhere / risk-control),
decode KickNTResponse and emit bot_offline {user_id, tag, message},
matching NapCat. Command + field layout (f4=tips_title, f3=tips_content)
RE-confirmed against wrapper.linux.node + Lagrange; not yet live-captured.
2026-07-07 15:25:35 +08:00
DrSmoothl eda1c32994 test(onebot): confirm profile_like decode against real captures
Validated the 0x210/39 ProfileLikeTip field layout byte-exact against three
real captured like pushes (detail.txt='赞了我的资料卡N次', f3=uin, f5=nick).
Drop the pending-validation caveat; test now uses the real txt shape.
2026-07-07 15:10:06 +08:00
DrSmoothl 447b40a259 feat(onebot): report profile like as notify/profile_like
Decode the multiplexed 0x210 subType-39 push: when body.msgContent is a
ProfileLikeTip with inner msgType==0/subType==203, emit notify/profile_like
{operator_id, operator_nick, times}. times is parsed from detail.txt.
Proto shape from NapCat's raw decoder; wiring + decode verified, but the
byte-exact wire is pending a real like capture to confirm.
2026-07-07 03:05:14 +08:00
DrSmoothl 0d4d58605f feat(onebot): report group special title grant as notify/title
Decode the 0x2DC subType-16 field13==6 push: eventParam carries the member
uin (f5) and a gray-tip template (f2) whose last rich token holds the title
text. Emit notify/title {group_id, user_id, title}, matching NapCat. Field
layout taken from a real on-wire sample, not ported blindly.
2026-07-07 02:53:38 +08:00
DrSmoothl bfde0daab4 feat(onebot): report group name change as notify/group_name
Dispatch the 0x2DC subType-16 push by NotifyMessageBody.field13 (was
emoji-react only); field13==12 decodes eventParam as GroupNameChange and
emits notify/group_name {group_id, user_id=operator, name_new}, matching
NapCat. Emoji-react (field13==35) path unchanged.
2026-07-07 02:05:52 +08:00
DrSmoothl 852870db59 feat(onebot): report C2C input status as notify/input_status
Decode the 0x210/0x115 input-state sys_msg (RE'd from wrapper.linux.node
aio_input_state_worker) into a friend_input_status event and map it to a
notify/input_status notice, matching NapCat's onInputStatusPush.
event_type 1 = typing, 3 = recording voice; status_text synthesised to
match the client's own strings.
2026-07-07 01:26:31 +08:00
DrSmoothl d35e276730 chore(release): v1.11.4 2026-07-06 15:04:55 +08:00
DrSmoothl e1e0c6b02f fix(video): zero-trust metadata on fast-upload group video
The width/height=0 → 文件已过期 fix only covered the normal upload path, which
ffprobes real dimensions from the video bytes. The fast-upload (fingerprint /
forward) path has NO bytes to probe, so every field it ships is caller-supplied
and untrusted — a video that arrived on the wire without metadata caches 0s, and
re-forwarding it then shipped width/height=0 (receiver renders 文件已过期) and
time=0 (every client renders 00:00).

Harden videoPayloadFromFingerprint so no degenerate value reaches the wire:
- width/height fall back with `||` (not `??`, so a cached 0 is replaced too);
- duration falls back the same way (0 → 1, never 00:00);
- the fallback cover is now a real 720x1280 image, and the fallback video/cover
  dimensions are derived FROM it (detectImageFormat), so the declared video size,
  the declared cover size and the actual cover pixels are the same numbers by
  construction — never a 1x1 image lying about its size.

Real cached values always win; c2c is unaffected (the wire builder forces 0 for
c2c). Verified on-target by forwarding a zeroed video (renders, not expired).
2026-07-05 18:10:05 +08:00
DrSmoothl ac20956076 fix(video): fall back to portrait dims on fast-upload group video
The width/height=0 → 文件已过期 fix only covered the normal upload path, which
ffprobes real dimensions. The fast-upload (fingerprint / forward) path has no
bytes to probe and takes dimensions from cached metadata, which is 0 when the
source video arrived on the wire without dimensions (e.g. forwarded from another
client that itself sent 0x0). Re-forwarding such a video to a group then shipped
0x0 and the receiver rendered a stale "expired" placeholder.

videoPayloadFromFingerprint now substitutes a neutral 720x1280 portrait when a
dimension is absent or zero (|| not ??, so a cached 0 is replaced too). Real
cached dimensions always win; c2c is unaffected (the wire builder forces 0 there
regardless). A forwarded dimensionless video now renders and plays instead of
showing 文件已过期; verified on-target by forwarding a zeroed-dim video.
2026-07-05 15:39:35 +08:00
DrSmoothl 0e6e2ec51d chore(release): v1.11.3 2026-07-05 14:26:10 +08:00
DrSmoothl 5f90cc9b6e fix(forward): preserve group merged-forward sender nickname (#201)
get_forward_msg returned empty sender.nickname for group merged-forward nodes.
The reporter blamed a mis-parsed group_id + a failing member-list enrichment
(0xfe7_3 0x899_AUTHORITY_FAIL), but on-target capture shows a mislabeled proto
field: the group response-head's sender display name lives in field 4, which we
had labeled `groupName`. It's populated on live pushes ("星屿" / the group card
"墨梓柒111") AND on merged-forward nodes (per-node sender name), while field 2
(`memberName`) is empty on live/forward and only set by the group-history fetch,
and the real group name is field 7. Lagrange labels field 4 `MemberName`.

Relabel ResponseGrp: field 4 `memberCard` (sender display name = card||nick),
field 7 `groupName` (the real group name); keep field 2 `memberName` for the
group-history slot. The group decoder now resolves the sender name by priority:
field-2 name → local member cache (clean nick/card split) → field-4 memberCard.
The last step is what lets a forward node from a group the bot isn't in keep its
per-node sender name with no member-list lookup, so the forward-specific fallback
and the AUTHORITY_FAIL enrichment path are no longer needed for the common case.

Verified on-target across scenarios: live message senderNick unchanged; a plain
forward, a forward from a never-joined group (placeholder uin 1094950020, four
distinct senders 金橘雪梨/Crel/503/Xiing0), and a nested forward all keep their
nicknames; no member-list call fires. group_id stays the payload's value.
2026-07-05 14:20:36 +08:00
DrSmoothl a4ab4e92b5 fix(forward): preserve group merged-forward sender nickname (#201)
get_forward_msg returned empty sender.nickname/card for group merged-forward
nodes. The reporter blamed a mis-parsed group_id + a failing member-list
enrichment (0xfe7_3 0x899_AUTHORITY_FAIL on a placeholder source group), but
on-target capture shows the sender display name IS in the payload: a
merged-forward node leaves `grp.memberName` null and instead carries the sender
name in `grp.groupName` (field 4) — QQ repurposes the group-name slot per node.
parseMsgPush only reads memberName (correct for live messages), so the name was
dropped and the code fell through to the member-list enrichment, which can't
resolve a group the bot isn't in.

Fix: in the forward-node decode, fall back to `grp.groupName` for the sender
nickname (scoped to the forward path — live-message decoding is untouched). This
also makes the AUTHORITY_FAIL enrichment fetch unnecessary for the common case.
Verified on a real forward (fromUin 1787882683 ↔ grp.groupName "墨梓柒"):
nickname now populated, no member-list call. group_id stays the payload's value
(genuinely a placeholder/source group, not a mis-parse).
2026-07-05 04:30:44 +08:00
DrSmoothl 1b968b9126 feat(file): stream group + offline file uploads from disk (phase 5)
Wire the streaming primitives into the group-file and private/offline-file
upload paths. Both now stage the source onto a local disk path
(stageSourceToDisk — hardlink/download), hash it in one streaming pass
(hashFileStreaming; the offline-file md510MCheckSum comes from the same pass via
headLimit=MD5_HEAD_LIMIT), and PUT the file through a FileChunkSource — so a file
up to the 4 GiB protocol ceiling never holds more than one 1 MiB chunk in RAM.
The old readFileSync/loadBinarySource + computeHashes + md5First10MB buffered
path is removed.

The highway session is resolved before the file handle is opened (the handle's
owner, uploadHighwayHttp, must be the next call so its finally-close always
runs); a size+mtime guard re-checks the staged file before the PUT so a source
mutated mid-send fails cleanly; and the staged temp is released in a finally on
every exit. The OIDB preflight fields, the remembered-upload cache, the ext
blobs, and the publish steps (group 0x6D9_4 / private sendC2cFile) are byte-for-
byte unchanged. Tests re-mock at the stage/hash seams and assert the streamed
FileChunkSource + cleanup for both group and c2c uploads.
2026-07-05 03:42:16 +08:00
DrSmoothl e83eb80f2c feat(video): stream video uploads from disk instead of buffering (phase 4)
Wire the streaming primitives into the video send path. `loadVideo` now stages
the source onto a local disk path (stageSourceToDisk — hardlink/download),
hashes it in one streaming pass (hashFileStreaming), and returns a `fileSource`
instead of a full in-memory buffer; the main video sub-file PUTs through a
`FileChunkSource`, so a multi-GiB video never holds more than one 1 MiB chunk in
RAM. The thumbnail and the fingerprint (noByteFallback) fast path stay
byte-based and unchanged.

`runPuts` now sizes its fast-path / fast-only checks off `fileSource ?? bytes`
and resolves the highway session before opening the file handle (the handle's
owner, uploadHighwayHttp, must be the next call so its finally-close always
runs). A size+mtime guard re-checks the staged file before the PUT so a source
mutated mid-send fails cleanly rather than shipping bytes that disagree with the
already-sent hashes. The staged temp is released in every exit path.

Output is byte-identical to the buffered path (the OIDB fileInfo md5/sha1/size
and the uploaded bytes are unchanged); the old readFileSync-based
stageVideoSource is removed. Tests cover the disk path (fileSource not bytes,
streamed hash == buffered hash), cleanup on upload failure, and empty sources.
2026-07-05 03:23:19 +08:00
DrSmoothl 0e4d958b5f feat(highway): add stageSourceToDisk + shared HTTP downloader (phase 3)
Stage any upload source onto a local disk path so the uploader can stream it
without buffering the whole file. `stageSourceToDisk(source, maxBytes)` unifies:
local file:// / raw path → hardlink into an owned stage dir (O(1); the inode
link pins the bytes against a caller-side unlink/retention), streamed-copy
fallback on any link() failure; http(s):// → stream to a .part file; base64://
→ maxBytes-checked decode to a .part file. Temps are finalized atomically
(fsync file → rename → fsync dir); a TTL reaper reaps crash-orphaned temps while
an in-use `active` set protects live uploads (a hardlink keeps the source's old
mtime, so the staged path is reserved in `active` before it appears on disk).

To avoid divergence, the existing `loadBinarySource` HTTP transport is extracted
into a shared `downloadHttp(source, name, maxBytes, makeSink)` engine (browser
UA, redirect, timeout, Referer retry, declared + streamed size caps, noRetry);
`loadBinarySource` now drives it with an in-memory sink — behavior unchanged.

Additive only — not yet wired into video/file uploads. Tests cover the
retention race (hardlink survives the original's deletion), the reaper's
active-set protection, size caps, symlink resolution, the copy fallback, the
Referer retry reusing the .part, and streamed-overflow .part cleanup.
2026-07-05 03:01:10 +08:00
DrSmoothl 7b91903f80 feat(highway): add streaming file hasher hashFileStreaming (phase 2)
Single-pass, disk-streaming replacement for the buffered
`computeHashes` + `computeVideoSha1Blocks` (+ offline-file head md5): reads the
file 1 MiB at a time so hashing a multi-GiB upload never buffers the whole file.
Reproduces the buffered helpers exactly — whole-file md5/sha1, the per-1 MiB
intermediate SHA1 state snapshots plus the finalized whole-file SHA1, and an
optional md5 over the first N bytes (`headLimit`, for `md510MCheckSum`).

Additive only — not wired into any caller yet; the old helpers stay in place.
Lands in a new leaf module to avoid a utils↔highway-client import cycle. A
byte-oracle test asserts identity vs the buffered functions across sizes around
the 1 MiB block boundary (empty, sub-block, exact multiples, ±1, multi-block +
tail) and head limits across chunk boundaries.
2026-07-05 02:37:56 +08:00
DrSmoothl 5324e2f014 refactor(highway): introduce ChunkSource for uploadHighwayHttp (phase 1)
Prep for streaming large media uploads from disk instead of buffering the
whole file in RAM. Add a `ChunkSource` abstraction — `BufferChunkSource`
(in-memory, byte-for-byte the old path) and `FileChunkSource` (disk-backed,
reads exact-length chunks via a FileHandle, loops on short reads, throws on
unexpected EOF) — and change `uploadHighwayHttp` to consume a `ChunkSource`
instead of a `Uint8Array`. The uploader now owns the source and closes it once
in `finally` (best-effort; never masks a primary upload error).

Pure structural refactor: no streaming is wired yet. All five call sites
(pipeline runPuts, group + private file upload, personal + group avatar) wrap
their bytes in `BufferChunkSource`, so behavior is unchanged. Adds a
byte-oracle test proving FileChunkSource ≡ BufferChunkSource across block
boundaries, plus short-read/EOF/empty-input coverage.
2026-07-05 02:29:13 +08:00
DrSmoothl 9a6c17cb27 fix(video): carry real width/height on group video sends (#145)
A group video uploaded with fileInfo width/height = 0x0 reaches receivers
as 文件已过期 even though the resource is fresh and downloadable: QQ-NT
clients (Android especially) can't lay out the video tile without real
dimensions, so they render the expired placeholder. iOS is lenient — it
shows the expired badge but still opens the video, which is exactly the
"iOS 显示过期可以点开、安卓过期点不开" split users reported.

Real-machine capture on tempserver confirmed a working QQ group video's
MsgInfo carries real dimensions (e.g. 296x640) while ours sent 0x0. Fill
width/height from the probed video for group sends; c2c stays at 0 because
the QQ-NT server rejects non-zero dimensions there with a schema mismatch
(no c2c regression coverage yet). Verified end-to-end: a fresh 11 MB group
video that rendered 已过期 now displays and plays.
2026-07-04 23:15:03 +08:00
Airline233andCopilot Autofix powered by AI d3eb3bae14 feat(qzone,onebot): add set_qzone_ban action for qzone blacklist management (#193)
* feat(onebot,qzone): add set_qzone_ban action for qzone blacklist management

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-04 14:32:40 +08:00
DrSmoothl 6cb7f2c6d0 chore(release): v1.11.2 2026-07-04 13:39:00 +08:00
DrSmoothl 07c08ed74d feat(onebot): enrich group/member info fields (#197)
Round out the OneBot v11 group-profile fields the issue asked for.

Group (get_group_info / get_group_list, inherited by get_group_info_ex /
get_group_detail_info):
- group_create_time + group_memo come free from the 0xFE5_2 list
  (createdTime / announcement), verified live (createdTime=1729505343,
  announcement populated).
- group_level lives only in the 0x88D_0 detail; get_group_info fetches it
  (memoized 5min) so a joined group doesn't hit the detail every call.
  get_group_list leaves it 0 — a per-group detail fetch across the whole
  list would be far too costly.

Member (get_group_member_info / get_group_member_list):
- Add area / unfriendly / title_expire_time / card_changeable. QQ NT does
  not expose these, so they're fixed placeholders ('' / false / 0 / true),
  matching go-cqhttp / NapCat. sex/age stay unknown/0 (real values need a
  per-member profile fetch — out of scope here).

typecheck 12 packages; onebot 523 / core 472 green.
2026-07-04 13:04:32 +08:00