summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-11-03 13:23:03 +0900
committerGitHub <noreply@github.com>2023-11-03 13:23:03 +0900
commit79346272f8792d35955efd3aaaa1e42e0cd2a6e3 (patch)
tree6f84fea79208862777db23aac434551494817595
parentCWを使用する場合、注釈を空にすることを許可しない (diff)
downloadsharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.tar.gz
sharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.tar.bz2
sharkey-79346272f8792d35955efd3aaaa1e42e0cd2a6e3.zip
feat: レジストリAPIをサードパーティから利用可能に (#12229)
* wip * wip * Update remove.ts * refactor
-rw-r--r--CHANGELOG.md1
-rw-r--r--packages/backend/src/core/CoreModule.ts6
-rw-r--r--packages/backend/src/core/RegistryApiService.ts147
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/get-all.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/get-detail.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/get.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/keys-with-type.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/keys.ts23
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/remove.ts25
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/scopes-with-domain.ts30
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/scopes.ts47
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/set.ts50
-rw-r--r--packages/frontend/src/pages/about-misskey.vue2
-rw-r--r--packages/frontend/src/pages/about.vue4
-rw-r--r--packages/frontend/src/pages/registry.keys.vue10
-rw-r--r--packages/frontend/src/pages/registry.value.vue6
-rw-r--r--packages/frontend/src/pages/registry.vue20
-rw-r--r--packages/frontend/src/router.ts6
-rw-r--r--packages/frontend/src/style.scss6
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md6
-rw-r--r--packages/misskey-js/src/api.types.ts1
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[]; };