feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする
This commit is contained in:
parent
1c99785e7e
commit
8868ada741
|
@ -93,6 +93,13 @@ export class QueueService {
|
||||||
repeat: { pattern: '0 0 * * *' },
|
repeat: { pattern: '0 0 * * *' },
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.systemQueue.add('checkModeratorsActivity', {
|
||||||
|
}, {
|
||||||
|
// 毎時30分に起動
|
||||||
|
repeat: { pattern: '30 * * * *' },
|
||||||
|
removeOnComplete: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
|
||||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||||
private notificationService: NotificationService;
|
private notificationService: NotificationService;
|
||||||
|
@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
private moderationLogService: ModerationLogService,
|
private moderationLogService: ModerationLogService,
|
||||||
private fanoutTimelineService: FanoutTimelineService,
|
private fanoutTimelineService: FanoutTimelineService,
|
||||||
) {
|
) {
|
||||||
|
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
|
||||||
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||||
|
|
||||||
|
@ -416,7 +418,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
|
public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
|
||||||
if (role == null) return false;
|
if (role == null) return false;
|
||||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||||
if (check == null) return false;
|
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 || r.isAdministrator)
|
||||||
: roles.filter(r => r.isModerator);
|
: roles.filter(r => r.isModerator);
|
||||||
|
|
||||||
// TODO: isRootなアカウントも含める
|
|
||||||
const assigns = moderatorRoles.length > 0
|
const assigns = moderatorRoles.length > 0
|
||||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
? 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 now = Date.now();
|
||||||
const result = [
|
const result = [
|
||||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||||
...new Set(
|
...new Set(
|
||||||
assigns
|
[
|
||||||
.filter(it =>
|
...assigns
|
||||||
(excludeExpire)
|
.filter(it =>
|
||||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
(excludeExpire)
|
||||||
: true,
|
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||||
)
|
: true,
|
||||||
.map(a => a.userId),
|
)
|
||||||
|
.map(a => a.userId),
|
||||||
|
rootUserId,
|
||||||
|
],
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -453,12 +465,13 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
|
public async getModerators(includeAdmins = true, excludeExpire = false): Promise<MiUser[]> {
|
||||||
const ids = await this.getModeratorIds(includeAdmins);
|
const ids = await this.getModeratorIds(includeAdmins, excludeExpire);
|
||||||
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
return ids.length > 0
|
||||||
id: In(ids),
|
? await this.usersRepository.findBy({
|
||||||
}) : [];
|
id: In(ids),
|
||||||
return users;
|
})
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -80,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
|
||||||
DeliverProcessorService,
|
DeliverProcessorService,
|
||||||
InboxProcessorService,
|
InboxProcessorService,
|
||||||
AggregateRetentionProcessorService,
|
AggregateRetentionProcessorService,
|
||||||
|
CheckExpiredMutingsProcessorService,
|
||||||
QueueProcessorService,
|
QueueProcessorService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
|
@ -120,6 +121,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
|
||||||
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
|
||||||
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
|
||||||
|
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
|
||||||
private cleanProcessorService: CleanProcessorService,
|
private cleanProcessorService: CleanProcessorService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger;
|
this.logger = this.queueLoggerService.logger;
|
||||||
|
@ -150,6 +152,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
|
||||||
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
|
||||||
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
|
||||||
|
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
|
||||||
case 'clean': return this.cleanProcessorService.process();
|
case 'clean': return this.cleanProcessorService.process();
|
||||||
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
default: throw new Error(`unrecognized job type ${job.name} for system`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue