diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index 601618553e..397626c49d 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -84,6 +84,8 @@ export class AuthenticateService implements OnApplicationShutdown { return [user, { id: accessToken.id, permission: app.permission, + appId: app.id, + app, } as MiAccessToken]; } else { return [user, accessToken]; diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 1c5a781fd9..a78c3e9ae6 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -128,6 +128,7 @@ export * as 'antennas/update' from './endpoints/antennas/update.js'; export * as 'ap/get' from './endpoints/ap/get.js'; export * as 'ap/show' from './endpoints/ap/show.js'; export * as 'app/create' from './endpoints/app/create.js'; +export * as 'app/current' from './endpoints/app/current.js'; export * as 'app/show' from './endpoints/app/show.js'; export * as 'auth/accept' from './endpoints/auth/accept.js'; export * as 'auth/session/generate' from './endpoints/auth/session/generate.js'; diff --git a/packages/backend/src/server/api/endpoints/app/current.ts b/packages/backend/src/server/api/endpoints/app/current.ts new file mode 100644 index 0000000000..39b5ef347c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/app/current.ts @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AppsRepository } from '@/models/_.js'; +import { AppEntityService } from '@/core/entities/AppEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['app'], + + errors: { + credentialRequired: { + message: 'Credential required.', + code: 'CREDENTIAL_REQUIRED', + id: '1384574d-a912-4b81-8601-c7b1c4085df1', + httpStatusCode: 401, + }, + noAppLogin: { + message: 'Not logged in with an app.', + code: 'NO_APP_LOGIN', + id: '339a4ad2-48c3-47fc-bd9d-2408f05120f8', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'App', + }, + + // 10 calls per 5 seconds + limit: { + duration: 1000 * 5, + max: 10, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.appsRepository) + private appsRepository: AppsRepository, + + private appEntityService: AppEntityService, + ) { + super(meta, paramDef, async (_, user, token) => { + if (!user) { + throw new ApiError(meta.errors.credentialRequired); + } + if (!token || !token.appId) { + throw new ApiError(meta.errors.noAppLogin); + } + + const app = token.app ?? await this.appsRepository.findOneByOrFail({ id: token.appId }); + + return await this.appEntityService.pack(app, user, { + detail: true, + includeSecret: false, + }); + }); + } +} diff --git a/packages/backend/src/server/api/mastodon/MastodonConverters.ts b/packages/backend/src/server/api/mastodon/MastodonConverters.ts index cf625d6e94..375ea1ef08 100644 --- a/packages/backend/src/server/api/mastodon/MastodonConverters.ts +++ b/packages/backend/src/server/api/mastodon/MastodonConverters.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { Entity, MastodonEntity } from 'megalodon'; +import { Entity, MastodonEntity, MisskeyEntity } from 'megalodon'; import mfm from '@transfem-org/sfm-js'; import { MastodonNotificationType } from 'megalodon/lib/src/mastodon/notification.js'; import { NotificationType } from 'megalodon/lib/src/notification.js'; @@ -369,6 +369,15 @@ export class MastodonConverters { type: convertNotificationType(notification.type as NotificationType), }; } + + public convertApplication(app: MisskeyEntity.App): MastodonEntity.Application { + return { + name: app.name, + scopes: app.permission, + redirect_uri: app.callbackUrl, + redirect_uris: [app.callbackUrl], + }; + } } function simpleConvert(data: T): T { @@ -459,4 +468,3 @@ export function convertRelationship(relationship: Partial & note: relationship.note ?? '', }; } - diff --git a/packages/backend/src/server/api/mastodon/endpoints/apps.ts b/packages/backend/src/server/api/mastodon/endpoints/apps.ts index 5fce838f47..72b520c74a 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/apps.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/apps.ts @@ -5,6 +5,7 @@ import { Injectable } from '@nestjs/common'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; +import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js'; import type { FastifyInstance } from 'fastify'; const readScope = [ @@ -59,6 +60,7 @@ type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload }; export class ApiAppsMastodon { constructor( private readonly clientService: MastodonClientService, + private readonly mastoConverters: MastodonConverters, ) {} public register(fastify: FastifyInstance): void { @@ -108,6 +110,13 @@ export class ApiAppsMastodon { return reply.send(response); }); + + fastify.get('/v1/apps/verify_credentials', async (_request, reply) => { + const client = this.clientService.getClient(_request); + const data = await client.verifyAppCredentials(); + const response = this.mastoConverters.convertApplication(data.data); + return reply.send(response); + }); } } diff --git a/packages/megalodon/src/index.ts b/packages/megalodon/src/index.ts index 50663c3ce5..bacd0574d4 100644 --- a/packages/megalodon/src/index.ts +++ b/packages/megalodon/src/index.ts @@ -9,6 +9,7 @@ import * as NotificationType from './notification' import FilterContext from './filter_context' import Converter from './converter' import MastodonEntity from './mastodon/entity'; +import MisskeyEntity from './misskey/entity'; export { Response, @@ -23,4 +24,5 @@ export { Entity, Converter, MastodonEntity, + MisskeyEntity, } diff --git a/packages/megalodon/src/mastodon/entities/application.ts b/packages/megalodon/src/mastodon/entities/application.ts index a3f07997ee..f402152bf6 100644 --- a/packages/megalodon/src/mastodon/entities/application.ts +++ b/packages/megalodon/src/mastodon/entities/application.ts @@ -3,5 +3,8 @@ namespace MastodonEntity { name: string website?: string | null vapid_key?: string | null + scopes: string[] + redirect_uris: string[] + redirect_uri?: string } } diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts index 669eb0f106..bc38e27ce5 100644 --- a/packages/megalodon/src/misskey.ts +++ b/packages/megalodon/src/misskey.ts @@ -102,7 +102,7 @@ export default class Misskey implements MegalodonInterface { website: null, redirect_uri: res.data.callbackUrl, client_id: '', - client_secret: res.data.secret + client_secret: res.data.secret! } return OAuth.AppData.from(appData) }) @@ -122,11 +122,8 @@ export default class Misskey implements MegalodonInterface { // ====================================== // apps // ====================================== - public async verifyAppCredentials(): Promise> { - return new Promise((_, reject) => { - const err = new NoImplementedError('misskey does not support') - reject(err) - }) + public async verifyAppCredentials(): Promise> { + return await this.client.post('/api/app/current'); } // ====================================== diff --git a/packages/megalodon/src/misskey/entities/app.ts b/packages/megalodon/src/misskey/entities/app.ts index 40a704b944..49c431596f 100644 --- a/packages/megalodon/src/misskey/entities/app.ts +++ b/packages/megalodon/src/misskey/entities/app.ts @@ -4,6 +4,6 @@ namespace MisskeyEntity { name: string callbackUrl: string permission: Array - secret: string + secret?: string } } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 18cb070af5..44700add31 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -552,6 +552,9 @@ type AppCreateRequest = operations['app___create']['requestBody']['content']['ap // @public (undocumented) type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json']; +// @public (undocumented) +type AppCurrentResponse = operations['app___current']['responses']['200']['content']['application/json']; + // @public (undocumented) type AppShowRequest = operations['app___show']['requestBody']['content']['application/json']; @@ -1643,6 +1646,7 @@ declare namespace entities { ApShowResponse, AppCreateRequest, AppCreateResponse, + AppCurrentResponse, AppShowRequest, AppShowResponse, AuthAcceptRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 75b3c5769e..0dfe042811 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1313,6 +1313,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 9293a5e950..b424927316 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -159,6 +159,7 @@ import type { ApShowResponse, AppCreateRequest, AppCreateResponse, + AppCurrentResponse, AppShowRequest, AppShowResponse, AuthAcceptRequest, @@ -778,6 +779,7 @@ export type Endpoints = { 'ap/get': { req: ApGetRequest; res: ApGetResponse }; 'ap/show': { req: ApShowRequest; res: ApShowResponse }; 'app/create': { req: AppCreateRequest; res: AppCreateResponse }; + 'app/current': { req: EmptyRequest; res: AppCurrentResponse }; 'app/show': { req: AppShowRequest; res: AppShowResponse }; 'auth/accept': { req: AuthAcceptRequest; res: EmptyResponse }; 'auth/session/generate': { req: AuthSessionGenerateRequest; res: AuthSessionGenerateResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index f71407a6ae..39359e3cfa 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -162,6 +162,7 @@ export type ApShowRequest = operations['ap___show']['requestBody']['content']['a export type ApShowResponse = operations['ap___show']['responses']['200']['content']['application/json']; export type AppCreateRequest = operations['app___create']['requestBody']['content']['application/json']; export type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json']; +export type AppCurrentResponse = operations['app___current']['responses']['200']['content']['application/json']; export type AppShowRequest = operations['app___show']['requestBody']['content']['application/json']; export type AppShowResponse = operations['app___show']['responses']['200']['content']['application/json']; export type AuthAcceptRequest = operations['auth___accept']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 57e98f2f88..077ea35729 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -1086,6 +1086,15 @@ export type paths = { */ post: operations['app___create']; }; + '/app/current': { + /** + * app/current + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['app___current']; + }; '/app/show': { /** * app/show @@ -13071,6 +13080,58 @@ export type operations = { }; }; }; + /** + * app/current + * @description No description provided. + * + * **Credential required**: *No* + */ + app___current: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['App']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * app/show * @description No description provided.