diff --git a/CHANGELOG.md b/CHANGELOG.md index 010d5aed7a..f52226a635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 - エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました - Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正 +- Fix: 自分がフォローしていないアカウントのフォロワー限定ノートが閲覧できることがある問題を修正 +- Fix: タイムラインのオプションで「リノートを表示」を無効にしている際、投票のみの引用リノートが流れてこない問題を修正 - エンドポイント`admin/emoji/update`の各種修正 - 必須パラメータを`id`または`name`のいずれかのみに - `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動) diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index ce9d7f5647..f45bf8622e 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -71,7 +71,15 @@ class HomeTimelineChannel extends Channel { } } - if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 純粋なリノート(引用リノートでないリノート)の場合 + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && note.poll == null) { + 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)) return; + } + } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; diff --git a/packages/backend/test/e2e/streaming.ts b/packages/backend/test/e2e/streaming.ts index 071daa275f..13d5a683ba 100644 --- a/packages/backend/test/e2e/streaming.ts +++ b/packages/backend/test/e2e/streaming.ts @@ -40,9 +40,9 @@ describe('Streaming', () => { let chinatsu: misskey.entities.SignupResponse; let takumi: misskey.entities.SignupResponse; - let kyokoNote: any; - let kanakoNote: any; - let takumiNote: any; + let kyokoNote: misskey.entities.Note; + let kanakoNote: misskey.entities.Note; + let takumiNote: misskey.entities.Note; let list: any; beforeAll(async () => { @@ -68,6 +68,9 @@ describe('Streaming', () => { // Follow: ayano => akari await follow(ayano, akari); + // Follow: kyoko => chitose + await api('following/create', { userId: chitose.id }, kyoko); + // Mute: chitose => kanako await api('mute/create', { userId: kanako.id }, chitose); @@ -170,7 +173,28 @@ describe('Streaming', () => { */ test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信が流れない', async () => { - // TODO + const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); + + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'reply to chitose\'s followers-only post', replyId: chitoseNote.id }, kyoko), // kyoko's reply to chitose's followers-only post + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); + }); + + test('フォローしているユーザーのフォローしていないユーザーの visibility: followers な投稿への返信のリノートが流れない', async () => { + const chitoseNote = await post(chitose, { text: 'followers-only post', visibility: 'followers' }); + const kyokoReply = await post(kyoko, { text: 'reply to followers-only post', replyId: chitoseNote.id }); + + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { renoteId: kyokoReply.id }, kyoko), // kyoko's renote of kyoko's reply to chitose's followers-only post + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + ); + + assert.strictEqual(fired, false); }); test('フォローしていないユーザーの投稿は流れない', async () => { @@ -202,6 +226,39 @@ describe('Streaming', () => { assert.strictEqual(fired, false); }); + + test('withRenotes: false のときリノートが流れない', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { renoteId: kyokoNote.id }, kyoko), // kyoko renote + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, false); + }); + + test('withRenotes: false のとき引用リノートが流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { text: 'quote', renoteId: kyokoNote.id }, kyoko), // kyoko quote + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, true); + }); + + test('withRenotes: false のとき投票のみのリノートが流れる', async () => { + const fired = await waitFire( + ayano, 'homeTimeline', // ayano:home + () => api('notes/create', { poll: { choices: ['kinoko', 'takenoko'] }, renoteId: kyokoNote.id }, kyoko), // kyoko renote with poll + msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko + { withRenotes: false }, + ); + + assert.strictEqual(fired, true); + }); }); // Home describe('Local Timeline', () => { diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index a2220ffae6..cd5dddd68d 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -355,7 +355,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise) => any, params?: any): Promise { +export function connectStream(user: UserToken, channel: C, listener: (message: Record) => any, params?: misskey.Channels[C]['params']): Promise { return new Promise((res, rej) => { const url = new URL(`ws://127.0.0.1:${port}/streaming`); const options: ClientOptions = {}; @@ -390,7 +390,7 @@ export function connectStream(user: UserToken, channel: string, listener: (messa }); } -export const waitFire = async (user: UserToken, channel: string, trgr: () => any, cond: (msg: Record) => boolean, params?: any) => { +export const waitFire = async (user: UserToken, channel: C, trgr: () => any, cond: (msg: Record) => boolean, params?: misskey.Channels[C]['params']) => { return new Promise(async (res, rej) => { let timer: NodeJS.Timeout | null = null; @@ -435,7 +435,7 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any */ export function makeStreamCatcher( user: UserToken, - channel: string, + channel: keyof misskey.Channels, cond: (message: Record) => boolean, extractor: (message: Record) => T, timeout = 60 * 1000): Promise {