diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-11-03 13:23:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-03 13:23:03 +0900 |
| commit | 79346272f8792d35955efd3aaaa1e42e0cd2a6e3 (patch) | |
| tree | 6f84fea79208862777db23aac434551494817595 | |
| parent | CWを使用する場合、注釈を空にすることを許可しない (diff) | |
| download | sharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.tar.gz sharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.tar.bz2 sharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.zip | |
feat: レジストリAPIをサードパーティから利用可能に (#12229)
* wip
* wip
* Update remove.ts
* refactor
23 files changed, 268 insertions, 230 deletions
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/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index c17ea9999a..9fb29e0e68 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -64,6 +64,7 @@ import { ClipService } from './ClipService.js'; import { FeaturedService } from './FeaturedService.js'; import { FunoutTimelineService } from './FunoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; +import { RegistryApiService } from './RegistryApiService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -195,6 +196,7 @@ const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipServic const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService }; const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; +const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -330,6 +332,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FeaturedService, FunoutTimelineService, ChannelFollowingService, + RegistryApiService, ChartLoggerService, FederationChart, NotesChart, @@ -458,6 +461,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FeaturedService, $FunoutTimelineService, $ChannelFollowingService, + $RegistryApiService, $ChartLoggerService, $FederationChart, $NotesChart, @@ -587,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FeaturedService, FunoutTimelineService, ChannelFollowingService, + RegistryApiService, FederationChart, NotesChart, UsersChart, @@ -714,6 +719,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FeaturedService, $FunoutTimelineService, $ChannelFollowingService, + $RegistryApiService, $FederationChart, $NotesChart, $UsersChart, diff --git a/packages/backend/src/core/RegistryApiService.ts b/packages/backend/src/core/RegistryApiService.ts new file mode 100644 index 0000000000..d340c5e480 --- /dev/null +++ b/packages/backend/src/core/RegistryApiService.ts @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiRegistryItem, RegistryItemsRepository } from '@/models/_.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import type { MiUser } from '@/models/User.js'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { bindThis } from '@/decorators.js'; + +@Injectable() +export class RegistryApiService { + constructor( + @Inject(DI.registryItemsRepository) + private registryItemsRepository: RegistryItemsRepository, + + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + } + + @bindThis + public async set(userId: MiUser['id'], domain: string | null, scope: string[], key: string, value: any) { + // TODO: 作成できるキーの数を制限する + + const query = this.registryItemsRepository.createQueryBuilder('item'); + if (domain) { + query.where('item.domain = :domain', { domain: domain }); + } else { + query.where('item.domain IS NULL'); + } + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.key = :key', { key: key }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const existingItem = await query.getOne(); + + if (existingItem) { + await this.registryItemsRepository.update(existingItem.id, { + updatedAt: new Date(), + value: value, + }); + } else { + await this.registryItemsRepository.insert({ + id: this.idService.gen(), + updatedAt: new Date(), + userId: userId, + domain: domain, + scope: scope, + key: key, + value: value, + }); + } + + if (domain == null) { + // TODO: サードパーティアプリが傍受出来てしまうのでどうにかする + this.globalEventService.publishMainStream(userId, 'registryUpdated', { + scope: scope, + key: key, + value: value, + }); + } + } + + @bindThis + public async getItem(userId: MiUser['id'], domain: string | null, scope: string[], key: string): Promise<MiRegistryItem | null> { + const query = this.registryItemsRepository.createQueryBuilder('item') + .where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }) + .andWhere('item.userId = :userId', { userId: userId }) + .andWhere('item.key = :key', { key: key }) + .andWhere('item.scope = :scope', { scope: scope }); + + const item = await query.getOne(); + + return item; + } + + @bindThis + public async getAllItemsOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<MiRegistryItem[]> { + const query = this.registryItemsRepository.createQueryBuilder('item'); + query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const items = await query.getMany(); + + return items; + } + + @bindThis + public async getAllKeysOfScope(userId: MiUser['id'], domain: string | null, scope: string[]): Promise<string[]> { + const query = this.registryItemsRepository.createQueryBuilder('item'); + query.select('item.key'); + query.where(domain == null ? 'item.domain IS NULL' : 'item.domain = :domain', { domain: domain }); + query.andWhere('item.userId = :userId', { userId: userId }); + query.andWhere('item.scope = :scope', { scope: scope }); + + const items = await query.getMany(); + + return items.map(x => x.key); + } + + @bindThis + public async getAllScopeAndDomains(userId: MiUser['id']): Promise<{ domain: string | null; scopes: string[][] }[]> { + const query = this.registryItemsRepository.createQueryBuilder('item') + .select(['item.scope', 'item.domain']) + .where('item.userId = :userId', { userId: userId }); + + 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; + } + + @bindThis + public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) { + const query = this.registryItemsRepository.createQueryBuilder().delete(); + if (domain) { + query.where('domain = :domain', { domain: domain }); + } else { + query.where('domain IS NULL'); + } + query.andWhere('userId = :userId', { userId: userId }); + query.andWhere('key = :key', { key: key }); + query.andWhere('scope = :scope', { scope: scope }); + + await query.execute(); + } +} diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 3f8a46d855..23067a9b26 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -230,7 +230,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; 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'; @@ -588,7 +588,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__ const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default }; 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 }; @@ -950,7 +950,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_registry_keysWithType, $i_registry_keys, $i_registry_remove, - $i_registry_scopes, + $i_registry_scopesWithDomain, $i_registry_set, $i_revokeToken, $i_signinHistory, @@ -1306,7 +1306,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_registry_keysWithType, $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..af798fd166 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -230,7 +230,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js'; import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js'; 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'; @@ -586,7 +586,7 @@ const eps = [ ['i/registry/keys-with-type', ep___i_registry_keysWithType], ['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..29fa0a29cc 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 @@ -5,13 +5,10 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,23 +17,18 @@ 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: [], + required: ['scope'], } 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, + private registryApiService: RegistryApiService, ) { - 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 }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); const res = {} as Record<string, any>; 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..5b460b45d6 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 @@ -5,15 +5,12 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ 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'], + required: ['key', 'scope'], } 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, + private registryApiService: RegistryApiService, ) { - 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 }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); 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..e8c28298ef 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/get.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/get.ts @@ -5,15 +5,12 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,24 +27,18 @@ 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'], + required: ['key', 'scope'], } 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, + private registryApiService: RegistryApiService, ) { - 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 }); - - const item = await query.getOne(); + super(meta, paramDef, async (ps, me, accessToken) => { + const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); if (item == null) { throw new ApiError(meta.errors.noSuchKey); 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..8953ee5d3d 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 @@ -5,13 +5,10 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,36 +17,31 @@ 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: [], + required: ['scope'], } 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, + private registryApiService: RegistryApiService, ) { - 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 }); - - const items = await query.getMany(); + super(meta, paramDef, async (ps, me, accessToken) => { + const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope); 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; + 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..04e120d752 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/keys.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/keys.ts @@ -5,13 +5,10 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -20,26 +17,18 @@ 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: [], + required: ['scope'], } 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, + private registryApiService: RegistryApiService, ) { - super(meta, paramDef, async (ps, me) => { - const query = this.registryItemsRepository.createQueryBuilder('item') - .select('item.key') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.id }) - .andWhere('item.scope = :scope', { scope: ps.scope }); - - const items = await query.getMany(); - - return items.map(x => x.key); + super(meta, paramDef, async (ps, me, accessToken) => { + return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), 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..ba8100b547 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/remove.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/remove.ts @@ -7,13 +7,12 @@ 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'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; import { ApiError } from '../../../error.js'; export const meta = { requireCredential: true, - secure: true, - errors: { noSuchKey: { message: 'No such key.', @@ -30,30 +29,18 @@ 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'], + required: ['key', 'scope'], } 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, + private registryApiService: RegistryApiService, ) { - 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 }); - - const item = await query.getOne(); - - if (item == null) { - throw new ApiError(meta.errors.noSuchKey); - } - - await this.registryItemsRepository.remove(item); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key); }); } } 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..1ff994b82c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts @@ -0,0 +1,30 @@ +/* + * 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 { RegistryApiService } from '@/core/RegistryApiService.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( + private registryApiService: RegistryApiService, + ) { + super(meta, paramDef, async (ps, me) => { + return await this.registryApiService.getAllScopeAndDomains(me.id); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts b/packages/backend/src/server/api/endpoints/i/registry/scopes.ts deleted file mode 100644 index 7637cdcf73..0000000000 --- a/packages/backend/src/server/api/endpoints/i/registry/scopes.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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') - .where('item.domain IS NULL') - .andWhere('item.userId = :userId', { userId: me.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; - }); - } -} 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..58bb450bce 100644 --- a/packages/backend/src/server/api/endpoints/i/registry/set.ts +++ b/packages/backend/src/server/api/endpoints/i/registry/set.ts @@ -5,15 +5,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { RegistryItemsRepository } from '@/models/_.js'; -import { IdService } from '@/core/IdService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; +import { RegistryApiService } from '@/core/RegistryApiService.js'; export const meta = { requireCredential: true, - - secure: true, } as const; export const paramDef = { @@ -24,51 +19,18 @@ 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', 'value'], + required: ['key', 'value', 'scope'], } 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, - - private idService: IdService, - private globalEventService: GlobalEventService, + private registryApiService: RegistryApiService, ) { - 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 }); - - const existingItem = await query.getOne(); - - if (existingItem) { - await this.registryItemsRepository.update(existingItem.id, { - updatedAt: new Date(), - value: ps.value, - }); - } else { - await this.registryItemsRepository.insert({ - id: this.idService.gen(), - updatedAt: new Date(), - userId: me.id, - domain: null, - 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, - }); + super(meta, paramDef, async (ps, me, accessToken) => { + await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value); }); } } diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 7a2c698d11..b446a4d554 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> </div> <FormSection> - <div class="_formLinks"> + <div class="_gaps_s"> <FormLink to="https://github.com/misskey-dev/misskey" external> <template #icon><i class="ti ti-code"></i></template> {{ i18n.ts._aboutMisskey.source }} diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index ee4043f9a5..4fa409ff4b 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkKeyValue> </FormSplit> <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> - <div class="_formLinks"> + <div class="_gaps_s"> <MkFolder v-if="instance.serverRules.length > 0"> <template #label>{{ i18n.ts.serverRules }}</template> @@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only <FormSection> <template #label>Well-known resources</template> - <div class="_formLinks"> + <div class="_gaps_s"> <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink> <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue index a1a5fd0cf3..387cb2f1f7 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 <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> @@ -23,8 +23,8 @@ 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> + <div class="_gaps_s"> + <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])); }); diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue index ebcb04e9f5..68d6c8c1a0 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 <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'); @@ -95,6 +97,7 @@ async function save() { scope, key, value: JSON5.parse(valueForEditor), + domain: props.domain === '@' ? null : props.domain, }); }); } @@ -108,6 +111,7 @@ function del() { os.apiWithDialog('i/registry/remove', { scope, key, + domain: props.domain === '@' ? null : props.domain, }); }); } diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue index 37a0b52511..d0a3df5deb 100644 --- a/packages/frontend/src/pages/registry.vue +++ b/packages/frontend/src/pages/registry.vue @@ -9,12 +9,14 @@ 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> - </FormSection> + <div v-if="scopesWithDomain" class="_gaps_m"> + <FormSection v-for="domain in scopesWithDomain" :key="domain.domain"> + <template #label>{{ domain.domain ? domain.domain.toUpperCase() : i18n.ts.system }}</template> + <div class="_gaps_s"> + <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> </MkSpacer> </MkStickyContainer> </template> @@ -28,11 +30,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; }); } diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index ef0f5343bb..b81811d2e7 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -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', diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 28fb5ba2a5..7bb443cece 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -344,12 +344,6 @@ hr { grid-gap: 12px; } -._formLinks { - > *:not(:last-child) { - margin-bottom: 8px; - } -} - ._beta { margin-left: 0.7em; font-size: 65%; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 8f389086c9..a15e5888e8 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1482,10 +1482,6 @@ export type Endpoints = { }; res: null; }; - 'i/registry/scopes': { - req: NoParams; - res: string[][]; - }; 'i/registry/set': { req: { key: string; @@ -3023,7 +3019,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u // // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts +// src/api.types.ts:632:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/entities.ts:116:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts // src/entities.ts:612:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index e1c2aaf51d..54b175fcf1 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -399,7 +399,6 @@ export type Endpoints = { 'i/registry/keys-with-type': { req: { scope?: string[]; }; res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; }; 'i/registry/keys': { req: { scope?: string[]; }; res: string[]; }; 'i/registry/remove': { req: { key: string; scope?: string[]; }; res: null; }; - 'i/registry/scopes': { req: NoParams; res: string[][]; }; 'i/registry/set': { req: { key: string; value: any; scope?: string[]; }; res: null; }; 'i/revoke-token': { req: TODO; res: TODO; }; 'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; }; |