From 8868ada741a9f90e5cd72da6cba2fbac0058d676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:19:37 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(backend):=207=E6=97=A5=E9=96=93?= =?UTF-8?q?=E9=81=8B=E5=96=B6=E3=81=AE=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=83=93=E3=83=86=E3=82=A3=E3=81=8C=E3=81=AA=E3=81=84=E3=82=B5?= =?UTF-8?q?=E3=83=BC=E3=83=90=E3=82=92=E8=87=AA=E5=8B=95=E7=9A=84=E3=81=AB?= =?UTF-8?q?=E6=8B=9B=E5=BE=85=E5=88=B6=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/QueueService.ts | 7 ++ packages/backend/src/core/RoleService.ts | 43 ++++--- .../backend/src/queue/QueueProcessorModule.ts | 1 + .../src/queue/QueueProcessorService.ts | 3 + ...CheckModeratorsActivityProcessorService.ts | 110 ++++++++++++++++++ 5 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index f35e456556df..37028026cc43 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -93,6 +93,13 @@ export class QueueService { repeat: { pattern: '0 0 * * *' }, removeOnComplete: true, }); + + this.systemQueue.add('checkModeratorsActivity', { + }, { + // 毎時30分に起動 + repeat: { pattern: '30 * * * *' }, + removeOnComplete: true, + }); } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 583eea1a3432..23458e8f27f1 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = { @Injectable() export class RoleService implements OnApplicationShutdown, OnModuleInit { + private rootUserIdCache: MemorySingleCache; private rolesCache: MemorySingleCache; private roleAssignmentByUserIdCache: MemoryKVCache; private notificationService: NotificationService; @@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { private moderationLogService: ModerationLogService, private fanoutTimelineService: FanoutTimelineService, ) { + this.rootUserIdCache = new MemorySingleCache(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに this.rolesCache = new MemorySingleCache(1000 * 60 * 60); // 1h this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 5); // 5m @@ -416,7 +418,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async isExplorable(role: { id: MiRole['id']} | null): Promise { + public async isExplorable(role: { id: MiRole['id'] } | null): Promise { if (role == null) return false; const check = await this.rolesRepository.findOneBy({ id: role.id }); if (check == null) return false; @@ -430,22 +432,32 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator); - // TODO: isRootなアカウントも含める const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) : []; + const rootUserId = await this.rootUserIdCache.fetch(async () => { + const it = await this.usersRepository.createQueryBuilder('users') + .select('id') + .where({ isRoot: true }) + .getOneOrFail(); + return it.id; + }); + const now = Date.now(); const result = [ // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) ...new Set( - assigns - .filter(it => - (excludeExpire) - ? (it.expiresAt == null || it.expiresAt.getTime() > now) - : true, - ) - .map(a => a.userId), + [ + ...assigns + .filter(it => + (excludeExpire) + ? (it.expiresAt == null || it.expiresAt.getTime() > now) + : true, + ) + .map(a => a.userId), + rootUserId, + ], ), ]; @@ -453,12 +465,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getModerators(includeAdmins = true): Promise { - const ids = await this.getModeratorIds(includeAdmins); - const users = ids.length > 0 ? await this.usersRepository.findBy({ - id: In(ids), - }) : []; - return users; + public async getModerators(includeAdmins = true, excludeExpire = false): Promise { + const ids = await this.getModeratorIds(includeAdmins, excludeExpire); + return ids.length > 0 + ? await this.usersRepository.findBy({ + id: In(ids), + }) + : []; } @bindThis diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 0027b5ef3d7a..3b7403b4ff9a 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -80,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, + CheckExpiredMutingsProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index e9e1c4522469..85e148e90067 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -10,6 +10,7 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -120,6 +121,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private aggregateRetentionProcessorService: AggregateRetentionProcessorService, private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService, private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, + private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private cleanProcessorService: CleanProcessorService, ) { this.logger = this.queueLoggerService.logger; @@ -150,6 +152,7 @@ export class QueueProcessorService implements OnApplicationShutdown { case 'aggregateRetention': return this.aggregateRetentionProcessorService.process(); case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process(); case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); + case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'clean': return this.cleanProcessorService.process(); default: throw new Error(`unrecognized job type ${job.name} for system`); } diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts new file mode 100644 index 000000000000..3a9864b2150c --- /dev/null +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -0,0 +1,110 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import type Logger from '@/logger.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; + +// モデレーターが不在と判断する日付の閾値 +const MODERATOR_INACTIVITY_LIMIT_DAYS = 7; +const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24; + +@Injectable() +export class CheckModeratorsActivityProcessorService { + private logger: Logger; + + constructor( + private metaService: MetaService, + private roleService: RoleService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity'); + } + + @bindThis + public async process(): Promise { + this.logger.info('start.'); + + const meta = await this.metaService.fetch(false); + if (!meta.disableRegistration) { + await this.processImpl(); + } else { + this.logger.warn('is already invitation only.'); + } + + this.logger.succ('finish.'); + } + + @bindThis + private async processImpl() { + const { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays(); + if (isModeratorsInactive) { + this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`); + await this.changeToInvitationOnly(); + } else { + if (inactivityLimitCountdown <= 2) { + this.logger.info(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`); + } + } + } + + /** + * モデレーターが不在であるかどうかを確認する。trueの場合はモデレーターが不在である。 + * isModerator, isAdministrator, isRootのいずれかがtrueのユーザを対象に、 + * {@link MiUser.lastActiveDate}の値が実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前よりも古いユーザがいるかどうかを確認する。 + * {@link MiUser.lastActiveDate}がnullの場合は、そのユーザは確認の対象外とする。 + * + * ----- + * + * ### サンプルパターン + * - 実行日時: 2022-01-30 12:00:00 + * - 判定基準: 2022-01-23 12:00:00(実行日時の{@link MODERATOR_INACTIVITY_LIMIT_DAYS}日前) + * + * #### パターン① + * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト + * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(ギリギリ残り0日) + * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) + * - モデレータD: lastActiveDate = null + * + * この場合、モデレータBのアクティビティのみ判定基準日よりも古くないため、モデレーターが在席と判断される。 + * + * #### パターン② + * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト + * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日) + * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) + * - モデレータC: lastActiveDate = null + * + * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。 + */ + @bindThis + public async evaluateModeratorsInactiveDays() { + const today = new Date(); + const inactivePeriod = new Date(today); + inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS); + + // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する + const moderators = await this.roleService.getModerators(true, true); + const inactiveModeratorCount = moderators + .map(it => it.lastActiveDate) + .filter(it => it != null) + .filter(it => it.getTime() < inactivePeriod.getTime()) + .length; + + return { + isModeratorsInactive: inactiveModeratorCount !== moderators.length, + inactivityLimitCountdown: MODERATOR_INACTIVITY_LIMIT_DAYS - Math.floor((today.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC), + }; + } + + @bindThis + public async changeToInvitationOnly() { + const meta = await this.metaService.fetch(true); + meta.disableRegistration = true; + await this.metaService.update(meta); + } +} From 7cbcd06ef7b0b070a25d961d6937f67dd2176feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:11:17 +0900 Subject: [PATCH 2/9] fix RoleService. --- .../core/AbuseReportNotificationService.ts | 10 +- packages/backend/src/core/RoleService.ts | 73 +++++--- .../server/api/endpoints/admin/show-users.ts | 4 +- packages/backend/test/unit/RoleService.ts | 176 ++++++++++++++++-- 4 files changed, 213 insertions(+), 50 deletions(-) diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index fb7c7bd2c3fc..7d030f2f1698 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -61,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { return; } - const moderatorIds = await this.roleService.getModeratorIds(true, true); + const moderatorIds = await this.roleService.getModeratorIds({ + includeAdmins: true, + excludeExpire: true, + }); for (const moderatorId of moderatorIds) { for (const abuseReport of abuseReports) { @@ -370,7 +373,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { } // モデレータ権限の有無で通知先設定を振り分ける - const authorizedUserIds = await this.roleService.getModeratorIds(true, true); + const authorizedUserIds = await this.roleService.getModeratorIds({ + includeAdmins: true, + excludeExpire: true, + }); const authorizedUserRecipients = Array.of(); const unauthorizedUserRecipients = Array.of(); for (const recipient of userRecipients) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 23458e8f27f1..be03e5107ecd 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -425,8 +425,23 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return check.isExplorable; } + /** + * モデレーター権限のロールが割り当てられているユーザID一覧を取得する. + * + * @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true) + * @param opts.includeRoot rootユーザも含めるか(デフォルト: false) + * @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false) + */ @bindThis - public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise { + public async getModeratorIds(opts?: { + includeAdmins?: boolean, + includeRoot?: boolean, + excludeExpire?: boolean, + }): Promise { + const includeAdmins = opts?.includeAdmins ?? true; + const includeRoot = opts?.includeRoot ?? false; + const excludeExpire = opts?.excludeExpire ?? false; + const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({})); const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) @@ -436,37 +451,39 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) : []; - const rootUserId = await this.rootUserIdCache.fetch(async () => { - const it = await this.usersRepository.createQueryBuilder('users') - .select('id') - .where({ isRoot: true }) - .getOneOrFail(); - return it.id; - }); - const now = Date.now(); - const result = [ - // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) - ...new Set( - [ - ...assigns - .filter(it => - (excludeExpire) - ? (it.expiresAt == null || it.expiresAt.getTime() > now) - : true, - ) - .map(a => a.userId), - rootUserId, - ], - ), - ]; - - return result.sort((x, y) => x.localeCompare(y)); + // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) + const result = new Set( + assigns + .filter(it => + (excludeExpire) + ? (it.expiresAt == null || it.expiresAt.getTime() > now) + : true, + ) + .map(a => a.userId), + ); + + if (includeRoot) { + const rootUserId = await this.rootUserIdCache.fetch(async () => { + const it = await this.usersRepository.createQueryBuilder('users') + .select('id') + .where({ isRoot: true }) + .getOneOrFail(); + return it.id; + }); + result.add(rootUserId); + } + + return [...result].sort((x, y) => x.localeCompare(y)); } @bindThis - public async getModerators(includeAdmins = true, excludeExpire = false): Promise { - const ids = await this.getModeratorIds(includeAdmins, excludeExpire); + public async getModerators(opts?: { + includeAdmins?: boolean, + includeRoot?: boolean, + excludeExpire?: boolean, + }): Promise { + const ids = await this.getModeratorIds(opts); return ids.length > 0 ? await this.usersRepository.findBy({ id: In(ids), diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts index 2fef9abbf966..2b2c8c60abbc 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -71,13 +71,13 @@ export default class extends Endpoint { // eslint- break; } case 'moderator': { - const moderatorIds = await this.roleService.getModeratorIds(false); + const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false }); if (moderatorIds.length === 0) return []; query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds }); break; } case 'adminOrModerator': { - const adminOrModeratorIds = await this.roleService.getModeratorIds(); + const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true }); if (adminOrModeratorIds.length === 0) return []; query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds }); break; diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index ef80d25f8125..78eb615623f0 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -10,6 +10,8 @@ import { jest } from '@jest/globals'; import { ModuleMocker } from 'jest-mock'; import { Test } from '@nestjs/testing'; import * as lolex from '@sinonjs/fake-timers'; +import type { TestingModule } from '@nestjs/testing'; +import type { MockFunctionMetadata } from 'jest-mock'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import { @@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js'; import { NotificationService } from '@/core/NotificationService.js'; import { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { TestingModule } from '@nestjs/testing'; -import type { MockFunctionMetadata } from 'jest-mock'; const moduleMocker = new ModuleMocker(global); @@ -277,9 +277,9 @@ describe('RoleService', () => { }); describe('getModeratorIds', () => { - test('includeAdmins = false, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), ]); const role1 = await createRole({ name: 'admin', isAdministrator: true }); @@ -295,13 +295,17 @@ describe('RoleService', () => { assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), ]); - const result = await roleService.getModeratorIds(false, false); + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: false, + excludeExpire: false, + }); expect(result).toEqual([modeUser1.id, modeUser2.id]); }); - test('includeAdmins = false, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), ]); const role1 = await createRole({ name: 'admin', isAdministrator: true }); @@ -317,13 +321,17 @@ describe('RoleService', () => { assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), ]); - const result = await roleService.getModeratorIds(false, true); + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: false, + excludeExpire: true, + }); expect(result).toEqual([modeUser1.id]); }); - test('includeAdmins = true, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), ]); const role1 = await createRole({ name: 'admin', isAdministrator: true }); @@ -339,13 +347,17 @@ describe('RoleService', () => { assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), ]); - const result = await roleService.getModeratorIds(true, false); + const result = await roleService.getModeratorIds({ + includeAdmins: true, + includeRoot: false, + excludeExpire: false, + }); expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]); }); - test('includeAdmins = true, excludeExpire = true', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), + test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), ]); const role1 = await createRole({ name: 'admin', isAdministrator: true }); @@ -361,9 +373,137 @@ describe('RoleService', () => { assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), ]); - const result = await roleService.getModeratorIds(true, true); + const result = await roleService.getModeratorIds({ + includeAdmins: true, + includeRoot: false, + excludeExpire: true, + }); expect(result).toEqual([adminUser1.id, modeUser1.id]); }); + + test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: true, + excludeExpire: false, + }); + expect(result).toEqual([rootUser.id]); + }); + + test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => { + const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), + ]); + + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: true, + excludeExpire: false, + }); + expect(result).toEqual([rootUser.id]); + }); + + test('root has moderator role', async () => { + const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser({ isRoot: true }), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: rootUser.id, roleId: role2.id }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + ]); + + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: true, + excludeExpire: false, + }); + expect(result).toEqual([modeUser1.id, rootUser.id]); + }); + + test('root has administrator role', async () => { + const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser({ isRoot: true }), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: rootUser.id, roleId: role1.id }), + assignRole({ userId: modeUser1.id, roleId: role2.id }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + ]); + + const result = await roleService.getModeratorIds({ + includeAdmins: true, + includeRoot: true, + excludeExpire: false, + }); + expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]); + }); + + test('root has moderator role(expire)', async () => { + const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([ + createUser(), createUser(), createUser(), createUser({ isRoot: true }), + ]); + + const role1 = await createRole({ name: 'admin', isAdministrator: true }); + const role2 = await createRole({ name: 'moderator', isModerator: true }); + const role3 = await createRole({ name: 'normal' }); + + await Promise.all([ + assignRole({ userId: adminUser1.id, roleId: role1.id }), + assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), + assignRole({ userId: normalUser1.id, roleId: role3.id }), + ]); + + const result = await roleService.getModeratorIds({ + includeAdmins: false, + includeRoot: true, + excludeExpire: true, + }); + expect(result).toEqual([rootUser.id]); + }); }); describe('conditional role', () => { From 9339a417944b67382483e2c85e3136f87f618ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 06:26:24 +0900 Subject: [PATCH 3/9] fix --- .../CheckModeratorsActivityProcessorService.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index 3a9864b2150c..9d1d11921ffb 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -48,7 +48,7 @@ export class CheckModeratorsActivityProcessorService { await this.changeToInvitationOnly(); } else { if (inactivityLimitCountdown <= 2) { - this.logger.info(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`); + this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`); } } } @@ -88,7 +88,11 @@ export class CheckModeratorsActivityProcessorService { inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS); // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する - const moderators = await this.roleService.getModerators(true, true); + const moderators = await this.roleService.getModerators({ + includeAdmins: true, + includeRoot: true, + excludeExpire: true, + }); const inactiveModeratorCount = moderators .map(it => it.lastActiveDate) .filter(it => it != null) @@ -103,8 +107,6 @@ export class CheckModeratorsActivityProcessorService { @bindThis public async changeToInvitationOnly() { - const meta = await this.metaService.fetch(true); - meta.disableRegistration = true; - await this.metaService.update(meta); + await this.metaService.update({ disableRegistration: true }); } } From 346074c14f2b3efe2d2890b842fcf11cfce54c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 06:30:10 +0900 Subject: [PATCH 4/9] fix --- packages/backend/src/core/RoleService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index be03e5107ecd..518ea7d49041 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -451,9 +451,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) }) : []; - const now = Date.now(); // Setを経由して重複を除去(ユーザIDは重複する可能性があるので) - const result = new Set( + const now = Date.now(); + const resultSet = new Set( assigns .filter(it => (excludeExpire) @@ -471,10 +471,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { .getOneOrFail(); return it.id; }); - result.add(rootUserId); + resultSet.add(rootUserId); } - return [...result].sort((x, y) => x.localeCompare(y)); + return [...resultSet].sort((x, y) => x.localeCompare(y)); } @bindThis From 3e5b42ba0b8901c876ab47d3a1f8a773ebb176e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 08:19:04 +0900 Subject: [PATCH 5/9] fix --- ...CheckModeratorsActivityProcessorService.ts | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index 9d1d11921ffb..a815908886b1 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -46,9 +46,14 @@ export class CheckModeratorsActivityProcessorService { if (isModeratorsInactive) { this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`); await this.changeToInvitationOnly(); + + // TODO: モデレータに通知メール+Misskey通知 + // TODO: SystemWebhook通知 } else { if (inactivityLimitCountdown <= 2) { this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`); + + // TODO: 警告メール } } } @@ -67,7 +72,7 @@ export class CheckModeratorsActivityProcessorService { * * #### パターン① * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト - * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(ギリギリ残り0日) + * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準と同値なのでギリギリ残り0日) * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) * - モデレータD: lastActiveDate = null * @@ -77,7 +82,7 @@ export class CheckModeratorsActivityProcessorService { * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日) * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日) - * - モデレータC: lastActiveDate = null + * - モデレータD: lastActiveDate = null * * この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。 */ @@ -87,26 +92,29 @@ export class CheckModeratorsActivityProcessorService { const inactivePeriod = new Date(today); inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS); - // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する - const moderators = await this.roleService.getModerators({ - includeAdmins: true, - includeRoot: true, - excludeExpire: true, - }); - const inactiveModeratorCount = moderators - .map(it => it.lastActiveDate) - .filter(it => it != null) - .filter(it => it.getTime() < inactivePeriod.getTime()) - .length; + const moderators = await this.fetchModerators(); + const inactiveModerators = moderators + .filter(it => it.lastActiveDate != null && it.lastActiveDate.getTime() < inactivePeriod.getTime()); return { - isModeratorsInactive: inactiveModeratorCount !== moderators.length, + isModeratorsInactive: inactiveModerators.length !== moderators.length, + inactiveModerators, inactivityLimitCountdown: MODERATOR_INACTIVITY_LIMIT_DAYS - Math.floor((today.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC), }; } @bindThis - public async changeToInvitationOnly() { + private async changeToInvitationOnly() { await this.metaService.update({ disableRegistration: true }); } + + @bindThis + private async fetchModerators() { + // TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する + return this.roleService.getModerators({ + includeAdmins: true, + includeRoot: true, + excludeExpire: true, + }); + } } From f9ee6b54dd205ba04e643eb7983a006f109cc98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:51:04 +0900 Subject: [PATCH 6/9] add test and fix --- ...CheckModeratorsActivityProcessorService.ts | 15 +- ...CheckModeratorsActivityProcessorService.ts | 235 ++++++++++++++++++ 2 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index a815908886b1..087e9427f4e4 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -92,14 +92,21 @@ export class CheckModeratorsActivityProcessorService { const inactivePeriod = new Date(today); inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS); - const moderators = await this.fetchModerators(); + const moderators = await this.fetchModerators() + .then(it => it.filter(it => it.lastActiveDate != null)); const inactiveModerators = moderators - .filter(it => it.lastActiveDate != null && it.lastActiveDate.getTime() < inactivePeriod.getTime()); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + .filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime()); + + // 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime()))); + const inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC); return { - isModeratorsInactive: inactiveModerators.length !== moderators.length, + isModeratorsInactive: inactiveModerators.length === moderators.length, inactiveModerators, - inactivityLimitCountdown: MODERATOR_INACTIVITY_LIMIT_DAYS - Math.floor((today.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC), + inactivityLimitCountdown, }; } diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts new file mode 100644 index 000000000000..b783320aa061 --- /dev/null +++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -0,0 +1,235 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { jest } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as lolex from '@sinonjs/fake-timers'; +import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns'; +import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; +import { MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import { IdService } from '@/core/IdService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { MetaService } from '@/core/MetaService.js'; +import { DI } from '@/di-symbols.js'; +import { QueueLoggerService } from '@/queue/QueueLoggerService.js'; + +const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0)); + +describe('CheckModeratorsActivityProcessorService', () => { + let app: TestingModule; + let clock: lolex.InstalledClock; + let service: CheckModeratorsActivityProcessorService; + + // -------------------------------------------------------------------------------------- + + let usersRepository: UsersRepository; + let userProfilesRepository: UserProfilesRepository; + let idService: IdService; + let roleService: jest.Mocked; + + // -------------------------------------------------------------------------------------- + + async function createUser(data: Partial = {}) { + const id = idService.gen(); + const user = await usersRepository + .insert({ + id: id, + username: `user_${id}`, + usernameLower: `user_${id}`.toLowerCase(), + ...data, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + + await userProfilesRepository.insert({ + userId: user.id, + }); + + return user; + } + + function mockModeratorRole(users: MiUser[]) { + roleService.getModerators.mockReset(); + roleService.getModerators.mockResolvedValue(users); + } + + // -------------------------------------------------------------------------------------- + + beforeAll(async () => { + app = await Test + .createTestingModule({ + imports: [ + GlobalModule, + ], + providers: [ + CheckModeratorsActivityProcessorService, + IdService, + { + provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }), + }, + { + provide: MetaService, useFactory: () => ({ fetch: jest.fn() }), + }, + { + provide: QueueLoggerService, useFactory: () => ({ + logger: ({ + createSubLogger: () => ({ + info: jest.fn(), + warn: jest.fn(), + succ: jest.fn(), + }), + }), + }), + }, + ], + }) + .compile(); + + usersRepository = app.get(DI.usersRepository); + userProfilesRepository = app.get(DI.userProfilesRepository); + + service = app.get(CheckModeratorsActivityProcessorService); + idService = app.get(IdService); + roleService = app.get(RoleService) as jest.Mocked; + + app.enableShutdownHooks(); + }); + + beforeEach(async () => { + clock = lolex.install({ + now: new Date(baseDate), + shouldClearNativeTimers: true, + }); + }); + + afterEach(async () => { + clock.uninstall(); + await usersRepository.delete({}); + await userProfilesRepository.delete({}); + roleService.getModerators.mockReset(); + }); + + afterAll(async () => { + await app.close(); + }); + + // -------------------------------------------------------------------------------------- + + describe('evaluateModeratorsInactiveDays', () => { + test('[isModeratorsInactive] inactiveなモデレーターがいても他のモデレーターがアクティブなら"運営が非アクティブ"としてみなされない', async () => { + const [user1, user2, user3, user4] = await Promise.all([ + // 期限よりも1秒新しいタイミングでアクティブ化(セーフ) + createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }), + // 期限ちょうどにアクティブ化(セーフ) + createUser({ lastActiveDate: subDays(baseDate, 7) }), + // 期限よりも1秒古いタイミングでアクティブ化(アウト) + createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }), + // 対象外 + createUser({ lastActiveDate: null }), + ]); + + mockModeratorRole([user1, user2, user3, user4]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(false); + expect(result.inactiveModerators).toEqual([user3]); + }); + + test('[isModeratorsInactive] 全員非アクティブなら"運営が非アクティブ"としてみなされる', async () => { + const [user1, user2] = await Promise.all([ + // 期限よりも1秒古いタイミングでアクティブ化(アウト) + createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }), + // 対象外 + createUser({ lastActiveDate: null }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(true); + expect(result.inactiveModerators).toEqual([user1]); + }); + + test('[countdown] 猶予まで24時間ある場合、猶予1日として計算される', async () => { + const [user1, user2] = await Promise.all([ + createUser({ lastActiveDate: subDays(baseDate, 8) }), + // 猶予はこのユーザ基準で計算される想定。 + // 期限まで残り24時間->猶予1日として計算されるはずである + createUser({ lastActiveDate: subDays(baseDate, 6) }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(false); + expect(result.inactiveModerators).toEqual([user1]); + expect(result.inactivityLimitCountdown).toBe(1); + }); + + test('[countdown] 猶予まで25時間ある場合、猶予1日として計算される', async () => { + const [user1, user2] = await Promise.all([ + createUser({ lastActiveDate: subDays(baseDate, 8) }), + // 猶予はこのユーザ基準で計算される想定。 + // 期限まで残り25時間->猶予1日として計算されるはずである + createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(false); + expect(result.inactiveModerators).toEqual([user1]); + expect(result.inactivityLimitCountdown).toBe(1); + }); + + test('[countdown] 猶予まで23時間ある場合、猶予0日として計算される', async () => { + const [user1, user2] = await Promise.all([ + createUser({ lastActiveDate: subDays(baseDate, 8) }), + // 猶予はこのユーザ基準で計算される想定。 + // 期限まで残り23時間->猶予0日として計算されるはずである + createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(false); + expect(result.inactiveModerators).toEqual([user1]); + expect(result.inactivityLimitCountdown).toBe(0); + }); + + test('[countdown] 期限ちょうどの場合、猶予0日として計算される', async () => { + const [user1, user2] = await Promise.all([ + createUser({ lastActiveDate: subDays(baseDate, 8) }), + // 猶予はこのユーザ基準で計算される想定。 + // 期限ちょうど->猶予0日として計算されるはずである + createUser({ lastActiveDate: subDays(baseDate, 7) }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(false); + expect(result.inactiveModerators).toEqual([user1]); + expect(result.inactivityLimitCountdown).toBe(0); + }); + + test('[countdown] 期限より1時間超過している場合、猶予-1日として計算される', async () => { + const [user1, user2] = await Promise.all([ + createUser({ lastActiveDate: subDays(baseDate, 8) }), + // 猶予はこのユーザ基準で計算される想定。 + // 期限より1時間超過->猶予-1日として計算されるはずである + createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }), + ]); + + mockModeratorRole([user1, user2]); + + const result = await service.evaluateModeratorsInactiveDays(); + expect(result.isModeratorsInactive).toBe(true); + expect(result.inactiveModerators).toEqual([user1, user2]); + expect(result.inactivityLimitCountdown).toBe(-1); + }); + }); +}); From cb5d7a5abeadd8056c98c7bd6afa1d90c2ca37ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 19:33:29 +0900 Subject: [PATCH 7/9] fix --- packages/backend/src/core/RoleService.ts | 5 +++-- packages/backend/src/queue/QueueProcessorModule.ts | 2 ++ .../processors/CheckModeratorsActivityProcessorService.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 518ea7d49041..5af6b0594253 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -468,8 +468,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { const it = await this.usersRepository.createQueryBuilder('users') .select('id') .where({ isRoot: true }) - .getOneOrFail(); - return it.id; + .getRawOne<{ id: string }>(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return it!.id; }); resultSet.add(rootUserId); } diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 3b7403b4ff9a..9044285bf67f 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -6,6 +6,7 @@ import { Module } from '@nestjs/common'; import { CoreModule } from '@/core/CoreModule.js'; import { GlobalModule } from '@/GlobalModule.js'; +import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; @@ -81,6 +82,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor InboxProcessorService, AggregateRetentionProcessorService, CheckExpiredMutingsProcessorService, + CheckModeratorsActivityProcessorService, QueueProcessorService, ], exports: [ diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts index 087e9427f4e4..f2677f8e5cc8 100644 --- a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts +++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts @@ -34,7 +34,7 @@ export class CheckModeratorsActivityProcessorService { if (!meta.disableRegistration) { await this.processImpl(); } else { - this.logger.warn('is already invitation only.'); + this.logger.info('is already invitation only.'); } this.logger.succ('finish.'); From ead558f8384202086b6896699455c82bb20f7c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:13:23 +0900 Subject: [PATCH 8/9] fix CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b449a1b91e03..030dbfda286d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ ## 2024.10.1 +### Note +- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、 +7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。 +詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。 ### Client - Enhance: l10nの更新 - Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正 ### Server +- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 ) - Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正 ## 2024.10.0 From 81da817875c71f647dedbf5a74e0b9eedf922810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?= <46447427+samunohito@users.noreply.github.com> Date: Fri, 11 Oct 2024 20:24:33 +0900 Subject: [PATCH 9/9] fix test --- packages/backend/test/unit/RoleService.ts | 28 +---------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index 78eb615623f0..9c1b1008d625 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -404,33 +404,7 @@ describe('RoleService', () => { includeRoot: true, excludeExpire: false, }); - expect(result).toEqual([rootUser.id]); - }); - - test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => { - const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([ - createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }), - ]); - - const role1 = await createRole({ name: 'admin', isAdministrator: true }); - const role2 = await createRole({ name: 'moderator', isModerator: true }); - const role3 = await createRole({ name: 'normal' }); - - await Promise.all([ - assignRole({ userId: adminUser1.id, roleId: role1.id }), - assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: modeUser1.id, roleId: role2.id }), - assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }), - assignRole({ userId: normalUser1.id, roleId: role3.id }), - assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }), - ]); - - const result = await roleService.getModeratorIds({ - includeAdmins: false, - includeRoot: true, - excludeExpire: false, - }); - expect(result).toEqual([rootUser.id]); + expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]); }); test('root has moderator role', async () => {