feat: レジストリAPIをサードパーティから利用可能に (#12229)

* wip

* wip

* Update remove.ts

* refactor
This commit is contained in:
syuilo 2023-11-03 13:23:03 +09:00 committed by GitHub
parent 82526ad4f3
commit 79346272f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 268 additions and 230 deletions

View file

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

View file

@ -64,6 +64,7 @@ import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js'; import { FeaturedService } from './FeaturedService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js'; import { FunoutTimelineService } from './FunoutTimelineService.js';
import { ChannelFollowingService } from './ChannelFollowingService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js';
import { RegistryApiService } from './RegistryApiService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js'; import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js'; import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.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 $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService }; const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@ -330,6 +332,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FeaturedService, FeaturedService,
FunoutTimelineService, FunoutTimelineService,
ChannelFollowingService, ChannelFollowingService,
RegistryApiService,
ChartLoggerService, ChartLoggerService,
FederationChart, FederationChart,
NotesChart, NotesChart,
@ -458,6 +461,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FunoutTimelineService,
$ChannelFollowingService, $ChannelFollowingService,
$RegistryApiService,
$ChartLoggerService, $ChartLoggerService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
@ -587,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FeaturedService, FeaturedService,
FunoutTimelineService, FunoutTimelineService,
ChannelFollowingService, ChannelFollowingService,
RegistryApiService,
FederationChart, FederationChart,
NotesChart, NotesChart,
UsersChart, UsersChart,
@ -714,6 +719,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FeaturedService, $FeaturedService,
$FunoutTimelineService, $FunoutTimelineService,
$ChannelFollowingService, $ChannelFollowingService,
$RegistryApiService,
$FederationChart, $FederationChart,
$NotesChart, $NotesChart,
$UsersChart, $UsersChart,

View file

@ -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();
}
}

View file

@ -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_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_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.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_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.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_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_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_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_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_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 }; 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_keysWithType,
$i_registry_keys, $i_registry_keys,
$i_registry_remove, $i_registry_remove,
$i_registry_scopes, $i_registry_scopesWithDomain,
$i_registry_set, $i_registry_set,
$i_revokeToken, $i_revokeToken,
$i_signinHistory, $i_signinHistory,
@ -1306,7 +1306,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keysWithType, $i_registry_keysWithType,
$i_registry_keys, $i_registry_keys,
$i_registry_remove, $i_registry_remove,
$i_registry_scopes, $i_registry_scopesWithDomain,
$i_registry_set, $i_registry_set,
$i_revokeToken, $i_revokeToken,
$i_signinHistory, $i_signinHistory,

View file

@ -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_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_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.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_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js'; import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.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-with-type', ep___i_registry_keysWithType],
['i/registry/keys', ep___i_registry_keys], ['i/registry/keys', ep___i_registry_keys],
['i/registry/remove', ep___i_registry_remove], ['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/registry/set', ep___i_registry_set],
['i/revoke-token', ep___i_revokeToken], ['i/revoke-token', ep___i_revokeToken],
['i/signin-history', ep___i_signinHistory], ['i/signin-history', ep___i_signinHistory],

View file

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -20,23 +17,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: [], required: ['scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
const res = {} as Record<string, any>; const res = {} as Record<string, any>;

View file

@ -5,15 +5,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
errors: { errors: {
noSuchKey: { noSuchKey: {
message: 'No such key.', message: 'No such key.',
@ -30,24 +27,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: ['key'], required: ['key', 'scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
.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) { if (item == null) {
throw new ApiError(meta.errors.noSuchKey); throw new ApiError(meta.errors.noSuchKey);

View file

@ -5,15 +5,12 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
errors: { errors: {
noSuchKey: { noSuchKey: {
message: 'No such key.', message: 'No such key.',
@ -30,24 +27,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: ['key'], required: ['key', 'scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') const item = await this.registryApiService.getItem(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
.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) { if (item == null) {
throw new ApiError(meta.errors.noSuchKey); throw new ApiError(meta.errors.noSuchKey);

View file

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -20,36 +17,31 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: [], required: ['scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') const items = await this.registryApiService.getAllItemsOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
.where('item.domain IS NULL')
.andWhere('item.userId = :userId', { userId: me.id })
.andWhere('item.scope = :scope', { scope: ps.scope });
const items = await query.getMany();
const res = {} as Record<string, string>; const res = {} as Record<string, string>;
for (const item of items) { for (const item of items) {
const type = typeof item.value; const type = typeof item.value;
res[item.key] = res[item.key] =
item.value === null ? 'null' : item.value === null ? 'null' :
Array.isArray(item.value) ? 'array' : Array.isArray(item.value) ? 'array' :
type === 'number' ? 'number' : type === 'number' ? 'number' :
type === 'string' ? 'string' : type === 'string' ? 'string' :
type === 'boolean' ? 'boolean' : type === 'boolean' ? 'boolean' :
type === 'object' ? 'object' : type === 'object' ? 'object' :
null as never; null as never;
} }
return res; return res;

View file

@ -5,13 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -20,26 +17,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: [], required: ['scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') return await this.registryApiService.getAllKeysOfScope(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
.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);
}); });
} }
} }

View file

@ -7,13 +7,12 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
errors: { errors: {
noSuchKey: { noSuchKey: {
message: 'No such key.', message: 'No such key.',
@ -30,30 +29,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} }, } },
domain: { type: 'string', nullable: true },
}, },
required: ['key'], required: ['key', 'scope'],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key);
.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);
}); });
} }
} }

View file

@ -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);
});
}
}

View file

@ -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;
});
}
}

View file

@ -5,15 +5,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js'; import { RegistryApiService } from '@/core/RegistryApiService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true,
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -24,51 +19,18 @@ export const paramDef = {
scope: { type: 'array', default: [], items: { scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1), 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; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.registryItemsRepository) private registryApiService: RegistryApiService,
private registryItemsRepository: RegistryItemsRepository,
private idService: IdService,
private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me, accessToken) => {
const query = this.registryItemsRepository.createQueryBuilder('item') await this.registryApiService.set(me.id, accessToken ? accessToken.id : (ps.domain ?? null), ps.scope, ps.key, ps.value);
.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,
});
}); });
} }
} }

View file

@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton> <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div> </div>
<FormSection> <FormSection>
<div class="_formLinks"> <div class="_gaps_s">
<FormLink to="https://github.com/misskey-dev/misskey" external> <FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="ti ti-code"></i></template> <template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts._aboutMisskey.source }} {{ i18n.ts._aboutMisskey.source }}

View file

@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue> </MkKeyValue>
</FormSplit> </FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink> <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"> <MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template> <template #label>{{ i18n.ts.serverRules }}</template>
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection> <FormSection>
<template #label>Well-known resources</template> <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`" external>host-meta</FormLink>
<FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink> <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
<FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink> <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>

View file

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

View file

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

View file

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

View file

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

View file

@ -344,12 +344,6 @@ hr {
grid-gap: 12px; grid-gap: 12px;
} }
._formLinks {
> *:not(:last-child) {
margin-bottom: 8px;
}
}
._beta { ._beta {
margin-left: 0.7em; margin-left: 0.7em;
font-size: 65%; font-size: 65%;

View file

@ -1482,10 +1482,6 @@ export type Endpoints = {
}; };
res: null; res: null;
}; };
'i/registry/scopes': {
req: NoParams;
res: string[][];
};
'i/registry/set': { 'i/registry/set': {
req: { req: {
key: string; 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: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: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: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/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 // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts

View file

@ -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-with-type': { req: { scope?: string[]; }; res: Record<string, 'null' | 'array' | 'number' | 'string' | 'boolean' | 'object'>; };
'i/registry/keys': { req: { scope?: string[]; }; res: string[]; }; 'i/registry/keys': { req: { scope?: string[]; }; res: string[]; };
'i/registry/remove': { req: { key: string; scope?: string[]; }; res: null; }; '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/registry/set': { req: { key: string; value: any; scope?: string[]; }; res: null; };
'i/revoke-token': { req: TODO; res: TODO; }; 'i/revoke-token': { req: TODO; res: TODO; };
'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; }; 'i/signin-history': { req: { limit?: number; sinceId?: Signin['id']; untilId?: Signin['id']; }; res: Signin[]; };