Compare commits

...

4 Commits

Author SHA1 Message Date
おさむのひと de3b4103e0
Merge fa21e98d06 into 763c708253 2024-11-20 14:10:14 +09:00
zawa-ch. 763c708253
Fix(backend): アカウント削除のモデレーションログが動作していないのを修正 (#14996) (#14997)
* アカウント削除のモデレーションログが動作していないのを修正

* update CHANGELOG
2024-11-19 21:12:40 +09:00
おさむのひと fa21e98d06 fix CHANGELOG.md 2024-11-16 21:42:05 +09:00
おさむのひと 0d0379def0 feat: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応 2024-11-16 21:35:25 +09:00
17 changed files with 449 additions and 30 deletions

View File

@ -11,6 +11,7 @@
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 ) - Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
- Enhance: l10nの更新 - Enhance: l10nの更新
- Enhance: 特定ロールポリシーによる運営アクティビティの検知範囲拡大に対応( #13437 )
### Client ### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
@ -64,6 +65,7 @@
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 - Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正 - Fix: User Webhookテスト機能のMock Payloadを修正
- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

4
locales/index.d.ts vendored
View File

@ -7001,6 +7001,10 @@ export interface Locale extends ILocale {
* *
*/ */
"canImportUserLists": string; "canImportUserLists": string;
/**
*
*/
"isModeratorInactivityCheckTarget": string;
}; };
"_condition": { "_condition": {
/** /**

View File

@ -1809,6 +1809,7 @@ _role:
canImportFollowing: "フォローのインポートを許可" canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可" canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可" canImportUserLists: "リストのインポートを許可"
isModeratorInactivityCheckTarget: "モデレーターの活動状況チェックの対象に含める"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"

View File

@ -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);

View File

@ -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を返すとキャッシュ無効扱いにします

View File

@ -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;

View File

@ -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);
} }
} }

View File

@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('cannot delete a root account'); throw new Error('cannot delete a root account');
} }
await this.deleteAccoountService.deleteAccount(user); await this.deleteAccoountService.deleteAccount(user, me);
}); });
} }
} }

View File

@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private deleteAccountService: DeleteAccountService, private deleteAccountService: DeleteAccountService,
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId }); const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) { if (user.isDeleted) {
return; return;
} }
await this.deleteAccountService.deleteAccount(user); await this.deleteAccountService.deleteAccount(user, me);
}); });
} }
} }

View File

@ -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([

View File

@ -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]);
});
});
});

View File

@ -106,6 +106,7 @@ export const ROLE_POLICIES = [
'canImportFollowing', 'canImportFollowing',
'canImportMuting', 'canImportMuting',
'canImportUserLists', 'canImportUserLists',
'isModeratorInactivityCheckTarget',
] as const; ] as const;
// なんか動かない // なんか動かない

View File

@ -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';

View File

@ -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>

View File

@ -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",

View File

@ -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 */

View File

@ -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