;
app.enableShutdownHooks();
});
@@ -102,6 +143,15 @@ describe('CheckModeratorsActivityProcessorService', () => {
now: new Date(baseDate),
shouldClearNativeTimers: true,
});
+
+ systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+ systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+ systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+ emailService.sendEmail.mockReturnValue(Promise.resolve());
+ announcementService.create.mockReturnValue(Promise.resolve({} as never));
+ systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+ systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
});
afterEach(async () => {
@@ -109,6 +159,9 @@ describe('CheckModeratorsActivityProcessorService', () => {
await usersRepository.delete({});
await userProfilesRepository.delete({});
roleService.getModerators.mockReset();
+ announcementService.create.mockReset();
+ emailService.sendEmail.mockReset();
+ systemWebhookService.enqueueSystemWebhook.mockReset();
});
afterAll(async () => {
@@ -152,7 +205,7 @@ describe('CheckModeratorsActivityProcessorService', () => {
expect(result.inactiveModerators).toEqual([user1]);
});
- test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
+ test('[remainingTime] 猶予まで24時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -165,10 +218,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(1);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(24);
});
- test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
+ test('[remainingTime] 猶予まで25時間ある場合、猶予1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -181,10 +235,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(1);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(25);
});
- test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
+ test('[remainingTime] 猶予まで23時間ある場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -197,10 +252,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(0);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(23);
});
- test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => {
+ test('[remainingTime] 期限ちょうどの場合、猶予0日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -213,10 +269,11 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(false);
expect(result.inactiveModerators).toEqual([user1]);
- expect(result.inactivityLimitCountdown).toBe(0);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(0);
});
- test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
+ test('[remainingTime] 期限より1時間超過している場合、猶予-1日として計算される', async () => {
const [user1, user2] = await Promise.all([
createUser({ lastActiveDate: subDays(baseDate, 8) }),
// 猶予はこのユーザ基準で計算される想定。
@@ -229,7 +286,94 @@ describe('CheckModeratorsActivityProcessorService', () => {
const result = await service.evaluateModeratorsInactiveDays();
expect(result.isModeratorsInactive).toBe(true);
expect(result.inactiveModerators).toEqual([user1, user2]);
- expect(result.inactivityLimitCountdown).toBe(-1);
+ expect(result.remainingTime.asDays).toBe(-1);
+ expect(result.remainingTime.asHours).toBe(-1);
+ });
+
+ test('[remainingTime] 期限より25時間超過している場合、猶予-2日として計算される', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 10) }),
+ // 猶予はこのユーザ基準で計算される想定。
+ // 期限より1時間超過->猶予-1日として計算されるはずである
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-2);
+ expect(result.remainingTime.asHours).toBe(-25);
+ });
+ });
+
+ describe('notifyInactiveModeratorsWarning', () => {
+ 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.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ 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] "inactiveModeratorsWarning"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+ });
+ });
+
+ describe('notifyChangeToInvitationOnly', () => {
+ 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.notifyChangeToInvitationOnly();
+
+ 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] "inactiveModeratorsInvitationOnlyChanged"が有効なSystemWebhookに対して送信される', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
});
});
});
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index a00cf0d9d311..485d003f93a3 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}
+
+
+
@@ -100,6 +112,8 @@ type EventType = {
abuseReport: boolean;
abuseReportResolved: boolean;
userCreated: boolean;
+ inactiveModeratorsWarning: boolean;
+ inactiveModeratorsInvitationOnlyChanged: boolean;
}
const emit = defineEmits<{
@@ -123,6 +137,8 @@ const events = ref({
abuseReport: true,
abuseReportResolved: true,
userCreated: true,
+ inactiveModeratorsWarning: true,
+ inactiveModeratorsInvitationOnlyChanged: true,
});
const isActive = ref(true);
@@ -130,6 +146,8 @@ const disabledEvents = ref({
abuseReport: false,
abuseReportResolved: false,
userCreated: false,
+ inactiveModeratorsWarning: false,
+ inactiveModeratorsInvitationOnlyChanged: false,
});
const disableSubmitButton = computed(() => {
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 6494614026f0..698c08826a06 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5048,7 +5048,7 @@ export type components = {
latestSentAt: string | null;
latestStatus: number | null;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -10249,7 +10249,7 @@ export type operations = {
'application/json': {
isActive: boolean;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -10359,7 +10359,7 @@ export type operations = {
content: {
'application/json': {
isActive?: boolean;
- on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
};
};
};
@@ -10472,7 +10472,7 @@ export type operations = {
id: string;
isActive: boolean;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -10531,7 +10531,7 @@ export type operations = {
/** Format: misskey:id */
webhookId: string;
/** @enum {string} */
- type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
+ type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
override?: {
url?: string;
secret?: string;