mirror of
https://github.com/kokonect-link/cherrypick
synced 2025-01-22 17:54:05 +09:00
feat: Misskey 2024.10.1에 적용된 스팸 대책
의 일부 개선 안내
- 이 변경은 다양한 상황에서 관리자가 보다 유연하게 운영할 수 있도록 합니다. - 기존의 `7일 경과 시 초대제로 전환` 정책을 세분화 합니다. - 7일 경과 시 `초대제로 전환` 여부를 선택할 수 있음 - 7일 경과 시 `공개 노트 허용` 여부를 선택할 수 있음
This commit is contained in:
parent
9c827e6e71
commit
1078cd098f
@ -28,6 +28,13 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
|
||||
기반 Misskey 버전: 2024.x.x<br>
|
||||
Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGELOG.md#2024xx) 문서를 참고하십시오.
|
||||
|
||||
## NOTE
|
||||
- Misskey 2024.10.1에 적용된 `스팸 대책`의 일부 개선 안내
|
||||
- 이 변경은 다양한 상황에서 관리자가 보다 유연하게 운영할 수 있도록 합니다.
|
||||
- 기존의 `7일 경과 시 초대제로 전환` 정책을 세분화 합니다.
|
||||
- 7일 경과 시 `초대제로 전환` 여부를 선택할 수 있음
|
||||
- 7일 경과 시 `공개 노트 허용` 여부를 선택할 수 있음
|
||||
|
||||
### General
|
||||
- Feat: 투표 내용 번역
|
||||
- 이제 투표 내용을 그대로 번역해서 볼 수 있으며, 번역된 투표 항목과 상호작용해 바로 투표할 수도 있습니다.
|
||||
|
@ -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:
|
||||
|
12
locales/index.d.ts
vendored
12
locales/index.d.ts
vendored
@ -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を削除しますか?
|
||||
|
@ -1,5 +1,7 @@
|
||||
_lang_: "日本語"
|
||||
|
||||
disableRegistrationWhenInactive: "モデレーターが一定期間非アクティブになったとき、新規登録を無効化"
|
||||
disablePublicNoteWhenInactive: "モデレーターが一定期間非アクティブになったとき、「パブリック投稿の許可」を無効化"
|
||||
youBlocked: "ブロックされています"
|
||||
youBlockedDescription: "{user}さんのフォローやポストの表示はできません。"
|
||||
schedulePost: "予約投稿"
|
||||
@ -2935,6 +2937,7 @@ _webhookSettings:
|
||||
userCreated: "ユーザーが作成されたとき"
|
||||
inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき"
|
||||
inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき"
|
||||
inactiveModeratorsDisablePublicNoteChanged: "モデレーターが一定期間非アクティブだったため、システムによりパブリック投稿へと変更されたとき"
|
||||
deleteConfirm: "Webhookを削除しますか?"
|
||||
testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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";`);
|
||||
}
|
||||
}
|
@ -463,6 +463,10 @@ export class WebhookTestService {
|
||||
send({});
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsDisablePublicNoteChanged': {
|
||||
send({});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ export const systemWebhookEventTypes = [
|
||||
'inactiveModeratorsWarning',
|
||||
// モデレータが一定期間不在のためシステムにより招待制へと変更された
|
||||
'inactiveModeratorsInvitationOnlyChanged',
|
||||
// モデレータが一定期間不在のためシステムによりパブリック投稿へと変更された
|
||||
'inactiveModeratorsDisablePublicNoteChanged',
|
||||
] as const;
|
||||
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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('<br>');
|
||||
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: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
|
||||
|
@ -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<typeof meta, typeof paramDef> { // eslint-
|
||||
skipCherryPickVersion: instance.skipCherryPickVersion,
|
||||
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
|
||||
customSplashText: instance.customSplashText,
|
||||
disableRegistrationWhenInactive: instance.disableRegistrationWhenInactive,
|
||||
disablePublicNoteWhenInactive: instance.disablePublicNoteWhenInactive,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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<typeof meta, typeof paramDef> { // 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);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -67,6 +67,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
<div :class="$style.switchBox">
|
||||
<MkSwitch v-model="events.inactiveModeratorsDisablePublicNoteChanged" :disabled="disabledEvents.inactiveModeratorsDisablePublicNoteChanged">
|
||||
<template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsDisablePublicNoteChanged }}</template>
|
||||
</MkSwitch>
|
||||
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsDisablePublicNoteChanged)" @click="test('inactiveModeratorsDisablePublicNoteChanged')"><i class="ti ti-send"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="mode === 'edit'" :class="$style.description">
|
||||
@ -114,6 +120,7 @@ type EventType = {
|
||||
userCreated: boolean;
|
||||
inactiveModeratorsWarning: boolean;
|
||||
inactiveModeratorsInvitationOnlyChanged: boolean;
|
||||
inactiveModeratorsDisablePublicNoteChanged: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -139,6 +146,7 @@ const events = ref<EventType>({
|
||||
userCreated: true,
|
||||
inactiveModeratorsWarning: true,
|
||||
inactiveModeratorsInvitationOnlyChanged: true,
|
||||
inactiveModeratorsDisablePublicNoteChanged: true,
|
||||
});
|
||||
const isActive = ref<boolean>(true);
|
||||
|
||||
@ -148,6 +156,7 @@ const disabledEvents = ref<EventType>({
|
||||
userCreated: false,
|
||||
inactiveModeratorsWarning: false,
|
||||
inactiveModeratorsInvitationOnlyChanged: false,
|
||||
inactiveModeratorsDisablePublicNoteChanged: false,
|
||||
});
|
||||
|
||||
const disableSubmitButton = computed(() => {
|
||||
|
@ -12,7 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
|
||||
<template #label>{{ i18n.ts.enableRegistration }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
|
||||
<template v-if="(enableRegistration && disableRegistrationWhenInactive) || disableRegistrationWhenInactive" #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="disableRegistrationWhenInactive" :disabled="!enableRegistration" @change="onChange_disableRegistrationWhenInactive">
|
||||
<template #label>{{ i18n.ts.disableRegistrationWhenInactive }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="disablePublicNoteWhenInactive" @change="onChange_disablePublicNoteWhenInactive">
|
||||
<template #label>{{ i18n.ts.disablePublicNoteWhenInactive }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
|
||||
@ -152,6 +160,8 @@ import FormLink from '@/components/form/link.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const enableRegistration = ref<boolean>(false);
|
||||
const disableRegistrationWhenInactive = ref<boolean>(false);
|
||||
const disablePublicNoteWhenInactive = ref<boolean>(false);
|
||||
const emailRequiredForSignup = ref<boolean>(false);
|
||||
const sensitiveWords = ref<string>('');
|
||||
const prohibitedWords = ref<string>('');
|
||||
@ -166,6 +176,8 @@ const trustedLinkUrlPatterns = ref<string>('');
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user