Merge branch 'develop' into serve-stream

This commit is contained in:
tamaina 2023-01-15 15:27:15 +00:00
commit 00866c754e
77 changed files with 360 additions and 294 deletions

View File

@ -39,7 +39,7 @@ describe('After user signed in', () => {
cy.get('.mk-widget-select select').select(widgetName, { force: true }); cy.get('.mk-widget-select select').select(widgetName, { force: true });
cy.get('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true }); cy.get('.data-cy-bg._modalBg.data-cy-transparent').click({ multiple: true, force: true });
cy.get('.mk-widget-add').click({ force: true }); cy.get('.mk-widget-add').click({ force: true });
cy.get(`.mkw-${widgetName}`).should('exist'); cy.get(`.data-cy-mkw-${widgetName}`).should('exist');
}); });
} }

View File

@ -44,7 +44,7 @@ services:
volumes: volumes:
- ./db:/var/lib/postgresql/data - ./db:/var/lib/postgresql/data
healthcheck: healthcheck:
test: "pg_isready" test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
interval: 5s interval: 5s
retries: 20 retries: 20

View File

@ -952,11 +952,17 @@ _role:
isPublic: "ロールを公開" isPublic: "ロールを公開"
descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。" descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。"
options: "オプション" options: "オプション"
policies: "ポリシー"
baseRole: "ベースロール" baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用" useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択" chooseRoleToAssign: "アサインするロールを選択"
canEditMembersByModerator: "モデレーターのメンバー編集を許可" canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。" descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"
_priority:
low: "低"
middle: "中"
high: "高"
_options: _options:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-rc.8", "version": "13.0.0-rc.9",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -0,0 +1,13 @@
export class Policies1673783015567 {
name = 'Policies1673783015567'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" RENAME COLUMN "options" TO "policies"`);
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "defaultRoleOverride" TO "policies"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" RENAME COLUMN "policies" TO "defaultRoleOverride"`);
await queryRunner.query(`ALTER TABLE "role" RENAME COLUMN "policies" TO "options"`);
}
}

View File

@ -479,8 +479,8 @@ export class DriveService {
if (user && !isLink) { if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user); const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
const role = await this.roleService.getUserRoleOptions(user.id); const policies = await this.roleService.getUserPolicies(user.id);
const driveCapacity = 1024 * 1024 * role.driveCapacityMb; const driveCapacity = 1024 * 1024 * policies.driveCapacityMb;
this.registerLogger.debug('drive capacity override applied'); this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);

View File

@ -226,7 +226,7 @@ export class NoteCreateService {
if (data.channel != null) data.localOnly = true; if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) { if (data.visibility === 'public' && data.channel == null) {
if ((await this.roleService.getUserRoleOptions(user.id)).canPublicNote === false) { if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home'; data.visibility = 'home';
} }
} }

View File

@ -57,7 +57,7 @@ export class NotePiningService {
const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id }); const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id });
if (pinings.length >= (await this.roleService.getUserRoleOptions(user.id)).pinLimit) { if (pinings.length >= (await this.roleService.getUserPolicies(user.id)).pinLimit) {
throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.');
} }

View File

@ -13,7 +13,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js'; import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RoleOptions = { export type RolePolicies = {
gtlAvailable: boolean; gtlAvailable: boolean;
ltlAvailable: boolean; ltlAvailable: boolean;
canPublicNote: boolean; canPublicNote: boolean;
@ -31,7 +31,7 @@ export type RoleOptions = {
rateLimitFactor: number; rateLimitFactor: number;
}; };
export const DEFAULT_ROLE: RoleOptions = { export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true, gtlAvailable: true,
ltlAvailable: true, ltlAvailable: true,
canPublicNote: true, canPublicNote: true,
@ -195,35 +195,44 @@ export class RoleService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public async getUserRoleOptions(userId: User['id'] | null): Promise<RoleOptions> { public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
if (userId == null) return baseRoleOptions; if (userId == null) return basePolicies;
const roles = await this.getUserRoles(userId); const roles = await this.getUserRoles(userId);
function getOptionValues(option: keyof RoleOptions) { function calc<T extends keyof RolePolicies>(name: T, aggregate: (values: RolePolicies[T][]) => RolePolicies[T]) {
if (roles.length === 0) return [baseRoleOptions[option]]; if (roles.length === 0) return basePolicies[name];
return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]);
const policies = roles.map(role => role.policies[name] ?? { priority: 0, useDefault: true });
const p2 = policies.filter(policy => policy.priority === 2);
if (p2.length > 0) return aggregate(p2.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
const p1 = policies.filter(policy => policy.priority === 1);
if (p1.length > 0) return aggregate(p2.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
return aggregate(policies.map(policy => policy.useDefault ? basePolicies[name] : policy.value));
} }
return { return {
gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true), gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: getOptionValues('canPublicNote').some(x => x === true), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canInvite: getOptionValues('canInvite').some(x => x === true), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
canManageCustomEmojis: getOptionValues('canManageCustomEmojis').some(x => x === true), canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
pinLimit: Math.max(...getOptionValues('pinLimit')), pinLimit: calc('pinLimit', vs => Math.max(...vs)),
antennaLimit: Math.max(...getOptionValues('antennaLimit')), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)),
wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)),
webhookLimit: Math.max(...getOptionValues('webhookLimit')), webhookLimit: calc('webhookLimit', vs => Math.max(...vs)),
clipLimit: Math.max(...getOptionValues('clipLimit')), clipLimit: calc('clipLimit', vs => Math.max(...vs)),
noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')), noteEachClipsLimit: calc('noteEachClipsLimit', vs => Math.max(...vs)),
userListLimit: Math.max(...getOptionValues('userListLimit')), userListLimit: calc('userListLimit', vs => Math.max(...vs)),
userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')), userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')), rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
}; };
} }

View File

@ -35,7 +35,7 @@ export class UserListService {
const currentCount = await this.userListJoiningsRepository.countBy({ const currentCount = await this.userListJoiningsRepository.countBy({
userListId: list.id, userListId: list.id,
}); });
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userEachUserListsLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).userEachUserListsLimit) {
throw new Error('Too many users'); throw new Error('Too many users');
} }

View File

@ -6,7 +6,7 @@ import type { Packed } from '@/misc/schema.js';
import type { User } from '@/models/entities/User.js'; import type { User } from '@/models/entities/User.js';
import type { Role } from '@/models/entities/Role.js'; import type { Role } from '@/models/entities/Role.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DEFAULT_ROLE } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
@Injectable() @Injectable()
@ -40,10 +40,11 @@ export class RoleEntityService {
roleId: role.id, roleId: role.id,
}); });
const roleOptions = { ...role.options }; const policies = { ...role.policies };
for (const [k, v] of Object.entries(DEFAULT_ROLE)) { for (const [k, v] of Object.entries(DEFAULT_POLICIES)) {
if (roleOptions[k] == null) roleOptions[k] = { if (policies[k] == null) policies[k] = {
useDefault: true, useDefault: true,
priority: 0,
value: v, value: v,
}; };
} }
@ -61,7 +62,7 @@ export class RoleEntityService {
isAdministrator: role.isAdministrator, isAdministrator: role.isAdministrator,
isModerator: role.isModerator, isModerator: role.isModerator,
canEditMembersByModerator: role.canEditMembersByModerator, canEditMembersByModerator: role.canEditMembersByModerator,
options: roleOptions, policies: policies,
usersCount: assigns.length, usersCount: assigns.length,
...(opts.detail ? { ...(opts.detail ? {
users: this.userEntityService.packMany(assigns.map(x => x.userId), me), users: this.userEntityService.packMany(assigns.map(x => x.userId), me),

View File

@ -423,7 +423,7 @@ export class UserEntityService implements OnModuleInit {
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null, bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null,
bannerBlurhash: user.banner?.blurhash ?? null, bannerBlurhash: user.banner?.blurhash ?? null,
isLocked: user.isLocked, isLocked: user.isLocked,
isSilenced: this.roleService.getUserRoleOptions(user.id).then(r => !r.canPublicNote), isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended ?? falsy, isSuspended: user.isSuspended ?? falsy,
description: profile!.description, description: profile!.description,
location: profile!.location, location: profile!.location,
@ -496,7 +496,7 @@ export class UserEntityService implements OnModuleInit {
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {
role: this.roleService.getUserRoleOptions(user.id), policies: this.roleService.getUserPolicies(user.id),
email: profile!.email, email: profile!.email,
emailVerified: profile!.emailVerified, emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled securityKeysList: profile!.twoFactorEnabled

View File

@ -458,5 +458,5 @@ export class Meta {
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })
public defaultRoleOverride: Record<string, any>; public policies: Record<string, any>;
} }

View File

@ -136,8 +136,9 @@ export class Role {
@Column('jsonb', { @Column('jsonb', {
default: { }, default: { },
}) })
public options: Record<string, { public policies: Record<string, {
useDefault: boolean; useDefault: boolean;
priority: number;
value: any; value: any;
}>; }>;
} }

View File

@ -10,7 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js'; import NotesChart from '@/core/chart/charts/notes.js';
import UsersChart from '@/core/chart/charts/users.js'; import UsersChart from '@/core/chart/charts/users.js';
import { DEFAULT_ROLE } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
const nodeinfo2_1path = '/nodeinfo/2.1'; const nodeinfo2_1path = '/nodeinfo/2.1';
@ -74,7 +74,7 @@ export class NodeinfoServerService {
const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null;
const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride }; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies };
return { return {
software: { software: {
@ -105,8 +105,8 @@ export class NodeinfoServerService {
repositoryUrl: meta.repositoryUrl, repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl, feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration, disableRegistration: meta.disableRegistration,
disableLocalTimeline: !baseRoleOptions.ltlAvailable, disableLocalTimeline: !basePolicies.ltlAvailable,
disableGlobalTimeline: !baseRoleOptions.gtlAvailable, disableGlobalTimeline: !basePolicies.gtlAvailable,
emailRequiredForSignup: meta.emailRequiredForSignup, emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha, enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha, enableRecaptcha: meta.enableRecaptcha,

View File

@ -225,7 +225,7 @@ export class ApiCallService implements OnApplicationShutdown {
} }
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい // TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1; const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => { await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
@ -274,9 +274,9 @@ export class ApiCallService implements OnApplicationShutdown {
} }
} }
if (ep.meta.requireRoleOption != null && !user!.isRoot) { if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
const myRole = await this.roleService.getUserRoleOptions(user!.id); const policies = await this.roleService.getUserPolicies(user!.id);
if (!myRole[ep.meta.requireRoleOption]) { if (!policies[ep.meta.requireRolePolicy]) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a required role.', message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED', code: 'ROLE_PERMISSION_DENIED',

View File

@ -65,7 +65,7 @@ import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@ -399,7 +399,7 @@ const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass:
const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default }; const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default };
const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default }; const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default }; const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
const $admin_roles_updateDefaultRoleOverride: Provider = { provide: 'ep:admin/roles/update-default-role-override', useClass: ep___admin_roles_updateDefaultRoleOverride.default }; const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default }; const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default }; const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default }; const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
@ -737,7 +737,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_update, $admin_roles_update,
$admin_roles_assign, $admin_roles_assign,
$admin_roles_unassign, $admin_roles_unassign,
$admin_roles_updateDefaultRoleOverride, $admin_roles_updateDefaultPolicies,
$announcements, $announcements,
$antennas_create, $antennas_create,
$antennas_delete, $antennas_delete,
@ -1069,7 +1069,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_roles_update, $admin_roles_update,
$admin_roles_assign, $admin_roles_assign,
$admin_roles_unassign, $admin_roles_unassign,
$admin_roles_updateDefaultRoleOverride, $admin_roles_updateDefaultPolicies,
$announcements, $announcements,
$antennas_create, $antennas_create,
$antennas_delete, $antennas_delete,

View File

@ -64,7 +64,7 @@ import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
import * as ep___admin_roles_update from './endpoints/admin/roles/update.js'; import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js'; import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'; import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js'; import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___announcements from './endpoints/announcements.js'; import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@ -396,7 +396,7 @@ const eps = [
['admin/roles/update', ep___admin_roles_update], ['admin/roles/update', ep___admin_roles_update],
['admin/roles/assign', ep___admin_roles_assign], ['admin/roles/assign', ep___admin_roles_assign],
['admin/roles/unassign', ep___admin_roles_unassign], ['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-role-override', ep___admin_roles_updateDefaultRoleOverride], ['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['announcements', ep___announcements], ['announcements', ep___announcements],
['antennas/create', ep___antennas_create], ['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete], ['antennas/delete', ep___antennas_delete],
@ -695,7 +695,7 @@ export interface IEndpointMeta {
*/ */
readonly requireAdmin?: boolean; readonly requireAdmin?: boolean;
readonly requireRoleOption?: string; readonly requireRolePolicy?: string;
/** /**
* *

View File

@ -8,7 +8,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -14,7 +14,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
errors: { errors: {
noSuchFile: { noSuchFile: {

View File

@ -14,7 +14,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
errors: { errors: {
noSuchEmoji: { noSuchEmoji: {

View File

@ -9,7 +9,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -10,7 +10,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
errors: { errors: {
noSuchEmoji: { noSuchEmoji: {

View File

@ -5,7 +5,7 @@ import { QueueService } from '@/core/QueueService.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
res: { res: {
type: 'array', type: 'array',

View File

@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
res: { res: {
type: 'array', type: 'array',

View File

@ -8,7 +8,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -8,7 +8,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -8,7 +8,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -9,7 +9,7 @@ export const meta = {
tags: ['admin'], tags: ['admin'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canManageCustomEmojis', requireRolePolicy: 'canManageCustomEmojis',
errors: { errors: {
noSuchEmoji: { noSuchEmoji: {

View File

@ -4,7 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { DEFAULT_ROLE } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@ -440,7 +440,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
deeplIsPro: instance.deeplIsPro, deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging, enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation, enableActiveEmailValidation: instance.enableActiveEmailValidation,
baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, policies: { ...DEFAULT_POLICIES, ...instance.policies },
}; };
}); });
} }

View File

@ -25,7 +25,7 @@ export const paramDef = {
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
options: { policies: {
type: 'object', type: 'object',
}, },
}, },
@ -39,7 +39,7 @@ export const paramDef = {
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'canEditMembersByModerator', 'canEditMembersByModerator',
'options', 'policies',
], ],
} as const; } as const;
@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator, isModerator: ps.isModerator,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
options: ps.options, policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('roleCreated', created); this.globalEventService.publishInternalEvent('roleCreated', created);

View File

@ -16,12 +16,12 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
options: { policies: {
type: 'object', type: 'object',
}, },
}, },
required: [ required: [
'options', 'policies',
], ],
} as const; } as const;
@ -34,9 +34,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) { ) {
super(meta, paramDef, async (ps) => { super(meta, paramDef, async (ps) => {
await this.metaService.update({ await this.metaService.update({
defaultRoleOverride: ps.options, policies: ps.policies,
}); });
this.globalEventService.publishInternalEvent('defaultRoleOverrideUpdated', ps.options); this.globalEventService.publishInternalEvent('policiesUpdated', ps.policies);
}); });
} }
} }

View File

@ -33,7 +33,7 @@ export const paramDef = {
isModerator: { type: 'boolean' }, isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' }, isAdministrator: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' }, canEditMembersByModerator: { type: 'boolean' },
options: { policies: {
type: 'object', type: 'object',
}, },
}, },
@ -48,7 +48,7 @@ export const paramDef = {
'isModerator', 'isModerator',
'isAdministrator', 'isAdministrator',
'canEditMembersByModerator', 'canEditMembersByModerator',
'options', 'policies',
], ],
} as const; } as const;
@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isModerator: ps.isModerator, isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator, isAdministrator: ps.isAdministrator,
canEditMembersByModerator: ps.canEditMembersByModerator, canEditMembersByModerator: ps.canEditMembersByModerator,
options: ps.options, policies: ps.policies,
}); });
const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId }); const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
this.globalEventService.publishInternalEvent('roleUpdated', updated); this.globalEventService.publishInternalEvent('roleUpdated', updated);

View File

@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
} }
const isModerator = await this.roleService.isModerator(user); const isModerator = await this.roleService.isModerator(user);
const isSilenced = !(await this.roleService.getUserRoleOptions(user.id)).canPublicNote; const isSilenced = !(await this.roleService.getUserPolicies(user.id)).canPublicNote;
const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) { if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
@ -94,6 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
lastActiveDate: user.lastActiveDate, lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote, moderationNote: profile.moderationNote,
signins, signins,
policies: await this.roleService.getUserPolicies(user.id),
roles: await this.roleEntityService.packMany(roles, me, { detail: false }), roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
}; };
}); });

View File

@ -92,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentAntennasCount = await this.antennasRepository.countBy({ const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentAntennasCount > (await this.roleService.getUserRoleOptions(me.id)).antennaLimit) { if (currentAntennasCount > (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas); throw new ApiError(meta.errors.tooManyAntennas);
} }

View File

@ -97,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentCount = await this.clipNotesRepository.countBy({ const currentCount = await this.clipNotesRepository.countBy({
clipId: clip.id, clipId: clip.id,
}); });
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).noteEachClipsLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).noteEachClipsLimit) {
throw new ApiError(meta.errors.tooManyClipNotes); throw new ApiError(meta.errors.tooManyClipNotes);
} }

View File

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentCount = await this.clipsRepository.countBy({ const currentCount = await this.clipsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).clipLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).clipLimit) {
throw new ApiError(meta.errors.tooManyClips); throw new ApiError(meta.errors.tooManyClips);
} }

View File

@ -47,10 +47,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Calculate drive usage // Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id); const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
const myRole = await this.roleService.getUserRoleOptions(me.id); const policies = await this.roleService.getUserPolicies(me.id);
return { return {
capacity: 1024 * 1024 * myRole.driveCapacityMb, capacity: 1024 * 1024 * policies.driveCapacityMb,
usage: usage, usage: usage,
}; };
}); });

View File

@ -173,7 +173,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.mutedWords !== undefined) { if (ps.mutedWords !== undefined) {
// TODO: ちゃんと数える // TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length; const length = JSON.stringify(ps.mutedWords).length;
if (length > (await this.roleService.getUserRoleOptions(user.id)).wordMuteLimit) { if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
throw new ApiError(meta.errors.tooManyMutedWords); throw new ApiError(meta.errors.tooManyMutedWords);
} }

View File

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentWebhooksCount = await this.webhooksRepository.countBy({ const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentWebhooksCount > (await this.roleService.getUserRoleOptions(me.id)).webhookLimit) { if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks); throw new ApiError(meta.errors.tooManyWebhooks);
} }

View File

@ -9,7 +9,7 @@ export const meta = {
tags: ['meta'], tags: ['meta'],
requireCredential: true, requireCredential: true,
requireRoleOption: 'canInvite', requireRolePolicy: 'canInvite',
res: { res: {
type: 'object', type: 'object',

View File

@ -7,7 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { DEFAULT_ROLE } from '@/core/RoleService.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@ -334,7 +334,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
translatorAvailable: instance.deeplAuthKey != null, translatorAvailable: instance.deeplAuthKey != null,
baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride }, policies: { ...DEFAULT_POLICIES, ...instance.policies },
...(ps.detail ? { ...(ps.detail ? {
pinnedPages: instance.pinnedPages, pinnedPages: instance.pinnedPages,

View File

@ -62,8 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const role = await this.roleService.getUserRoleOptions(me ? me.id : null); const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!role.gtlAvailable) { if (!policies.gtlAvailable) {
throw new ApiError(meta.errors.gtlDisabled); throw new ApiError(meta.errors.gtlDisabled);
} }

View File

@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const role = await this.roleService.getUserRoleOptions(me.id); const policies = await this.roleService.getUserPolicies(me.id);
if (!role.ltlAvailable) { if (!policies.ltlAvailable) {
throw new ApiError(meta.errors.stlDisabled); throw new ApiError(meta.errors.stlDisabled);
} }

View File

@ -67,8 +67,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const role = await this.roleService.getUserRoleOptions(me ? me.id : null); const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!role.ltlAvailable) { if (!policies.ltlAvailable) {
throw new ApiError(meta.errors.ltlDisabled); throw new ApiError(meta.errors.ltlDisabled);
} }

View File

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentCount = await this.userListsRepository.countBy({ const currentCount = await this.userListsRepository.countBy({
userId: me.id, userId: me.id,
}); });
if (currentCount > (await this.roleService.getUserRoleOptions(me.id)).userListLimit) { if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
throw new ApiError(meta.errors.tooManyUserLists); throw new ApiError(meta.errors.tooManyUserLists);
} }

View File

@ -29,8 +29,8 @@ class GlobalTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!role.gtlAvailable) return; if (!policies.gtlAvailable) return;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -30,8 +30,8 @@ class HybridTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any): Promise<void> { public async init(params: any): Promise<void> {
const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!role.ltlAvailable) return; if (!policies.ltlAvailable) return;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -28,8 +28,8 @@ class LocalTimelineChannel extends Channel {
@bindThis @bindThis
public async init(params: any) { public async init(params: any) {
const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null); const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!role.ltlAvailable) return; if (!policies.ltlAvailable) return;
// Subscribe events // Subscribe events
this.subscriber.on('notesStream', this.onNote); this.subscriber.on('notesStream', this.onNote);

View File

@ -30,7 +30,7 @@ export interface InternalStreamTypes {
remoteUserUpdated: Serialized<{ id: User['id']; }>; remoteUserUpdated: Serialized<{ id: User['id']; }>;
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
defaultRoleOverrideUpdated: Serialized<Role['options']>; policiesUpdated: Serialized<Role['options']>;
roleCreated: Serialized<Role>; roleCreated: Serialized<Role>;
roleDeleted: Serialized<Role>; roleDeleted: Serialized<Role>;
roleUpdated: Serialized<Role>; roleUpdated: Serialized<Role>;

View File

@ -194,8 +194,6 @@ const fetchMore = async (): Promise<void> => {
limit: SECOND_FETCH_LIMIT + 1, limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? { ...(props.pagination.offsetMode ? {
offset: offset.value, offset: offset.value,
} : props.pagination.reversed ? {
sinceId: items.value[0].id,
} : { } : {
untilId: items.value[items.value.length - 1].id, untilId: items.value[items.value.length - 1].id,
}), }),
@ -262,8 +260,6 @@ const fetchMoreAhead = async (): Promise<void> => {
limit: SECOND_FETCH_LIMIT + 1, limit: SECOND_FETCH_LIMIT + 1,
...(props.pagination.offsetMode ? { ...(props.pagination.offsetMode ? {
offset: offset.value, offset: offset.value,
} : props.pagination.reversed ? {
untilId: items.value[0].id,
} : { } : {
sinceId: items.value[items.value.length - 1].id, sinceId: items.value[items.value.length - 1].id,
}), }),

View File

@ -17,6 +17,7 @@ import { i18n } from '@/i18n';
.root { .root {
padding: 32px; padding: 32px;
text-align: center; text-align: center;
align-items: center;
} }
.text { .text {
@ -29,6 +30,7 @@ import { i18n } from '@/i18n';
.img { .img {
vertical-align: bottom; vertical-align: bottom;
width: 128px;
height: 128px; height: 128px;
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 16px; border-radius: 16px;

View File

@ -36,193 +36,239 @@
</MkFolder> </MkFolder>
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts._role.options }}</template> <template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ options_rateLimitFactor_useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options_rateLimitFactor_value * 100)}%` }}</template> <template #suffix>{{ policies.rateLimitFactor.useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(policies.rateLimitFactor.value * 100)}%` }} <i :class="getPriorityIcon(policies.rateLimitFactor)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_rateLimitFactor_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.rateLimitFactor.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkRange :model-value="options_rateLimitFactor_value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor_value = (v / 100)"> <MkRange :model-value="policies.rateLimitFactor.value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor.value = (v / 100)">
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template> <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
</MkRange> </MkRange>
<MkRange v-model="policies.rateLimitFactor.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ policies.gtlAvailable.useDefault ? i18n.ts._role.useBaseValue : (policies.gtlAvailable.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(policies.gtlAvailable)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_gtlAvailable_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.gtlAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="options_gtlAvailable_value" :disabled="options_gtlAvailable_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.gtlAvailable.value" :disabled="policies.gtlAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="policies.gtlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template> <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
<template #suffix>{{ options_ltlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_ltlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ policies.ltlAvailable.useDefault ? i18n.ts._role.useBaseValue : (policies.ltlAvailable.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(policies.ltlAvailable)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_ltlAvailable_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.ltlAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="options_ltlAvailable_value" :disabled="options_ltlAvailable_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.ltlAvailable.value" :disabled="policies.ltlAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="policies.ltlAvailable.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template> <template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
<template #suffix>{{ options_canPublicNote_useDefault ? i18n.ts._role.useBaseValue : (options_canPublicNote_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ policies.canPublicNote.useDefault ? i18n.ts._role.useBaseValue : (policies.canPublicNote.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(policies.canPublicNote)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_canPublicNote_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canPublicNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="options_canPublicNote_value" :disabled="options_canPublicNote_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canPublicNote.value" :disabled="policies.canPublicNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="policies.canPublicNote.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>{{ options_canInvite_useDefault ? i18n.ts._role.useBaseValue : (options_canInvite_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ policies.canInvite.useDefault ? i18n.ts._role.useBaseValue : (policies.canInvite.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(policies.canInvite)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_canInvite_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canInvite.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="options_canInvite_value" :disabled="options_canInvite_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canInvite.value" :disabled="policies.canInvite.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="policies.canInvite.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
<template #suffix>{{ options_canManageCustomEmojis_useDefault ? i18n.ts._role.useBaseValue : (options_canManageCustomEmojis_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ policies.canManageCustomEmojis.useDefault ? i18n.ts._role.useBaseValue : (policies.canManageCustomEmojis.value ? i18n.ts.yes : i18n.ts.no) }} <i :class="getPriorityIcon(policies.canManageCustomEmojis)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_canManageCustomEmojis_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canManageCustomEmojis.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="options_canManageCustomEmojis_value" :disabled="options_canManageCustomEmojis_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.canManageCustomEmojis.value" :disabled="policies.canManageCustomEmojis.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
<MkRange v-model="policies.canManageCustomEmojis.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template> <template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<template #suffix>{{ options_driveCapacityMb_useDefault ? i18n.ts._role.useBaseValue : (options_driveCapacityMb_value + 'MB') }}</template> <template #suffix>{{ policies.driveCapacityMb.useDefault ? i18n.ts._role.useBaseValue : (policies.driveCapacityMb.value + 'MB') }} <i :class="getPriorityIcon(policies.driveCapacityMb)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_driveCapacityMb_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.driveCapacityMb.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_driveCapacityMb_value" :disabled="options_driveCapacityMb_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.driveCapacityMb.value" :disabled="policies.driveCapacityMb.useDefault" type="number" :readonly="readonly">
<template #suffix>MB</template> <template #suffix>MB</template>
</MkInput> </MkInput>
<MkRange v-model="policies.driveCapacityMb.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ options_pinLimit_useDefault ? i18n.ts._role.useBaseValue : (options_pinLimit_value) }}</template> <template #suffix>{{ policies.pinLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.pinLimit.value) }} <i :class="getPriorityIcon(policies.pinLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_pinLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.pinLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_pinLimit_value" :disabled="options_pinLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.pinLimit.value" :disabled="policies.pinLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.pinLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.antennaMax }}</template> <template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template> <template #suffix>{{ policies.antennaLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.antennaLimit.value) }} <i :class="getPriorityIcon(policies.antennaLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_antennaLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.antennaLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_antennaLimit_value" :disabled="options_antennaLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.antennaLimit.value" :disabled="policies.antennaLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.antennaLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template> <template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
<template #suffix>{{ options_wordMuteLimit_useDefault ? i18n.ts._role.useBaseValue : (options_wordMuteLimit_value) }}</template> <template #suffix>{{ policies.wordMuteLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.wordMuteLimit.value) }} <i :class="getPriorityIcon(policies.wordMuteLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_wordMuteLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.wordMuteLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_wordMuteLimit_value" :disabled="options_wordMuteLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.wordMuteLimit.value" :disabled="policies.wordMuteLimit.useDefault" type="number" :readonly="readonly">
<template #suffix>chars</template> <template #suffix>chars</template>
</MkInput> </MkInput>
<MkRange v-model="policies.wordMuteLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.webhookMax }}</template> <template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<template #suffix>{{ options_webhookLimit_useDefault ? i18n.ts._role.useBaseValue : (options_webhookLimit_value) }}</template> <template #suffix>{{ policies.webhookLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.webhookLimit.value) }} <i :class="getPriorityIcon(policies.webhookLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_webhookLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.webhookLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_webhookLimit_value" :disabled="options_webhookLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.webhookLimit.value" :disabled="policies.webhookLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.webhookLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.clipMax }}</template> <template #label>{{ i18n.ts._role._options.clipMax }}</template>
<template #suffix>{{ options_clipLimit_useDefault ? i18n.ts._role.useBaseValue : (options_clipLimit_value) }}</template> <template #suffix>{{ policies.clipLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.clipLimit.value) }} <i :class="getPriorityIcon(policies.clipLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_clipLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.clipLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_clipLimit_value" :disabled="options_clipLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.clipLimit.value" :disabled="policies.clipLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.clipLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template> <template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<template #suffix>{{ options_noteEachClipsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_noteEachClipsLimit_value) }}</template> <template #suffix>{{ policies.noteEachClipsLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.noteEachClipsLimit.value) }} <i :class="getPriorityIcon(policies.noteEachClipsLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_noteEachClipsLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.noteEachClipsLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_noteEachClipsLimit_value" :disabled="options_noteEachClipsLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.noteEachClipsLimit.value" :disabled="policies.noteEachClipsLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.noteEachClipsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.userListMax }}</template> <template #label>{{ i18n.ts._role._options.userListMax }}</template>
<template #suffix>{{ options_userListLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userListLimit_value) }}</template> <template #suffix>{{ policies.userListLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.userListLimit.value) }} <i :class="getPriorityIcon(policies.userListLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_userListLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.userListLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_userListLimit_value" :disabled="options_userListLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.userListLimit.value" :disabled="policies.userListLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.userListLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template> <template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<template #suffix>{{ options_userEachUserListsLimit_useDefault ? i18n.ts._role.useBaseValue : (options_userEachUserListsLimit_value) }}</template> <template #suffix>{{ policies.userEachUserListsLimit.useDefault ? i18n.ts._role.useBaseValue : (policies.userEachUserListsLimit.value) }} <i :class="getPriorityIcon(policies.userEachUserListsLimit)"></i></template>
<div class="_gaps"> <div class="_gaps">
<MkSwitch v-model="options_userEachUserListsLimit_useDefault" :readonly="readonly"> <MkSwitch v-model="policies.userEachUserListsLimit.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template> <template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch> </MkSwitch>
<MkInput v-model="options_userEachUserListsLimit_value" :disabled="options_userEachUserListsLimit_useDefault" type="number" :readonly="readonly"> <MkInput v-model="policies.userEachUserListsLimit.value" :disabled="policies.userEachUserListsLimit.useDefault" type="number" :readonly="readonly">
</MkInput> </MkInput>
<MkRange v-model="policies.userEachUserListsLimit.priority" :min="0" :max="2" :step="1" easing :text-converter="(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> </div>
</MkFolder> </MkFolder>
</div> </div>
@ -245,7 +291,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from 'vue'; import { computed, reactive, watch } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import RolesEditorFormula from './RolesEditorFormula.vue'; import RolesEditorFormula from './RolesEditorFormula.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
@ -260,6 +306,24 @@ import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { instance } from '@/instance'; import { instance } from '@/instance';
const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'canInvite',
'canManageCustomEmojis',
'driveCapacityMb',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
] as const;
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'created', payload: any): void; (ev: 'created', payload: any): void;
(ev: 'updated'): void; (ev: 'updated'): void;
@ -280,36 +344,16 @@ let target = $ref(role?.target ?? 'manual');
let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' }); let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
let isPublic = $ref(role?.isPublic ?? false); let isPublic = $ref(role?.isPublic ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true);
let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? instance.baseRole.gtlAvailable); const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({});
let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true); for (const ROLE_POLICY of ROLE_POLICIES) {
let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? instance.baseRole.ltlAvailable); const _policies = role?.policies ?? {};
let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true); policies[ROLE_POLICY] = {
let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? instance.baseRole.canPublicNote); useDefault: _policies[ROLE_POLICY]?.useDefault ?? true,
let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true); priority: _policies[ROLE_POLICY]?.priority ?? 0,
let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? instance.baseRole.canInvite); value: _policies[ROLE_POLICY]?.value ?? instance.policies[ROLE_POLICY],
let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true); };
let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? instance.baseRole.canManageCustomEmojis); }
let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? instance.baseRole.driveCapacityMb);
let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true);
let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? instance.baseRole.pinLimit);
let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? instance.baseRole.antennaLimit);
let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? instance.baseRole.wordMuteLimit);
let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? instance.baseRole.webhookLimit);
let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true);
let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? instance.baseRole.clipLimit);
let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true);
let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? instance.baseRole.noteEachClipsLimit);
let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true);
let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? instance.baseRole.userListLimit);
let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true);
let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor_useDefault = $ref(role?.options?.rateLimitFactor?.useDefault ?? true);
let options_rateLimitFactor_value = $ref(role?.options?.rateLimitFactor?.value ?? instance.baseRole.rateLimitFactor);
if (_DEV_) { if (_DEV_) {
watch($$(condFormula), () => { watch($$(condFormula), () => {
@ -317,24 +361,10 @@ if (_DEV_) {
}, { deep: true }); }, { deep: true });
} }
function getOptions() { function getPriorityIcon(option) {
return { if (option.priority === 2) return 'ti ti-arrows-up';
gtlAvailable: { useDefault: options_gtlAvailable_useDefault, value: options_gtlAvailable_value }, if (option.priority === 1) return 'ti ti-arrow-narrow-up';
ltlAvailable: { useDefault: options_ltlAvailable_useDefault, value: options_ltlAvailable_value }, return 'ti ti-point';
canPublicNote: { useDefault: options_canPublicNote_useDefault, value: options_canPublicNote_value },
canInvite: { useDefault: options_canInvite_useDefault, value: options_canInvite_value },
canManageCustomEmojis: { useDefault: options_canManageCustomEmojis_useDefault, value: options_canManageCustomEmojis_value },
driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
pinLimit: { useDefault: options_pinLimit_useDefault, value: options_pinLimit_value },
antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
clipLimit: { useDefault: options_clipLimit_useDefault, value: options_clipLimit_value },
noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value },
};
} }
async function save() { async function save() {
@ -351,7 +381,7 @@ async function save() {
isModerator: rolePermission === 'moderator', isModerator: rolePermission === 'moderator',
isPublic, isPublic,
canEditMembersByModerator, canEditMembersByModerator,
options: getOptions(), policies,
}); });
emit('updated'); emit('updated');
} else { } else {
@ -365,7 +395,7 @@ async function save() {
isModerator: rolePermission === 'moderator', isModerator: rolePermission === 'moderator',
isPublic, isPublic,
canEditMembersByModerator, canEditMembersByModerator,
options: getOptions(), policies,
}); });
emit('created', created); emit('created', created);
} }

View File

@ -10,114 +10,114 @@
<div class="_gaps"> <div class="_gaps">
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template> <template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ Math.floor(options_rateLimitFactor * 100) }}%</template> <template #suffix>{{ Math.floor(policies.rateLimitFactor * 100) }}%</template>
<MkRange :model-value="options_rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor = (v / 100)"> <MkRange :model-value="policies.rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => policies.rateLimitFactor = (v / 100)">
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template> <template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
</MkRange> </MkRange>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_gtlAvailable"> <MkSwitch v-model="policies.gtlAvailable">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.ltlAvailable }}</template> <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
<template #suffix>{{ options_ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_ltlAvailable"> <MkSwitch v-model="policies.ltlAvailable">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template> <template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
<template #suffix>{{ options_canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_canPublicNote"> <MkSwitch v-model="policies.canPublicNote">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>{{ options_canInvite ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_canInvite"> <MkSwitch v-model="policies.canInvite">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template> <template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
<template #suffix>{{ options_canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="options_canManageCustomEmojis"> <MkSwitch v-model="policies.canManageCustomEmojis">
<template #label>{{ i18n.ts.enable }}</template> <template #label>{{ i18n.ts.enable }}</template>
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template> <template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<template #suffix>{{ options_driveCapacityMb }}MB</template> <template #suffix>{{ policies.driveCapacityMb }}MB</template>
<MkInput v-model="options_driveCapacityMb" type="number"> <MkInput v-model="policies.driveCapacityMb" type="number">
<template #suffix>MB</template> <template #suffix>MB</template>
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.pinMax }}</template> <template #label>{{ i18n.ts._role._options.pinMax }}</template>
<template #suffix>{{ options_pinLimit }}</template> <template #suffix>{{ policies.pinLimit }}</template>
<MkInput v-model="options_pinLimit" type="number"> <MkInput v-model="policies.pinLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.antennaMax }}</template> <template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<template #suffix>{{ options_antennaLimit }}</template> <template #suffix>{{ policies.antennaLimit }}</template>
<MkInput v-model="options_antennaLimit" type="number"> <MkInput v-model="policies.antennaLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template> <template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
<template #suffix>{{ options_wordMuteLimit }}</template> <template #suffix>{{ policies.wordMuteLimit }}</template>
<MkInput v-model="options_wordMuteLimit" type="number"> <MkInput v-model="policies.wordMuteLimit" type="number">
<template #suffix>chars</template> <template #suffix>chars</template>
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.webhookMax }}</template> <template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<template #suffix>{{ options_webhookLimit }}</template> <template #suffix>{{ policies.webhookLimit }}</template>
<MkInput v-model="options_webhookLimit" type="number"> <MkInput v-model="policies.webhookLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.clipMax }}</template> <template #label>{{ i18n.ts._role._options.clipMax }}</template>
<template #suffix>{{ options_clipLimit }}</template> <template #suffix>{{ policies.clipLimit }}</template>
<MkInput v-model="options_clipLimit" type="number"> <MkInput v-model="policies.clipLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template> <template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<template #suffix>{{ options_noteEachClipsLimit }}</template> <template #suffix>{{ policies.noteEachClipsLimit }}</template>
<MkInput v-model="options_noteEachClipsLimit" type="number"> <MkInput v-model="policies.noteEachClipsLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.userListMax }}</template> <template #label>{{ i18n.ts._role._options.userListMax }}</template>
<template #suffix>{{ options_userListLimit }}</template> <template #suffix>{{ policies.userListLimit }}</template>
<MkInput v-model="options_userListLimit" type="number"> <MkInput v-model="policies.userListLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template> <template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<template #suffix>{{ options_userEachUserListsLimit }}</template> <template #suffix>{{ policies.userEachUserListsLimit }}</template>
<MkInput v-model="options_userEachUserListsLimit" type="number"> <MkInput v-model="policies.userEachUserListsLimit" type="number">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
@ -134,7 +134,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, reactive } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
@ -150,45 +150,36 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { instance } from '@/instance'; import { instance } from '@/instance';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'canInvite',
'canManageCustomEmojis',
'driveCapacityMb',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
] as const;
const router = useRouter(); const router = useRouter();
const roles = await os.api('admin/roles/list'); const roles = await os.api('admin/roles/list');
let options_gtlAvailable = $ref(instance.baseRole.gtlAvailable); const policies = reactive<Record<typeof ROLE_POLICIES[number], any>>({});
let options_ltlAvailable = $ref(instance.baseRole.ltlAvailable); for (const ROLE_POLICY of ROLE_POLICIES) {
let options_canPublicNote = $ref(instance.baseRole.canPublicNote); policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
let options_canInvite = $ref(instance.baseRole.canInvite); }
let options_canManageCustomEmojis = $ref(instance.baseRole.canManageCustomEmojis);
let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
let options_pinLimit = $ref(instance.baseRole.pinLimit);
let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
let options_webhookLimit = $ref(instance.baseRole.webhookLimit);
let options_clipLimit = $ref(instance.baseRole.clipLimit);
let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit);
let options_userListLimit = $ref(instance.baseRole.userListLimit);
let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor = $ref(instance.baseRole.rateLimitFactor);
async function updateBaseRole() { async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-role-override', { await os.apiWithDialog('admin/roles/update-default-policies', {
options: { policies,
gtlAvailable: options_gtlAvailable,
ltlAvailable: options_ltlAvailable,
canPublicNote: options_canPublicNote,
canInvite: options_canInvite,
canManageCustomEmojis: options_canManageCustomEmojis,
driveCapacityMb: options_driveCapacityMb,
pinLimit: options_pinLimit,
antennaLimit: options_antennaLimit,
wordMuteLimit: options_wordMuteLimit,
webhookLimit: options_webhookLimit,
clipLimit: options_clipLimit,
noteEachClipsLimit: options_noteEachClipsLimit,
userListLimit: options_userListLimit,
userEachUserListsLimit: options_userEachUserListsLimit,
rateLimitFactor: options_rateLimitFactor,
},
}); });
} }

View File

@ -35,8 +35,8 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue')); const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
const isLocalTimelineAvailable = ($i == null && instance.baseRole.ltlAvailable) || ($i != null && $i.role.ltlAvailable); const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.baseRole.gtlAvailable) || ($i != null && $i.role.gtlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const keymap = { const keymap = {
't': focus, 't': focus,
}; };

View File

@ -86,16 +86,28 @@
</div> </div>
</FormSection> </FormSection>
</div> </div>
<div v-else-if="tab === 'moderation'" class="_gaps_m"> <div v-else-if="tab === 'moderation'" class="_gaps_m">
<MkSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch> <MkSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
<div> <div>
<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton> <MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton> <MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div> </div>
<MkFolder>
<template #icon><i class="ti ti-license"></i></template>
<template #label>{{ i18n.ts._role.policies }}</template>
<div class="_gaps">
<div v-for="policy in Object.keys(info.policies)" :key="policy">
{{ policy }} ... {{ info.policies[policy] }}
</div>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-badges"></i></template> <template #icon><i class="ti ti-badges"></i></template>
<template #label>{{ i18n.ts.roles }}</template> <template #label>{{ i18n.ts.roles }}</template>
<div class="_gaps"> <div class="_gaps">
<MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton> <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
@ -105,6 +117,7 @@
</div> </div>
</div> </div>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-password"></i></template> <template #icon><i class="ti ti-password"></i></template>
<template #label>IP</template> <template #label>IP</template>
@ -117,16 +130,18 @@
</div> </div>
</template> </template>
</MkFolder> </MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-cloud"></i></template> <template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.files }}</template> <template #label>{{ i18n.ts.files }}</template>
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/> <MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
</MkFolder> </MkFolder>
<MkTextarea v-model="moderationNote" manual-save> <MkTextarea v-model="moderationNote" manual-save>
<template #label>Moderation note</template> <template #label>Moderation note</template>
</MkTextarea> </MkTextarea>
</div> </div>
<div v-else-if="tab === 'chart'" class="_gaps_m"> <div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshm"> <div class="cmhjzshm">
<div class="selects"> <div class="selects">
@ -142,6 +157,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else-if="tab === 'raw'" class="_gaps_m"> <div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info && $i.isAdmin" tall :value="info"> <MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView> </MkObjectView>

View File

@ -47,7 +47,7 @@ export function openInstanceMenu(ev: MouseEvent) {
to: '/clicker', to: '/clicker',
text: '🍪👈', text: '🍪👈',
icon: 'ti ti-cookie', icon: 'ti ti-cookie',
}, ($i && ($i.isAdmin || $i.role.canInvite) && instance.disableRegistration) ? { }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
text: i18n.ts.invite, text: i18n.ts.invite,
icon: 'ti ti-user-plus', icon: 'ti ti-user-plus',
action: () => { action: () => {
@ -63,7 +63,7 @@ export function openInstanceMenu(ev: MouseEvent) {
}); });
}); });
}, },
} : undefined, ($i && ($i.isAdmin || $i.role.canManageCustomEmojis)) ? { } : undefined, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
type: 'link', type: 'link',
to: '/custom-emojis-manager', to: '/custom-emojis-manager',
text: i18n.ts.manageCustomEmojis, text: i18n.ts.manageCustomEmojis,

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity"> <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" class="mkw-activity data-cy-mkw-activity">
<template #icon><i class="ti ti-chart-line"></i></template> <template #icon><i class="ti ti-chart-line"></i></template>
<template #header>{{ i18n.ts._widgets.activity }}</template> <template #header>{{ i18n.ts._widgets.activity }}</template>
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-aichan"> <MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-aichan data-cy-mkw-aichan">
<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe> <iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe>
</MkContainer> </MkContainer>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript"> <MkContainer :show-header="widgetProps.showHeader" class="mkw-aiscript data-cy-mkw-aiscript">
<template #icon><i class="ti ti-terminal-2"></i></template> <template #icon><i class="ti ti-terminal-2"></i></template>
<template #header>{{ i18n.ts._widgets.aiscript }}</template> <template #header>{{ i18n.ts._widgets.aiscript }}</template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-button"> <div class="mkw-button data-cy-mkw-button">
<MkButton :primary="widgetProps.colored" full @click="run"> <MkButton :primary="widgetProps.colored" full @click="run">
{{ widgetProps.label }} {{ widgetProps.label }}
</MkButton> </MkButton>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="[$style.root, { _panel: !widgetProps.transparent }]"> <div :class="[$style.root, { _panel: !widgetProps.transparent }]" class="data-cy-mkw-calendar">
<div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]"> <div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]">
<p :class="$style.monthAndYear"> <p :class="$style.monthAndYear">
<span :class="$style.year">{{ $t('yearX', { year }) }}</span> <span :class="$style.year">{{ $t('yearX', { year }) }}</span>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock"> <MkContainer :naked="widgetProps.transparent" :show-header="false" class="mkw-clock data-cy-mkw-clock">
<div class="vubelbmv" :class="widgetProps.size"> <div class="vubelbmv" :class="widgetProps.size">
<div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label a abbrev">{{ tzAbbrev }}</div> <div v-if="widgetProps.label === 'tz' || widgetProps.label === 'timeAndTz'" class="_monospace label a abbrev">{{ tzAbbrev }}</div>
<MkAnalogClock <MkAnalogClock

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="_monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }"> <div class="data-cy-mkw-digitalClock _monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }">
<div v-if="widgetProps.showLabel" :class="$style.label">{{ tzAbbrev }}</div> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzAbbrev }}</div>
<div> <div>
<MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/> <MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" class="mkw-federation"> <MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable" class="mkw-federation data-cy-mkw-federation">
<template #icon><i class="ti ti-whirl"></i></template> <template #icon><i class="ti ti-whirl"></i></template>
<template #header>{{ i18n.ts._widgets.federation }}</template> <template #header>{{ i18n.ts._widgets.federation }}</template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }"> <div class="mkw-jobQueue data-cy-mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }">
<div class="inbox"> <div class="inbox">
<div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div> <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="ti ti-alert-triangle icon"></i></div>
<div class="values"> <div class="values">

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" class="mkw-memo"> <MkContainer :show-header="widgetProps.showHeader" class="mkw-memo data-cy-mkw-memo">
<template #icon><i class="ti ti-note"></i></template> <template #icon><i class="ti ti-note"></i></template>
<template #header>{{ i18n.ts._widgets.memo }}</template> <template #header>{{ i18n.ts._widgets.memo }}</template>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" class="mkw-notifications"> <MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true" class="mkw-notifications data-cy-mkw-notifications">
<template #icon><i class="ti ti-bell"></i></template> <template #icon><i class="ti ti-bell"></i></template>
<template #header>{{ i18n.ts.notifications }}</template> <template #header>{{ i18n.ts.notifications }}</template>
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configureNotification()"><i class="ti ti-settings"></i></button></template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }"> <div class="mkw-onlineUsers data-cy-mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }">
<I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text"> <I18n v-if="onlineUsersCount" :src="i18n.ts.onlineUsersCount" text-tag="span" class="text">
<template #n><b>{{ onlineUsersCount }}</b></template> <template #n><b>{{ onlineUsersCount }}</b></template>
</I18n> </I18n>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" class="mkw-photos"> <MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null" class="mkw-photos data-cy-mkw-photos">
<template #icon><i class="ti ti-camera"></i></template> <template #icon><i class="ti ti-camera"></i></template>
<template #header>{{ i18n.ts._widgets.photos }}</template> <template #header>{{ i18n.ts._widgets.photos }}</template>

View File

@ -1,5 +1,5 @@
<template> <template>
<XPostForm class="_panel mkw-postForm" :fixed="true" :autofocus="false"/> <XPostForm class="_panel mkw-postForm data-cy-mkw-postForm" :fixed="true" :autofocus="false"/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" class="mkw-rss"> <MkContainer :show-header="widgetProps.showHeader" class="mkw-rss data-cy-mkw-rss">
<template #icon><i class="ti ti-rss"></i></template> <template #icon><i class="ti ti-rss"></i></template>
<template #header>RSS</template> <template #header>RSS</template>
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="configure"><i class="ti ti-settings"></i></button></template>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="kvausudm _panel mkw-slideshow" :style="{ height: widgetProps.height + 'px' }"> <div class="kvausudm _panel mkw-slideshow data-cy-mkw-slideshow" :style="{ height: widgetProps.height + 'px' }">
<div @click="choose"> <div @click="choose">
<p v-if="widgetProps.folderId == null"> <p v-if="widgetProps.folderId == null">
{{ i18n.ts.folder }} {{ i18n.ts.folder }}

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" class="mkw-timeline"> <MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true" class="mkw-timeline data-cy-mkw-timeline">
<template #icon> <template #icon>
<i v-if="widgetProps.src === 'home'" class="ti ti-home"></i> <i v-if="widgetProps.src === 'home'" class="ti ti-home"></i>
<i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i> <i v-else-if="widgetProps.src === 'local'" class="ti ti-planet"></i>

View File

@ -1,5 +1,5 @@
<template> <template>
<MkContainer :show-header="widgetProps.showHeader" class="mkw-trends"> <MkContainer :show-header="widgetProps.showHeader" class="mkw-trends data-cy-mkw-trends">
<template #icon><i class="ti ti-hash"></i></template> <template #icon><i class="ti ti-hash"></i></template>
<template #header>{{ i18n.ts._widgets.trends }}</template> <template #header>{{ i18n.ts._widgets.trends }}</template>

View File

@ -4,7 +4,7 @@
<template #header>{{ i18n.ts._widgets.serverMetric }}</template> <template #header>{{ i18n.ts._widgets.serverMetric }}</template>
<template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template> <template #func="{ buttonStyleClass }"><button class="_button" :class="buttonStyleClass" @click="toggleView()"><i class="ti ti-selector"></i></button></template>
<div v-if="meta" class="mkw-serverMetric"> <div v-if="meta" class="mkw-serverMetric data-cy-mkw-serverMetric">
<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/> <XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/> <XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/> <XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>