Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(backend): リノート時のHTLへのストリーミングの意図しない挙動を修正 #13425

Merged
merged 9 commits into from
Feb 28, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`を変更する挙動)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
65 changes: 61 additions & 4 deletions packages/backend/test/e2e/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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);

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ export const uploadUrl = async (user: UserToken, url: string): Promise<Packed<'D
return catcher;
};

export function connectStream(user: UserToken, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
export function connectStream<C extends keyof misskey.Channels>(user: UserToken, channel: C, listener: (message: Record<string, any>) => any, params?: misskey.Channels[C]['params']): Promise<WebSocket> {
return new Promise((res, rej) => {
const url = new URL(`ws://127.0.0.1:${port}/streaming`);
const options: ClientOptions = {};
Expand Down Expand Up @@ -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<string, any>) => boolean, params?: any) => {
export const waitFire = async <C extends keyof misskey.Channels>(user: UserToken, channel: C, trgr: () => any, cond: (msg: Record<string, any>) => boolean, params?: misskey.Channels[C]['params']) => {
return new Promise<boolean>(async (res, rej) => {
let timer: NodeJS.Timeout | null = null;

Expand Down Expand Up @@ -435,7 +435,7 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any
*/
export function makeStreamCatcher<T>(
user: UserToken,
channel: string,
channel: keyof misskey.Channels,
cond: (message: Record<string, any>) => boolean,
extractor: (message: Record<string, any>) => T,
timeout = 60 * 1000): Promise<T> {
Expand Down
Loading