From a40734d417f589ad1e25f58c00cdc0616f962e8e Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Wed, 4 Oct 2023 11:24:46 +0900
Subject: [PATCH] =?UTF-8?q?fix(backend):=20[2023.10.1.beta-1]=20[=E3=83=8E?=
 =?UTF-8?q?=E3=83=BC=E3=83=88]=E3=82=BF=E3=83=96=E3=81=A7=E3=81=AF?=
 =?UTF-8?q?=E8=A6=8B=E3=81=88=E3=82=8B=E6=8A=95=E7=A8=BF=E3=81=8C[?=
 =?UTF-8?q?=E5=85=A8=E3=81=A6]=E3=82=BF=E3=83=96=E3=81=AB=E5=87=BA?=
 =?UTF-8?q?=E3=81=A6=E3=81=93=E3=81=AA=E3=81=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Fix #11960
---
 .../src/server/api/endpoints/users/notes.ts   | 27 +++++++--
 packages/backend/test/e2e/timelines.ts        | 58 +++++++++++++++++++
 2 files changed, 79 insertions(+), 6 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index abcc02eac..4613bfcf2 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -76,16 +76,31 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
 			let noteIdsRes: [string, string[]][] = [];
+			let repliesNoteIdsRes: [string, string[]][] = [];
 
 			if (!ps.sinceId && !ps.sinceDate) {
-				noteIdsRes = await this.redisForTimelines.xrevrange(
-					ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : ps.withReplies ? `userTimelineWithReplies:${ps.userId}` : `userTimeline:${ps.userId}`,
-					ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
-					'-',
-					'COUNT', limit);
+				[noteIdsRes, repliesNoteIdsRes] = 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 ?? '+',
+						'-',
+						'COUNT', limit),
+					ps.withReplies
+						? this.redisForTimelines.xrevrange(
+							`userTimelineWithReplies:${ps.userId}`,
+							ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
+							'-',
+							'COUNT', limit)
+						: Promise.resolve([]),
+				]);
 			}
 
-			const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
+			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),
+			]));
+			noteIds.sort((a, b) => a > b ? -1 : 1);
+			noteIds = noteIds.slice(0, ps.limit);
 
 			if (noteIds.length === 0) {
 				return [];
diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts
index f4c7ffc82..03311ac83 100644
--- a/packages/backend/test/e2e/timelines.ts
+++ b/packages/backend/test/e2e/timelines.ts
@@ -696,6 +696,64 @@ describe('Timelines', () => {
 		}, 1000 * 10);
 	});
 
+	describe('User TL', () => {
+		test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
+
+			await sleep(100); // redisに追加されるのを待つ
+
+			const res = await api('/users/notes', {}, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
+		});
+
+		test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
+			const [alice, bob] = await Promise.all([signup(), signup()]);
+
+			await api('/following/create', { userId: bob.id }, alice);
+			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
+
+			await sleep(100); // redisに追加されるのを待つ
+
+			const res = await api('/users/notes', {}, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
+			assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
+		});
+
+		test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const carolNote = await post(carol, { text: 'hi' });
+			const bobNote1 = await post(bob, { text: 'hi' });
+			const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
+
+			await sleep(100); // redisに追加されるのを待つ
+
+			const res = await api('/users/notes', {}, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
+		});
+
+		test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
+			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
+
+			const carolNote = await post(carol, { text: 'hi' });
+			const bobNote1 = await post(bob, { text: 'hi' });
+			const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
+
+			await sleep(100); // redisに追加されるのを待つ
+
+			const res = await api('/users/notes', { withReplies: true }, alice);
+
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
+			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
+		});
+	});
+
 	// TODO: リノートミュート済みユーザーのテスト
 	// TODO: ページネーションのテスト
 });