Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop

This commit is contained in:
tamaina 2022-07-04 15:26:21 +00:00
commit 2fe4a51d26
28 changed files with 144 additions and 38 deletions

View file

@ -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} と入力してください"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,9 +13,6 @@ const props = defineProps<{
router?: Router;
}>();
const emit = defineEmits<{
}>();
const router = props.router ?? inject('router');
if (router == null) {

View file

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

View file

@ -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 => ({

View file

@ -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(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, () => {