This commit is contained in:
syuilo 2023-11-03 10:40:52 +09:00
parent 82526ad4f3
commit def7959bb3
16 changed files with 147 additions and 71 deletions

View File

@ -53,6 +53,7 @@
- Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197
### Server
- Feat: Registry APIがサードパーティから利用可能になりました
- Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように
- Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善
- Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました

View File

@ -231,6 +231,7 @@ import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
@ -589,6 +590,7 @@ const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-t
const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
const $i_registry_scopes: Provider = { provide: 'ep:i/registry/scopes', useClass: ep___i_registry_scopes.default };
const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: ep___i_signinHistory.default };
@ -951,6 +953,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keys,
$i_registry_remove,
$i_registry_scopes,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,
$i_signinHistory,
@ -1307,6 +1310,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keys,
$i_registry_remove,
$i_registry_scopes,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,
$i_signinHistory,

View File

@ -231,6 +231,7 @@ import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_scopes from './endpoints/i/registry/scopes.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
@ -587,6 +588,7 @@ const eps = [
['i/registry/keys', ep___i_registry_keys],
['i/registry/remove', ep___i_registry_remove],
['i/registry/scopes', ep___i_registry_scopes],
['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
['i/registry/set', ep___i_registry_set],
['i/revoke-token', ep___i_revokeToken],
['i/signin-history', ep___i_signinHistory],

View File

@ -10,8 +10,6 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -30,11 +28,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item');
if (accessToken) {
query.where('item.domain = :domain', { domain: accessToken.id });
}
query.andWhere('item.userId = :userId', { userId: me.id });
query.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();

View File

@ -12,8 +12,6 @@ import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -30,6 +28,7 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['key'],
} as const;
@ -40,12 +39,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item');
if (accessToken) {
query.where('item.domain = :domain', { domain: accessToken.id });
} else {
if (ps.domain) {
query.where('item.domain = :domain', { domain: ps.domain });
} else {
query.where('item.domain IS NULL');
}
}
query.andWhere('item.userId = :userId', { userId: me.id });
query.andWhere('item.key = :key', { key: ps.key });
query.andWhere('item.scope = :scope', { scope: ps.scope });
const item = await query.getOne();

View File

@ -12,8 +12,6 @@ import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -40,9 +38,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.where(accessToken == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: accessToken?.id })
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });

View File

@ -10,8 +10,6 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -20,6 +18,7 @@ export const paramDef = {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: [],
} as const;
@ -30,11 +29,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item');
if (accessToken) {
query.where('item.domain = :domain', { domain: accessToken.id });
} else {
if (ps.domain) {
query.where('item.domain = :domain', { domain: ps.domain });
} else {
query.where('item.domain IS NULL');
}
}
query.andWhere('item.userId = :userId', { userId: me.id });
query.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
@ -43,13 +50,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
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;
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;

View File

@ -10,8 +10,6 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -30,10 +28,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select('item.key')
.where('item.domain IS NULL')
.where(accessToken == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: accessToken?.id })
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });

View File

@ -12,8 +12,6 @@ import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
errors: {
noSuchKey: {
message: 'No such key.',
@ -40,9 +38,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.where(accessToken == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: accessToken?.id })
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });

View File

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select(['item.scope', 'item.domain'])
.where('item.userId = :userId', { userId: me.id });
const items = await query.getMany();
const res = [] as { domain: string | null; scopes: string[][] }[];
for (const item of items) {
const target = res.find(x => x.domain === item.domain);
if (target) {
if (target.scopes.some(scope => scope.join('.') === item.scope.join('.'))) continue;
target.scopes.push(item.scope);
} else {
res.push({
domain: item.domain,
scopes: [item.scope],
});
}
}
return res;
});
}
}

View File

@ -10,8 +10,6 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -26,10 +24,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.registryItemsRepository)
private registryItemsRepository: RegistryItemsRepository,
) {
super(meta, paramDef, async (ps, me) => {
super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item')
.select('item.scope')
.where('item.domain IS NULL')
.where(accessToken == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: accessToken?.id })
.andWhere('item.userId = :userId', { userId: me.id });
const items = await query.getMany();

View File

@ -12,8 +12,6 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
secure: true,
} as const;
export const paramDef = {
@ -37,9 +35,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
super(meta, paramDef, async (ps, me, accessToken) => {
// TODO: 作成できるキーの数を制限する
const query = this.registryItemsRepository.createQueryBuilder('item')
.where('item.domain IS NULL')
.where(accessToken == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: accessToken?.id })
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.key = :key', { key: ps.key })
.andWhere('item.scope = :scope', { scope: ps.scope });
@ -56,19 +56,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: this.idService.gen(),
updatedAt: new Date(),
userId: me.id,
domain: null,
domain: accessToken == null ? null : accessToken.id,
scope: ps.scope,
key: ps.key,
value: ps.value,
});
}
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
this.globalEventService.publishMainStream(me.id, 'registryUpdated', {
scope: ps.scope,
key: ps.key,
value: ps.value,
});
if (accessToken == null) {
// TODO: サードパーティアプリが傍受出来てしまうのでどうにかする
this.globalEventService.publishMainStream(me.id, 'registryUpdated', {
scope: ps.scope,
key: ps.key,
value: ps.value,
});
}
});
}
}

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.domain }}</template>
<template #value>{{ i18n.ts.system }}</template>
<template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.scope }}</template>
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="keys">
<template #label>{{ i18n.ts.keys }}</template>
<div class="_formLinks">
<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
<FormLink v-for="key in keys" :to="`/registry/value/${props.domain}/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
</div>
</FormSection>
</div>
@ -46,15 +46,17 @@ import FormSplit from '@/components/form/split.vue';
const props = defineProps<{
path: string;
domain: string;
}>();
const scope = $computed(() => props.path.split('/'));
const scope = $computed(() => props.path ? props.path.split('/') : []);
let keys = $ref(null);
function fetchKeys() {
os.api('i/registry/keys-with-type', {
scope: scope,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0]));
});

View File

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSplit>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.domain }}</template>
<template #value>{{ i18n.ts.system }}</template>
<template #value>{{ props.domain === '@' ? i18n.ts.system : props.domain.toUpperCase() }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts._registry.scope }}</template>
@ -58,6 +58,7 @@ import FormInfo from '@/components/MkInfo.vue';
const props = defineProps<{
path: string;
domain: string;
}>();
const scope = $computed(() => props.path.split('/').slice(0, -1));
@ -70,6 +71,7 @@ function fetchValue() {
os.api('i/registry/get-detail', {
scope,
key,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
value = res;
valueForEditor = JSON5.stringify(res.value, null, '\t');

View File

@ -9,12 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="600" :marginMin="16">
<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton>
<FormSection v-if="scopes">
<template #label>{{ i18n.ts.system }}</template>
<div class="_formLinks">
<FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
<div v-if="scopesWithDomain">
<div v-for="domain in scopesWithDomain" :key="domain.domain">
<FormSection>
<template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template>
<div class="_formLinks">
<FormLink v-for="scope in domain.scopes" :to="`/registry/keys/${domain.domain ?? '@'}/${scope.join('/')}`" class="_monospace">{{ scope.length === 0 ? '(root)' : scope.join('/') }}</FormLink>
</div>
</FormSection>
</div>
</FormSection>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@ -28,11 +32,11 @@ import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
let scopes = $ref(null);
let scopesWithDomain = $ref(null);
function fetchScopes() {
os.api('i/registry/scopes').then(res => {
scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
os.api('i/registry/scopes-with-domain').then(res => {
scopesWithDomain = res;
});
}

View File

@ -4,7 +4,7 @@
*/
import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
import { Router } from '@/nirax';
import { Router } from '@/nirax.js';
import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
@ -318,10 +318,10 @@ export const routes = [{
name: 'avatarDecorations',
component: page(() => import('./pages/avatar-decorations.vue')),
}, {
path: '/registry/keys/system/:path(*)?',
path: '/registry/keys/:domain/:path(*)?',
component: page(() => import('./pages/registry.keys.vue')),
}, {
path: '/registry/value/system/:path(*)?',
path: '/registry/value/:domain/:path(*)?',
component: page(() => import('./pages/registry.value.vue')),
}, {
path: '/registry',