fix(backend): ダイレクト投稿がタイムライン上に正常に表示されない問題を修正 (#11993)
* DMをredisにpushするように
* add test
* add CHANGELOG
* Update NoteCreateService.ts
* lint
* ✌️
* 前のバージョンから発生した問題ではないため不要
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
			
			
This commit is contained in:
		
							parent
							
								
									457b4cf608
								
							
						
					
					
						commit
						ca07459f5e
					
				
					 4 changed files with 193 additions and 44 deletions
				
			
		| 
						 | 
				
			
			@ -494,11 +494,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
		// Increment notes count (user)
 | 
			
		||||
		this.incNotesCountOfUser(user);
 | 
			
		||||
 | 
			
		||||
		if (data.visibility === 'specified') {
 | 
			
		||||
			// TODO?
 | 
			
		||||
		} else {
 | 
			
		||||
			this.pushToTl(note, user);
 | 
			
		||||
		}
 | 
			
		||||
		this.pushToTl(note, user);
 | 
			
		||||
 | 
			
		||||
		this.antennaService.addNoteToAntennas(note, user);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -861,24 +857,34 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// TODO: キャッシュ?
 | 
			
		||||
			const followings = await this.followingsRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					followeeId: user.id,
 | 
			
		||||
					followerHost: IsNull(),
 | 
			
		||||
					isFollowerHibernated: false,
 | 
			
		||||
				},
 | 
			
		||||
				select: ['followerId', 'withReplies'],
 | 
			
		||||
			});
 | 
			
		||||
			// eslint-disable-next-line prefer-const
 | 
			
		||||
			let [followings, userListMemberships] = await Promise.all([
 | 
			
		||||
				this.followingsRepository.find({
 | 
			
		||||
					where: {
 | 
			
		||||
						followeeId: user.id,
 | 
			
		||||
						followerHost: IsNull(),
 | 
			
		||||
						isFollowerHibernated: false,
 | 
			
		||||
					},
 | 
			
		||||
					select: ['followerId', 'withReplies'],
 | 
			
		||||
				}),
 | 
			
		||||
				this.userListMembershipsRepository.find({
 | 
			
		||||
					where: {
 | 
			
		||||
						userId: user.id,
 | 
			
		||||
					},
 | 
			
		||||
					select: ['userListId', 'userListUserId', 'withReplies'],
 | 
			
		||||
				}),
 | 
			
		||||
			]);
 | 
			
		||||
 | 
			
		||||
			const userListMemberships = await this.userListMembershipsRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					userId: user.id,
 | 
			
		||||
				},
 | 
			
		||||
				select: ['userListId', 'withReplies'],
 | 
			
		||||
			});
 | 
			
		||||
			if (note.visibility === 'followers') {
 | 
			
		||||
				// TODO: 重そうだから何とかしたい Set 使う?
 | 
			
		||||
				userListMemberships = userListMemberships.filter(x => followings.some(f => f.followerId === x.userListUserId));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// TODO: あまりにも数が多いと redisPipeline.exec に失敗する(理由は不明)ため、3万件程度を目安に分割して実行するようにする
 | 
			
		||||
			for (const following of followings) {
 | 
			
		||||
				// 基本的にvisibleUserIdsには自身のidが含まれている前提であること
 | 
			
		||||
				if (note.visibility === 'specified' && !note.visibleUserIds.some(v => v === following.followerId)) continue;
 | 
			
		||||
 | 
			
		||||
				// 自分自身以外への返信
 | 
			
		||||
				if (note.replyId && note.replyUserId !== note.userId) {
 | 
			
		||||
					if (!following.withReplies) continue;
 | 
			
		||||
| 
						 | 
				
			
			@ -899,13 +905,13 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// TODO
 | 
			
		||||
			//if (note.visibility === 'followers') {
 | 
			
		||||
			//	// TODO: 重そうだから何とかしたい Set 使う?
 | 
			
		||||
			//	userLists = userLists.filter(x => followings.some(f => f.followerId === x.userListUserId));
 | 
			
		||||
			//}
 | 
			
		||||
 | 
			
		||||
			for (const userListMembership of userListMemberships) {
 | 
			
		||||
				// ダイレクトのとき、そのリストが対象外のユーザーの場合
 | 
			
		||||
				if (
 | 
			
		||||
					note.visibility === 'specified' &&
 | 
			
		||||
					!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
 | 
			
		||||
				) continue;
 | 
			
		||||
 | 
			
		||||
				// 自分自身以外への返信
 | 
			
		||||
				if (note.replyId && note.replyUserId !== note.userId) {
 | 
			
		||||
					if (!userListMembership.withReplies) continue;
 | 
			
		||||
| 
						 | 
				
			
			@ -926,7 +932,7 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		|||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			{ // 自分自身のHTL
 | 
			
		||||
			if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
 | 
			
		||||
				redisPipeline.xadd(
 | 
			
		||||
					`homeTimeline:${user.id}`,
 | 
			
		||||
					'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -136,6 +136,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
			
		|||
						}
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if (note.visibility === 'specified' && (!me || (me.id !== note.userId && !note.visibleUserIds.some(v => v === me.id)))) return false;
 | 
			
		||||
					if (note.visibility === 'followers' && !isFollowing) return false;
 | 
			
		||||
 | 
			
		||||
					return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,9 @@
 | 
			
		|||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// How to run:
 | 
			
		||||
// pnpm jest -- e2e/timelines.ts
 | 
			
		||||
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -378,6 +381,104 @@ describe('Timelines', () => {
 | 
			
		|||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('自分の visibility: specified なノートが含まれる', async () => {
 | 
			
		||||
			const [alice] = await Promise.all([signup()]);
 | 
			
		||||
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 | 
			
		||||
			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'hi');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, 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('フォローしていないユーザーの自身を visibleUserIds に指定した visibility: specified なノートが含まれない', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('フォローしているユーザーの自身を visibleUserIds に指定していない visibility: specified なノートが含まれない', async () => {
 | 
			
		||||
			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			await api('/following/create', { userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('フォローしていないユーザーからの visibility: specified なノートに返信したときの自身のノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'ok', visibility: 'specified', visibleUserIds: [bob.id], replyId: bobNote.id });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 | 
			
		||||
			assert.strictEqual(res.body.find((note: any) => note.id === aliceNote.id).text, 'ok');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		/* TODO
 | 
			
		||||
		test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] });
 | 
			
		||||
			const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, 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, 'ok');
 | 
			
		||||
		});
 | 
			
		||||
		*/
 | 
			
		||||
 | 
			
		||||
		// ↑の挙動が理想だけど実装が面倒かも
 | 
			
		||||
		test.concurrent('自身の visibility: specified なノートへのフォローしていないユーザーからの返信が含まれない', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified', visibleUserIds: [bob.id] });
 | 
			
		||||
			const bobNote = await post(bob, { text: 'ok', visibility: 'specified', visibleUserIds: [alice.id], replyId: aliceNote.id });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/timeline', {}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('Local TL', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -630,7 +731,6 @@ describe('Timelines', () => {
 | 
			
		|||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		/* 未実装
 | 
			
		||||
		test.concurrent('リスインしているフォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -645,23 +745,6 @@ describe('Timelines', () => {
 | 
			
		|||
 | 
			
		||||
			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()]);
 | 
			
		||||
 | 
			
		||||
			const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
 | 
			
		||||
			await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/user-list-timeline', { listId: list.id }, 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, null);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('リスインしているフォローしていないユーザーの他人への返信が含まれない', async () => {
 | 
			
		||||
			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 | 
			
		||||
| 
						 | 
				
			
			@ -778,6 +861,38 @@ describe('Timelines', () => {
 | 
			
		|||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
 | 
			
		||||
		}, 1000 * 10);
 | 
			
		||||
 | 
			
		||||
		test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
 | 
			
		||||
			await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/user-list-timeline', { listId: list.id }, 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('リスインしているユーザーの自身宛てではない visibility: specified なノートが含まれない', async () => {
 | 
			
		||||
			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
 | 
			
		||||
			await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
 | 
			
		||||
			await api('/users/lists/push', { listId: list.id, userId: carol.id }, alice);
 | 
			
		||||
			await sleep(1000);
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	describe('User TL', () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -951,6 +1066,30 @@ describe('Timelines', () => {
 | 
			
		|||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote3.id), true);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('自身の visibility: specified なノートが含まれる', async () => {
 | 
			
		||||
			const [alice] = await Promise.all([signup()]);
 | 
			
		||||
 | 
			
		||||
			const aliceNote = await post(alice, { text: 'hi', visibility: 'specified' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/users/notes', { userId: alice.id, withReplies: true }, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		test.concurrent('visibleUserIds に指定されてない visibility: specified なノートが含まれない', async () => {
 | 
			
		||||
			const [alice, bob] = await Promise.all([signup(), signup()]);
 | 
			
		||||
 | 
			
		||||
			const bobNote = await post(bob, { text: 'hi', visibility: 'specified' });
 | 
			
		||||
 | 
			
		||||
			await waitForPushToTl();
 | 
			
		||||
 | 
			
		||||
			const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// TODO: リノートミュート済みユーザーのテスト
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,9 @@ module.exports = {
 | 
			
		|||
			'before': true,
 | 
			
		||||
			'after': true,
 | 
			
		||||
		}],
 | 
			
		||||
		'brace-style': ['error', '1tbs', {
 | 
			
		||||
			'allowSingleLine': true,
 | 
			
		||||
		}],
 | 
			
		||||
		'padded-blocks': ['error', 'never'],
 | 
			
		||||
		/* TODO: path aliasを使わないとwarnする
 | 
			
		||||
		'no-restricted-imports': ['warn', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue