From 1078cd098f75c0b3bc1d708f1cd4048ff1416a24 Mon Sep 17 00:00:00 2001 From: NoriDev Date: Tue, 12 Nov 2024 16:10:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Misskey=202024.10.1=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=EB=90=9C=20`=EC=8A=A4=ED=8C=B8=20=EB=8C=80?= =?UTF-8?q?=EC=B1=85`=EC=9D=98=20=EC=9D=BC=EB=B6=80=20=EA=B0=9C=EC=84=A0?= =?UTF-8?q?=20=EC=95=88=EB=82=B4=20=20=20-=20=EC=9D=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9D=80=20=EB=8B=A4=EC=96=91=ED=95=9C=20=EC=83=81?= =?UTF-8?q?=ED=99=A9=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EB=B3=B4=EB=8B=A4=20=EC=9C=A0=EC=97=B0=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EC=9A=B4=EC=98=81=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=A9=EB=8B=88=EB=8B=A4.=20=20=20-=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=EC=9D=98=20`7=EC=9D=BC=20=EA=B2=BD=EA=B3=BC?= =?UTF-8?q?=20=EC=8B=9C=20=EC=B4=88=EB=8C=80=EC=A0=9C=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98`=20=EC=A0=95=EC=B1=85=EC=9D=84=20=EC=84=B8=EB=B6=84?= =?UTF-8?q?=ED=99=94=20=ED=95=A9=EB=8B=88=EB=8B=A4.=20=20=20=20=20-=207?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EA=B3=BC=20=EC=8B=9C=20`=EC=B4=88?= =?UTF-8?q?=EB=8C=80=EC=A0=9C=EB=A1=9C=20=EC=A0=84=ED=99=98`=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EB=A5=BC=20=EC=84=A0=ED=83=9D=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EC=9D=8C=20=20=20=20=20-=207=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=BC=20=EC=8B=9C=20`=EA=B3=B5=EA=B0=9C=20=EB=85=B8?= =?UTF-8?q?=ED=8A=B8=20=ED=97=88=EC=9A=A9`=20=EC=97=AC=EB=B6=80=EB=A5=BC?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG_CHERRYPICK.md | 7 ++ locales/en-US.yml | 3 + locales/index.d.ts | 12 ++ locales/ja-JP.yml | 3 + locales/ko-KR.yml | 3 + .../1731385181000-checkModeratorInactive.js | 16 +++ .../backend/src/core/WebhookTestService.ts | 4 + .../src/core/entities/MetaEntityService.ts | 2 + packages/backend/src/models/Meta.ts | 10 ++ packages/backend/src/models/SystemWebhook.ts | 2 + .../backend/src/models/json-schema/meta.ts | 8 ++ ...CheckModeratorsActivityProcessorService.ts | 107 ++++++++++++++++-- .../src/server/api/endpoints/admin/meta.ts | 10 ++ .../server/api/endpoints/admin/update-meta.ts | 10 ++ ...CheckModeratorsActivityProcessorService.ts | 44 ++++++- packages/cherrypick-js/src/autogen/types.ts | 16 ++- .../src/components/MkSystemWebhookEditor.vue | 9 ++ .../frontend/src/pages/admin/moderation.vue | 30 ++++- 18 files changed, 280 insertions(+), 16 deletions(-) create mode 100644 packages/backend/migration/1731385181000-checkModeratorInactive.js diff --git a/CHANGELOG_CHERRYPICK.md b/CHANGELOG_CHERRYPICK.md index ad4ba45236..9130a0a8cc 100644 --- a/CHANGELOG_CHERRYPICK.md +++ b/CHANGELOG_CHERRYPICK.md @@ -28,6 +28,13 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE 기반 Misskey 버전: 2024.x.x
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGELOG.md#2024xx) 문서를 참고하십시오. +## NOTE +- Misskey 2024.10.1에 적용된 `스팸 대책`의 일부 개선 안내 + - 이 변경은 다양한 상황에서 관리자가 보다 유연하게 운영할 수 있도록 합니다. + - 기존의 `7일 경과 시 초대제로 전환` 정책을 세분화 합니다. + - 7일 경과 시 `초대제로 전환` 여부를 선택할 수 있음 + - 7일 경과 시 `공개 노트 허용` 여부를 선택할 수 있음 + ### General - Feat: 투표 내용 번역 - 이제 투표 내용을 그대로 번역해서 볼 수 있으며, 번역된 투표 항목과 상호작용해 바로 투표할 수도 있습니다. diff --git a/locales/en-US.yml b/locales/en-US.yml index c3d7441e23..e4c7bdbaf4 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1,5 +1,7 @@ --- _lang_: "English" +disableRegistrationWhenInactive: "Disable new user registration when moderator is inactivated" +disablePublicNoteWhenInactive: "Disable 'Can send public notes' when moderator is inactivated" youBlocked: "You’re blocked" youBlockedDescription: "You can’t follow or see {user}’s posts." schedulePost: "Posting a scheduled note" @@ -2848,6 +2850,7 @@ _webhookSettings: abuseReport: "When received a new report" abuseReportResolved: "When resolved report" userCreated: "When user is created" + inactiveModeratorsDisablePublicNoteChanged: "If a moderator is inactive for a certain period of time and the system disabled 'Can send public notes'." deleteConfirm: "Are you sure you want to delete the Webhook?" testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data." _abuseReport: diff --git a/locales/index.d.ts b/locales/index.d.ts index 8f9ee26fed..a12045a321 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -13,6 +13,14 @@ export interface Locale extends ILocale { * 日本語 */ "_lang_": string; + /** + * モデレーターが一定期間非アクティブになったとき、新規登録を無効化 + */ + "disableRegistrationWhenInactive": string; + /** + * モデレーターが一定期間非アクティブになったとき、「パブリック投稿の許可」を無効化 + */ + "disablePublicNoteWhenInactive": string; /** * ブロックされています */ @@ -11109,6 +11117,10 @@ export interface Locale extends ILocale { * モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき */ "inactiveModeratorsInvitationOnlyChanged": string; + /** + * モデレーターが一定期間非アクティブだったため、システムによりパブリック投稿へと変更されたとき + */ + "inactiveModeratorsDisablePublicNoteChanged": string; }; /** * Webhookを削除しますか? diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c6d0f3ea7e..78f62f55ae 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1,5 +1,7 @@ _lang_: "日本語" +disableRegistrationWhenInactive: "モデレーターが一定期間非アクティブになったとき、新規登録を無効化" +disablePublicNoteWhenInactive: "モデレーターが一定期間非アクティブになったとき、「パブリック投稿の許可」を無効化" youBlocked: "ブロックされています" youBlockedDescription: "{user}さんのフォローやポストの表示はできません。" schedulePost: "予約投稿" @@ -2935,6 +2937,7 @@ _webhookSettings: userCreated: "ユーザーが作成されたとき" inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき" inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき" + inactiveModeratorsDisablePublicNoteChanged: "モデレーターが一定期間非アクティブだったため、システムによりパブリック投稿へと変更されたとき" deleteConfirm: "Webhookを削除しますか?" testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index ccc39e2318..76b4e2509d 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1,5 +1,7 @@ --- _lang_: "한국어" +disableRegistrationWhenInactive: "모더레이터 부재 시 신규 회원가입 비활성화" +disablePublicNoteWhenInactive: "모더레이터 부재 시 '공개 노트 허용' 비활성화" youBlocked: "앗.. 차단당했어요.." youBlockedDescription: "{user} 님을 팔로우하거나 해당 사용자의 게시물을 볼 수 없어요." schedulePost: "노트 게시 예약" @@ -2857,6 +2859,7 @@ _webhookSettings: userCreated: "사용자가 생성되었을 때" inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우" inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우" + inactiveModeratorsDisablePublicNoteChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 '공개 노트 허용'이 비활성화로 바뀐 경우" deleteConfirm: "이 Webhook을 삭제할까요?" testRemarks: "스위치 오른쪽에 있는 버튼을 클릭해 더미 데이터를 사용한 테스트용 Webhook을 보낼 수 있어요." _abuseReport: diff --git a/packages/backend/migration/1731385181000-checkModeratorInactive.js b/packages/backend/migration/1731385181000-checkModeratorInactive.js new file mode 100644 index 0000000000..91259a3e8c --- /dev/null +++ b/packages/backend/migration/1731385181000-checkModeratorInactive.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class checkModeratorInactive1731385181000 { + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "disableRegistrationWhenInactive" boolean NOT NULL DEFAULT true`); + await queryRunner.query(`ALTER TABLE "meta" ADD "disablePublicNoteWhenInactive" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableRegistrationWhenInactive";`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disablePublicNoteWhenInactive";`); + } +} diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 55c8a52705..94c65de853 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -463,6 +463,10 @@ export class WebhookTestService { send({}); break; } + case 'inactiveModeratorsDisablePublicNoteChanged': { + send({}); + break; + } } } } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 976780e29a..99aca74703 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -87,6 +87,8 @@ export class MetaEntityService { privacyPolicyUrl: instance.privacyPolicyUrl, inquiryUrl: instance.inquiryUrl, disableRegistration: instance.disableRegistration, + disableRegistrationWhenInactive: instance.disableRegistrationWhenInactive, + disablePublicNoteWhenInactive: instance.disablePublicNoteWhenInactive, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index c222852eda..7b1e15ba4d 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -813,4 +813,14 @@ export class MiMeta { default: '{}', }) public customSplashText: string[]; + + @Column('boolean', { + default: true, + }) + public disableRegistrationWhenInactive: boolean; + + @Column('boolean', { + default: false, + }) + public disablePublicNoteWhenInactive: boolean; } diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts index 1a7ce4962b..0bffbfbc34 100644 --- a/packages/backend/src/models/SystemWebhook.ts +++ b/packages/backend/src/models/SystemWebhook.ts @@ -18,6 +18,8 @@ export const systemWebhookEventTypes = [ 'inactiveModeratorsWarning', // モデレータが一定期間不在のためシステムにより招待制へと変更された 'inactiveModeratorsInvitationOnlyChanged', + // モデレータが一定期間不在のためシステムによりパブリック投稿へと変更された + 'inactiveModeratorsDisablePublicNoteChanged', ] as const; export type SystemWebhookEventType = typeof systemWebhookEventTypes[number]; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 7bc032fdc7..9cb61faff8 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -277,6 +277,14 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + disableRegistrationWhenInactive: { + type: 'boolean', + optional: false, nullable: false, + }, + disablePublicNoteWhenInactive: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index d3dd354c7f..e575869d2c 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -8,7 +8,7 @@ import { In } from 'typeorm'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; -import { RoleService } from '@/core/RoleService.js'; +import { DEFAULT_POLICIES, RoleService } from '@/core/RoleService.js'; import { EmailService } from '@/core/EmailService.js'; import { MiUser, type UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -38,7 +38,7 @@ export type ModeratorInactivityRemainingTime = { }; function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) { - const subject = 'Moderator Inactivity Warning / モデレーター不在の通知'; + const subject = 'Moderator Inactivity Warning / モデレーター不在の通知 / 모더레이터 부재 안내'; const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`; const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`; @@ -76,7 +76,7 @@ function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemai } function generateInvitationOnlyChangedMail() { - const subject = 'Change to Invitation-Only / 招待制に変更されました'; + const subject = 'Change to Invitation-Only / 招待制に変更されました / 초대제로 변경되었습니다'; const message = [ 'To Moderators,', @@ -110,6 +110,41 @@ function generateInvitationOnlyChangedMail() { }; } +function generateDisablePublicNoteChangedMail() { + const subject = 'Change to Public Note Disabled / パブリック投稿が無効になりました / 공개 노트가 비활성화 되었습니다'; + + const message = [ + 'To Moderators,', + '', + `Changed to public note disabled because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`, + 'To cancel the public note disabled, you need to access the control panel.', + '', + '---------------', + '', + 'To モデレーター各位', + '', + `モデレーターの活動が${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出されなかったため、パブリック投稿が無効に変更されました。`, + 'パブリック投稿無効を解除するには、コントロールパネルにアクセスする必要があります。', + '', + '---------------', + '', + 'To 모더레이터 여러분께', + '', + `모더레이터가 ${MODERATOR_INACTIVITY_LIMIT_DAYS}일간 활동이 확인되지 않아 '공개 노트 허용'이 비활성화로 변경되었어요.`, + '다시 허용하려면 `제어판 - 역할`에 접속해서 변경해야 해요.', + '', + ]; + + const html = message.join('
'); + const text = message.join('\n'); + + return { + subject, + html, + text, + }; +} + @Injectable() export class CheckModeratorsActivityProcessorService { private logger: Logger; @@ -132,7 +167,8 @@ export class CheckModeratorsActivityProcessorService { this.logger.info('start.'); const meta = await this.metaService.fetch(false); - if (!meta.disableRegistration) { + const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; + if ((!meta.disableRegistration && meta.disableRegistrationWhenInactive) || (basePolicies.canPublicNote && meta.disablePublicNoteWhenInactive)) { await this.processImpl(); } else { this.logger.info('is already invitation only.'); @@ -144,16 +180,28 @@ export class CheckModeratorsActivityProcessorService { @bindThis private async processImpl() { const evaluateResult = await this.evaluateModeratorsInactiveDays(); + const meta = await this.metaService.fetch(false); + const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; if (evaluateResult.isModeratorsInactive) { - this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`); + if (!meta.disableRegistration && meta.disableRegistrationWhenInactive) { + this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`); - await this.changeToInvitationOnly(); - await this.notifyChangeToInvitationOnly(); + await this.changeToInvitationOnly(); + await this.notifyChangeToInvitationOnly(); + } + + if (basePolicies.canPublicNote && meta.disablePublicNoteWhenInactive) { + this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will disable public note.`); + + await this.changeToDisablePublicNote(); + await this.notifyChangeToDisablePublicNote(); + } } else { const remainingTime = evaluateResult.remainingTime; if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) { const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`; - this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`); + if (meta.disableRegistrationWhenInactive) this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`); + if (meta.disablePublicNoteWhenInactive) this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to disable public note.`); if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) { // ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する @@ -227,6 +275,11 @@ export class CheckModeratorsActivityProcessorService { await this.metaService.update({ disableRegistration: true }); } + @bindThis + private async changeToDisablePublicNote() { + await this.metaService.update({ policies: { canPublicNote: false } }); + } + @bindThis public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) { // -- モデレータへのメール送信 @@ -295,6 +348,44 @@ export class CheckModeratorsActivityProcessorService { } } + @bindThis + public async notifyChangeToDisablePublicNote() { + // -- モデレータへのメールとお知らせ(個人向け)送信 + + const moderators = await this.fetchModerators(); + const moderatorProfiles = await this.userProfilesRepository + .findBy({ userId: In(moderators.map(it => it.id)) }) + .then(it => new Map(it.map(it => [it.userId, it]))); + + const mail = generateDisablePublicNoteChangedMail(); + for (const moderator of moderators) { + this.announcementService.create({ + title: mail.subject, + text: mail.text, + forExistingUsers: true, + needConfirmationToRead: true, + userId: moderator.id, + }); + + const profile = moderatorProfiles.get(moderator.id); + if (profile && profile.email && profile.emailVerified) { + this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text); + } + } + + // -- SystemWebhook + + const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks() + .then(it => it.filter(it => it.on.includes('inactiveModeratorsDisablePublicNoteChanged'))); + for (const systemWebhook of systemWebhooks) { + this.systemWebhookService.enqueueSystemWebhook( + systemWebhook, + 'inactiveModeratorsDisablePublicNoteChanged', + {}, + ); + } + } + @bindThis private async fetchModerators() { // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 1de2ecdf22..6777f39880 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -617,6 +617,14 @@ export const meta = { type: 'string', }, }, + disableRegistrationWhenInactive: { + type: 'boolean', + optional: false, nullable: false, + }, + disablePublicNoteWhenInactive: { + type: 'boolean', + optional: false, nullable: false, + }, }, }, } as const; @@ -788,6 +796,8 @@ export default class extends Endpoint { // eslint- skipCherryPickVersion: instance.skipCherryPickVersion, trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns, customSplashText: instance.customSplashText, + disableRegistrationWhenInactive: instance.disableRegistrationWhenInactive, + disablePublicNoteWhenInactive: instance.disablePublicNoteWhenInactive, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index da27d27f11..87976ddf69 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -222,6 +222,8 @@ export const paramDef = { type: 'string', }, }, + disableRegistrationWhenInactive: { type: 'boolean', nullable: true }, + disablePublicNoteWhenInactive: { type: 'boolean', nullable: true }, }, required: [], } as const; @@ -823,6 +825,14 @@ export default class extends Endpoint { // eslint- set.customSplashText = ps.customSplashText.filter(Boolean); } + if (typeof ps.disableRegistrationWhenInactive === 'boolean') { + set.disableRegistrationWhenInactive = ps.disableRegistrationWhenInactive; + } + + if (typeof ps.disablePublicNoteWhenInactive === 'boolean') { + set.disablePublicNoteWhenInactive = ps.disablePublicNoteWhenInactive; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts index 1506283a3c..a222e99ce8 100644 --- a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -39,6 +39,7 @@ describe('CheckModeratorsActivityProcessorService', () => { let systemWebhook1: MiSystemWebhook; let systemWebhook2: MiSystemWebhook; let systemWebhook3: MiSystemWebhook; + let systemWebhook4: MiSystemWebhook; // -------------------------------------------------------------------------------------- @@ -146,11 +147,12 @@ describe('CheckModeratorsActivityProcessorService', () => { systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] }); systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] }); - systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] }); + systemWebhook3 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsDisablePublicNoteChanged'] }); + systemWebhook4 = crateSystemWebhook({ on: ['abuseReport'] }); emailService.sendEmail.mockReturnValue(Promise.resolve()); announcementService.create.mockReturnValue(Promise.resolve({} as never)); - systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]); + systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3, systemWebhook4]); systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never)); }); @@ -337,6 +339,7 @@ describe('CheckModeratorsActivityProcessorService', () => { expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2); expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1); expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2); + expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook3); }); }); @@ -376,4 +379,41 @@ describe('CheckModeratorsActivityProcessorService', () => { expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2); }); }); + + describe('notifyChangeToDisablePublicNote', () => { + test('[notification + mail] 通知はモデレータ全員に発信され、メールはメールアドレスが存在+認証済みの場合のみ', async () => { + const [user1, user2, user3, user4, root] = await Promise.all([ + createUser({}, { email: 'user1@example.com', emailVerified: true }), + createUser({}, { email: 'user2@example.com', emailVerified: false }), + createUser({}, { email: null, emailVerified: false }), + createUser({}, { email: 'user4@example.com', emailVerified: true }), + createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }), + ]); + + mockModeratorRole([user1, user2, user3, root]); + await service.notifyChangeToDisablePublicNote(); + + expect(announcementService.create).toHaveBeenCalledTimes(4); + expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id); + expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id); + expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id); + expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id); + + expect(emailService.sendEmail).toHaveBeenCalledTimes(2); + expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com'); + expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com'); + }); + + test('[systemWebhook] "inactiveModeratorsDisablePublicNoteChanged"が有効なSystemWebhookに対して送信される', async () => { + const [user1] = await Promise.all([ + createUser({}, { email: 'user1@example.com', emailVerified: true }), + ]); + + mockModeratorRole([user1]); + await service.notifyChangeToDisablePublicNote(); + + expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1); + expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook3); + }); + }); }); diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 0184c140e7..2591a77ce0 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -5412,6 +5412,8 @@ export type components = { */ noteSearchableScope: 'local' | 'global'; maxFileSize: number; + disableRegistrationWhenInactive: boolean; + disablePublicNoteWhenInactive: boolean; }; MetaDetailedOnly: { features?: { @@ -5443,7 +5445,7 @@ export type components = { latestSentAt: string | null; latestStatus: number | null; name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged' | 'inactiveModeratorsDisablePublicNoteChanged')[]; url: string; secret: string; }; @@ -5628,6 +5630,8 @@ export type operations = { skipCherryPickVersion?: string | null; trustedLinkUrlPatterns: string[]; customSplashText: string[]; + disableRegistrationWhenInactive: boolean; + disablePublicNoteWhenInactive: boolean; }; }; }; @@ -10438,6 +10442,8 @@ export type operations = { skipCherryPickVersion?: string | null; trustedLinkUrlPatterns?: string[] | null; customSplashText?: string[] | null; + disableRegistrationWhenInactive?: boolean | null; + disablePublicNoteWhenInactive?: boolean | null; }; }; }; @@ -11111,7 +11117,7 @@ export type operations = { 'application/json': { isActive: boolean; name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged' | 'inactiveModeratorsDisablePublicNoteChanged')[]; url: string; secret: string; }; @@ -11221,7 +11227,7 @@ export type operations = { content: { 'application/json': { isActive?: boolean; - on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; + on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged' | 'inactiveModeratorsDisablePublicNoteChanged')[]; }; }; }; @@ -11334,7 +11340,7 @@ export type operations = { id: string; isActive: boolean; name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged' | 'inactiveModeratorsDisablePublicNoteChanged')[]; url: string; secret: string; }; @@ -11393,7 +11399,7 @@ export type operations = { /** Format: misskey:id */ webhookId: string; /** @enum {string} */ - type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged'; + type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged' | 'inactiveModeratorsDisablePublicNoteChanged'; override?: { url?: string; secret?: string; diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index 49d89db3a0..fc94306581 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -67,6 +67,12 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ + + + +
@@ -114,6 +120,7 @@ type EventType = { userCreated: boolean; inactiveModeratorsWarning: boolean; inactiveModeratorsInvitationOnlyChanged: boolean; + inactiveModeratorsDisablePublicNoteChanged: boolean; } const emit = defineEmits<{ @@ -139,6 +146,7 @@ const events = ref({ userCreated: true, inactiveModeratorsWarning: true, inactiveModeratorsInvitationOnlyChanged: true, + inactiveModeratorsDisablePublicNoteChanged: true, }); const isActive = ref(true); @@ -148,6 +156,7 @@ const disabledEvents = ref({ userCreated: false, inactiveModeratorsWarning: false, inactiveModeratorsInvitationOnlyChanged: false, + inactiveModeratorsDisablePublicNoteChanged: false, }); const disableSubmitButton = computed(() => { diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index aca808fcbc..48ce08fa93 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -12,7 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- + + + + + + + + + @@ -152,6 +160,8 @@ import FormLink from '@/components/form/link.vue'; import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref(false); +const disableRegistrationWhenInactive = ref(false); +const disablePublicNoteWhenInactive = ref(false); const emailRequiredForSignup = ref(false); const sensitiveWords = ref(''); const prohibitedWords = ref(''); @@ -166,6 +176,8 @@ const trustedLinkUrlPatterns = ref(''); async function init() { const meta = await misskeyApi('admin/meta'); enableRegistration.value = !meta.disableRegistration; + disableRegistrationWhenInactive.value = meta.disableRegistrationWhenInactive; + disablePublicNoteWhenInactive.value = meta.disablePublicNoteWhenInactive; emailRequiredForSignup.value = meta.emailRequiredForSignup; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); @@ -186,6 +198,22 @@ function onChange_enableRegistration(value: boolean) { }); } +function onChange_disableRegistrationWhenInactive(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disableRegistrationWhenInactive: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_disablePublicNoteWhenInactive(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disablePublicNoteWhenInactive: value, + }).then(() => { + fetchInstance(true); + }); +} + function onChange_emailRequiredForSignup(value: boolean) { os.apiWithDialog('admin/update-meta', { emailRequiredForSignup: value,