Commit Graph
33 Commits
Author SHA1 Message Date
DrSmoothl 7c0519f75f lint 2026-10-12 15:43:34 +08:00
DrSmoothl 62ab170b21 feat(onebot): add upload_video_to_qun_album
Group albums already list videos, but upload only accepted images.
Add a dedicated video action that extracts a cover and returns the new video id.

Fixed #331
2026-09-13 22:50:11 +08:00
DrSmoothl 7ef256c487 feat(onebot): include download urls on group notice images
_get_group_notice already returned picture ids. Add a fetchable url
on each image so callers do not need a separate action.

Fixes #440
2026-09-13 21:25:59 +08:00
DrSmoothl c0e73cd738 fix(protocol): handle empty group essence lists
Return an empty result when a successful, completed group essence response omits its message list instead of reporting a malformed response.

Keep upstream errors and incomplete pagination responses explicit, and cover omitted and null list variants with regression tests.

Fixes #320.
2026-08-06 22:36:05 +08:00
DrSmoothl 672802b424 fix(group-album): validate image upload inputs
Reject unsupported media with a clear error before processing begins. Existing image upload behavior remains unchanged.

Refs #310
2026-08-04 15:25:23 +08:00
DrSmoothl f6e2410dae fix(onebot): normalize group essence message results
Translate group essence responses into the established OneBot shape instead of exposing the upstream digest payload.

Return validated sender, operator, timestamp, message identifier, and message segment fields for text, face, image, and video entries. Retain the authoritative message metadata in one transaction so returned identifiers can be used by later essence actions without partial cache state.

Reject malformed, cross-group, incomplete, or unknown digest entries, and surface pagination or upstream failures instead of returning truncated success results.

Fixes #300.
2026-07-31 15:40:53 +08:00
DrSmoothl 8dd986a565 style(repo): clear workspace lint violations
Align existing managed source with the repository lint rules so the workspace can adopt one reliable root validation command.

This normalizes the private file upload block, removes stale test suppressions, makes an error boundary explicit, and records a stable settings dependency without changing user-facing behavior.

The cleanup creates a deterministic baseline before lint becomes a required pull request gate.
2026-07-31 13:08:33 +08:00
DrSmoothl a67614fa09 feat(logging): complete runtime trace diagnostics
Complete byte-level diagnostics for network, upload, runtime lifecycle, authenticated write, and background operation paths.

Preserve streaming behavior, file-storage boundaries, and default log volume while making failures, retries, cancellation, and final outcomes independently diagnosable.

Add focused regression coverage for correlation, partial data, resource context ownership, and terminal-state accuracy.
2026-07-31 11:21:12 +08:00
DrSmoothl 13a32a2865 fix(protocol): harden friend dress queries
Require the dress page to identify the requested account and every returned item before exposing the result. Reject malformed nested media metadata instead of silently degrading it.

Bound the HTML request to a 10-second redirect-chain deadline and a 2 MiB response body. Secure redirects by closing previous connections, stripping cross-origin credentials, rejecting cross-origin write forwarding, and surfacing malformed locations.

Replace the captured page with a minimal sanitized fixture and add protocol plus OneBot Action regression coverage for resource limits, redirect safety, response ownership, and structured failures.
2026-07-25 20:22:49 +08:00
H3CoF6 3517257aa6 feat(onebot): expose friend dress queries (#277)
Add a read-only action for querying QQ profile decorations from the authenticated web page.

Validate and classify page responses, expose structured errors, and cover parsing with a sanitized fixture.
2026-07-25 19:53:24 +08:00
H3CoF6 679756d1a7 fix(protocol): correct collection summary protobuf fields (#269)
Correct type 2 link and type 8 rich-media summary field mappings so collection responses no longer fail with unterminated groups.

Add wire-level regression coverage for both layouts.
2026-07-25 19:53:11 +08:00
DrSmoothl 810d72fa04 feat(onebot): implement collection list retrieval
Replace the get_collection_list placeholder with authenticated, read-only collection retrieval through the existing bridge API.

Add strict binary envelope validation, bounded pagination, accurate category filtering, preserved 64-bit identifiers, and explicit ticket, transport, service, and malformed-response failures. Cap item counts, page traversal, response bytes, per-request waits, and the total operation time.

Expose the compatible response model through Core, OneBot, the SDK, and the generated MCP catalog. Cover protocol framing, cursor behavior, filtered hasMore semantics, cleanup failures, bridge ticket acquisition, and Action validation with regression tests.

Verified with the live test bot without changing native components or adding any collection write behavior.
2026-07-19 20:22:16 +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
H3CoF6andClaude Opus 4.8 0d03b00643 Fix(Qzone) -10000风控修复 && Fix(port-probe) PID探测多pid逻辑修复和偶数端口 (#192)
* fix(protocol): retry qzone msglist via user.qzone route on -10000

emotion_cgi_msglist_v6 through the legacy h5.qzone.qq.com →
taotao.qzone.qq.com gateway can be rejected with `-10000 使用人数过多`
for some accounts (issue #182). Real-device captures show the same CGI
via user.qzone.qq.com → taotao.qq.com is not rate-limited, so retry once
through that route when the legacy route returns -10000.

Behaviour-preserving for accounts the legacy route already serves: the
retry only fires on a -10000 envelope; all other codes, transport
errors, and the missing-msglist case are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(port-probe): fix port probe logic (parse accounts more than 2) and add description for 4302-4310 port.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:33:02 +08:00
Airline233 884db940df feat(qzone): add set_qzone_msg_right to change a feed's visibility (#186)
* feat(qzone): add set_qzone_msg_right to change a feed's visibility

Add updateQzoneMsgRight in the protocol layer, porting php-qzone's
updateRight: fetch the feed via emotion_cgi_msgdetail_v6, rebuild the
full publish-shaped payload (richval/pic_bo/con), then override
ugc_right/allow_uins and POST to emotion_cgi_update. Expose it through
QzoneApi.updateRight and the OneBot set_qzone_msg_right action, with the
same ugc_right/target_uins validation as send_qzone_msg. Covered by
protocol, core and onebot tests.

* fix(qzone): make qzoneParse more stable and fixed a test example
2026-07-02 22:38:05 +08:00
DrSmoothl 63ab7a1846 fix(protocol): parse Qzone feeds without eval + stop getCookies hanging (#182)
真机抓包定位到 get_qzone_feeds 的两个问题:

1) feeds3_html_more 的响应体是**JS 对象字面量**(键无引号、单引号字符串、\xNN
   转义、数组含 undefined),不是 JSON → JSON.parse 报 "Expected property name
   at position 66"(报告者看到的「响应结构问题」)。实测调参数(outputhtmlfeed/
   format)改不掉这个格式,且 feeds 是纯远程 H5(native i64 里无相关 CGI),没有
   干净 JSON 端点。改用**非执行的递归下降解析器** parseJsLiteral 把它当数据解析——
   绝不 eval 远程内容(vm 沙箱不是安全边界,被篡改的响应可逃逸到 RCE)。解析器只认
   对象/数组/字符串/数字/true|false|null|undefined,丢弃 __proto__ 键防原型污染;
   mapFeeds 过滤数组尾部的 undefined 洞。

2) getCookies 卡死:HttpsGetCookies 无超时、无深度上限地递归跟随 302;当某跳目标
   不可达(数据中心/Docker 上 qzone.qq.com 裸域超时)→ 永久挂死所有 cookie-backed
   web 动作。加 8s 超时 + 深度上限(5) + 失败/超时保留已收集 cookie(不再 reject)。

两处修复均在 tempserver 真机验证:getCookies 从卡死 3 分钟变正常返回;抓到的真实
feeds 响应体做成测试 fixture,非执行解析器正确解出 uin/nickname/html(\x3C→<)。
读接口加了 TRACE 原始体日志 + 结构失败错误带响应片段,方便后续排查。
(msglist 的 -10000「使用人数过多」判定为 taotao 服务端风控——同 cookie 下 feeds
能成功、msglist 单独挂,非代码 bug,代码已正确透传。)

protocol 572 全绿、typecheck 干净;两轮对抗性 Agent:先揪出 vm 版可 RCE 逼我换非执行
解析器,再确认新解析器构造上不执行代码、无原型污染、真机数据解析正确。
2026-07-01 21:56:28 +08:00
Airline233 2f374ea8c9 feat(qzone): support post visibility rights when publishing qzone posts (#178)
Add ugc_right and target_uins to the qzone publish path across protocol,
bridge, and the send_qzone_msg OneBot action, regenerate the MCP catalog,
and cover restricted visibility (16/128) with validation tests.
2026-06-30 23:57:39 +08:00
Airline233 62eb8fd763 feat: Qzone支持上传图片(发说说、评论附图) (#152)
* feat(core,onebot,protocol): support publish shuoshuo and comment with image

* Fix: Accidentally changed notes
2026-06-30 01:47:21 +08:00
DrSmoothl b9d8247309 fix(qzone): complete re_feeds param set + correct qzreferrer/topicId honesty
Adversarial review (NEEDS-FIXES) — the high-risk parts (topicId base
hostUin_tid, uin/hostUin not swapped, format=fs, platformid=52) verified
correct against 3 community impls; the gaps:
- Added the re_feeds param family all 3 impls send and the publish sibling
  already sends for this CGI family: feedsType=100, private=0, paramstr=1,
  richtype/richval/isSignIn=''. Tests assert the load-bearing ones.
- qzreferrer used hostUin; all impls use the commenter's own uin → selfUin.
- topicId __1 suffix is impl-dependent (2/3 include it, 1 omits); kept as
  default but the comment now flags the suffix specifically as the
  unverified piece (base shape is confirmed), and the test no longer
  presents it as canonical.
2026-06-20 02:10:12 +08:00
DrSmoothl 95f24ffc4a feat(qzone): comment_qzone — comment on a 说说 via web API
commentQzoneMsg POSTs a form body to taotao.qzone.qq.com's
emotion_cgi_re_feeds (proxied through h5.qzone), keyed by topicId
<hostUin>_<tid>__1, as the bot (uin=self) on target_uin's feed (defaults
to the bot's own space). Returns the new comment id best-effort — the
response field name varies (commentid/commentId) so a missing id with
code 0 is still success. Throws on non-zero code/subcode; rejects empty
tid/content pre-flight. WRITE — rate-limit (风控).

topicId shape + comment-id field are extrapolated from community impls,
pending a live capture (labeled in the helper comment).

- core: QzoneApi.comment; onebot: comment_qzone action; regen catalog.
- tests: re_feeds form lock (topicId/uin/hostUin/content/format=fs),
  commentid + camelCase commentId fallback + missing-id success, empty
  tid/content preflight, non-zero code throw, bridge owner-defaulting.
2026-06-20 02:05:56 +08:00
DrSmoothl 870504ae3a fix(qzone): thread abstime into like, flag unlike endpoint as unverified
Adversarial review (NEEDS-FIXES) of the like/unlike slice:
- Missing abstime: every real dolike impl (QLiker.py, CSDN) sends the
  target feed's post time. Threaded abstime through setQzoneLike →
  QzoneApi.like → like_qzone/unlike_qzone actions (optional, default 0;
  pass the feed's real time for reliability). Test asserts it.
- internal_unlike_app is the conventional paired CGI but NO public bot
  impl exercises unlike, so it's best-guess. Labeled UNVERIFIED/pending a
  live capture in the helper comment, the unlike action summary, and the
  unlike test — instead of pinning it as confirmed truth (avoiding the
  re-codify-a-wrong-constant trap the last 3 slices hit).
- Documented the success-signal (code/subcode) as extrapolated from
  sibling CGIs (dolike may use a succ/fail token instead).

The confirmed mechanics (dolike CGI, opuin=liker, unikey/curkey mood
shape, appid=311, fid=tid) are noted as such.
2026-06-20 02:02:25 +08:00
DrSmoothl d497f1e6c7 feat(qzone): like_qzone / unlike_qzone — like/unlike a 说说 via web API
setQzoneLike POSTs a form body to w.qzone.qq.com's internal_dolike_app
(like) or internal_unlike_app (unlike), proxied through h5.qzone, keyed by
the feed's mood unikey/curkey (http://user.qzone.qq.com/<uin>/mood/<tid>)
and fid=tid, as the bot (opUin). target_uin selects whose 说说 (defaults
to the bot's own); scope is 说说 (mood, appid 311). Throws on non-zero
code/subcode. Rejects empty tid pre-flight. WRITE — rate-limit (风控).

- core: QzoneApi.like(tid, targetUin?, like); onebot: like_qzone +
  unlike_qzone actions; regen catalog.
- tests: dolike vs unlike CGI selection, unikey/curkey/fid/opuin form
  lock, empty-tid preflight, code+subcode throws, and bridge owner-
  defaulting (self vs friend) + like-flag threading.
2026-06-20 01:56:23 +08:00
DrSmoothl 4d75dc9ae3 fix(qzone): delete_v6 uses format=fs, drop bogus json param, harden success check
Adversarial review (NEEDS-FIXES) — two wrong-constant defects (same family
as the publish t1_tid bug) + a destructive-write hardening:
- format=json → format=fs. Every working reference (silica-github/
  qq_zone_delete, zhihu, daimagongfang) uses fs for the delete CGI; the
  tests re-codified the wrong value.
- Removed the invented json=1 body param (no source includes it); test now
  asserts it's absent.
- Also throw on a non-zero subcode (not just code), and documented that the
  code/subcode success signal is EXTRAPOLATED from sibling CGIs pending a
  live capture (public delete impls only check HTTP 2xx). Added subcode-throw
  and code-absent-resolves tests to lock the branch.
2026-06-20 01:52:47 +08:00
DrSmoothl a704f12945 feat(qzone): delete_qzone_msg — delete a 说说 by tid via web API
deleteQzoneMsg POSTs a form-urlencoded tid to taotao.qzone.qq.com's
emotion_cgi_delete_v6 (proxied through h5.qzone), on the bot's own space.
Success has no positive payload beyond code 0, so the contract is
throw-on-non-zero-code (foreign/unknown tid or auth failure); the server
only deletes feeds the hostUin owns. Rejects empty tid pre-flight.

- core: QzoneApi.delete; onebot: delete_qzone_msg action; regen catalog.
- tests: tid form-body/url/headers lock, empty-tid preflight, non-zero
  code throw, and bridge self-space threading.
2026-06-20 01:47:50 +08:00
DrSmoothl 9e21c3a7ef fix(qzone): read real t1_tid/t1_time publish fields — was false-throwing every success
Adversarial review (verdict NEEDS-FIXES, CRITICAL): emotion_cgi_publish_v6's
success envelope names the new feed id `t1_tid` and post time `t1_time`
(a STRING), NOT `tid`/`now`. So every successful publish hit the
'缺少 tid' guard and was reported as a failure (risking caller-retry
duplicate 说说). The tests passed only because they mocked the wrong field
names — they codified the bug.

- Read t1_tid (primary) with tid as a defensive fallback; t1_time||now
  via Number() (t1_time is a string on the wire). Verified against the
  Go reference lib starim00/AnimeAPI EmotionPublishVo.
- Added who=1 to the form body (both working references send it).
- Tests now encode the real contract (t1_tid/t1_time string), plus a
  tid/now fallback case and a no-feed-id throw case.
2026-06-20 01:44:50 +08:00
DrSmoothl 9f2eb37232 feat(qzone): send_qzone_msg — publish a text 说说 via web API
First Qzone WRITE path. publishQzoneMsg POSTs a form-urlencoded body to
taotao.qzone.qq.com's emotion_cgi_publish_v6 (proxied through h5.qzone),
g_tk in the query, on the bot's own space. Returns the server-assigned
tid (the handle for later delete/comment/like) + post time.

- Throws on transport failure, non-zero Qzone code (content rejected /
  rate-limited), or a success body with no tid — never reports a publish
  as succeeded without the feed id. Rejects empty content pre-flight.
- 带图 (image) publishing is a later slice (needs the upload CGI first).
- Action carries a风控/限流 caveat: publishing is an active action and
  high frequency is rate-limited by Qzone, same as sending messages.

- core: QzoneApi.publish; onebot: send_qzone_msg action; regen catalog.
- tests: form-body/url/headers lock, empty-content preflight, non-zero
  code throw, missing-tid throw, and bridge self-space threading.
2026-06-20 01:40:06 +08:00
DrSmoothl b00f5ab759 test(qzone): align feeds JSONP fixture to requested _preloadCallback name
Cosmetic fidelity nit from re-review: the fixture used _Callback(...) while
the request pins callback=_preloadCallback. Parser is name-agnostic so
behavior is unchanged; this just makes the fixture mirror slice-1.
2026-06-20 01:37:55 +08:00
DrSmoothl 5164ba9e39 fix(qzone): route feeds3_html_more through proxy + pin JSONP format
Adversarial review of the get_qzone_feeds slice (verdict NEEDS-FIXES):
- Host: hit ic2.qzone.qq.com directly, bypassing the proxy gateway that
  slice-1/group-album use. The qzone.qq.com cookie jar only authenticates
  against the proxy origin, so the direct call would fail referer/origin.
  Now routed through h5.qzone.qq.com/proxy/domain/ic2.qzone.qq.com/...
- Format: requested format=json but the test fed a JSONP body, so the
  requested format was never pinned. Aligned to slice-1's
  format=jsonp + callback=_preloadCallback; the test now asserts format.
- Pagination: documented honestly that page_num is reliable only for the
  first page (deep paging needs a time cursor, deferred) — in the
  docstring and the action summary/param desc.
- Clarified the key/feedskey fallback is the per-feed handle, not a cursor.
2026-06-20 01:36:04 +08:00
DrSmoothl 094dc12f0c feat(qzone): get_qzone_feeds — read friend feeds (好友动态) via web API
Second read-only Qzone slice. getQzoneFeeds over ic2.qzone.qq.com's
feeds3_html_more CGI, same cookie/g_tk plumbing + JSONP-tolerant parse +
throw-on-auth-failure contract as get_qzone_msg_list. Surfaces the stable
structured fields (uin/nickname/time/appid/key) and passes the
pre-rendered html blob through verbatim — deep HTML→segment parsing is
left to callers. Paginated via page_num (1-based) + count.

- protocol/web/qzone.ts: getQzoneFeeds + mapFeeds.
- core: QzoneApi.getFeeds (always the bot's own identity).
- onebot: get_qzone_feeds action; regenerated MCP catalog.
- tests: feeds mapping, HTTP url/params, empty-feed→[], auth-throw,
  and bridge pageNum/count threading.

Exact CGI field names / pagination cursor to be re-confirmed against a
live capture (same posture as the msglist helper).
2026-06-20 01:31:36 +08:00
DrSmoothl 0083a8fb07 test(qzone): cover empty-space happy path + bridge target_uin defaulting
Adversarial review of the get_qzone_msg_list slice flagged two real
coverage gaps (verdict was SHIP, no correctness defects):
- protocol: empty space ({code:0,msglist:[]}) must return [] not throw —
  the exact distinction the throw-on-auth-failure contract promises.
- core: QzoneApi.getMsgList target_uin defaulting + the >0 guard, the
  only place the string(identity.uin)/number(param) boundary is crossed.
2026-06-20 01:28:57 +08:00
DrSmoothl 61b4bd04a6 feat(qzone): get_qzone_msg_list — read personal Qzone 说说 list via web API
First slice of personal QQ-Zone (个人空间) support, an exclusive feature
vs NapCat/SL (both only do group albums). Read-only to prove out the
auth+parse chain before write ops (publish/like/comment).

- protocol/web/qzone.ts: getQzoneMsgList over taotao.qzone.qq.com's
  emotion_cgi_msglist_v6 (h5.qzone proxy), reusing the same cookie/g_tk
  plumbing as group-album. JSONP-tolerant parse; throws on auth failure
  rather than swallowing to [] (mirrors group-signin contract).
- core: QzoneApi bridge class wired into ApiHub.
- onebot: get_qzone_msg_list action (target_uin/pos/num), regenerated
  MCP catalog + new 空间 doc category.
- HTTP-layer tests lock URL/params, JSONP stripping, field mapping, and
  the throw-on-auth-failure paths (9 cases, all green).

Exact CGI params/response fields to be re-confirmed against a live
capture when extending (same maintenance posture as group-album).
2026-06-20 01:11:07 +08:00
DrSmoothl 9b787dfdc1 fix(onebot): TierB ① — propagate signed-list errors instead of fail-soft []
Adversarial review: swallowing transport/auth errors to [] made an expired
cookie indistinguishable from a group with zero sign-ins, and left the
onebot action's try/catch as dead code. Now getDaySignedListWebAPI
propagates transport errors and throws on a page-less response (matches
NapCat, which throws on missing page) — so auth failure becomes a proper
'failed' OneBot response; a genuinely empty group still maps to [].

Also: cover the previously-untested HTTP layer (exact URL + g_tk + json
body + error propagation), harden the rank de-skew with Number(), and
document the deliberate local-date (vs NapCat UTC) choice for dayYmd.
2026-06-18 19:28:52 +08:00
DrSmoothl 36d557e5ea feat(onebot): TierB ① — get_group_signed_list via qun.qq.com web API
Group sign-in (打卡) list is NapCat's one kernel-free item here: it's a
plain HTTP POST to qun.qq.com/v2/signin/trpc/GetDaySignedList with g_tk
from p_skey, not OIDB. SnowLuma already has the cookie/bkn infra (same as
group notice/essence/honor), so this is a straight port — no RE.

- protocol/web/group-signin.ts: getDaySignedListWebAPI + pure
  mapDaySignedList (de-skews signInRank via (r-1)/2+1, [] when no page).
- core WebApi.getSignedList(groupId) wires the qun.qq.com cookie jar.
- onebot get_group_signed_list groupAction (readOnly), [] on no sign-ins.
- Tests pin the rank/field transform and the action wiring.
2026-06-18 19:24:42 +08:00