自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 (#13835)
* fix: reply to my follower notes are not shown on the home timeline * fix: reply to follower note by non-following is on social timeline * docs: changelog * test: add endpoint test for changes * test(e2e): 自分のfollowers投稿に対するリプライが流れる * test(e2e/streaming): 自分のfollowers投稿に対するリプライが流れる * test(e2e/streaming): フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題 * test(e2e/timelines): try fixing typecheck error --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									86b4f49880
								
							
						
					
					
						commit
						8f40f932e4
					
				|  | @ -109,6 +109,8 @@ | |||
|   - NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます | ||||
| 	  Migrationではカラム定義の変更のみが行われます。 | ||||
| 		サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です | ||||
| - Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正 | ||||
| - Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正 | ||||
| 
 | ||||
| ### Misskey.js | ||||
| - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) | ||||
|  |  | |||
|  | @ -143,6 +143,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				]; | ||||
| 			} | ||||
| 
 | ||||
| 			const [ | ||||
| 				followings, | ||||
| 			] = await Promise.all([ | ||||
| 				this.cacheService.userFollowingsCache.fetch(me.id), | ||||
| 			]); | ||||
| 
 | ||||
| 			const redisTimeline = await this.fanoutTimelineEndpointService.timeline({ | ||||
| 				untilId, | ||||
| 				sinceId, | ||||
|  | @ -153,6 +159,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				useDbFallback: serverSettings.enableFanoutTimelineDbFallback, | ||||
| 				alwaysIncludeMyNotes: true, | ||||
| 				excludePureRenotes: !ps.withRenotes, | ||||
| 				noteFilter: note => { | ||||
| 					if (note.reply && note.reply.visibility === 'followers') { | ||||
| 						if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; | ||||
| 					} | ||||
| 
 | ||||
| 					return true; | ||||
| 				}, | ||||
| 				dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({ | ||||
| 					untilId, | ||||
| 					sinceId, | ||||
|  |  | |||
|  | @ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- | |||
| 				excludePureRenotes: !ps.withRenotes, | ||||
| 				noteFilter: note => { | ||||
| 					if (note.reply && note.reply.visibility === 'followers') { | ||||
| 						if (!Object.hasOwn(followings, note.reply.userId)) return false; | ||||
| 						if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false; | ||||
| 					} | ||||
| 
 | ||||
| 					return true; | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ class HomeTimelineChannel extends Channel { | |||
| 			const reply = note.reply; | ||||
| 			if (this.following[note.userId]?.withReplies) { | ||||
| 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
 | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; | ||||
| 			} else { | ||||
| 				// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 | ||||
| 				if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; | ||||
|  | @ -73,7 +73,7 @@ class HomeTimelineChannel extends Channel { | |||
| 			if (note.renote.reply) { | ||||
| 				const reply = note.renote.reply; | ||||
| 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
 | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -76,14 +76,22 @@ class HybridTimelineChannel extends Channel { | |||
| 			const reply = note.reply; | ||||
| 			if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) { | ||||
| 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
 | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return; | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; | ||||
| 			} else { | ||||
| 				// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
 | ||||
| 				if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return; | ||||
| 		// 純粋なリノート(引用リノートでないリノート)の場合
 | ||||
| 		if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) { | ||||
| 			if (!this.withRenotes) return; | ||||
| 			if (note.renote.reply) { | ||||
| 				const reply = note.renote.reply; | ||||
| 				// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
 | ||||
| 				if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.user && note.renoteId && !note.text) { | ||||
| 			if (note.renote && Object.keys(note.renote.reactions).length > 0) { | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ describe('Streaming', () => { | |||
| 		let kyoko: misskey.entities.SignupResponse; | ||||
| 		let chitose: misskey.entities.SignupResponse; | ||||
| 		let kanako: misskey.entities.SignupResponse; | ||||
| 		let erin: misskey.entities.SignupResponse; | ||||
| 
 | ||||
| 		// Remote users
 | ||||
| 		let akari: misskey.entities.SignupResponse; | ||||
|  | @ -53,6 +54,7 @@ describe('Streaming', () => { | |||
| 			kyoko = await signup({ username: 'kyoko' }); | ||||
| 			chitose = await signup({ username: 'chitose' }); | ||||
| 			kanako = await signup({ username: 'kanako' }); | ||||
| 			erin = await signup({ username: 'erin' }); // erin:  A generic fifth participant
 | ||||
| 
 | ||||
| 			akari = await signup({ username: 'akari', host: 'example.com' }); | ||||
| 			chinatsu = await signup({ username: 'chinatsu', host: 'example.com' }); | ||||
|  | @ -71,6 +73,12 @@ describe('Streaming', () => { | |||
| 			// Follow: kyoko => chitose
 | ||||
| 			await api('following/create', { userId: chitose.id }, kyoko); | ||||
| 
 | ||||
| 			// Follow: erin <=> ayano each other.
 | ||||
| 			// erin => ayano: withReplies: true
 | ||||
| 			await api('following/create', { userId: ayano.id, withReplies: true }, erin); | ||||
| 			// ayano => erin: withReplies: false
 | ||||
| 			await api('following/create', { userId: erin.id, withReplies: false }, ayano); | ||||
| 
 | ||||
| 			// Mute: chitose => kanako
 | ||||
| 			await api('mute/create', { userId: kanako.id }, chitose); | ||||
| 
 | ||||
|  | @ -297,6 +305,28 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 				assert.strictEqual(fired, true); | ||||
| 			}); | ||||
| 
 | ||||
| 			test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { | ||||
| 				const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); | ||||
| 				const fired = await waitFire( | ||||
| 					erin, 'homeTimeline',	// erin:home
 | ||||
| 					() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano),	// ayano reply to erin's followers post
 | ||||
| 					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
 | ||||
| 				); | ||||
| 
 | ||||
| 				assert.strictEqual(fired, true); | ||||
| 			}); | ||||
| 
 | ||||
| 			test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { | ||||
| 				const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); | ||||
| 				const fired = await waitFire( | ||||
| 					ayano, 'homeTimeline',	// ayano:home
 | ||||
| 					() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin),	// erin reply to ayano's followers post
 | ||||
| 					msg => msg.type === 'note' && msg.body.userId === erin.id,	// wait erin
 | ||||
| 				); | ||||
| 
 | ||||
| 				assert.strictEqual(fired, true); | ||||
| 			}); | ||||
| 		});	// Home
 | ||||
| 
 | ||||
| 		describe('Local Timeline', () => { | ||||
|  | @ -475,6 +505,38 @@ describe('Streaming', () => { | |||
| 
 | ||||
| 				assert.strictEqual(fired, false); | ||||
| 			}); | ||||
| 
 | ||||
| 			test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => { | ||||
| 				const erinNote = await post(erin, { text: 'hi', visibility: 'followers' }); | ||||
| 				const fired = await waitFire( | ||||
| 					erin, 'homeTimeline',	// erin:home
 | ||||
| 					() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano),	// ayano reply to erin's followers post
 | ||||
| 					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
 | ||||
| 				); | ||||
| 
 | ||||
| 				assert.strictEqual(fired, true); | ||||
| 			}); | ||||
| 
 | ||||
| 			test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => { | ||||
| 				const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' }); | ||||
| 				const fired = await waitFire( | ||||
| 					ayano, 'homeTimeline',	// ayano:home
 | ||||
| 					() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin),	// erin reply to ayano's followers post
 | ||||
| 					msg => msg.type === 'note' && msg.body.userId === erin.id,	// wait erin
 | ||||
| 				); | ||||
| 
 | ||||
| 				assert.strictEqual(fired, true); | ||||
| 			}); | ||||
| 
 | ||||
| 			test('withReplies: true のフォローしていない人のfollowersノートに対するリプライが流れない', async () => { | ||||
| 				const fired = await waitFire( | ||||
| 					erin, 'homeTimeline',	// erin:home
 | ||||
| 					() => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano),	// ayano reply to chitose's post
 | ||||
| 					msg => msg.type === 'note' && msg.body.userId === ayano.id,	// wait ayano
 | ||||
| 				); | ||||
| 
 | ||||
| 				assert.strictEqual(fired, false); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		describe('Global Timeline', () => { | ||||
|  |  | |||
|  | @ -127,6 +127,7 @@ describe('Timelines', () => { | |||
| 		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { | ||||
| 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); | ||||
| 
 | ||||
| 			await api('following/create', { userId: carol.id }, bob); | ||||
| 			await api('following/create', { userId: bob.id }, alice); | ||||
| 			await api('following/update', { userId: bob.id, withReplies: true }, alice); | ||||
| 			await setTimeout(1000); | ||||
|  | @ -161,6 +162,24 @@ describe('Timelines', () => { | |||
| 			assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi'); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { | ||||
| 			const [alice, bob] = await Promise.all([signup(), signup()]); | ||||
| 
 | ||||
| 			await api('following/create', { userId: bob.id }, alice); | ||||
| 			await api('following/create', { userId: alice.id }, bob); | ||||
| 			await api('following/update', { userId: bob.id, withReplies: true }, alice); | ||||
| 			await setTimeout(1000); | ||||
| 			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); | ||||
| 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); | ||||
| 
 | ||||
| 			await waitForPushToTl(); | ||||
| 
 | ||||
| 			const res = await api('notes/timeline', { limit: 100 }, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => { | ||||
| 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); | ||||
| 
 | ||||
|  | @ -768,6 +787,62 @@ describe('Timelines', () => { | |||
| 			assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => { | ||||
| 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); | ||||
| 
 | ||||
| 			await api('following/create', { userId: carol.id }, bob); | ||||
| 			await api('following/create', { userId: bob.id }, alice); | ||||
| 			await api('following/update', { userId: bob.id, withReplies: true }, alice); | ||||
| 			await setTimeout(1000); | ||||
| 			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); | ||||
| 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); | ||||
| 
 | ||||
| 			await waitForPushToTl(); | ||||
| 
 | ||||
| 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => { | ||||
| 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); | ||||
| 
 | ||||
| 			await api('following/create', { userId: bob.id }, alice); | ||||
| 			await api('following/create', { userId: carol.id }, alice); | ||||
| 			await api('following/create', { userId: carol.id }, bob); | ||||
| 			await api('following/update', { userId: bob.id, withReplies: true }, alice); | ||||
| 			await setTimeout(1000); | ||||
| 			const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); | ||||
| 			const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); | ||||
| 
 | ||||
| 			await waitForPushToTl(); | ||||
| 
 | ||||
| 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true); | ||||
| 			assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi'); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => { | ||||
| 			const [alice, bob] = await Promise.all([signup(), signup()]); | ||||
| 
 | ||||
| 			await api('following/create', { userId: bob.id }, alice); | ||||
| 			await api('following/create', { userId: alice.id }, bob); | ||||
| 			await api('following/update', { userId: bob.id, withReplies: true }, alice); | ||||
| 			await setTimeout(1000); | ||||
| 			const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); | ||||
| 			const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); | ||||
| 
 | ||||
| 			await waitForPushToTl(); | ||||
| 
 | ||||
| 			const res = await api('notes/hybrid-timeline', { limit: 100 }, alice); | ||||
| 
 | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true); | ||||
| 			assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true); | ||||
| 		}); | ||||
| 
 | ||||
| 		test.concurrent('他人の他人への返信が含まれない', async () => { | ||||
| 			const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue