Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop
This commit is contained in:
commit
2fe4a51d26
|
@ -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} と入力してください"
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
];
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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})`);
|
||||
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -13,9 +13,6 @@ const props = defineProps<{
|
|||
router?: Router;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const router = props.router ?? inject('router');
|
||||
|
||||
if (router == null) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
@ -118,7 +119,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,
|
||||
});
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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] = [];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -36,9 +36,10 @@ export const useWidgetPropsManager = <F extends Form & Record<string, { default:
|
|||
|
||||
const mergeProps = () => {
|
||||
for (const prop of Object.keys(propsDef)) {
|
||||
if (widgetProps.hasOwnProperty(prop)) continue;
|
||||
if (typeof widgetProps[prop] === 'undefined') {
|
||||
widgetProps[prop] = propsDef[prop].default;
|
||||
}
|
||||
}
|
||||
};
|
||||
watch(widgetProps, () => {
|
||||
mergeProps();
|
||||
|
|
Loading…
Reference in a new issue