From 864827f788cd1671a4db2ebc159c1c8ab031b7ad Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 23 Nov 2023 18:56:20 +0900 Subject: [PATCH] Hard mute (#12376) * feat(backend,misskey-js): hard mute storage in backend * fix(backend,misskey-js): mute word record type * chore(frontend): generalize XWordMute * feat(frontend): configure hard mute * feat(frontend): hard mute notes on the timelines * lint(backend,frontend): fix lint failure * chore(misskey-js): update api.md * fix(backend): test failure * chore(frontend): check word mute for reply * chore: limit hard mute count --- locales/index.d.ts | 1 + locales/ja-JP.yml | 1 + .../migration/1700383825690-hard-mute.js | 11 ++++++ .../src/core/entities/UserEntityService.ts | 1 + packages/backend/src/models/UserProfile.ts | 7 +++- .../backend/src/models/json-schema/user.ts | 12 +++++++ .../src/server/api/endpoints/i/update.ts | 34 +++++++++++++++---- packages/backend/test/e2e/users.ts | 1 + packages/frontend/src/components/MkNote.vue | 17 ++++++++-- packages/frontend/src/components/MkNotes.vue | 2 +- .../src/components/MkNotifications.vue | 2 +- .../src/pages/settings/mute-block.vue | 18 +++++++++- .../pages/settings/mute-block.word-mute.vue | 22 ++++++------ packages/misskey-js/etc/misskey-js.api.md | 12 ++++--- packages/misskey-js/src/api.types.ts | 3 +- packages/misskey-js/src/entities.ts | 3 +- 16 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 packages/backend/migration/1700383825690-hard-mute.js diff --git a/locales/index.d.ts b/locales/index.d.ts index 39fbb57799..3ed2706298 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -642,6 +642,7 @@ export interface Locale { "smtpSecureInfo": string; "testEmail": string; "wordMute": string; + "hardWordMute": string; "regexpError": string; "regexpErrorDescription": string; "instanceMute": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3757715c0f..5982e71716 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -639,6 +639,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" smtpSecureInfo: "STARTTLS使用時はオフにします。" testEmail: "配信テスト" wordMute: "ワードミュート" +hardWordMute: "ハードワードミュート" regexpError: "正規表現エラー" regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:" instanceMute: "サーバーミュート" diff --git a/packages/backend/migration/1700383825690-hard-mute.js b/packages/backend/migration/1700383825690-hard-mute.js new file mode 100644 index 0000000000..afd3247f5c --- /dev/null +++ b/packages/backend/migration/1700383825690-hard-mute.js @@ -0,0 +1,11 @@ +export class HardMute1700383825690 { + name = 'HardMute1700383825690' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 17e7988176..917f4e06d0 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit { hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), unreadNotificationsCount: notificationsInfo?.unreadCount, mutedWords: profile!.mutedWords, + hardMutedWords: profile!.hardMutedWords, mutedInstances: profile!.mutedInstances, mutingNotificationTypes: [], // 後方互換性のため notificationRecieveConfig: profile!.notificationRecieveConfig, diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index d6d85c5609..8a43b60039 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -215,7 +215,12 @@ export class MiUserProfile { @Column('jsonb', { default: [], }) - public mutedWords: string[][]; + public mutedWords: (string[] | string)[]; + + @Column('jsonb', { + default: [], + }) + public hardMutedWords: (string[] | string)[]; @Column('jsonb', { default: [], diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index b0e18db01a..a2ec203e96 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -530,6 +530,18 @@ export const packedMeDetailedOnlySchema = { }, }, }, + hardMutedWords: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'array', + nullable: false, optional: false, + items: { + type: 'string', + nullable: false, optional: false, + }, + }, + }, mutedInstances: { type: 'array', nullable: true, optional: false, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b00aa87bee..8ba29c5658 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -123,6 +123,11 @@ export const meta = { }, } as const; +const muteWords = { type: 'array', items: { oneOf: [ + { type: 'array', items: { type: 'string' } }, + { type: 'string' } +] } } as const; + export const paramDef = { type: 'object', properties: { @@ -171,7 +176,8 @@ export const paramDef = { autoSensitive: { type: 'boolean' }, ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] }, pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true }, - mutedWords: { type: 'array' }, + mutedWords: muteWords, + hardMutedWords: muteWords, mutedInstances: { type: 'array', items: { type: 'string', } }, @@ -234,16 +240,20 @@ export default class extends Endpoint { // eslint- if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; - if (ps.mutedWords !== undefined) { + + function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { // TODO: ちゃんと数える const length = JSON.stringify(ps.mutedWords).length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } + } - // validate regular expression syntax - ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => { - const regexp = x.match(/^\/(.+)\/(.*)$/); + function validateMuteWordRegex(mutedWords: (string[] | string)[]) { + for (const mutedWord of mutedWords) { + if (typeof mutedWord !== "string") continue; + + const regexp = mutedWord.match(/^\/(.+)\/(.*)$/); if (!regexp) throw new ApiError(meta.errors.invalidRegexp); try { @@ -251,11 +261,21 @@ export default class extends Endpoint { // eslint- } catch (err) { throw new ApiError(meta.errors.invalidRegexp); } - }); + } + } + + if (ps.mutedWords !== undefined) { + checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } + if (ps.hardMutedWords !== undefined) { + checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + validateMuteWordRegex(ps.hardMutedWords); + profileUpdates.hardMutedWords = ps.hardMutedWords; + } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 1867525cc8..2ce8fbc129 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -168,6 +168,7 @@ describe('ユーザー', () => { hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest, unreadAnnouncements: user.unreadAnnouncements, mutedWords: user.mutedWords, + hardMutedWords: user.hardMutedWords, mutedInstances: user.mutedInstances, mutingNotificationTypes: user.mutingNotificationTypes, notificationRecieveConfig: user.notificationRecieveConfig, diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index e300ef88a5..6349df2e30 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only