feat: アンテナでセンシティブなチャンネルからのノートを除外できるように ()

* feat(db): マイグレーションを追加

* feat(backend): カラムの定義を追加

* wip

* feat: フラグを設定出来るように

* feat: /notesエンドポイントを対応

* feat: websocketを対応

* test: テストを追加

* docs: CHANGELOGを更新

* docs: CHANGELOGの追加場所を修正

* chore: api.jsonを更新

* docs(CHANGELOG): General欄に移動

* docs: フォーマットを揃える

* chore: クエリを削除

* revert: 英訳を消す

* chore: note.channelを追加するところを変える

* docs: CHANGELOGを更新する

* docs(CHANGELOG): 2025.3.2に移動

* chore: changelogを下に移動

* ci: CI再実行用の空コミット

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
Nanashi. 2025-04-02 16:01:24 +09:00 committed by GitHub
parent 0bf49818c4
commit 98eadd7093
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 93 additions and 1 deletions

View file

@ -18,6 +18,7 @@
- 外部サービスであるSentryへエラー情報が送信されます。ご利用の地域の法令に従い、適切なプライバシーポリシーを策定の上で運用してください。
- Enhance: ミュートしているユーザーをユーザー検索の結果から除外するように
- Fix: 通知のページネーションで2つ以上読み込めなくなることがある問題を修正
- Enhance: アンテナでセンシティブなチャンネルのノートを除外できるように `#14177`
### Client
- Feat: 設定の管理が強化されました

4
locales/index.d.ts vendored
View file

@ -1714,6 +1714,10 @@ export interface Locale extends ILocale {
*
*/
"withFileAntenna": string;
/**
*
*/
"hideNotesInSensitiveChannel": string;
/**
*
*/

View file

@ -424,6 +424,7 @@ antennaExcludeBots: "Botアカウントを除外"
antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります"
notifyAntenna: "新しいノートを通知する"
withFileAntenna: "ファイルが添付されたノートのみ"
hideNotesInSensitiveChannel: "センシティブなチャンネルのノートを非表示"
enableServiceworker: "ブラウザへのプッシュ通知を有効にする"
antennaUsersDescription: "ユーザー名を改行で区切って指定します"
caseSensitive: "大文字小文字を区別する"

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddAntennaHideNotesInSensitiveChannel1736230492103 {
name = 'AddAntennaHideNotesInSensitiveChannel1736230492103'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" ADD "hideNotesInSensitiveChannel" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "hideNotesInSensitiveChannel"`);
}
}

View file

@ -114,6 +114,8 @@ export class AntennaService implements OnApplicationShutdown {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
if (antenna.hideNotesInSensitiveChannel && note.channel?.isSensitive) return false;
if (antenna.excludeBots && noteUser.isBot) return false;
if (antenna.localOnly && noteUser.host != null) return false;

View file

@ -532,7 +532,10 @@ export class NoteCreateService implements OnApplicationShutdown {
this.pushToTl(note, user);
this.antennaService.addNoteToAntennas(note, user);
this.antennaService.addNoteToAntennas({
...note,
channel: data.channel ?? null,
}, user);
if (data.reply) {
this.saveReply(data.reply, note);

View file

@ -41,6 +41,7 @@ export class AntennaEntityService {
excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
hideNotesInSensitiveChannel: antenna.hideNotesInSensitiveChannel,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
notify: false, // 後方互換性のため

View file

@ -100,4 +100,9 @@ export class MiAntenna {
default: false,
})
public localOnly: boolean;
@Column('boolean', {
default: false,
})
public hideNotesInSensitiveChannel: boolean;
}

View file

@ -100,5 +100,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
hideNotesInSensitiveChannel: {
type: 'boolean',
optional: false, nullable: false,
default: false,
},
},
} as const;

View file

@ -73,6 +73,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
hideNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@ -133,6 +134,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);

View file

@ -108,6 +108,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);

View file

@ -72,6 +72,7 @@ export const paramDef = {
excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
hideNotesInSensitiveChannel: { type: 'boolean' },
},
required: ['antennaId'],
} as const;
@ -129,6 +130,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
hideNotesInSensitiveChannel: ps.hideNotesInSensitiveChannel,
isActive: true,
lastUsedAt: new Date(),
});

View file

@ -146,6 +146,7 @@ describe('アンテナ', () => {
caseSensitive: false,
createdAt: new Date(response.createdAt).toISOString(),
excludeKeywords: [['']],
hideNotesInSensitiveChannel: false,
hasUnreadNote: false,
isActive: true,
keywords: [['keyword']],
@ -217,6 +218,8 @@ describe('アンテナ', () => {
{ parameters: () => ({ withReplies: true }) },
{ parameters: () => ({ withFile: false }) },
{ parameters: () => ({ withFile: true }) },
{ parameters: () => ({ hideNotesInSensitiveChannel: false }) },
{ parameters: () => ({ hideNotesInSensitiveChannel: true }) },
];
test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => {
const response = await successfulApiCall({
@ -626,6 +629,42 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
test('が取得できること(センシティブチャンネルのノートを除く)', async () => {
const keyword = 'キーワード';
const antenna = await successfulApiCall({
endpoint: 'antennas/create',
parameters: { ...defaultParam, keywords: [[keyword]], hideNotesInSensitiveChannel: true },
user: alice,
});
const nonSensitiveChannel = await successfulApiCall({
endpoint: 'channels/create',
parameters: { name: 'test', isSensitive: false },
user: alice,
});
const sensitiveChannel = await successfulApiCall({
endpoint: 'channels/create',
parameters: { name: 'test', isSensitive: true },
user: alice,
});
const noteInLocal = await post(bob, { text: `test ${keyword}` });
const noteInNonSensitiveChannel = await post(bob, { text: `test ${keyword}`, channelId: nonSensitiveChannel.id });
await post(bob, { text: `test ${keyword}`, channelId: sensitiveChannel.id });
const response = await successfulApiCall({
endpoint: 'antennas/notes',
parameters: { antennaId: antenna.id },
user: alice,
});
// 最後に投稿したものが先頭に来る。
const expected = [
noteInNonSensitiveChannel,
noteInLocal,
];
assert.deepStrictEqual(response, expected);
});
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
test.each([
{ label: 'ID指定', offsetBy: 'id' },

View file

@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
<MkSwitch v-model="hideNotesInSensitiveChannel">{{ i18n.ts.hideNotesInSensitiveChannel }}</MkSwitch>
</div>
<div :class="$style.actions">
<div class="_buttons">
@ -86,6 +87,7 @@ const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
caseSensitive: false,
localOnly: false,
withFile: false,
hideNotesInSensitiveChannel: false,
isActive: true,
hasUnreadNote: false,
notify: false,
@ -108,6 +110,7 @@ const localOnly = ref<boolean>(initialAntenna.localOnly);
const excludeBots = ref<boolean>(initialAntenna.excludeBots);
const withReplies = ref<boolean>(initialAntenna.withReplies);
const withFile = ref<boolean>(initialAntenna.withFile);
const hideNotesInSensitiveChannel = ref<boolean>(initialAntenna.hideNotesInSensitiveChannel);
const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => {
@ -124,6 +127,7 @@ async function saveAntenna() {
excludeBots: excludeBots.value,
withReplies: withReplies.value,
withFile: withFile.value,
hideNotesInSensitiveChannel: hideNotesInSensitiveChannel.value,
caseSensitive: caseSensitive.value,
localOnly: localOnly.value,
users: users.value.trim().split('\n').map(x => x.trim()),

View file

@ -4898,6 +4898,8 @@ export type components = {
hasUnreadNote: boolean;
/** @default false */
notify: boolean;
/** @default false */
hideNotesInSensitiveChannel: boolean;
};
Clip: {
/**
@ -11305,6 +11307,7 @@ export type operations = {
excludeBots?: boolean;
withReplies: boolean;
withFile: boolean;
hideNotesInSensitiveChannel?: boolean;
};
};
};
@ -11586,6 +11589,7 @@ export type operations = {
excludeBots?: boolean;
withReplies?: boolean;
withFile?: boolean;
hideNotesInSensitiveChannel?: boolean;
};
};
};