feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする (#14746)

* feat(backend): 7日間運営のアクティビティがないサーバを自動的に招待制にする

* fix RoleService.

* fix

* fix

* fix

* add test and fix

* fix

* fix CHANGELOG.md

* fix test
This commit is contained in:
おさむのひと
2024-10-11 20:59:36 +09:00
committed by GitHub
parent 12bc671511
commit a2cd6a7709
10 changed files with 575 additions and 44 deletions

View File

@@ -101,6 +101,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
@@ -136,6 +137,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
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.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@@ -416,49 +418,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@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;
const check = await this.rolesRepository.findOneBy({ id: role.id });
if (check == null) return false;
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<MiUser['id'][]> {
public async getModeratorIds(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser['id'][]> {
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)
: roles.filter(r => r.isModerator);
// TODO: isRootなアカウントも含める
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];
// Setを経由して重複を除去ユーザ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),
),
];
const resultSet = new Set(
assigns
.filter(it =>
(excludeExpire)
? (it.expiresAt == null || it.expiresAt.getTime() > now)
: true,
)
.map(a => a.userId),
);
return result.sort((x, y) => x.localeCompare(y));
if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
.getRawOne<{ id: string }>();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return it!.id;
});
resultSet.add(rootUserId);
}
return [...resultSet].sort((x, y) => x.localeCompare(y));
}
@bindThis
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
const ids = await this.getModeratorIds(includeAdmins);
const users = ids.length > 0 ? await this.usersRepository.findBy({
id: In(ids),
}) : [];
return users;
public async getModerators(opts?: {
includeAdmins?: boolean,
includeRoot?: boolean,
excludeExpire?: boolean,
}): Promise<MiUser[]> {
const ids = await this.getModeratorIds(opts);
return ids.length > 0
? await this.usersRepository.findBy({
id: In(ids),
})
: [];
}
@bindThis