From 5b00fa6f825d5cf498ecf14c1a005762f84b923d Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 5 Oct 2023 09:48:45 +0900 Subject: [PATCH] =?UTF-8?q?enhance(backend):=20users/notes=E3=81=A7?= =?UTF-8?q?=E3=83=81=E3=83=A3=E3=83=B3=E3=83=8D=E3=83=AB=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E3=82=92=E5=90=AB=E3=82=81=E3=82=8B=E3=82=AA=E3=83=97=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve #11965 --- packages/backend/src/core/NoteCreateService.ts | 6 ++++++ .../backend/src/server/api/endpoints/users/notes.ts | 12 +++++++++++- packages/backend/test/e2e/timelines.ts | 13 +++++++++++++ packages/frontend/src/pages/user/index.timeline.vue | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 3ea8af5cda..e8e9973b6e 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -806,6 +806,12 @@ export class NoteCreateService implements OnApplicationShutdown { const redisPipeline = this.redisForTimelines.pipeline(); if (note.channelId) { + redisPipeline.xadd( + `userTimelineWithChannel:${user.id}`, + 'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(), + '*', + 'note', note.id); + const channelFollowings = await this.channelFollowingsRepository.find({ where: { followeeId: note.channelId, diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 09adcf20aa..4374863f2b 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -44,6 +44,7 @@ export const paramDef = { userId: { type: 'string', format: 'misskey:id' }, withReplies: { type: 'boolean', default: false }, withRenotes: { type: 'boolean', default: true }, + withChannelNotes: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -82,9 +83,10 @@ export default class extends Endpoint { // eslint- const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 let noteIdsRes: [string, string[]][] = []; let repliesNoteIdsRes: [string, string[]][] = []; + let channelNoteIdsRes: [string, string[]][] = []; if (!ps.sinceId && !ps.sinceDate) { - [noteIdsRes, repliesNoteIdsRes] = await Promise.all([ + [noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([ this.redisForTimelines.xrevrange( ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`, ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', @@ -97,12 +99,20 @@ export default class extends Endpoint { // eslint- '-', 'COUNT', limit) : Promise.resolve([]), + ps.withChannelNotes + ? this.redisForTimelines.xrevrange( + `userTimelineWithChannel:${ps.userId}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + '-', + 'COUNT', limit) + : Promise.resolve([]), ]); } let noteIds = Array.from(new Set([ ...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId), ...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId), + ...channelNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId), ])); noteIds.sort((a, b) => a > b ? -1 : 1); noteIds = noteIds.slice(0, ps.limit); diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 8bd6f0d195..81301502d4 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -877,6 +877,19 @@ describe('Timelines', () => { assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true); }, 1000 * 10); + test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body); + const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); + + await waitForPushToTl(); + + const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice); + + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); + }); + test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 42040f5304..2e0455df65 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -38,6 +38,7 @@ const pagination = { userId: props.user.id, withRenotes: include.value === 'all', withReplies: include.value === 'all' || include.value === 'files', + withChannelNotes: include.value === 'all', withFiles: include.value === 'files', })), };