feat: ロールによるコンテンツの操作の制限 (#120)

This commit is contained in:
まっちゃとーにゅ 2023-07-28 04:21:59 +09:00 committed by GitHub
parent 0bed053b7d
commit 46f8a0435c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 228 additions and 11 deletions

View File

@ -1404,6 +1404,9 @@ _role:
gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes"
canCreateContent: "Can create contents"
canUpdateContent: "Can edit contents"
canDeleteContent: "Can delete contents"
canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"

3
locales/index.d.ts vendored
View File

@ -1490,6 +1490,9 @@ export interface Locale {
"gtlAvailable": string;
"ltlAvailable": string;
"canPublicNote": string;
"canCreateContent": string;
"canUpdateContent": string;
"canDeleteContent": string;
"canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;

View File

@ -1412,6 +1412,9 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canCreateContent: "コンテンツの作成"
canUpdateContent: "コンテンツの編集"
canDeleteContent: "コンテンツの削除"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"

View File

@ -20,6 +20,9 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
canDeleteContent: boolean;
canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
@ -44,6 +47,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
canCreateContent: true,
canUpdateContent: true,
canDeleteContent: true,
canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
@ -287,6 +293,9 @@ export class RoleService implements OnApplicationShutdown {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

View File

@ -351,8 +351,9 @@ export class UserEntityService implements OnModuleInit {
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
const isModerator = isMe && opts.detail ? await this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? await this.roleService.isAdministrator(user) : null;
const policies = opts.detail ? await this.roleService.getUserPolicies(user.id) : null;
const falsy = opts.detail ? false : undefined;
@ -396,7 +397,8 @@ export class UserEntityService implements OnModuleInit {
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSilenced: !policies?.canPublicNote,
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent),
isSuspended: user.isSuspended ?? falsy,
description: profile!.description,
location: profile!.location,
@ -473,7 +475,7 @@ export class UserEntityService implements OnModuleInit {
emailNotificationTypes: profile!.emailNotificationTypes,
achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id),
policies: policies,
} : {}),
...(opts.includeSecrets ? {

View File

@ -121,6 +121,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
isLimited: {
type: 'boolean',
nullable: false, optional: false,
},
isSuspended: {
type: 'boolean',
nullable: false, optional: false,

View File

@ -51,8 +51,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found');
}
const policies = await this.roleService.getUserPolicies(user.id);
const isModerator = await this.roleService.isModerator(user);
const isSilenced = !(await this.roleService.getUserPolicies(user.id)).canPublicNote;
const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent);
const isSilenced = !policies.canPublicNote;
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
@ -80,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: isModerator,
isSilenced: isSilenced,
isLimited: isLimited,
isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote ?? '',

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['antennas'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['antennas'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['antennas'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -17,6 +17,7 @@ export const meta = {
},
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:blocks',

View File

@ -17,6 +17,7 @@ export const meta = {
},
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:blocks',

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['channels'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:channels',

View File

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
prohibitMoved: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['clips'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['clips'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['clip'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['clips'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -14,6 +14,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:drive',

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:drive',

View File

@ -18,6 +18,7 @@ export const meta = {
description: 'Request the server to download a new drive file from the specified URL.',
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
kind: 'write:drive',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:drive',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['drive'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:drive',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['flash'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['flashs'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:flash',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['flash'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['flash'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['flash'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['gallery'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['gallery'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:gallery',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['gallery'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['gallery'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['gallery'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
secure: true,
} as const;

View File

@ -11,6 +11,8 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,
limit: {

View File

@ -10,6 +10,8 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
limit: {

View File

@ -10,6 +10,8 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
limit: {

View File

@ -10,7 +10,10 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
max: 1,

View File

@ -23,7 +23,10 @@ export const meta = {
secure: true,
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
limit: {
duration: ms('1day'),
max: 5,

View File

@ -8,6 +8,8 @@ export const meta = {
tags: ['account', 'notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
kind: 'write:account',

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['account', 'notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -12,10 +12,11 @@ import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { ApiError } from '../../error.js';
export const meta = {
requireCredential: true,
secure: true,
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
limit: {
duration: ms('1hour'),
max: 3,

View File

@ -30,6 +30,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['webhooks'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
kind: 'write:account',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['webhooks'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['webhooks'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -11,6 +11,8 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
kind: 'write:mutes',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:mutes',

View File

@ -17,6 +17,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:notes',

View File

@ -12,6 +12,8 @@ export const meta = {
tags: ['notes', 'favorites'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
kind: 'write:favorites',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['notes', 'favorites'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:favorites',

View File

@ -16,6 +16,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['reactions', 'notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['reactions', 'notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:reactions',

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:notes',

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['pages'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['pages'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:pages',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['pages'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['pages'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['pages'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -13,6 +13,8 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,
kind: 'write:mutes',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:mutes',

View File

@ -12,7 +12,10 @@ import { UserListService } from '@/core/UserListService.js';
export const meta = {
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,
res: {
type: 'object',
optional: false, nullable: false,

View File

@ -12,6 +12,7 @@ export const meta = {
tags: ['lists'],
requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true,

View File

@ -8,6 +8,7 @@ export const meta = {
tags: ['lists'],
requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account',

View File

@ -7,6 +7,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
errors: {
noSuchList: {
message: 'No such user list.',

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['lists', 'users'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -11,6 +11,7 @@ export const meta = {
tags: ['lists', 'users'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true,

View File

@ -6,6 +6,8 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
errors: {
noSuchList: {
message: 'No such user list.',

View File

@ -9,6 +9,7 @@ export const meta = {
tags: ['lists'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'],
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account',

View File

@ -91,6 +91,7 @@ describe('ユーザー', () => {
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: user.isSilenced,
isLimited: user.isLimited,
isSuspended: user.isSuspended,
description: user.description,
location: user.location,
@ -356,6 +357,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.bannerBlurhash, null);
assert.strictEqual(response.isLocked, false);
assert.strictEqual(response.isSilenced, false);
assert.strictEqual(response.isLimited, false);
assert.strictEqual(response.isSuspended, false);
assert.strictEqual(response.description, null);
assert.strictEqual(response.location, null);

View File

@ -99,6 +99,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
isModerator: false,
isMuted: false,
isSilenced: false,
isLimited: false,
isSuspended: false,
lang: 'en',
location: 'Fediverse',

View File

@ -1,5 +1,5 @@
<template>
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, gray: user.isLimited, red: user.isSuspended }]">
<MkAvatar class="avatar" :user="user" indicator/>
<div class="body">
<span class="name"><MkUserName class="name" :user="user"/></span>

View File

@ -56,6 +56,9 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'canCreateContent',
'canUpdateContent',
'canDeleteContent',
'canInvite',
'inviteLimit',
'inviteLimitCycle',

View File

@ -155,6 +155,66 @@
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
<template #suffix>
<span v-if="role.policies.canCreateContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canCreateContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canCreateContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canCreateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canCreateContent.value" :disabled="role.policies.canCreateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canCreateContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateContent, 'canUpdateContent'])">
<template #label>{{ i18n.ts._role._options.canUpdateContent }}</template>
<template #suffix>
<span v-if="role.policies.canUpdateContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUpdateContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUpdateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUpdateContent.value" :disabled="role.policies.canUpdateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUpdateContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canDeleteContent, 'canDeleteContent'])">
<template #label>{{ i18n.ts._role._options.canDeleteContent }}</template>
<template #suffix>
<span v-if="role.policies.canDeleteContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canDeleteContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canDeleteContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canDeleteContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canDeleteContent.value" :disabled="role.policies.canDeleteContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canDeleteContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>

View File

@ -43,6 +43,30 @@
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
<template #suffix>{{ policies.canCreateContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canCreateContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateContent, 'canUpdateContent'])">
<template #label>{{ i18n.ts._role._options.canUpdateContent }}</template>
<template #suffix>{{ policies.canUpdateContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUpdateContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canDeleteContent, 'canDeleteContent'])">
<template #label>{{ i18n.ts._role._options.canDeleteContent }}</template>
<template #suffix>{{ policies.canDeleteContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canDeleteContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>
@ -57,7 +81,7 @@
<MkInput v-model="policies.inviteLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>

View File

@ -11,6 +11,7 @@
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span class="state">
<span v-if="suspended" class="suspended">Suspended</span>
<span v-if="limited" class="limited">Limited</span>
<span v-if="silenced" class="silenced">Silenced</span>
<span v-if="moderator" class="moderator">Moderator</span>
</span>
@ -219,6 +220,7 @@ let ips = $ref(null);
let ap = $ref(null);
let moderator = $ref(false);
let silenced = $ref(false);
let limited = $ref(false);
let suspended = $ref(false);
let moderationNote = $ref('');
const filesPagination = {
@ -244,6 +246,7 @@ function createFetcher() {
ips = _ips;
moderator = info.isModerator;
silenced = info.isSilenced;
limited = info.isLimited;
suspended = info.isSuspended;
moderationNote = info.moderationNote;
@ -485,7 +488,7 @@ definePageMetadata(computed(() => ({
display: none;
}
> .suspended, > .silenced, > .moderator {
> .suspended, > .limited, > .silenced, > .moderator {
display: inline-block;
border: solid 1px;
border-radius: 6px;
@ -498,6 +501,11 @@ definePageMetadata(computed(() => ({
border-color: var(--error);
}
> .limited {
color: var(--error);
border-color: var(--error);
}
> .silenced {
color: var(--warn);
border-color: var(--warn);

View File

@ -2770,6 +2770,7 @@ type UserDetailed = UserLite & {
isModerator: boolean;
isMuted: boolean;
isSilenced: boolean;
isLimited: boolean;
isSuspended: boolean;
lang: string | null;
lastFetchedAt?: DateString;

View File

@ -53,6 +53,7 @@ export type UserDetailed = UserLite & {
isModerator: boolean;
isMuted: boolean;
isSilenced: boolean;
isLimited: boolean;
isSuspended: boolean;
lang: string | null;
lastFetchedAt?: DateString;
@ -410,7 +411,7 @@ export type Announcement = {
imageUrl: string | null;
isRead?: boolean;
isPrivate: boolean;
closeDuration: number;
closeDuration: number;
};
export type Antenna = {