* 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
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.
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.
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.
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.
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.
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.
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).
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.