Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応 #14974

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Unreleased

### General
-
- Enhance: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応( #13437 )

### Client
-
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7001,6 +7001,10 @@ export interface Locale extends ILocale {
* リストのインポートを許可
*/
"canImportUserLists": string;
/**
* モデレーターの活動状況チェックの対象に含める
*/
"isModeratorInactivityCheckTarget": string;
};
"_condition": {
/**
Expand Down
1 change: 1 addition & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1809,6 +1809,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
isModeratorInactivityCheckTarget: "モデレーターの活動状況チェックの対象に含める"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
Expand Down
28 changes: 28 additions & 0 deletions packages/backend/src/core/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type RolePolicies = {
canImportFollowing: boolean;
canImportMuting: boolean;
canImportUserLists: boolean;
isModeratorInactivityCheckTarget: boolean;
};

export const DEFAULT_POLICIES: RolePolicies = {
Expand Down Expand Up @@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
isModeratorInactivityCheckTarget: false,
};

@Injectable()
Expand Down Expand Up @@ -402,9 +404,23 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
isModeratorInactivityCheckTarget: calc('isModeratorInactivityCheckTarget', vs => vs.some(v => v === true)),
};
}

@bindThis
public async getUsersByRoleIds(roleIds: MiRole['id'][]): Promise<MiUser[]> {
// 今のところこの関数の使用頻度は低めなのでキャッシュは作らない.
// 使用頻度が増えた場合はroleAssignmentByUserIdCacheのようなキャッシュを作るべきか否かを検討する必要がある.
const users = await this.roleAssignmentsRepository.createQueryBuilder('roleAssignment')
.innerJoinAndSelect('roleAssignment.user', 'user')
.where('roleAssignment.roleId IN (:...roleIds)', { roleIds })
.getMany()
.then(it => it.map(it => it.user).filter(it => it != null));

return [...new Map(users.map(it => [it.id, it])).values()];
}

@bindThis
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
if (user == null) return false;
Expand Down Expand Up @@ -465,6 +481,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {

if (includeRoot) {
const rootUserId = await this.rootUserIdCache.fetch(async () => {
// rootは必ず1人存在するという前提のもと
const it = await this.usersRepository.createQueryBuilder('users')
.select('id')
.where({ isRoot: true })
Expand Down Expand Up @@ -687,6 +704,17 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}

/**
* Service内部で保持しているキャッシュをすべて削除する.
* 主にテスト向けの機能で、通常はこのメソッドを呼ぶ必要はない.
*/
@bindThis
public flushCaches(): void {
this.rootUserIdCache.delete();
this.rolesCache.delete();
this.roleAssignmentByUserIdCache.deleteAll();
}

@bindThis
public dispose(): void {
this.redisForSub.off('message', this.onMessage);
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/misc/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,11 @@ export class MemoryKVCache<T> {
this.cache.delete(key);
}

@bindThis
public deleteAll() {
this.cache.clear();
}

/**
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isModeratorInactivityCheckTarget: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { RolePolicies, RoleService } from '@/core/RoleService.js';
import { EmailService } from '@/core/EmailService.js';
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
Expand Down Expand Up @@ -281,12 +281,47 @@ export class CheckModeratorsActivityProcessorService {
}

@bindThis
private async fetchModerators() {
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
return this.roleService.getModerators({
includeAdmins: true,
includeRoot: true,
excludeExpire: true,
});
public async fetchModerators() {
const resultMap = await this.roleService
.getModerators({ includeAdmins: true, includeRoot: true, excludeExpire: true })
.then(it => new Map(it.map(it => [it.id, it])));

const additionalUsers = await this.fetchAdditionalTargetUsers();
for (const user of additionalUsers) {
resultMap.set(user.id, user);
}

return [...resultMap.values()];
}

@bindThis
private async fetchAdditionalTargetUsers() {
const roles = await this.roleService.getRoles();
const targetRoleIds = roles
.filter(it => (it.policies as unknown as Partial<RolePolicies>).isModeratorInactivityCheckTarget ?? false)
.map(it => it.id);
if (targetRoleIds.length === 0) {
// 該当ポリシーが有効なロールが存在しない
return [];
}

const tmpTargetUsers = await this.roleService.getUsersByRoleIds(targetRoleIds)
.then(it => [...new Map(it.map(it => [it.id, it])).values()]);
if (tmpTargetUsers.length === 0) {
// 該当ポリシーが有効なロールにアサインされたユーザが存在しない
return [];
}

const tmpTargetUsersWithPolicies = await Promise.all(
tmpTargetUsers.map(async user => {
// 複数ロールを組み合わせた最終的なポリシーを計算する必要がある
const policies = await this.roleService.getUserPolicies(user.id);
return { user, policies };
}),
);

return tmpTargetUsersWithPolicies
.filter(it => it.policies.isModeratorInactivityCheckTarget)
.map(it => it.user);
}
}
21 changes: 21 additions & 0 deletions packages/backend/test/unit/RoleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,27 @@ describe('RoleService', () => {
});
});

describe('getUsersByRoleIds', () => {
test('get users by role ids', async () => {
const [user1, user2, user3, role1, role2, role3] = await Promise.all([
createUser(),
createUser(),
createUser(),
createRole(),
createRole(),
createRole(),
]);
await Promise.all([
roleService.assign(user1.id, role1.id),
roleService.assign(user2.id, role1.id),
roleService.assign(user3.id, role2.id),
]);

const result = await roleService.getUsersByRoleIds([role1.id]);
expect(result.map(u => u.id)).toEqual([user1.id, user2.id]);
});
});

describe('conditional role', () => {
test('~かつ~', async () => {
const [user1, user2, user3, user4] = await Promise.all([
Expand Down
Loading
Loading