From def7959bb3fb881caf3e94c7df9d177a55ef1145 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 3 Nov 2023 10:40:52 +0900 Subject: [PATCH] wip --- CHANGELOG.md | 1 + .../backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../api/endpoints/i/registry/get-all.ts | 14 ++--- .../api/endpoints/i/registry/get-detail.ts | 23 +++++--- .../server/api/endpoints/i/registry/get.ts | 6 +-- .../endpoints/i/registry/keys-with-type.ts | 35 +++++++----- .../server/api/endpoints/i/registry/keys.ts | 6 +-- .../server/api/endpoints/i/registry/remove.ts | 6 +-- .../i/registry/scopes-with-domain.ts | 53 +++++++++++++++++++ .../server/api/endpoints/i/registry/scopes.ts | 6 +-- .../server/api/endpoints/i/registry/set.ts | 24 +++++---- packages/frontend/src/pages/registry.keys.vue | 8 +-- .../frontend/src/pages/registry.value.vue | 4 +- packages/frontend/src/pages/registry.vue | 20 ++++--- packages/frontend/src/router.ts | 6 +-- 16 files changed, 147 insertions(+), 71 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 82822c903d..bbf99bee95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ - Fix: 絵文字ピッカーでバッテリーの絵文字が複数表示される問題を修正 #12197 ### Server +- Feat: Registry APIがサードパーティから利用可能になりました - Enhance: RedisへのTLのキャッシュ(FTT)をオフにできるように - Enhance: フォローしているチャンネルをフォロー解除した時(またはその逆)、タイムラインに反映される間隔を改善 - Enhance: プロフィールの自己紹介欄のMFMが連合するようになりました diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 3f8a46d855..d3093dfe44 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -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, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e87e1df591..f14de6a006 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -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], diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts index 211e6637dc..7a2ac9561d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-all.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-all.ts @@ -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 { // 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(); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts index 9c6f2d6781..a484d5ed94 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get-detail.ts @@ -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 { // 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(); diff --git a/packages/backend/src/server/api/endpoints/i/registry/get.ts b/packages/backend/src/server/api/endpoints/i/registry/get.ts index 729e729b8c..6b575c5a7d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -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 { // 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 }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts index ffd2860fde..ed8ca7235d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts @@ -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 { // 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 { // 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; diff --git a/packages/backend/src/server/api/endpoints/i/registry/keys.ts b/packages/backend/src/server/api/endpoints/i/registry/keys.ts index 7239bb66e1..b91d730c7d 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -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 { // 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 }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/remove.ts b/packages/backend/src/server/api/endpoints/i/registry/remove.ts index ae687fefe9..dc6a06fed4 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -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 { // 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 }); diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts new file mode 100644 index 0000000000..b374c9d6db --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -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 { // 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; + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts index 7637cdcf73..303e2bf429 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts @@ -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 { // 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(); diff --git a/packages/backend/src/server/api/endpoints/i/registry/set.ts b/packages/backend/src/server/api/endpoints/i/registry/set.ts index 6203e7aa8b..529ed97418 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -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 { // 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 { // 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, + }); + } }); } } diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index a1a5fd0cf3..3f64f7ad00 100644 --- a/packages/frontend/src/pages/registry.keys.vue +++ b/packages/frontend/src/pages/registry.keys.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -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])); }); diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index ebcb04e9f5..69ab8e809d 100644 --- a/packages/frontend/src/pages/registry.value.vue +++ b/packages/frontend/src/pages/registry.value.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + @@ -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'); diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 37a0b52511..8f8df18cfa 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -9,12 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._registry.createKey }} - - -