From 3cd35a76dd5f91ed54da009ded2840df40aa12d3 Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Wed, 16 Sep 2026 08:30:32 +0800 Subject: [PATCH] fix(core): write album comments as the feed owner DoQunComment still returned 10016 after the feed lookup because we stamped the bot onto StFeed.cellUserInfo. Official FeedWorker copies the feed owner's uin there and only puts the bot on StComment. Fixes #436 --- packages/core/src/bridge/apis/group-album.ts | 49 +++++++++--- .../core/tests/actions/group-album.test.ts | 74 ++++++++++++++++++- .../src/oidb-actions/group-album.ts | 15 +++- 3 files changed, 120 insertions(+), 18 deletions(-) diff --git a/packages/core/src/bridge/apis/group-album.ts b/packages/core/src/bridge/apis/group-album.ts index 8b707ff0..1c977158 100644 --- a/packages/core/src/bridge/apis/group-album.ts +++ b/packages/core/src/bridge/apis/group-album.ts @@ -2,6 +2,7 @@ import type { JsonObject, JsonValue } from '@snowluma/common/json'; import { createLogger } from '@snowluma/common/logger'; import type { AlbumCreator, + CommentReqPhotoInfo, CommentRespData, DeleteMediasRequest, DeleteMediasResponse, @@ -326,7 +327,13 @@ export class GroupAlbumApi { } const mediaLloc = commentMediaLloc(resolved, lloc); const feed = await this.getQunFeedDetail(groupId, albumId, batchId, mediaLloc); - const media = mediaInfoForComment(resolved, mediaLloc); + const ownerUin = feed.ownerUin || resolved?.uploader || ''; + const photoInfo = commentPhotoInfo( + feed.media, + mediaInfoForComment(resolved, mediaLloc), + albumId, + batchId, + ); const body = protobuf_encode({ field1: DO_QUN_COMMENT_SEQ, @@ -340,14 +347,8 @@ export class GroupAlbumApi { time: feed.time, feedId: feed.feedId, }, - field2: { - field1: { uin }, - }, - field5: { - medias: [media], - albumId, - batchId, - }, + ...(ownerUin ? { field2: { field1: { uin: ownerUin } } } : {}), + field5: photoInfo, }, field5: { user: { uin }, @@ -557,7 +558,7 @@ export class GroupAlbumApi { albumId: string, batchId: bigint, lloc: string, - ): Promise<{ time: bigint; feedId: string }> { + ): Promise<{ time: bigint; feedId: string; ownerUin: string; media?: CommentReqPhotoInfo }> { const traceId = `_${Date.now()}_${Math.floor(Math.random() * 100000)}`; const body = protobuf_encode({ seq: 0, @@ -590,7 +591,8 @@ export class GroupAlbumApi { ); } - const cell = resp.data?.feed?.feed?.cellCommon; + const feed = resp.data?.feed?.feed; + const cell = feed?.cellCommon; const feedId = cell?.feedId ?? ''; if (!feedId) { throw new Error('comment album media error: empty feed'); @@ -598,6 +600,8 @@ export class GroupAlbumApi { return { time: cell?.time ?? 0n, feedId, + ownerUin: feed?.cellUserInfo?.user?.uin ?? '', + media: feed?.cellMedia, }; } } @@ -667,18 +671,41 @@ function commentMediaLloc(item: AlbumCommentMediaItem | undefined, lloc: string) } function mediaInfoForComment(item: AlbumCommentMediaItem | undefined, lloc: string): MediaInfo { + const batchId = optionalBatchId(item?.batchId); + const shared: MediaInfo = { + ...(item?.uploader ? { uploader: item.uploader } : {}), + ...(batchId !== undefined ? { batchId } : {}), + }; if (item?.video) { return { + ...shared, type: 1, video: { cover: { lloc: item.video.cover?.lloc || lloc } }, }; } return { + ...shared, type: 0, image: { lloc: item?.image?.lloc || lloc }, }; } +function commentPhotoInfo( + feedMedia: CommentReqPhotoInfo | undefined, + fallback: MediaInfo, + albumId: string, + batchId: bigint, +): CommentReqPhotoInfo { + if (feedMedia?.medias?.length) { + return { + medias: feedMedia.medias, + albumId: feedMedia.albumId || albumId, + batchId: feedMedia.batchId ?? batchId, + }; + } + return { medias: [fallback], albumId, batchId }; +} + function findCommentMedia( mediaList: ReadonlyArray, mediaKey: string, diff --git a/packages/core/tests/actions/group-album.test.ts b/packages/core/tests/actions/group-album.test.ts index 1fbdc416..88565adf 100644 --- a/packages/core/tests/actions/group-album.test.ts +++ b/packages/core/tests/actions/group-album.test.ts @@ -584,6 +584,14 @@ describe('apis/group-album', () => { function packFeedDetail( feedId = 'official-feed-id', time = 1700000123n, + extra: { + ownerUin?: string; + cellMedia?: { + medias?: Array<{ type?: number; image?: { lloc?: string }; video?: { cover?: { lloc?: string } } }>; + albumId?: string; + batchId?: bigint; + }; + } = {}, ): ReturnType { return { success: true, @@ -596,6 +604,8 @@ describe('apis/group-album', () => { feed: { feed: { cellCommon: { time, feedId }, + ...(extra.ownerUin ? { cellUserInfo: { user: { uin: extra.ownerUin } } } : {}), + ...(extra.cellMedia ? { cellMedia: extra.cellMedia } : {}), }, }, }, @@ -616,7 +626,9 @@ describe('apis/group-album', () => { const override = extra(cmd); if (override) return override; if (cmd.endsWith('GetMediaList')) return packPhotoMedia(); - if (cmd.endsWith('GetQunFeedDetail')) return packFeedDetail(); + if (cmd.endsWith('GetQunFeedDetail')) { + return packFeedDetail('official-feed-id', 1700000123n, { ownerUin: '3119936551' }); + } return packCommentOk({ data: { id: 'cmt-1', @@ -629,14 +641,18 @@ describe('apis/group-album', () => { }; } - function commentRequestOf(bridge: ReturnType): DoQunCommentRequest { + function commentCallBytes(bridge: ReturnType): Uint8Array { const commentCall = bridge.sendRawPacket.mock.calls.find((call) => String(call[0]).endsWith('DoQunComment'), ); expect(commentCall?.[0]).toBe( 'QunAlbum.trpc.qzone.webapp_qun_operation.FeedsWriter.DoQunComment', ); - return protobuf_decode(commentCall![1] as Uint8Array); + return commentCall![1] as Uint8Array; + } + + function commentRequestOf(bridge: ReturnType): DoQunCommentRequest { + return protobuf_decode(commentCallBytes(bridge)); } function feedDetailRequestOf(bridge: ReturnType): GetQunFeedDetailRequest { @@ -682,7 +698,7 @@ describe('apis/group-album', () => { time: 1700000123n, feedId: 'official-feed-id', }, - field2: { field1: { uin: '10001' } }, + field2: { field1: { uin: '3119936551' } }, field5: { albumId: 'album-id', batchId: 77n, @@ -697,6 +713,56 @@ describe('apis/group-album', () => { }, }); expect(request.body?.reqBody?.field5?.medias?.[0]?.type ?? 0).toBe(0); + expect(Buffer.from(commentCallBytes(bridge)).subarray(0, 7).toString('hex')) + .toBe('08cf4212001a00'); + }); + + it('falls back to the media uploader when the official feed has no owner cell', async () => { + const bridge = mockBridge(); + bridge.sendRawPacket.mockImplementation(commentMocks((cmd) => { + if (cmd.endsWith('GetMediaList')) { + return packMediaList({ + mediaList: [{ type: 1, image: { lloc: 'photo-lloc' }, batchId: 77n, uploader: '3119936551' }], + }); + } + if (cmd.endsWith('GetQunFeedDetail')) return packFeedDetail(); + return undefined; + })); + + await new GroupAlbumApi(bridge as never).comment(12345, 'album-id', 'photo-lloc', 'hello'); + + expect(commentRequestOf(bridge).body?.reqBody?.field2?.field1?.uin).toBe('3119936551'); + expect(commentRequestOf(bridge).body?.field5?.user?.uin).toBe('10001'); + }); + + it('copies the official feed media cell instead of a reconstructed lloc-only cell', async () => { + const bridge = mockBridge(); + bridge.sendRawPacket.mockImplementation(commentMocks((cmd) => { + if (cmd.endsWith('GetQunFeedDetail')) { + return packFeedDetail('official-feed-id', 1700000123n, { + ownerUin: '3119936551', + cellMedia: { + albumId: 'feed-album', + batchId: 88n, + medias: [{ + type: 0, + image: { lloc: 'feed-lloc' }, + }], + }, + }); + } + return undefined; + })); + + await new GroupAlbumApi(bridge as never).comment(12345, 'album-id', 'photo-lloc', 'hello'); + + expect(commentRequestOf(bridge).body?.reqBody?.field5).toMatchObject({ + albumId: 'feed-album', + batchId: 88n, + medias: [{ + image: { lloc: 'feed-lloc' }, + }], + }); }); it('comments a video with the cover location and video media type', async () => { diff --git a/packages/proto-defs/src/oidb-actions/group-album.ts b/packages/proto-defs/src/oidb-actions/group-album.ts index df125233..a3349a3c 100644 --- a/packages/proto-defs/src/oidb-actions/group-album.ts +++ b/packages/proto-defs/src/oidb-actions/group-album.ts @@ -179,8 +179,8 @@ export interface DoQunCommentRequestBody { } export interface DoQunCommentRequest { field1?: pb<1, int_32>; - field2?: pb<2, bytes>; - field3?: pb<3, bytes>; + field2?: pb_optional<2, bytes>; + field3?: pb_optional<3, bytes>; body?: pb<4, DoQunCommentRequestBody>; traceId?: pb<5, string>; extMap?: pb_repeated<10, ExtMapEntry>; @@ -284,8 +284,17 @@ export interface QunFeedCellCommon { time?: pb<3, uint_64>; feedId?: pb<4, string>; } +export interface QunFeedCellUser { + uin?: pb<13, string>; +} +export interface QunFeedCellUserInfo { + user?: pb<1, QunFeedCellUser>; +} export interface QunFeed { - cellCommon?: pb<1, QunFeedCellCommon>; + cellCommon?: pb<1, QunFeedCellCommon>; + cellUserInfo?: pb<2, QunFeedCellUserInfo>; + // FeedWorker copies this cell into DoQunComment body.4.5. + cellMedia?: pb<5, CommentReqPhotoInfo>; } export interface QunClientFeed { feed?: pb<1, QunFeed>;