From 935fce338ab032e7acc41a3dd75f95f0e9ab1689 Mon Sep 17 00:00:00 2001 From: Johann150 <johann.galle@protonmail.com> Date: Mon, 4 Jul 2022 16:17:07 +0200 Subject: [PATCH 1/6] refactor: remove unnecessary computed Fixes lint no-const-assign. --- packages/client/src/pages/admin/bot-protection.vue | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue index d2e7919b4f..d316f973bc 100644 --- a/packages/client/src/pages/admin/bot-protection.vue +++ b/packages/client/src/pages/admin/bot-protection.vue @@ -61,27 +61,22 @@ let hcaptchaSecretKey: string | null = $ref(null); let recaptchaSiteKey: string | null = $ref(null); let recaptchaSecretKey: string | null = $ref(null); -const enableHcaptcha = $computed(() => provider === 'hcaptcha'); -const enableRecaptcha = $computed(() => provider === 'recaptcha'); - async function init() { const meta = await os.api('admin/meta'); - enableHcaptcha = meta.enableHcaptcha; hcaptchaSiteKey = meta.hcaptchaSiteKey; hcaptchaSecretKey = meta.hcaptchaSecretKey; - enableRecaptcha = meta.enableRecaptcha; recaptchaSiteKey = meta.recaptchaSiteKey; recaptchaSecretKey = meta.recaptchaSecretKey; - provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null; + provider = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : null; } function save() { os.apiWithDialog('admin/update-meta', { - enableHcaptcha, + enableHcaptcha: provider === 'hcaptcha', hcaptchaSiteKey, hcaptchaSecretKey, - enableRecaptcha, + enableRecaptcha: provider === 'recaptcha', recaptchaSiteKey, recaptchaSecretKey, }).then(() => { From 2bd4323b17a65ded8e86fd0faf59b2d8321b3dbe Mon Sep 17 00:00:00 2001 From: Johann150 <johann.galle@protonmail.com> Date: Mon, 4 Jul 2022 16:22:11 +0200 Subject: [PATCH 2/6] fix lint: use let instead of const for $ref Fixes lint no-const-assign. --- packages/client/src/pages/admin/overview.user.vue | 2 +- packages/client/src/pages/gallery/post.vue | 4 ++-- packages/client/src/pages/theme-editor.vue | 2 +- packages/client/src/ui/classic.vue | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/src/pages/admin/overview.user.vue b/packages/client/src/pages/admin/overview.user.vue index 40592b280b..d70336f3c2 100644 --- a/packages/client/src/pages/admin/overview.user.vue +++ b/packages/client/src/pages/admin/overview.user.vue @@ -19,7 +19,7 @@ const props = defineProps<{ user: misskey.entities.User; }>(); -const chart = $ref(null); +let chart = $ref(null); os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16, span: 'day' }).then(res => { chart = res; diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue index e16ccc3150..e87a541e98 100644 --- a/packages/client/src/pages/gallery/post.vue +++ b/packages/client/src/pages/gallery/post.vue @@ -74,8 +74,8 @@ const props = defineProps<{ postId: string; }>(); -const post = $ref(null); -const error = $ref(null); +let post = $ref(null); +let error = $ref(null); const otherPostsPagination = { endpoint: 'users/gallery/posts' as const, limit: 6, diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index cec3833594..d0a26c9cfe 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -118,7 +118,7 @@ const fgColors = [ { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' }, ]; -const theme = $ref<Partial<Theme>>({ +let theme = $ref<Partial<Theme>>({ base: 'light', props: lightTheme.props, }); diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue index 310232aec0..a2c26f536e 100644 --- a/packages/client/src/ui/classic.vue +++ b/packages/client/src/ui/classic.vue @@ -60,8 +60,8 @@ const DESKTOP_THRESHOLD = 1100; let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); -const widgetsShowing = $ref(false); -const fullView = $ref(false); +let widgetsShowing = $ref(false); +let fullView = $ref(false); let globalHeaderHeight = $ref(0); const wallpaper = localStorage.getItem('wallpaper') != null; const showMenuOnTop = $computed(() => defaultStore.state.menuDisplay === 'top'); From a5c3fcea6e9b4796cdeeb12655e72cab00a2255b Mon Sep 17 00:00:00 2001 From: Johann150 <johann.galle@protonmail.com> Date: Mon, 4 Jul 2022 16:33:55 +0200 Subject: [PATCH 3/6] fix lint no-undef --- packages/client/src/components/code-core.vue | 2 +- packages/client/src/pages/admin/_header_.vue | 1 - packages/client/src/pages/my-antennas/editor.vue | 1 + packages/client/src/pages/my-lists/list.vue | 1 + packages/client/src/pages/theme-editor.vue | 1 + packages/client/src/ui/deck/main-column.vue | 2 +- 6 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue index 45a38afe04..65dee5cdae 100644 --- a/packages/client/src/components/code-core.vue +++ b/packages/client/src/components/code-core.vue @@ -5,7 +5,7 @@ <script lang="ts" setup> import { computed } from 'vue'; -import 'prismjs'; +import { Prism } from 'prismjs'; import 'prismjs/themes/prism-okaidia.css'; const props = defineProps<{ diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue index 73747e1164..aea2663c39 100644 --- a/packages/client/src/pages/admin/_header_.vue +++ b/packages/client/src/pages/admin/_header_.vue @@ -75,7 +75,6 @@ const hasTabs = computed(() => { const showTabsPopup = (ev: MouseEvent) => { if (!hasTabs.value) return; - if (!narrow.value) return; ev.preventDefault(); ev.stopPropagation(); const menu = props.tabs.map(tab => ({ diff --git a/packages/client/src/pages/my-antennas/editor.vue b/packages/client/src/pages/my-antennas/editor.vue index 6f3c4afbfe..9470257c6c 100644 --- a/packages/client/src/pages/my-antennas/editor.vue +++ b/packages/client/src/pages/my-antennas/editor.vue @@ -46,6 +46,7 @@ <script lang="ts" setup> import { watch } from 'vue'; +import * as Acct from 'misskey-js/built/acct'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue index 5bc0bf41dd..892878ae88 100644 --- a/packages/client/src/pages/my-lists/list.vue +++ b/packages/client/src/pages/my-lists/list.vue @@ -41,6 +41,7 @@ import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { mainRouter } from '@/router'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { i18n } from '@/i18n'; const props = defineProps<{ listId: string; diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue index d0a26c9cfe..44b5a05f27 100644 --- a/packages/client/src/pages/theme-editor.vue +++ b/packages/client/src/pages/theme-editor.vue @@ -78,6 +78,7 @@ import FormButton from '@/components/ui/button.vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormFolder from '@/components/form/folder.vue'; +import { $i } from '@/account'; import { Theme, applyTheme } from '@/scripts/theme'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue index 670b4a212b..9a5fd43af7 100644 --- a/packages/client/src/ui/deck/main-column.vue +++ b/packages/client/src/ui/deck/main-column.vue @@ -53,7 +53,7 @@ function onContextmenu(ev: MouseEvent) { if (isLink(ev.target as HTMLElement)) return; if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return; if (window.getSelection()?.toString() !== '') return; - const path = router.currentRoute.value.path; + const path = mainRouter.currentRoute.value.path; os.contextMenu([{ type: 'label', text: path, From d748ba2c51211d3a2833fd1cb6ef898bc149d486 Mon Sep 17 00:00:00 2001 From: Johann150 <johann.galle@protonmail.com> Date: Mon, 4 Jul 2022 16:39:04 +0200 Subject: [PATCH 4/6] fix lint no-prototype-builtins --- packages/client/src/components/form-dialog.vue | 2 +- packages/client/src/plugin.ts | 2 +- packages/client/src/scripts/array.ts | 2 +- packages/client/src/widgets/widget.ts | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/form-dialog.vue b/packages/client/src/components/form-dialog.vue index 5fd9ec460b..f05dde16f8 100644 --- a/packages/client/src/components/form-dialog.vue +++ b/packages/client/src/components/form-dialog.vue @@ -98,7 +98,7 @@ export default defineComponent({ created() { for (const item in this.form) { - this.values[item] = this.form[item].hasOwnProperty('default') ? this.form[item].default : null; + this.values[item] = this.form[item].default ?? null; } }, diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts index ca7b4b73d3..de1c955675 100644 --- a/packages/client/src/plugin.ts +++ b/packages/client/src/plugin.ts @@ -38,7 +38,7 @@ export function install(plugin) { function createPluginEnv(opts) { const config = new Map(); for (const [k, v] of Object.entries(opts.plugin.config || {})) { - config.set(k, jsToVal(opts.plugin.configData.hasOwnProperty(k) ? opts.plugin.configData[k] : v.default)); + config.set(k, jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); } return { diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts index 29d027de14..26c6195d66 100644 --- a/packages/client/src/scripts/array.ts +++ b/packages/client/src/scripts/array.ts @@ -98,7 +98,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { return collections.reduce((obj: Record<string, T[]>, item: T) => { const key = keySelector(item); - if (!obj.hasOwnProperty(key)) { + if (typeof obj[key] === 'undefined') { obj[key] = []; } diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts index 9626d01619..9fdfe7f3e1 100644 --- a/packages/client/src/widgets/widget.ts +++ b/packages/client/src/widgets/widget.ts @@ -36,8 +36,9 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default: const mergeProps = () => { for (const prop of Object.keys(propsDef)) { - if (widgetProps.hasOwnProperty(prop)) continue; - widgetProps[prop] = propsDef[prop].default; + if (typeof widgetProps[prop] === 'undefined') { + widgetProps[prop] = propsDef[prop].default; + } } }; watch(widgetProps, () => { From a228d1ddaa66c8b1acafc38b05a958b497582a6a Mon Sep 17 00:00:00 2001 From: Johann150 <johann.galle@protonmail.com> Date: Mon, 4 Jul 2022 16:46:48 +0200 Subject: [PATCH 5/6] fix lint @typescript-eslint/ban-types --- packages/client/src/components/global/router-view.vue | 3 --- packages/client/src/components/tag-cloud.vue | 2 -- packages/client/src/scripts/autocomplete.ts | 2 +- packages/client/src/scripts/hotkey.ts | 8 +++++--- packages/client/src/scripts/url.ts | 2 +- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/client/src/components/global/router-view.vue b/packages/client/src/components/global/router-view.vue index 7138faaa9d..fca2371f0d 100644 --- a/packages/client/src/components/global/router-view.vue +++ b/packages/client/src/components/global/router-view.vue @@ -13,9 +13,6 @@ const props = defineProps<{ router?: Router; }>(); -const emit = defineEmits<{ -}>(); - const router = props.router ?? inject('router'); if (router == null) { diff --git a/packages/client/src/components/tag-cloud.vue b/packages/client/src/components/tag-cloud.vue index 5ffa7321e4..9f3bc1c603 100644 --- a/packages/client/src/components/tag-cloud.vue +++ b/packages/client/src/components/tag-cloud.vue @@ -13,8 +13,6 @@ import { onMounted, ref, watch, PropType, onBeforeUnmount } from 'vue'; import tinycolor from 'tinycolor2'; -const props = defineProps<{}>(); - const loaded = !!window.TagCanvas; const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz'; const computedStyle = getComputedStyle(document.documentElement); diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts index 8d9bdee8f5..3ef6224175 100644 --- a/packages/client/src/scripts/autocomplete.ts +++ b/packages/client/src/scripts/autocomplete.ts @@ -8,7 +8,7 @@ export class Autocomplete { x: Ref<number>; y: Ref<number>; q: Ref<string | null>; - close: Function; + close: () => void; } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts index fd9c74f6c8..bd8c3b6cab 100644 --- a/packages/client/src/scripts/hotkey.ts +++ b/packages/client/src/scripts/hotkey.ts @@ -1,6 +1,8 @@ import keyCode from './keycode'; -type Keymap = Record<string, Function>; +type Callback = (ev: KeyboardEvent) => void; + +type Keymap = Record<string, Callback>; type Pattern = { which: string[]; @@ -11,14 +13,14 @@ type Pattern = { type Action = { patterns: Pattern[]; - callback: Function; + callback: Callback; allowRepeat: boolean; }; const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { const result = { patterns: [], - callback: callback, + callback, allowRepeat: true } as Action; diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts index 542b00e0f0..86735de9f0 100644 --- a/packages/client/src/scripts/url.ts +++ b/packages/client/src/scripts/url.ts @@ -1,4 +1,4 @@ -export function query(obj: {}): string { +export function query(obj: Record<string, any>): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); From cd07eb222e1990b30cae74b5fab9f12277322ef6 Mon Sep 17 00:00:00 2001 From: CyberRex <hspwinx86@gmail.com> Date: Tue, 5 Jul 2022 00:21:01 +0900 Subject: [PATCH 6/6] Add additional drive capacity change support (#8867) * Add additional drive capacity change support * Update packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts Co-authored-by: Johann150 <johann@qwertqwefsday.eu> * :art: * show instance default capacity in placeholder * fix * update api/drive * fix * remove : * fix lint Co-authored-by: Johann150 <johann@qwertqwefsday.eu> Co-authored-by: tamaina <tamaina@hotmail.co.jp> --- locales/ja-JP.yml | 3 ++ .../1655813815729-driveCapacityOverrideMb.js | 13 +++++ packages/backend/src/models/entities/user.ts | 6 +++ .../backend/src/models/repositories/user.ts | 1 + packages/backend/src/server/api/endpoints.ts | 2 + .../admin/drive-capacity-override.ts | 47 +++++++++++++++++++ .../backend/src/server/api/endpoints/drive.ts | 2 +- .../backend/src/services/drive/add-file.ts | 11 ++++- packages/client/src/pages/user-info.vue | 38 +++++++++++++-- 9 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1655813815729-driveCapacityOverrideMb.js create mode 100644 packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9684489927..ce25095d5a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -203,6 +203,7 @@ done: "完了" processing: "処理中" preview: "プレビュー" default: "デフォルト" +defaultValueIs: "デフォルト: {value}" noCustomEmojis: "絵文字はありません" noJobs: "ジョブはありません" federating: "連合中" @@ -855,6 +856,8 @@ noEmailServerWarning: "メールサーバーの設定がされていません。 thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。" recommended: "推奨" check: "チェック" +driveCapOverrideLabel: "このユーザーのドライブ容量上限を変更" +driveCapOverrideCaption: "0以下を指定すると解除されます。" requireAdminForView: "閲覧するには管理者アカウントでログインしている必要があります。" isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" typeToConfirm: "この操作を行うには {x} と入力してください" diff --git a/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js new file mode 100644 index 0000000000..f257cd112f --- /dev/null +++ b/packages/backend/migration/1655813815729-driveCapacityOverrideMb.js @@ -0,0 +1,13 @@ +export class driveCapacityOverrideMb1655813815729 { + name = 'driveCapacityOverrideMb1655813815729' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`); + await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`); + } + + async down(queryRunner) { + await queryRunner.query(`COMMENT ON COLUMN "user"."driveCapacityOverrideMb" IS 'Overrides user drive capacity limit'`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`); + } +} diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index df92fb8259..bc9446be41 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -218,6 +218,12 @@ export class User { }) public token: string | null; + @Column('integer', { + nullable: true, + comment: 'Overrides user drive capacity limit', + }) + public driveCapacityOverrideMb: number | null; + constructor(data: Partial<User>) { if (data == null) return; diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 8a4e48efdd..645091395a 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -315,6 +315,7 @@ export const UserRepository = db.getRepository(User).extend({ } : undefined) : undefined, emojis: populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), + driveCapacityOverrideMb: user.driveCapacityOverrideMb, ...(opts.detail ? { url: profile!.url, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 4a2ecebd86..4644f34d94 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -314,6 +314,7 @@ import * as ep___users_search from './endpoints/users/search.js'; import * as ep___users_show from './endpoints/users/show.js'; import * as ep___users_stats from './endpoints/users/stats.js'; import * as ep___fetchRss from './endpoints/fetch-rss.js'; +import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -629,6 +630,7 @@ const eps = [ ['users/search', ep___users_search], ['users/show', ep___users_show], ['users/stats', ep___users_stats], + ['admin/drive-capacity-override', ep___admin_driveCapOverride], ['fetch-rss', ep___fetchRss], ]; diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts new file mode 100644 index 0000000000..a4b29770e1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts @@ -0,0 +1,47 @@ +import define from '../../define.js'; +import { Users } from '@/models/index.js'; +import { User } from '@/models/entities/user.js'; +import { insertModerationLog } from '@/services/insert-moderation-log.js'; +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + overrideMb: { type: 'number', nullable: true }, + }, + required: ['userId', 'overrideMb'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const user = await Users.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + if (!Users.isLocalUser(user)) { + throw new Error('user is not local user'); + } + + /*if (user.isAdmin) { + throw new Error('cannot suspend admin'); + } + if (user.isModerator) { + throw new Error('cannot suspend moderator'); + }*/ + + await Users.update(user.id, { + driveCapacityOverrideMb: ps.overrideMb, + }); + + insertModerationLog(me, 'change-drive-capacity-override', { + targetId: user.id, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 47e940cddd..82497adefa 100644 --- a/packages/backend/src/server/api/endpoints/drive.ts +++ b/packages/backend/src/server/api/endpoints/drive.ts @@ -39,7 +39,7 @@ export default define(meta, paramDef, async (ps, user) => { const usage = await DriveFiles.calcDriveUsageOf(user.id); return { - capacity: 1024 * 1024 * instance.localDriveCapacityMb, + capacity: 1024 * 1024 * (user.driveCapacityOverrideMb || instance.localDriveCapacityMb), usage: usage, }; }); diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index a25413187b..0dfad11cfb 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -307,7 +307,7 @@ async function deleteOldFile(user: IRemoteUser) { type AddFileArgs = { /** User who wish to add file */ - user: { id: User['id']; host: User['host'] } | null; + user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null; /** File path */ path: string; /** Name */ @@ -371,9 +371,16 @@ export async function addFile({ //#region Check drive usage if (user && !isLink) { const usage = await DriveFiles.calcDriveUsageOf(user); + const u = await Users.findOneBy({ id: user.id }); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + let driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + + if (Users.isLocalUser(user) && u?.driveCapacityOverrideMb != null) { + driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb; + logger.debug('drive capacity override applied'); + logger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`); + } logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 204ece7eb6..51d224dfdd 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -85,6 +85,17 @@ </FormSection> </div> <div v-else-if="tab === 'moderation'" class="_formRoot"> + <FormSection> + <template #label>Drive Capacity Override</template> + + <FormInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride"> + <template #label>{{ i18n.ts.driveCapOverrideLabel }}</template> + <template #suffix>MB</template> + <template #caption> + {{ i18n.ts.driveCapOverrideCaption }} + </template> + </FormInput> + </FormSection> <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch> <FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch> @@ -141,7 +152,7 @@ </template> <script lang="ts" setup> -import { computed, defineAsyncComponent, defineComponent, watch } from 'vue'; +import { computed, watch } from 'vue'; import * as misskey from 'misskey-js'; import MkChart from '@/components/chart.vue'; import MkObjectView from '@/components/object-view.vue'; @@ -150,6 +161,8 @@ import FormSwitch from '@/components/form/switch.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import FormButton from '@/components/ui/button.vue'; +import FormInput from '@/components/form/input.vue'; +import FormSplit from '@/components/form/split.vue'; import FormFolder from '@/components/form/folder.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; @@ -164,6 +177,7 @@ import { userPage, acct } from '@/filters/user'; import { definePageMetadata } from '@/scripts/page-metadata'; import { i18n } from '@/i18n'; import { iAmAdmin, iAmModerator } from '@/account'; +import { instance } from '@/instance'; const props = defineProps<{ userId: string; @@ -172,13 +186,14 @@ const props = defineProps<{ let tab = $ref('overview'); let chartSrc = $ref('per-user-notes'); let user = $ref<null | misskey.entities.UserDetailed>(); -let init = $ref(); +let init = $ref<ReturnType<typeof createFetcher>>(); let info = $ref(); let ips = $ref(null); let ap = $ref(null); let moderator = $ref(false); let silenced = $ref(false); let suspended = $ref(false); +let driveCapacityOverrideMb: number | null = $ref(0); let moderationNote = $ref(''); const filesPagination = { endpoint: 'admin/drive/files' as const, @@ -203,6 +218,7 @@ function createFetcher() { moderator = info.isModerator; silenced = info.isSilenced; suspended = info.isSuspended; + driveCapacityOverrideMb = user.driveCapacityOverrideMb; moderationNote = info.moderationNote; watch($$(moderationNote), async () => { @@ -289,6 +305,22 @@ async function deleteAllFiles() { await refreshUser(); } +async function applyDriveCapacityOverride() { + let driveCapOrMb = driveCapacityOverrideMb; + if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) { + driveCapOrMb = null; + } + try { + await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb }); + await refreshUser(); + } catch (e) { + os.alert({ + type: 'error', + text: e.toString(), + }); + } +} + async function deleteAccount() { const confirm = await os.confirm({ type: 'warning', @@ -319,7 +351,7 @@ watch(() => props.userId, () => { immediate: true, }); -watch(() => user, () => { +watch($$(user), () => { os.api('ap/get', { uri: user.uri ?? `${url}/users/${user.id}`, }).then(res => {