mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-24 10:36:43 -06:00
Registry (#7073)
* wip * wip * wip * wip * wip * Update registry.value.vue * wip * wip * wip * wip * typo
This commit is contained in:
parent
1286dee1ab
commit
6c975275f8
37 changed files with 1017 additions and 100 deletions
|
@ -685,6 +685,19 @@ accentColor: "アクセント"
|
|||
textColor: "文字"
|
||||
saveAs: "名前を付けて保存"
|
||||
advanced: "高度"
|
||||
value: "値"
|
||||
updatedAt: "更新日時"
|
||||
saveConfirm: "保存しますか?"
|
||||
deleteConfirm: "削除しますか?"
|
||||
invalidValue: "有効な値ではありません。"
|
||||
registry: "レジストリ"
|
||||
|
||||
_registry:
|
||||
scope: "スコープ"
|
||||
key: "キー"
|
||||
keys: "キー"
|
||||
domain: "ドメイン"
|
||||
createKey: "キーを作成"
|
||||
|
||||
_aboutMisskey:
|
||||
about: "Misskeyはsyuiloによって2014年から開発されている、オープンソースのソフトウェアです。"
|
||||
|
@ -1558,6 +1571,7 @@ _deck:
|
|||
swapDown: "下に移動"
|
||||
stackLeft: "左に重ねる"
|
||||
popRight: "右に出す"
|
||||
profile: "プロファイル"
|
||||
|
||||
_columns:
|
||||
main: "メイン"
|
||||
|
|
22
migration/1610277136869-registry.ts
Normal file
22
migration/1610277136869-registry.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry1610277136869 implements MigrationInterface {
|
||||
name = 'registry1610277136869'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "registry_item" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "key" character varying(1024) NOT NULL, "scope" character varying(1024) array NOT NULL DEFAULT '{}'::varchar[], "domain" character varying(512), CONSTRAINT "PK_64b3f7e6008b4d89b826cd3af95" PRIMARY KEY ("id")); COMMENT ON COLUMN "registry_item"."createdAt" IS 'The created date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."updatedAt" IS 'The updated date of the RegistryItem.'; COMMENT ON COLUMN "registry_item"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "registry_item"."key" IS 'The key of the RegistryItem.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_fb9d21ba0abb83223263df6bcb" ON "registry_item" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_22baca135bb8a3ea1a83d13df3" ON "registry_item" ("scope") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad" ON "registry_item" ("domain") `);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP CONSTRAINT "FK_fb9d21ba0abb83223263df6bcb3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_0a72bdfcdb97c0eca11fe7ecad"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_22baca135bb8a3ea1a83d13df3"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_fb9d21ba0abb83223263df6bcb"`);
|
||||
await queryRunner.query(`DROP TABLE "registry_item"`);
|
||||
}
|
||||
|
||||
}
|
16
migration/1610277585759-registry2.ts
Normal file
16
migration/1610277585759-registry2.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry21610277585759 implements MigrationInterface {
|
||||
name = 'registry21610277585759'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ADD "value" jsonb NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "registry_item"."value" IS 'The value of the RegistryItem.'`);
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" DROP COLUMN "value"`);
|
||||
}
|
||||
|
||||
}
|
14
migration/1610283021566-registry3.ts
Normal file
14
migration/1610283021566-registry3.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class registry31610283021566 implements MigrationInterface {
|
||||
name = 'registry31610283021566'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "registry_item" ALTER COLUMN "value" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -98,6 +98,7 @@
|
|||
"@types/sharp": "0.26.1",
|
||||
"@types/sinonjs__fake-timers": "6.0.1",
|
||||
"@types/speakeasy": "2.0.5",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.2",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@types/uuid": "8.3.0",
|
||||
|
@ -232,6 +233,7 @@
|
|||
"syuilo-password-strength": "0.0.1",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.117.1",
|
||||
"throttle-debounce": "3.0.1",
|
||||
"tinycolor2": "1.4.2",
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "8.0.11",
|
||||
|
|
|
@ -7,7 +7,6 @@ import { waiting } from '@/os';
|
|||
type Account = {
|
||||
id: string;
|
||||
token: string;
|
||||
clientData: Record<string, any>;
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('account');
|
||||
|
|
|
@ -262,7 +262,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// keep cw when reply
|
||||
if (this.$store.keepCw && this.reply && this.reply.cw) {
|
||||
if (this.$store.state.keepCw && this.reply && this.reply.cw) {
|
||||
this.useCw = true;
|
||||
this.cw = this.reply.cw;
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default defineComponent({
|
|||
font-size: 90%;
|
||||
background: var(--infoBg);
|
||||
color: var(--infoFg);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius);
|
||||
|
||||
&.warn {
|
||||
background: var(--infoWarnBg);
|
||||
|
|
|
@ -347,14 +347,6 @@ if ($i) {
|
|||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
updateAccount({
|
||||
clientData: {
|
||||
[x.key]: x.value
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
<span>{{ $ts._deck.columnMargin }}</span>
|
||||
<template #suffix>px</template>
|
||||
</FormInput>
|
||||
|
||||
<FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
|
@ -31,7 +33,7 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
|
@ -42,7 +44,7 @@ import * as os from '@/os';
|
|||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormLink,
|
||||
FormInput,
|
||||
FormRadios,
|
||||
FormBase,
|
||||
|
@ -67,6 +69,7 @@ export default defineComponent({
|
|||
columnAlign: deckStore.makeGetterSetter('columnAlign'),
|
||||
columnMargin: deckStore.makeGetterSetter('columnMargin'),
|
||||
columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
|
||||
profile: deckStore.makeGetterSetter('profile'),
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -85,5 +88,19 @@ export default defineComponent({
|
|||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setProfile() {
|
||||
const { canceled, result: name } = await os.dialog({
|
||||
title: this.$ts._deck.profile,
|
||||
input: {
|
||||
allowEmpty: false
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
this.profile = name;
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -35,13 +35,13 @@
|
|||
</FormGroup>
|
||||
</FormBase>
|
||||
<div class="main">
|
||||
<component :is="component" @info="onInfo"/>
|
||||
<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { i18n } from '@/i18n';
|
||||
|
@ -78,7 +78,9 @@ export default defineComponent({
|
|||
const onInfo = (viewInfo) => {
|
||||
INFO.value = viewInfo;
|
||||
};
|
||||
const pageProps = ref({});
|
||||
const component = computed(() => {
|
||||
if (props.page == null) return null;
|
||||
switch (props.page) {
|
||||
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
|
||||
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
|
||||
|
@ -104,16 +106,35 @@ export default defineComponent({
|
|||
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
|
||||
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
|
||||
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
|
||||
case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
|
||||
case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
|
||||
default: return null;
|
||||
}
|
||||
if (props.page.startsWith('registry/keys/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.keys.vue'));
|
||||
}
|
||||
if (props.page.startsWith('registry/value/system/')) {
|
||||
return defineAsyncComponent(() => import('./registry.value.vue'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(component, () => {
|
||||
pageProps.value = {};
|
||||
|
||||
if (props.page) {
|
||||
if (props.page.startsWith('registry/keys/system/')) {
|
||||
pageProps.value.scope = props.page.replace('registry/keys/system/', '').split('/');
|
||||
}
|
||||
if (props.page.startsWith('registry/value/system/')) {
|
||||
const path = props.page.replace('registry/value/system/', '').split('/');
|
||||
pageProps.value.xKey = path.pop();
|
||||
pageProps.value.scope = path;
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el.value, 0);
|
||||
});
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
narrow.value = el.value.offsetWidth < 1025;
|
||||
|
@ -125,6 +146,7 @@ export default defineComponent({
|
|||
view,
|
||||
el,
|
||||
onInfo,
|
||||
pageProps,
|
||||
component,
|
||||
logout: () => {
|
||||
signout();
|
||||
|
|
|
@ -15,16 +15,17 @@
|
|||
DEBUG MODE
|
||||
</FormSwitch>
|
||||
<template v-if="debug">
|
||||
<FormLink to="/settings/regedit">RegEdit</FormLink>
|
||||
<FormButton @click="taskmanager">Task Manager</FormButton>
|
||||
</template>
|
||||
</FormGroup>
|
||||
|
||||
<FormLink to="/settings/registry"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.registry }}</FormLink>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faEllipsisH, faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
|
@ -53,7 +54,8 @@ export default defineComponent({
|
|||
title: this.$ts.other,
|
||||
icon: faEllipsisH
|
||||
},
|
||||
debug
|
||||
debug,
|
||||
faCogs
|
||||
}
|
||||
},
|
||||
|
||||
|
|
115
src/client/pages/settings/registry.keys.vue
Normal file
115
src/client/pages/settings/registry.keys.vue
Normal file
|
@ -0,0 +1,115 @@
|
|||
<template>
|
||||
<FormBase>
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="keys">
|
||||
<template #label>{{ $ts._registry.keys }}</template>
|
||||
<FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
|
||||
</FormGroup>
|
||||
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
keys: null,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
scope() {
|
||||
this.fetch();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/keys-with-type', {
|
||||
scope: this.scope
|
||||
}).then(keys => {
|
||||
this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
default: this.scope.join('/')
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
149
src/client/pages/settings/registry.value.vue
Normal file
149
src/client/pages/settings/registry.value.vue
Normal file
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<FormBase>
|
||||
<MkInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</MkInfo>
|
||||
|
||||
<template v-if="value">
|
||||
<FormGroup>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.domain }}</template>
|
||||
<template #value>{{ $ts.system }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.scope }}</template>
|
||||
<template #value>{{ scope.join('/') }}</template>
|
||||
</FormKeyValueView>
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts._registry.key }}</template>
|
||||
<template #value>{{ xKey }}</template>
|
||||
</FormKeyValueView>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormTextarea tall v-model:value="valueForEditor" class="_monospace" style="tab-size: 2;">
|
||||
<span>{{ $ts.value }} (JSON)</span>
|
||||
</FormTextarea>
|
||||
<FormButton @click="save" primary><Fa :icon="faSave"/> {{ $ts.save }}</FormButton>
|
||||
</FormGroup>
|
||||
|
||||
<FormKeyValueView>
|
||||
<template #key>{{ $ts.updatedAt }}</template>
|
||||
<template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
|
||||
</FormKeyValueView>
|
||||
|
||||
<FormButton danger @click="del"><Fa :icon="faTrash"/> {{ $ts.delete }}</FormButton>
|
||||
</template>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
props: {
|
||||
scope: {
|
||||
required: true
|
||||
},
|
||||
xKey: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
value: null,
|
||||
valueForEditor: null,
|
||||
faSave, faTrash,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
key() {
|
||||
this.fetch();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/get-detail', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
}).then(value => {
|
||||
this.value = value;
|
||||
this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
try {
|
||||
JSON5.parse(this.valueForEditor);
|
||||
} catch (e) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: this.$ts.invalidValue
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.saveConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: this.scope,
|
||||
key: this.xKey,
|
||||
value: JSON5.parse(this.valueForEditor)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
del() {
|
||||
os.dialog({
|
||||
type: 'warning',
|
||||
text: this.$ts.deleteConfirm,
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/remove', {
|
||||
scope: this.scope,
|
||||
key: this.xKey
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
91
src/client/pages/settings/registry.vue
Normal file
91
src/client/pages/settings/registry.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<FormBase>
|
||||
<FormGroup v-if="scopes">
|
||||
<template #label>{{ $ts.system }}</template>
|
||||
<FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
|
||||
</FormGroup>
|
||||
<FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
|
||||
</FormBase>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as JSON5 from 'json5';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormBase from '@/components/form/base.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormButton from '@/components/form/button.vue';
|
||||
import FormKeyValueView from '@/components/form/key-value-view.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInfo,
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
FormKeyValueView,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
INFO: {
|
||||
title: this.$ts.registry,
|
||||
icon: faCogs
|
||||
},
|
||||
scopes: null,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this.INFO);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetch() {
|
||||
os.api('i/registry/scopes').then(scopes => {
|
||||
this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
|
||||
});
|
||||
},
|
||||
|
||||
async createKey() {
|
||||
const { canceled, result } = await os.form(this.$ts._registry.createKey, {
|
||||
key: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.key,
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
label: this.$ts.value,
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
label: this.$ts._registry.scope,
|
||||
}
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('i/registry/set', {
|
||||
scope: result.scope.split('/'),
|
||||
key: result.key,
|
||||
value: JSON5.parse(result.value),
|
||||
}).then(() => {
|
||||
this.fetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
|
@ -11,6 +11,7 @@ type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
|||
|
||||
export class Storage<T extends StateDef> {
|
||||
public readonly key: string;
|
||||
public readonly keyForLocalStorage: string;
|
||||
|
||||
public readonly def: T;
|
||||
|
||||
|
@ -19,20 +20,22 @@ export class Storage<T extends StateDef> {
|
|||
public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
|
||||
|
||||
constructor(key: string, def: T) {
|
||||
this.key = 'pizzax::' + key;
|
||||
this.key = key;
|
||||
this.keyForLocalStorage = 'pizzax::' + key;
|
||||
this.def = def;
|
||||
|
||||
// TODO: indexedDBにする
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
|
||||
const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}') : {};
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
|
||||
const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
|
||||
const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
|
||||
|
||||
const state = {};
|
||||
const reactiveState = {};
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||
state[k] = deviceState[k];
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call($i.clientData, k)) {
|
||||
state[k] = $i.clientData[k];
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
state[k] = registryCache[k];
|
||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
state[k] = deviceAccountState[k];
|
||||
} else {
|
||||
|
@ -47,16 +50,24 @@ export class Storage<T extends StateDef> {
|
|||
this.reactiveState = reactiveState as any;
|
||||
|
||||
if ($i) {
|
||||
watch($i, () => {
|
||||
if (_DEV_) console.log('$i updated');
|
||||
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'account' && Object.prototype.hasOwnProperty.call($i!.clientData, k)) {
|
||||
state[k] = $i!.clientData[k];
|
||||
reactiveState[k].value = $i!.clientData[k];
|
||||
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
|
||||
setTimeout(() => {
|
||||
api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
|
||||
for (const [k, v] of Object.entries(def)) {
|
||||
if (v.where === 'account') {
|
||||
if (Object.prototype.hasOwnProperty.call(kvs, k)) {
|
||||
state[k] = kvs[k];
|
||||
reactiveState[k].value = kvs[k];
|
||||
} else {
|
||||
state[k] = v.default;
|
||||
reactiveState[k].value = v.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 1);
|
||||
|
||||
// TODO: streamingのuser storage updateイベントを監視して更新
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,21 +79,26 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
switch (this.def[key].where) {
|
||||
case 'device': {
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
|
||||
const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
|
||||
deviceState[key] = value;
|
||||
localStorage.setItem(this.key, JSON.stringify(deviceState));
|
||||
localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
|
||||
break;
|
||||
}
|
||||
case 'deviceAccount': {
|
||||
if ($i == null) break;
|
||||
const deviceAccountState = JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}');
|
||||
const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
|
||||
deviceAccountState[key] = value;
|
||||
localStorage.setItem(this.key + '::' + $i.id, JSON.stringify(deviceAccountState));
|
||||
localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
|
||||
break;
|
||||
}
|
||||
case 'account': {
|
||||
api('i/update-client-setting', {
|
||||
name: key,
|
||||
if ($i == null) break;
|
||||
const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
|
||||
cache[key] = value;
|
||||
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
|
||||
api('i/registry/set', {
|
||||
scope: ['client', this.key],
|
||||
key: key,
|
||||
value: value
|
||||
});
|
||||
break;
|
||||
|
|
|
@ -81,7 +81,6 @@ export const router = createRouter({
|
|||
{ path: '/miauth/:session', component: page('miauth') },
|
||||
{ path: '/authorize-follow', component: page('follow') },
|
||||
{ path: '/share', component: page('share') },
|
||||
{ path: '/test', component: page('test') },
|
||||
{ path: '/:catchAll(.*)', component: page('not-found') }
|
||||
],
|
||||
// なんかHacky
|
||||
|
|
|
@ -41,7 +41,7 @@ import { getScrollContainer } from '@/scripts/scroll';
|
|||
import * as os from '@/os';
|
||||
import { sidebarDef } from '@/sidebar';
|
||||
import XCommon from './_common_/common.vue';
|
||||
import { deckStore, addColumn } from './deck/deck-store';
|
||||
import { deckStore, addColumn, loadDeck } from './deck/deck-store';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -88,6 +88,7 @@ export default defineComponent({
|
|||
document.documentElement.style.overflowY = 'hidden';
|
||||
document.documentElement.style.scrollBehavior = 'auto';
|
||||
window.addEventListener('wheel', this.onWheel);
|
||||
loadDeck();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { throttle } from 'throttle-debounce';
|
||||
import { i18n } from '@/i18n';
|
||||
import { markRaw } from 'vue';
|
||||
import { api } from '@/os';
|
||||
import { markRaw, watch } from 'vue';
|
||||
import { Storage } from '../../pizzax';
|
||||
|
||||
type ColumnWidget = {
|
||||
|
@ -21,23 +23,17 @@ function copy<T>(x: T): T {
|
|||
}
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
profile: {
|
||||
where: 'deviceAccount',
|
||||
default: 'default'
|
||||
},
|
||||
columns: {
|
||||
where: 'deviceAccount',
|
||||
default: [{
|
||||
id: 'a',
|
||||
type: 'main',
|
||||
name: i18n.locale._deck._columns.main,
|
||||
width: 350,
|
||||
}, {
|
||||
id: 'b',
|
||||
type: 'notifications',
|
||||
name: i18n.locale._deck._columns.notifications,
|
||||
width: 330,
|
||||
}] as Column[]
|
||||
default: [] as Column[]
|
||||
},
|
||||
layout: {
|
||||
where: 'deviceAccount',
|
||||
default: [['a'], ['b']] as Column['id'][][]
|
||||
default: [] as Column['id'][][]
|
||||
},
|
||||
columnAlign: {
|
||||
where: 'deviceAccount',
|
||||
|
@ -61,10 +57,60 @@ export const deckStore = markRaw(new Storage('deck', {
|
|||
},
|
||||
}));
|
||||
|
||||
export const loadDeck = async () => {
|
||||
let deck;
|
||||
|
||||
try {
|
||||
deck = await api('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (deckStore.state.profile === 'default') {
|
||||
saveDeck();
|
||||
return;
|
||||
}
|
||||
|
||||
deckStore.set('columns', [{
|
||||
id: 'a',
|
||||
type: 'main',
|
||||
name: i18n.locale._deck._columns.main,
|
||||
width: 350,
|
||||
}, {
|
||||
id: 'b',
|
||||
type: 'notifications',
|
||||
name: i18n.locale._deck._columns.notifications,
|
||||
width: 330,
|
||||
}]);
|
||||
deckStore.set('layout', [['a'], ['b']]);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
deckStore.set('columns', deck.columns);
|
||||
deckStore.set('layout', deck.layout);
|
||||
};
|
||||
|
||||
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
|
||||
export const saveDeck = throttle(1000, () => {
|
||||
api('i/registry/set', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
value: {
|
||||
columns: deckStore.reactiveState.columns.value,
|
||||
layout: deckStore.reactiveState.layout.value,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export function addColumn(column: Column) {
|
||||
if (column.name == undefined) column.name = null;
|
||||
deckStore.push('columns', column);
|
||||
deckStore.push('layout', [column.id]);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumn(id: Column['id']) {
|
||||
|
@ -72,6 +118,7 @@ export function removeColumn(id: Column['id']) {
|
|||
deckStore.set('layout', deckStore.state.layout
|
||||
.map(ids => ids.filter(_id => _id !== id))
|
||||
.filter(ids => ids.length > 0));
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
|
@ -83,6 +130,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
|
|||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapLeftColumn(id: Column['id']) {
|
||||
|
@ -98,6 +146,7 @@ export function swapLeftColumn(id: Column['id']) {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapRightColumn(id: Column['id']) {
|
||||
|
@ -113,6 +162,7 @@ export function swapRightColumn(id: Column['id']) {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapUpColumn(id: Column['id']) {
|
||||
|
@ -132,6 +182,7 @@ export function swapUpColumn(id: Column['id']) {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function swapDownColumn(id: Column['id']) {
|
||||
|
@ -151,6 +202,7 @@ export function swapDownColumn(id: Column['id']) {
|
|||
return true;
|
||||
}
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function stackLeftColumn(id: Column['id']) {
|
||||
|
@ -160,6 +212,7 @@ export function stackLeftColumn(id: Column['id']) {
|
|||
layout[i - 1].push(id);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function popRightColumn(id: Column['id']) {
|
||||
|
@ -169,6 +222,7 @@ export function popRightColumn(id: Column['id']) {
|
|||
layout.splice(i + 1, 0, [id]);
|
||||
layout = layout.filter(ids => ids.length > 0);
|
||||
deckStore.set('layout', layout);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
|
@ -180,6 +234,7 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
column.widgets.unshift(widget);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
||||
|
@ -190,6 +245,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
column.widgets = column.widgets.filter(w => w.id != widget.id);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
||||
|
@ -200,6 +256,7 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
|
|||
column.widgets = widgets;
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
|
||||
|
@ -213,6 +270,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, data: any
|
|||
} : w);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
||||
export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
||||
|
@ -225,4 +283,5 @@ export function updateColumn(id: Column['id'], column: Partial<Column>) {
|
|||
}
|
||||
columns[columnIndex] = currentColumn;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ import { MutedNote } from '../models/entities/muted-note';
|
|||
import { Channel } from '../models/entities/channel';
|
||||
import { ChannelFollowing } from '../models/entities/channel-following';
|
||||
import { ChannelNotePining } from '../models/entities/channel-note-pining';
|
||||
import { RegistryItem } from '../models/entities/registry-item';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -159,6 +160,7 @@ export const entities = [
|
|||
Channel,
|
||||
ChannelFollowing,
|
||||
ChannelNotePining,
|
||||
RegistryItem,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
|
|
58
src/models/entities/registry-item.ts
Normal file
58
src/models/entities/registry-item.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
|
||||
// TODO: 同じdomain、同じscope、同じkeyのレコードは二つ以上存在しないように制約付けたい
|
||||
@Entity()
|
||||
export class RegistryItem {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the RegistryItem.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The updated date of the RegistryItem.'
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.'
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
comment: 'The key of the RegistryItem.'
|
||||
})
|
||||
public key: string;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {}, nullable: true,
|
||||
comment: 'The value of the RegistryItem.'
|
||||
})
|
||||
public value: any | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 1024, array: true, default: '{}'
|
||||
})
|
||||
public scope: string[];
|
||||
|
||||
// サードパーティアプリに開放するときのためのカラム
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 512, nullable: true
|
||||
})
|
||||
public domain: string | null;
|
||||
}
|
|
@ -94,6 +94,7 @@ export class UserProfile {
|
|||
})
|
||||
public password: string | null;
|
||||
|
||||
// TODO: そのうち消す
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
comment: 'The client-specific data of the User.'
|
||||
|
|
|
@ -57,6 +57,7 @@ import { ChannelRepository } from './repositories/channel';
|
|||
import { MutedNote } from './entities/muted-note';
|
||||
import { ChannelFollowing } from './entities/channel-following';
|
||||
import { ChannelNotePining } from './entities/channel-note-pining';
|
||||
import { RegistryItem } from './entities/registry-item';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -116,3 +117,4 @@ export const MutedNotes = getRepository(MutedNote);
|
|||
export const Channels = getCustomRepository(ChannelRepository);
|
||||
export const ChannelFollowings = getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = getRepository(ChannelNotePining);
|
||||
export const RegistryItems = getRepository(RegistryItem);
|
||||
|
|
|
@ -261,7 +261,6 @@ export class UserRepository extends Repository<User> {
|
|||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
clientData: profile!.clientData,
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
|
|
|
@ -11,7 +11,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
|
|||
const reply = (x?: any, y?: ApiError) => {
|
||||
if (x == null) {
|
||||
ctx.status = 204;
|
||||
} else if (typeof x === 'number') {
|
||||
} else if (typeof x === 'number' && y) {
|
||||
ctx.status = x;
|
||||
ctx.body = {
|
||||
error: {
|
||||
|
@ -23,7 +23,8 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => {
|
|||
}
|
||||
};
|
||||
} else {
|
||||
ctx.body = x;
|
||||
// 文字列を返す場合は、JSON.stringify通さないとJSONと認識されない
|
||||
ctx.body = typeof x === 'string' ? JSON.stringify(x) : x;
|
||||
}
|
||||
res();
|
||||
};
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import define from '../define';
|
||||
import { Users } from '../../../models';
|
||||
import { RegistryItems, UserProfiles, Users } from '../../../models';
|
||||
import { ensure } from '../../../prelude/ensure';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -22,6 +24,27 @@ export const meta = {
|
|||
export default define(meta, async (ps, user, token) => {
|
||||
const isSecure = token == null;
|
||||
|
||||
// TODO: そのうち消す
|
||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||
for (const [k, v] of Object.entries(profile.clientData)) {
|
||||
await RegistryItems.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
userId: user.id,
|
||||
domain: null,
|
||||
scope: ['client', 'base'],
|
||||
key: k,
|
||||
value: v
|
||||
});
|
||||
}
|
||||
await UserProfiles.createQueryBuilder().update()
|
||||
.set({
|
||||
clientData: {},
|
||||
})
|
||||
.where('userId = :id', { id: user.id })
|
||||
.execute();
|
||||
|
||||
return await Users.pack(user, user, {
|
||||
detail: true,
|
||||
includeSecrets: isSecure
|
||||
|
|
|
@ -80,7 +80,7 @@ export default define(meta, async (ps, user) => {
|
|||
.where('muting.muterId = :muterId', { muterId: user.id });
|
||||
|
||||
const suspendedQuery = Users.createQueryBuilder('users')
|
||||
.select('id')
|
||||
.select('users.id')
|
||||
.where('users.isSuspended = TRUE');
|
||||
|
||||
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
|
||||
|
|
33
src/server/api/endpoints/i/registry/get-all.ts
Normal file
33
src/server/api/endpoints/i/registry/get-all.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const items = await query.getMany();
|
||||
|
||||
const res = {} as Record<string, any>;
|
||||
|
||||
for (const item of items) {
|
||||
res[item.key] = item.value;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
48
src/server/api/endpoints/i/registry/get-detail.ts
Normal file
48
src/server/api/endpoints/i/registry/get-detail.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
key: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchKey: {
|
||||
message: 'No such key.',
|
||||
code: 'NO_SUCH_KEY',
|
||||
id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const item = await query.getOne();
|
||||
|
||||
if (item == null) {
|
||||
throw new ApiError(meta.errors.noSuchKey);
|
||||
}
|
||||
|
||||
return {
|
||||
updatedAt: item.updatedAt,
|
||||
value: item.value,
|
||||
};
|
||||
});
|
45
src/server/api/endpoints/i/registry/get.ts
Normal file
45
src/server/api/endpoints/i/registry/get.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
key: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchKey: {
|
||||
message: 'No such key.',
|
||||
code: 'NO_SUCH_KEY',
|
||||
id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const item = await query.getOne();
|
||||
|
||||
if (item == null) {
|
||||
throw new ApiError(meta.errors.noSuchKey);
|
||||
}
|
||||
|
||||
return item.value;
|
||||
});
|
41
src/server/api/endpoints/i/registry/keys-with-type.ts
Normal file
41
src/server/api/endpoints/i/registry/keys-with-type.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const items = await query.getMany();
|
||||
|
||||
const res = {} as Record<string, string>;
|
||||
|
||||
for (const item of items) {
|
||||
const type = typeof item.value;
|
||||
res[item.key] =
|
||||
item.value === null ? 'null' :
|
||||
Array.isArray(item.value) ? 'array' :
|
||||
type === 'number' ? 'number' :
|
||||
type === 'string' ? 'string' :
|
||||
type === 'boolean' ? 'boolean' :
|
||||
type === 'object' ? 'object' :
|
||||
null as never;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
28
src/server/api/endpoints/i/registry/keys.ts
Normal file
28
src/server/api/endpoints/i/registry/keys.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.select('item.key')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const items = await query.getMany();
|
||||
|
||||
return items.map(x => x.key);
|
||||
});
|
45
src/server/api/endpoints/i/registry/remove.ts
Normal file
45
src/server/api/endpoints/i/registry/remove.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
import { ApiError } from '../../../error';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
key: {
|
||||
validator: $.str
|
||||
},
|
||||
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchKey: {
|
||||
message: 'No such key.',
|
||||
code: 'NO_SUCH_KEY',
|
||||
id: '1fac4e8a-a6cd-4e39-a4a5-3a7e11f1b019'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const item = await query.getOne();
|
||||
|
||||
if (item == null) {
|
||||
throw new ApiError(meta.errors.noSuchKey);
|
||||
}
|
||||
|
||||
RegistryItems.remove(item);
|
||||
});
|
30
src/server/api/endpoints/i/registry/scopes.ts
Normal file
30
src/server/api/endpoints/i/registry/scopes.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.select('item.scope')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id });
|
||||
|
||||
const items = await query.getMany();
|
||||
|
||||
const res = [] as string[][];
|
||||
|
||||
for (const item of items) {
|
||||
if (res.some(scope => scope.join('.') === item.scope.join('.'))) continue;
|
||||
res.push(item.scope);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
61
src/server/api/endpoints/i/registry/set.ts
Normal file
61
src/server/api/endpoints/i/registry/set.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import $ from 'cafy';
|
||||
import { publishMainStream } from '../../../../../services/stream';
|
||||
import define from '../../../define';
|
||||
import { RegistryItems } from '../../../../../models';
|
||||
import { genId } from '../../../../../misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
key: {
|
||||
validator: $.str.min(1)
|
||||
},
|
||||
|
||||
value: {
|
||||
validator: $.nullable.any
|
||||
},
|
||||
|
||||
scope: {
|
||||
validator: $.optional.arr($.str.match(/^[a-zA-Z0-9_]+$/)),
|
||||
default: [],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const query = RegistryItems.createQueryBuilder('item')
|
||||
.where('item.domain IS NULL')
|
||||
.andWhere('item.userId = :userId', { userId: user.id })
|
||||
.andWhere('item.key = :key', { key: ps.key })
|
||||
.andWhere('item.scope = :scope', { scope: ps.scope });
|
||||
|
||||
const existingItem = await query.getOne();
|
||||
|
||||
if (existingItem) {
|
||||
await RegistryItems.update(existingItem.id, {
|
||||
updatedAt: new Date(),
|
||||
value: ps.value
|
||||
});
|
||||
} else {
|
||||
await RegistryItems.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
userId: user.id,
|
||||
domain: null,
|
||||
scope: ps.scope,
|
||||
key: ps.key,
|
||||
value: ps.value
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
|
||||
publishMainStream(user.id, 'registryUpdated', {
|
||||
scope: ps.scope,
|
||||
key: ps.key,
|
||||
value: ps.value
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
import $ from 'cafy';
|
||||
import { publishMainStream } from '../../../../services/stream';
|
||||
import define from '../../define';
|
||||
import { UserProfiles } from '../../../../models';
|
||||
import { ensure } from '../../../../prelude/ensure';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
name: {
|
||||
validator: $.str.match(/^[a-zA-Z]+$/)
|
||||
},
|
||||
|
||||
value: {
|
||||
validator: $.nullable.any
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||
|
||||
await UserProfiles.createQueryBuilder().update()
|
||||
.set({
|
||||
clientData: Object.assign(profile.clientData, {
|
||||
[ps.name]: ps.value
|
||||
}),
|
||||
})
|
||||
.where('userId = :id', { id: user.id })
|
||||
.execute();
|
||||
|
||||
// Publish event
|
||||
publishMainStream(user.id, 'clientSettingUpdated', {
|
||||
key: ps.name,
|
||||
value: ps.value
|
||||
});
|
||||
});
|
10
yarn.lock
10
yarn.lock
|
@ -911,6 +911,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.5.tgz#9adbc12950582aa65ead76bffdf39fe0c27a3c02"
|
||||
integrity sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==
|
||||
|
||||
"@types/throttle-debounce@2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
|
||||
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
|
||||
|
||||
"@types/tinycolor2@1.4.2":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
|
||||
|
@ -10025,6 +10030,11 @@ three@0.117.1:
|
|||
resolved "https://registry.yarnpkg.com/three/-/three-0.117.1.tgz#a49bcb1a6ddea2f250003e42585dc3e78e92b9d3"
|
||||
integrity sha512-t4zeJhlNzUIj9+ub0l6nICVimSuRTZJOqvk3Rmlu+YGdTOJ49Wna8p7aumpkXJakJfITiybfpYE1XN1o1Z34UQ==
|
||||
|
||||
throttle-debounce@3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
|
||||
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
|
||||
|
||||
through2-filter@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
|
||||
|
|
Loading…
Reference in a new issue