リストタイムラインでミュートが貫通してしまう問題に対処 (#12534)
* ユーザリストTL系の各種動作を修正・統一 * fix * fix CHANGELOG.md * テスト追加
This commit is contained in:
parent
4de4a2e143
commit
c68d87538a
7 changed files with 130 additions and 6 deletions
|
@ -49,6 +49,7 @@
|
||||||
- Fix: 招待コードが使い回せる問題を修正
|
- Fix: 招待コードが使い回せる問題を修正
|
||||||
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
|
- Fix: 特定の条件下でチャンネルやユーザーのノート一覧に最新のノートが表示されなくなる問題を修正
|
||||||
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
- Fix: 何もノートしていないユーザーのフィードにアクセスするとエラーになる問題を修正
|
||||||
|
- Fix: リストタイムラインにてミュートが機能しないケースがある問題と、チャンネル投稿がストリーミングで流れてきてしまう問題を修正 #10443
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MiNote } from '@/models/Note.js';
|
||||||
import type { Packed } from './json-schema.js';
|
import type { Packed } from './json-schema.js';
|
||||||
|
|
||||||
export function isInstanceMuted(note: Packed<'Note'>, mutedInstances: Set<string>): boolean {
|
export function isInstanceMuted(note: Packed<'Note'> | MiNote, mutedInstances: Set<string>): boolean {
|
||||||
if (mutedInstances.has(note.user.host ?? '')) return true;
|
if (mutedInstances.has(note.user?.host ?? '')) return true;
|
||||||
if (mutedInstances.has(note.reply?.user.host ?? '')) return true;
|
if (mutedInstances.has(note.reply?.user?.host ?? '')) return true;
|
||||||
if (mutedInstances.has(note.renote?.user.host ?? '')) return true;
|
if (mutedInstances.has(note.renote?.user?.host ?? '')) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { QueryService } from '@/core/QueryService.js';
|
||||||
import { MiLocalUser } from '@/models/User.js';
|
import { MiLocalUser } from '@/models/User.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
|
||||||
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -124,10 +125,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMuting,
|
||||||
userIdsWhoMeMutingRenotes,
|
userIdsWhoMeMutingRenotes,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
|
userMutedInstances,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
this.cacheService.userProfileCache.fetch(me.id).then(p => new Set(p.mutedInstances)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
const timeline = await this.fanoutTimelineEndpointService.timeline({
|
||||||
|
@ -150,6 +153,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.withRenotes === false) return false;
|
if (ps.withRenotes === false) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isInstanceMuted(note, userMutedInstances)) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,6 +36,7 @@ export default class Connection {
|
||||||
public userIdsWhoMeMuting: Set<string> = new Set();
|
public userIdsWhoMeMuting: Set<string> = new Set();
|
||||||
public userIdsWhoBlockingMe: Set<string> = new Set();
|
public userIdsWhoBlockingMe: Set<string> = new Set();
|
||||||
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
|
||||||
|
public userMutedInstances: Set<string> = new Set();
|
||||||
private fetchIntervalId: NodeJS.Timeout | null = null;
|
private fetchIntervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -69,6 +70,7 @@ export default class Connection {
|
||||||
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
|
||||||
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
|
||||||
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
|
||||||
|
this.userMutedInstances = new Set(userProfile.mutedInstances);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -41,6 +41,10 @@ export default abstract class Channel {
|
||||||
return this.connection.userIdsWhoBlockingMe;
|
return this.connection.userIdsWhoBlockingMe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get userMutedInstances() {
|
||||||
|
return this.connection.userMutedInstances;
|
||||||
|
}
|
||||||
|
|
||||||
protected get followingChannels() {
|
protected get followingChannels() {
|
||||||
return this.connection.followingChannels;
|
return this.connection.followingChannels;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
|
||||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
|
|
||||||
class UserListChannel extends Channel {
|
class UserListChannel extends Channel {
|
||||||
|
@ -80,6 +80,9 @@ class UserListChannel extends Channel {
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
const isMe = this.user!.id === note.userId;
|
const isMe = this.user!.id === note.userId;
|
||||||
|
|
||||||
|
// チャンネル投稿は無視する
|
||||||
|
if (note.channelId) return;
|
||||||
|
|
||||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
|
||||||
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
|
||||||
|
@ -115,6 +118,9 @@ class UserListChannel extends Channel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する
|
||||||
|
if (isInstanceMuted(note, this.userMutedInstances)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
|
|
|
@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { MiFollowing } from '@/models/Following.js';
|
import { MiFollowing } from '@/models/Following.js';
|
||||||
import { connectStream, signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
|
import { signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
|
||||||
|
@ -34,12 +34,16 @@ describe('Streaming', () => {
|
||||||
let ayano: misskey.entities.MeSignup;
|
let ayano: misskey.entities.MeSignup;
|
||||||
let kyoko: misskey.entities.MeSignup;
|
let kyoko: misskey.entities.MeSignup;
|
||||||
let chitose: misskey.entities.MeSignup;
|
let chitose: misskey.entities.MeSignup;
|
||||||
|
let kanako: misskey.entities.MeSignup;
|
||||||
|
|
||||||
// Remote users
|
// Remote users
|
||||||
let akari: misskey.entities.MeSignup;
|
let akari: misskey.entities.MeSignup;
|
||||||
let chinatsu: misskey.entities.MeSignup;
|
let chinatsu: misskey.entities.MeSignup;
|
||||||
|
let takumi: misskey.entities.MeSignup;
|
||||||
|
|
||||||
let kyokoNote: any;
|
let kyokoNote: any;
|
||||||
|
let kanakoNote: any;
|
||||||
|
let takumiNote: any;
|
||||||
let list: any;
|
let list: any;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
@ -50,11 +54,15 @@ describe('Streaming', () => {
|
||||||
ayano = await signup({ username: 'ayano' });
|
ayano = await signup({ username: 'ayano' });
|
||||||
kyoko = await signup({ username: 'kyoko' });
|
kyoko = await signup({ username: 'kyoko' });
|
||||||
chitose = await signup({ username: 'chitose' });
|
chitose = await signup({ username: 'chitose' });
|
||||||
|
kanako = await signup({ username: 'kanako' });
|
||||||
|
|
||||||
akari = await signup({ username: 'akari', host: 'example.com' });
|
akari = await signup({ username: 'akari', host: 'example.com' });
|
||||||
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
|
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
|
||||||
|
takumi = await signup({ username: 'takumi', host: 'example.com' });
|
||||||
|
|
||||||
kyokoNote = await post(kyoko, { text: 'foo' });
|
kyokoNote = await post(kyoko, { text: 'foo' });
|
||||||
|
kanakoNote = await post(kanako, { text: 'hoge' });
|
||||||
|
takumiNote = await post(takumi, { text: 'piyo' });
|
||||||
|
|
||||||
// Follow: ayano => kyoko
|
// Follow: ayano => kyoko
|
||||||
await api('following/create', { userId: kyoko.id }, ayano);
|
await api('following/create', { userId: kyoko.id }, ayano);
|
||||||
|
@ -62,6 +70,9 @@ describe('Streaming', () => {
|
||||||
// Follow: ayano => akari
|
// Follow: ayano => akari
|
||||||
await follow(ayano, akari);
|
await follow(ayano, akari);
|
||||||
|
|
||||||
|
// Mute: chitose => kanako
|
||||||
|
await api('mute/create', { userId: kanako.id }, chitose);
|
||||||
|
|
||||||
// List: chitose => ayano, kyoko
|
// List: chitose => ayano, kyoko
|
||||||
list = await api('users/lists/create', {
|
list = await api('users/lists/create', {
|
||||||
name: 'my list',
|
name: 'my list',
|
||||||
|
@ -76,6 +87,11 @@ describe('Streaming', () => {
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
userId: kyoko.id,
|
userId: kyoko.id,
|
||||||
}, chitose);
|
}, chitose);
|
||||||
|
|
||||||
|
await api('users/lists/push', {
|
||||||
|
listId: list.id,
|
||||||
|
userId: takumi.id,
|
||||||
|
}, chitose);
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -452,6 +468,96 @@ describe('Streaming', () => {
|
||||||
|
|
||||||
assert.strictEqual(fired, false);
|
assert.strictEqual(fired, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('チャンネル投稿は流れない', async () => {
|
||||||
|
// リスインしている kyoko が 任意のチャンネルに投降した時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { text: 'foo', channelId: 'dummy' }, kyoko),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('ミュートしているユーザへのリプライがリストTLに流れない', async () => {
|
||||||
|
// chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako にリプライした時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { text: 'foo', replyId: kanakoNote.id }, kyoko),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('ミュートしているユーザの投稿をリノートしたときリストTLに流れない', async () => {
|
||||||
|
// chitose が kanako をミュートしている状態で、リスインしている kyoko が kanako のノートをリノートした時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { renoteId: kanakoNote.id }, kyoko),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('ミュートしているサーバのノートがリストTLに流れない', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
mutedInstances: ['example.com'],
|
||||||
|
}, chitose);
|
||||||
|
|
||||||
|
// chitose が example.com をミュートしている状態で、リスインしている takumi が ノートした時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { text: 'foo' }, takumi),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('ミュートしているサーバのノートに対するリプライがリストTLに流れない', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
mutedInstances: ['example.com'],
|
||||||
|
}, chitose);
|
||||||
|
|
||||||
|
// chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートにリプライした時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { text: 'foo', replyId: takumiNote.id }, kyoko),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// #10443
|
||||||
|
test('ミュートしているサーバのノートに対するリノートがリストTLに流れない', async () => {
|
||||||
|
await api('/i/update', {
|
||||||
|
mutedInstances: ['example.com'],
|
||||||
|
}, chitose);
|
||||||
|
|
||||||
|
// chitose が example.com をミュートしている状態で、リスインしている kyoko が takumi のノートをリノートした時の動きを見たい
|
||||||
|
const fired = await waitFire(
|
||||||
|
chitose, 'userList',
|
||||||
|
() => api('notes/create', { renoteId: takumiNote.id }, kyoko),
|
||||||
|
msg => msg.type === 'note' && msg.body.userId === kyoko.id,
|
||||||
|
{ listId: list.id },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(fired, false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
|
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
|
||||||
|
|
Loading…
Reference in a new issue