mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-28 16:38:23 +01:00
feat: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応
This commit is contained in:
parent
eef0c895bc
commit
0d0379def0
14 changed files with 444 additions and 27 deletions
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -6989,6 +6989,10 @@ export interface Locale extends ILocale {
|
||||||
* リストのインポートを許可
|
* リストのインポートを許可
|
||||||
*/
|
*/
|
||||||
"canImportUserLists": string;
|
"canImportUserLists": string;
|
||||||
|
/**
|
||||||
|
* モデレーターの活動状況チェックの対象に含める
|
||||||
|
*/
|
||||||
|
"isModeratorInactivityCheckTarget": string;
|
||||||
};
|
};
|
||||||
"_condition": {
|
"_condition": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1806,6 +1806,7 @@ _role:
|
||||||
canImportFollowing: "フォローのインポートを許可"
|
canImportFollowing: "フォローのインポートを許可"
|
||||||
canImportMuting: "ミュートのインポートを許可"
|
canImportMuting: "ミュートのインポートを許可"
|
||||||
canImportUserLists: "リストのインポートを許可"
|
canImportUserLists: "リストのインポートを許可"
|
||||||
|
isModeratorInactivityCheckTarget: "モデレーターの活動状況チェックの対象に含める"
|
||||||
_condition:
|
_condition:
|
||||||
roleAssignedTo: "マニュアルロールにアサイン済み"
|
roleAssignedTo: "マニュアルロールにアサイン済み"
|
||||||
isLocal: "ローカルユーザー"
|
isLocal: "ローカルユーザー"
|
||||||
|
|
|
@ -63,6 +63,7 @@ export type RolePolicies = {
|
||||||
canImportFollowing: boolean;
|
canImportFollowing: boolean;
|
||||||
canImportMuting: boolean;
|
canImportMuting: boolean;
|
||||||
canImportUserLists: boolean;
|
canImportUserLists: boolean;
|
||||||
|
isModeratorInactivityCheckTarget: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_POLICIES: RolePolicies = {
|
export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
|
@ -97,6 +98,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
canImportFollowing: true,
|
canImportFollowing: true,
|
||||||
canImportMuting: true,
|
canImportMuting: true,
|
||||||
canImportUserLists: true,
|
canImportUserLists: true,
|
||||||
|
isModeratorInactivityCheckTarget: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -402,9 +404,23 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
|
canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
|
||||||
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
|
canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
|
||||||
canImportUserLists: calc('canImportUserLists', 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
|
@bindThis
|
||||||
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
|
public async isModerator(user: { id: MiUser['id']; isRoot: MiUser['isRoot'] } | null): Promise<boolean> {
|
||||||
if (user == null) return false;
|
if (user == null) return false;
|
||||||
|
@ -465,6 +481,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
|
|
||||||
if (includeRoot) {
|
if (includeRoot) {
|
||||||
const rootUserId = await this.rootUserIdCache.fetch(async () => {
|
const rootUserId = await this.rootUserIdCache.fetch(async () => {
|
||||||
|
// rootは必ず1人存在するという前提のもと
|
||||||
const it = await this.usersRepository.createQueryBuilder('users')
|
const it = await this.usersRepository.createQueryBuilder('users')
|
||||||
.select('id')
|
.select('id')
|
||||||
.where({ isRoot: true })
|
.where({ isRoot: true })
|
||||||
|
@ -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
|
@bindThis
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.redisForSub.off('message', this.onMessage);
|
this.redisForSub.off('message', this.onMessage);
|
||||||
|
|
|
@ -242,6 +242,11 @@ export class MemoryKVCache<T> {
|
||||||
this.cache.delete(key);
|
this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public deleteAll() {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||||
|
|
|
@ -292,6 +292,10 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isModeratorInactivityCheckTarget: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { In } from 'typeorm';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.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 { EmailService } from '@/core/EmailService.js';
|
||||||
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
|
import { MiUser, type UserProfilesRepository } from '@/models/_.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
@ -281,12 +281,47 @@ export class CheckModeratorsActivityProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchModerators() {
|
public async fetchModerators() {
|
||||||
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
|
const resultMap = await this.roleService
|
||||||
return this.roleService.getModerators({
|
.getModerators({ includeAdmins: true, includeRoot: true, excludeExpire: true })
|
||||||
includeAdmins: true,
|
.then(it => new Map(it.map(it => [it.id, it])));
|
||||||
includeRoot: true,
|
|
||||||
excludeExpire: true,
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
describe('conditional role', () => {
|
||||||
test('~かつ~', async () => {
|
test('~かつ~', async () => {
|
||||||
const [user1, user2, user3, user4] = await Promise.all([
|
const [user1, user2, user3, user4] = await Promise.all([
|
||||||
|
|
|
@ -8,7 +8,17 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import * as lolex from '@sinonjs/fake-timers';
|
import * as lolex from '@sinonjs/fake-timers';
|
||||||
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
|
import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
|
||||||
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||||
import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
import {
|
||||||
|
MiRole,
|
||||||
|
MiRoleAssignment,
|
||||||
|
MiSystemWebhook,
|
||||||
|
MiUser,
|
||||||
|
MiUserProfile,
|
||||||
|
RoleAssignmentsRepository,
|
||||||
|
RolesRepository,
|
||||||
|
UserProfilesRepository,
|
||||||
|
UsersRepository,
|
||||||
|
} from '@/models/_.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { GlobalModule } from '@/GlobalModule.js';
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
@ -18,6 +28,12 @@ import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
|
||||||
import { EmailService } from '@/core/EmailService.js';
|
import { EmailService } from '@/core/EmailService.js';
|
||||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||||
|
import { genAidx } from '@/misc/id/aidx.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
|
const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
|
||||||
|
|
||||||
|
@ -93,7 +109,10 @@ describe('CheckModeratorsActivityProcessorService', () => {
|
||||||
CheckModeratorsActivityProcessorService,
|
CheckModeratorsActivityProcessorService,
|
||||||
IdService,
|
IdService,
|
||||||
{
|
{
|
||||||
provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
|
provide: RoleService, useFactory: () => ({
|
||||||
|
getModerators: jest.fn(),
|
||||||
|
getRoles: () => Promise.resolve([]),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
|
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
|
||||||
|
@ -377,3 +396,269 @@ describe('CheckModeratorsActivityProcessorService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 本物のRoleServiceと結合しないと出来ないテスト
|
||||||
|
describe('CheckModeratorsActivityProcessorService with RoleService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let clock: lolex.InstalledClock;
|
||||||
|
let service: CheckModeratorsActivityProcessorService;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
let rolesRepository: RolesRepository;
|
||||||
|
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||||
|
let idService: IdService;
|
||||||
|
let roleService: RoleService;
|
||||||
|
|
||||||
|
let root: MiUser;
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
|
||||||
|
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,
|
||||||
|
...profile,
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRole(data: Partial<MiRole> = {}) {
|
||||||
|
const x = await rolesRepository.insert({
|
||||||
|
id: genAidx(Date.now()),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
lastUsedAt: new Date(),
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
return await rolesRepository.findOneByOrFail(x.identifiers[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assignRole(args: Partial<MiRoleAssignment>) {
|
||||||
|
const id = genAidx(Date.now());
|
||||||
|
await roleAssignmentsRepository.insert({
|
||||||
|
id,
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await roleAssignmentsRepository.findOneByOrFail({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test
|
||||||
|
.createTestingModule({
|
||||||
|
imports: [
|
||||||
|
GlobalModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CheckModeratorsActivityProcessorService,
|
||||||
|
IdService,
|
||||||
|
RoleService,
|
||||||
|
GlobalEventService,
|
||||||
|
CacheService,
|
||||||
|
{
|
||||||
|
provide: ModerationLogService, useFactory: () => ({ log: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: FanoutTimelineService, useFactory: () => ({ push: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: UserEntityService, useFactory: () => ({ pack: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SystemWebhookService, useFactory: () => ({
|
||||||
|
fetchActiveSystemWebhooks: jest.fn(),
|
||||||
|
enqueueSystemWebhook: 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);
|
||||||
|
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
|
||||||
|
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||||
|
|
||||||
|
service = app.get(CheckModeratorsActivityProcessorService);
|
||||||
|
idService = app.get(IdService);
|
||||||
|
roleService = app.get(RoleService);
|
||||||
|
|
||||||
|
app.enableShutdownHooks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
clock = lolex.install({
|
||||||
|
now: new Date(baseDate),
|
||||||
|
shouldClearNativeTimers: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
root = await createUser({ isRoot: true, lastActiveDate: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
clock.uninstall();
|
||||||
|
await usersRepository.delete({});
|
||||||
|
await userProfilesRepository.delete({});
|
||||||
|
await roleAssignmentsRepository.delete({});
|
||||||
|
await rolesRepository.delete({});
|
||||||
|
|
||||||
|
roleService.flushCaches();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('fetchModerators', () => {
|
||||||
|
function expectUsers(users: MiUser[], expected: MiUser[]) {
|
||||||
|
expect(users.sort((x, y) => x.id.localeCompare(y.id)))
|
||||||
|
.toEqual(expected.sort((x, y) => x.id.localeCompare(y.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
test('モデレーターロール無し -> root', async () => {
|
||||||
|
const [user1, user2, user3] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [role1, role2] = await Promise.all([
|
||||||
|
createRole({ isModerator: false }),
|
||||||
|
createRole({ isModerator: false }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: user1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user2.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role2.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.fetchModerators();
|
||||||
|
expectUsers(result, [root]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('モデレーターロール有り -> root, user2, user3', async () => {
|
||||||
|
const [user1, user2, user3] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [role1, role2] = await Promise.all([
|
||||||
|
createRole({ isModerator: false }),
|
||||||
|
createRole({ isModerator: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: user1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user2.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role2.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.fetchModerators();
|
||||||
|
expectUsers(result, [root, user2, user3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('モデレーターロール無し + 特殊ポリシーロール -> root, user1, user3', async () => {
|
||||||
|
const [user1, user2, user3] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [role1, role2] = await Promise.all([
|
||||||
|
createRole({
|
||||||
|
isModerator: false, policies: {
|
||||||
|
isModeratorInactivityCheckTarget: {
|
||||||
|
useDefault: false,
|
||||||
|
value: true,
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createRole({ isModerator: false }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: user1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user2.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role2.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.fetchModerators();
|
||||||
|
expectUsers(result, [root, user1, user3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('モデレーターロールあり + 特殊ポリシーロール -> root, user1, user2, user3', async () => {
|
||||||
|
const [user1, user2, user3] = await Promise.all([
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
createUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [role1, role2] = await Promise.all([
|
||||||
|
createRole({
|
||||||
|
isModerator: false, policies: {
|
||||||
|
isModeratorInactivityCheckTarget: {
|
||||||
|
useDefault: false,
|
||||||
|
value: true,
|
||||||
|
priority: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createRole({ isModerator: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
assignRole({ userId: user1.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user2.id, roleId: role2.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role1.id }),
|
||||||
|
assignRole({ userId: user3.id, roleId: role2.id }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await service.fetchModerators();
|
||||||
|
expectUsers(result, [root, user1, user2, user3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -106,6 +106,7 @@ export const ROLE_POLICIES = [
|
||||||
'canImportFollowing',
|
'canImportFollowing',
|
||||||
'canImportMuting',
|
'canImportMuting',
|
||||||
'canImportUserLists',
|
'canImportUserLists',
|
||||||
|
'isModeratorInactivityCheckTarget',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// なんか動かない
|
// なんか動かない
|
||||||
|
|
|
@ -690,6 +690,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkRange>
|
</MkRange>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.isModeratorInactivityCheckTarget, 'isModeratorInactivityCheckTarget'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.isModeratorInactivityCheckTarget }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.isModeratorInactivityCheckTarget.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.isModeratorInactivityCheckTarget.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.isModeratorInactivityCheckTarget)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.isModeratorInactivityCheckTarget.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.isModeratorInactivityCheckTarget.value" :disabled="role.policies.isModeratorInactivityCheckTarget.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.isModeratorInactivityCheckTarget.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -698,6 +718,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref, computed } from 'vue';
|
import { watch, ref, computed } from 'vue';
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
|
import { ROLE_POLICIES } from '@@/js/const.js';
|
||||||
import RolesEditorFormula from './RolesEditorFormula.vue';
|
import RolesEditorFormula from './RolesEditorFormula.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkColorInput from '@/components/MkColorInput.vue';
|
import MkColorInput from '@/components/MkColorInput.vue';
|
||||||
|
@ -708,7 +729,6 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkRange from '@/components/MkRange.vue';
|
import MkRange from '@/components/MkRange.vue';
|
||||||
import FormSlot from '@/components/form/slot.vue';
|
import FormSlot from '@/components/form/slot.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { ROLE_POLICIES } from '@@/js/const.js';
|
|
||||||
import { instance } from '@/instance.js';
|
import { instance } from '@/instance.js';
|
||||||
import { deepClone } from '@/scripts/clone.js';
|
import { deepClone } from '@/scripts/clone.js';
|
||||||
|
|
||||||
|
|
|
@ -256,6 +256,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.isModeratorInactivityCheckTarget, 'isModeratorInactivityCheckTarget'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.isModeratorInactivityCheckTarget }}</template>
|
||||||
|
<template #suffix>{{ policies.isModeratorInactivityCheckTarget ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
<MkSwitch v-model="policies.isModeratorInactivityCheckTarget">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@types/node": "22.9.0",
|
"@types/node": "22.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
|
"eslint": "9.14.0",
|
||||||
"openapi-types": "12.1.3",
|
"openapi-types": "12.1.3",
|
||||||
"openapi-typescript": "6.7.3",
|
"openapi-typescript": "6.7.3",
|
||||||
"ts-case-convert": "2.1.0",
|
"ts-case-convert": "2.1.0",
|
||||||
|
|
|
@ -4881,6 +4881,7 @@ export type components = {
|
||||||
canImportFollowing: boolean;
|
canImportFollowing: boolean;
|
||||||
canImportMuting: boolean;
|
canImportMuting: boolean;
|
||||||
canImportUserLists: boolean;
|
canImportUserLists: boolean;
|
||||||
|
isModeratorInactivityCheckTarget: boolean;
|
||||||
};
|
};
|
||||||
ReversiGameLite: {
|
ReversiGameLite: {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
|
|
|
@ -1368,6 +1368,9 @@ importers:
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: 7.17.0
|
specifier: 7.17.0
|
||||||
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
|
version: 7.17.0(eslint@9.14.0)(typescript@5.6.3)
|
||||||
|
eslint:
|
||||||
|
specifier: 9.14.0
|
||||||
|
version: 9.14.0
|
||||||
openapi-types:
|
openapi-types:
|
||||||
specifier: 12.1.3
|
specifier: 12.1.3
|
||||||
version: 12.1.3
|
version: 12.1.3
|
||||||
|
@ -11780,7 +11783,7 @@ snapshots:
|
||||||
'@babel/traverse': 7.23.5
|
'@babel/traverse': 7.23.5
|
||||||
'@babel/types': 7.24.7
|
'@babel/types': 7.24.7
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
@ -11800,7 +11803,7 @@ snapshots:
|
||||||
'@babel/traverse': 7.24.7
|
'@babel/traverse': 7.24.7
|
||||||
'@babel/types': 7.24.7
|
'@babel/types': 7.24.7
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
@ -12059,7 +12062,7 @@ snapshots:
|
||||||
'@babel/helper-split-export-declaration': 7.22.6
|
'@babel/helper-split-export-declaration': 7.22.6
|
||||||
'@babel/parser': 7.25.6
|
'@babel/parser': 7.25.6
|
||||||
'@babel/types': 7.24.7
|
'@babel/types': 7.24.7
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -12074,7 +12077,7 @@ snapshots:
|
||||||
'@babel/helper-split-export-declaration': 7.24.7
|
'@babel/helper-split-export-declaration': 7.24.7
|
||||||
'@babel/parser': 7.25.6
|
'@babel/parser': 7.25.6
|
||||||
'@babel/types': 7.25.6
|
'@babel/types': 7.25.6
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
globals: 11.12.0
|
globals: 11.12.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -12465,7 +12468,7 @@ snapshots:
|
||||||
'@eslint/config-array@0.18.0':
|
'@eslint/config-array@0.18.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/object-schema': 2.1.4
|
'@eslint/object-schema': 2.1.4
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
minimatch: 3.1.2
|
minimatch: 3.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -12475,7 +12478,7 @@ snapshots:
|
||||||
'@eslint/eslintrc@3.1.0':
|
'@eslint/eslintrc@3.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
espree: 10.3.0
|
espree: 10.3.0
|
||||||
globals: 14.0.0
|
globals: 14.0.0
|
||||||
ignore: 5.3.1
|
ignore: 5.3.1
|
||||||
|
@ -15637,7 +15640,7 @@ snapshots:
|
||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
optional: true
|
optional: true
|
||||||
|
@ -17248,7 +17251,7 @@ snapshots:
|
||||||
|
|
||||||
esbuild-register@3.5.0(esbuild@0.24.0):
|
esbuild-register@3.5.0(esbuild@0.24.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
esbuild: 0.24.0
|
esbuild: 0.24.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -17490,7 +17493,7 @@ snapshots:
|
||||||
ajv: 6.12.6
|
ajv: 6.12.6
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 8.2.0
|
eslint-scope: 8.2.0
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
@ -17935,7 +17938,7 @@ snapshots:
|
||||||
|
|
||||||
follow-redirects@1.15.9(debug@4.3.7):
|
follow-redirects@1.15.9(debug@4.3.7):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
|
|
||||||
for-each@0.3.3:
|
for-each@0.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -18805,7 +18808,7 @@ snapshots:
|
||||||
|
|
||||||
istanbul-lib-source-maps@4.0.1:
|
istanbul-lib-source-maps@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -19236,7 +19239,7 @@ snapshots:
|
||||||
whatwg-encoding: 3.1.1
|
whatwg-encoding: 3.1.1
|
||||||
whatwg-mimetype: 4.0.0
|
whatwg-mimetype: 4.0.0
|
||||||
whatwg-url: 14.0.0
|
whatwg-url: 14.0.0
|
||||||
ws: 8.18.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
|
ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||||
xml-name-validator: 5.0.0
|
xml-name-validator: 5.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
|
@ -19936,7 +19939,7 @@ snapshots:
|
||||||
micromark@4.0.0:
|
micromark@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
decode-named-character-reference: 1.0.2
|
decode-named-character-reference: 1.0.2
|
||||||
devlop: 1.1.0
|
devlop: 1.1.0
|
||||||
micromark-core-commonmark: 2.0.0
|
micromark-core-commonmark: 2.0.0
|
||||||
|
@ -21396,7 +21399,7 @@ snapshots:
|
||||||
|
|
||||||
require-in-the-middle@7.3.0:
|
require-in-the-middle@7.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
module-details-from-path: 1.0.3
|
module-details-from-path: 1.0.3
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -21821,7 +21824,7 @@ snapshots:
|
||||||
socks-proxy-agent@8.0.2:
|
socks-proxy-agent@8.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.0
|
agent-base: 7.1.0
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
socks: 2.7.1
|
socks: 2.7.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -21930,7 +21933,7 @@ snapshots:
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
bluebird: 3.7.2
|
bluebird: 3.7.2
|
||||||
check-more-types: 2.24.0
|
check-more-types: 2.24.0
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
debug: 4.3.7(supports-color@8.1.1)
|
||||||
execa: 5.1.1
|
execa: 5.1.1
|
||||||
lazy-ass: 1.6.0
|
lazy-ass: 1.6.0
|
||||||
ps-tree: 1.2.0
|
ps-tree: 1.2.0
|
||||||
|
|
Loading…
Reference in a new issue