summaryrefslogtreecommitdiff
path: root/packages/backend/src/server
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-05-08 15:58:48 +0000
committerHazelnoot <acomputerdog@gmail.com>2025-05-08 15:58:48 +0000
commitb8ff44147433baa877fb9bcd8ee43924455f1067 (patch)
tree58838b95b3af6dbc59ad18df97ed0e662616d9c8 /packages/backend/src/server
parentmerge: Add missing paused state (!992) (diff)
parentfix oauth data (diff)
downloadsharkey-b8ff44147433baa877fb9bcd8ee43924455f1067.tar.gz
sharkey-b8ff44147433baa877fb9bcd8ee43924455f1067.tar.bz2
sharkey-b8ff44147433baa877fb9bcd8ee43924455f1067.zip
merge: Fix Mastodon API requests with multipart/form-data encoding (resolves #1024, #839, #699, #574, and #486) (!987)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/987 Closes #1024, #839, #699, #574, and #486 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
Diffstat (limited to 'packages/backend/src/server')
-rw-r--r--packages/backend/src/server/ServerModule.ts2
-rw-r--r--packages/backend/src/server/ServerUtilityService.ts162
-rw-r--r--packages/backend/src/server/api/AuthenticateService.ts2
-rw-r--r--packages/backend/src/server/api/endpoint-list.ts1
-rw-r--r--packages/backend/src/server/api/endpoints/app/current.ts73
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts188
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonConverters.ts12
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonLogger.ts145
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/account.ts83
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/apps.ts31
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/filter.ts17
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/instance.ts2
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/notifications.ts15
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/search.ts8
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/status.ts48
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/timeline.ts26
-rw-r--r--packages/backend/src/server/oauth/OAuth2ProviderService.ts52
17 files changed, 542 insertions, 325 deletions
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 6726d4aa67..8ff8da380a 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -16,6 +16,7 @@ import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
+import { ServerUtilityService } from '@/server/ServerUtilityService.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
@@ -126,6 +127,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
ApiSearchMastodon,
ApiStatusMastodon,
ApiTimelineMastodon,
+ ServerUtilityService,
],
exports: [
ServerService,
diff --git a/packages/backend/src/server/ServerUtilityService.ts b/packages/backend/src/server/ServerUtilityService.ts
new file mode 100644
index 0000000000..c2a3132489
--- /dev/null
+++ b/packages/backend/src/server/ServerUtilityService.ts
@@ -0,0 +1,162 @@
+/*
+ * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import querystring from 'querystring';
+import multipart from '@fastify/multipart';
+import { Inject, Injectable } from '@nestjs/common';
+import { FastifyInstance } from 'fastify';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { saveToTempFile } from '@/misc/create-temp.js';
+
+@Injectable()
+export class ServerUtilityService {
+ constructor(
+ @Inject(DI.config)
+ private readonly config: Config,
+ ) {}
+
+ public addMultipartFormDataContentType(fastify: FastifyInstance): void {
+ fastify.register(multipart, {
+ limits: {
+ fileSize: this.config.maxFileSize,
+ files: 1,
+ },
+ });
+
+ // Default behavior saves files to memory - we don't want that!
+ // Store to temporary file instead, and copy the body fields while we're at it.
+ fastify.addHook<{ Body?: Record<string, string | string[] | undefined> }>('preValidation', async request => {
+ if (request.isMultipart()) {
+ // We can't use saveRequestFiles() because it erases all the data fields.
+ // Instead, recreate it manually.
+ // https://github.com/fastify/fastify-multipart/issues/549
+
+ for await (const part of request.parts()) {
+ if (part.type === 'field') {
+ const k = part.fieldname;
+ const v = part.value;
+ const body = request.body ??= {};
+
+ // Value can be string, buffer, or undefined.
+ // We only support the first one.
+ if (typeof(v) !== 'string') continue;
+
+ // This is just progressive conversion from undefined -> string -> string[]
+ if (!body[k]) {
+ body[k] = v;
+ } else if (Array.isArray(body[k])) {
+ body[k].push(v);
+ } else {
+ body[k] = [body[k], v];
+ }
+ } else { // Otherwise it's a file
+ try {
+ const filepath = await saveToTempFile(part.file);
+
+ const tmpUploads = (request.tmpUploads ??= []);
+ tmpUploads.push(filepath);
+
+ const requestSavedFiles = (request.savedRequestFiles ??= []);
+ requestSavedFiles.push({
+ ...part,
+ filepath,
+ });
+ } catch (e) {
+ // Cleanup to avoid file leak in case of errors
+ await request.cleanRequestFiles();
+ request.tmpUploads = null;
+ request.savedRequestFiles = null;
+ throw e;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public addFormUrlEncodedContentType(fastify: FastifyInstance) {
+ fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
+ let body = '';
+ payload.on('data', (data) => {
+ body += data;
+ });
+ payload.on('end', () => {
+ try {
+ const parsed = querystring.parse(body);
+ done(null, parsed);
+ } catch (e) {
+ done(e as Error);
+ }
+ });
+ payload.on('error', done);
+ });
+ }
+
+ public addCORS(fastify: FastifyInstance) {
+ fastify.addHook('preHandler', (_, reply, done) => {
+ // Allow web-based clients to connect from other origins.
+ reply.header('Access-Control-Allow-Origin', '*');
+
+ // Mastodon uses all types of request methods.
+ reply.header('Access-Control-Allow-Methods', '*');
+
+ // Allow web-based clients to access Link header - required for mastodon pagination.
+ // https://stackoverflow.com/a/54928828
+ // https://docs.joinmastodon.org/api/guidelines/#pagination
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers
+ reply.header('Access-Control-Expose-Headers', 'Link');
+
+ // Cache to avoid extra pre-flight requests
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
+ reply.header('Access-Control-Max-Age', 60 * 60 * 24); // 1 day in seconds
+
+ done();
+ });
+ }
+
+ public addFlattenedQueryType(fastify: FastifyInstance) {
+ // Remove trailing "[]" from query params
+ fastify.addHook<{ Querystring?: Record<string, string | string[] | undefined> }>('preValidation', (request, _reply, done) => {
+ if (!request.query || typeof(request.query) !== 'object') {
+ return done();
+ }
+
+ for (const key of Object.keys(request.query)) {
+ if (!key.endsWith('[]')) {
+ continue;
+ }
+ if (request.query[key] == null) {
+ continue;
+ }
+
+ const newKey = key.substring(0, key.length - 2);
+ const newValue = request.query[key];
+ const oldValue = request.query[newKey];
+
+ // Move the value to the correct key
+ if (oldValue != null) {
+ if (Array.isArray(oldValue)) {
+ // Works for both array and single values
+ request.query[newKey] = oldValue.concat(newValue);
+ } else if (Array.isArray(newValue)) {
+ // Preserve order
+ request.query[newKey] = [oldValue, ...newValue];
+ } else {
+ // Preserve order
+ request.query[newKey] = [oldValue, newValue];
+ }
+ } else {
+ request.query[newKey] = newValue;
+ }
+
+ // Remove the invalid key
+ delete request.query[key];
+ }
+
+ return done();
+ });
+ }
+}
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<typeof meta, typeof paramDef> { // 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/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index 59ab3b71aa..74fd9d7d59 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -3,13 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import querystring from 'querystring';
-import multer from 'fastify-multer';
-import { Inject, Injectable } from '@nestjs/common';
-import { DI } from '@/di-symbols.js';
+import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
-import type { Config } from '@/config.js';
-import { getErrorData, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
+import { getErrorData, getErrorException, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
@@ -20,6 +16,7 @@ import { ApiNotificationsMastodon } from '@/server/api/mastodon/endpoints/notifi
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
import { ApiError } from '@/server/api/error.js';
+import { ServerUtilityService } from '@/server/ServerUtilityService.js';
import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js';
import { convertAnnouncement, convertAttachment, MastodonConverters, convertRelationship } from './MastodonConverters.js';
import type { Entity } from 'megalodon';
@@ -28,9 +25,6 @@ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable()
export class MastodonApiServerService {
constructor(
- @Inject(DI.config)
- private readonly config: Config,
-
private readonly mastoConverters: MastodonConverters,
private readonly logger: MastodonLogger,
private readonly clientService: MastodonClientService,
@@ -42,115 +36,47 @@ export class MastodonApiServerService {
private readonly apiSearchMastodon: ApiSearchMastodon,
private readonly apiStatusMastodon: ApiStatusMastodon,
private readonly apiTimelineMastodon: ApiTimelineMastodon,
+ private readonly serverUtilityService: ServerUtilityService,
) {}
@bindThis
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
- const upload = multer({
- storage: multer.diskStorage({}),
- limits: {
- fileSize: this.config.maxFileSize || 262144000,
- files: 1,
- },
- });
-
- fastify.addHook('onRequest', (_, reply, done) => {
- // Allow web-based clients to connect from other origins.
- reply.header('Access-Control-Allow-Origin', '*');
-
- // Mastodon uses all types of request methods.
- reply.header('Access-Control-Allow-Methods', '*');
-
- // Allow web-based clients to access Link header - required for mastodon pagination.
- // https://stackoverflow.com/a/54928828
- // https://docs.joinmastodon.org/api/guidelines/#pagination
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers
- reply.header('Access-Control-Expose-Headers', 'Link');
-
- // Cache to avoid extra pre-flight requests
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
- reply.header('Access-Control-Max-Age', 60 * 60 * 24); // 1 day in seconds
-
- done();
- });
-
- fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
- let body = '';
- payload.on('data', (data) => {
- body += data;
- });
- payload.on('end', () => {
- try {
- const parsed = querystring.parse(body);
- done(null, parsed);
- } catch (e) {
- done(e as Error);
- }
- });
- payload.on('error', done);
- });
-
- // Remove trailing "[]" from query params
- fastify.addHook('preValidation', (request, _reply, done) => {
- if (!request.query || typeof(request.query) !== 'object') {
- return done();
- }
-
- // Same object aliased with a different type
- const query = request.query as Record<string, string | string[] | undefined>;
-
- for (const key of Object.keys(query)) {
- if (!key.endsWith('[]')) {
- continue;
- }
- if (query[key] == null) {
- continue;
- }
-
- const newKey = key.substring(0, key.length - 2);
- const newValue = query[key];
- const oldValue = query[newKey];
-
- // Move the value to the correct key
- if (oldValue != null) {
- if (Array.isArray(oldValue)) {
- // Works for both array and single values
- query[newKey] = oldValue.concat(newValue);
- } else if (Array.isArray(newValue)) {
- // Preserve order
- query[newKey] = [oldValue, ...newValue];
- } else {
- // Preserve order
- query[newKey] = [oldValue, newValue];
- }
- } else {
- query[newKey] = newValue;
- }
-
- // Remove the invalid key
- delete query[key];
- }
-
- return done();
- });
+ this.serverUtilityService.addMultipartFormDataContentType(fastify);
+ this.serverUtilityService.addFormUrlEncodedContentType(fastify);
+ this.serverUtilityService.addCORS(fastify);
+ this.serverUtilityService.addFlattenedQueryType(fastify);
+ // Convert JS exceptions into error responses
fastify.setErrorHandler((error, request, reply) => {
const data = getErrorData(error);
const status = getErrorStatus(error);
+ const exception = getErrorException(error);
- this.logger.error(request, data, status);
+ if (exception) {
+ this.logger.exception(request, exception);
+ }
- reply.code(status).send(data);
+ return reply.code(status).send(data);
});
- fastify.register(multer.contentParser);
+ // Log error responses (including converted JSON exceptions)
+ fastify.addHook('onSend', (request, reply, payload, done) => {
+ if (reply.statusCode >= 400) {
+ if (typeof(payload) === 'string' && String(reply.getHeader('content-type')).toLowerCase().includes('application/json')) {
+ const body = JSON.parse(payload);
+ const data = getErrorData(body);
+ this.logger.error(request, data, reply.statusCode);
+ }
+ }
+ done();
+ });
// External endpoints
- this.apiAccountMastodon.register(fastify, upload);
- this.apiAppsMastodon.register(fastify, upload);
- this.apiFilterMastodon.register(fastify, upload);
+ this.apiAccountMastodon.register(fastify);
+ this.apiAppsMastodon.register(fastify);
+ this.apiFilterMastodon.register(fastify);
this.apiInstanceMastodon.register(fastify);
- this.apiNotificationsMastodon.register(fastify, upload);
+ this.apiNotificationsMastodon.register(fastify);
this.apiSearchMastodon.register(fastify);
this.apiStatusMastodon.register(fastify);
this.apiTimelineMastodon.register(fastify);
@@ -158,7 +84,7 @@ export class MastodonApiServerService {
fastify.get('/v1/custom_emojis', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getInstanceCustomEmojis();
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get('/v1/announcements', async (_request, reply) => {
@@ -166,7 +92,7 @@ export class MastodonApiServerService {
const data = await client.getInstanceAnnouncements();
const response = data.data.map((announcement) => convertAnnouncement(announcement));
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
@@ -175,64 +101,62 @@ export class MastodonApiServerService {
const client = this.clientService.getClient(_request);
const data = await client.dismissInstanceAnnouncement(_request.body.id);
- reply.send(data.data);
+ return reply.send(data.data);
});
- fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const multipartData = await _request.file();
+ fastify.post('/v1/media', async (_request, reply) => {
+ const multipartData = _request.savedRequestFiles?.[0];
if (!multipartData) {
- reply.code(401).send({ error: 'No image' });
- return;
+ return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
}
const client = this.clientService.getClient(_request);
const data = await client.uploadMedia(multipartData);
const response = convertAttachment(data.data as Entity.Attachment);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<{ Body: { description?: string; focus?: string } }>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
- const multipartData = await _request.file();
+ fastify.post<{ Body: { description?: string; focus?: string } }>('/v2/media', async (_request, reply) => {
+ const multipartData = _request.savedRequestFiles?.[0];
if (!multipartData) {
- reply.code(401).send({ error: 'No image' });
- return;
+ return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
}
const client = this.clientService.getClient(_request);
const data = await client.uploadMedia(multipartData, _request.body);
const response = convertAttachment(data.data as Entity.Attachment);
- reply.send(response);
+ return reply.send(response);
});
fastify.get('/v1/trends', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getInstanceTrends();
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get('/v1/trends/tags', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getInstanceTrends();
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get('/v1/trends/links', async (_request, reply) => {
// As we do not have any system for news/links this will just return empty
- reply.send([]);
+ return reply.send([]);
});
fastify.get('/v1/preferences', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getPreferences();
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get('/v1/followed_tags', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getFollowedTags();
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => {
@@ -241,7 +165,7 @@ export class MastodonApiServerService {
const data = await client.getBookmarks(parseTimelineArgs(_request.query));
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => {
@@ -263,7 +187,7 @@ export class MastodonApiServerService {
const data = await client.getFavourites(args);
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => {
@@ -272,7 +196,7 @@ export class MastodonApiServerService {
const data = await client.getMutes(parseTimelineArgs(_request.query));
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => {
@@ -281,7 +205,7 @@ export class MastodonApiServerService {
const data = await client.getBlocks(parseTimelineArgs(_request.query));
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: { limit?: string } }>('/v1/follow_requests', async (_request, reply) => {
@@ -291,27 +215,27 @@ export class MastodonApiServerService {
const data = await client.getFollowRequests(limit);
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account as Entity.Account)));
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/authorize', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
const data = await client.acceptFollowRequest(_request.params.id);
const response = convertRelationship(data.data);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/reject', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
const data = await client.rejectFollowRequest(_request.params.id);
const response = convertRelationship(data.data);
- reply.send(response);
+ return reply.send(response);
});
//#endregion
@@ -325,7 +249,7 @@ export class MastodonApiServerService {
focus?: string,
is_sensitive?: string,
},
- }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
+ }>('/v1/media/:id', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const options = {
@@ -336,7 +260,7 @@ export class MastodonApiServerService {
const data = await client.updateMedia(_request.params.id, options);
const response = convertAttachment(data.data);
- reply.send(response);
+ return reply.send(response);
});
done();
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<T>(data: T): T {
@@ -459,4 +468,3 @@ export function convertRelationship(relationship: Partial<Entity.Relationship> &
note: relationship.note ?? '',
};
}
-
diff --git a/packages/backend/src/server/api/mastodon/MastodonLogger.ts b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
index 81d3e8f03d..5ea69ed151 100644
--- a/packages/backend/src/server/api/mastodon/MastodonLogger.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonLogger.ts
@@ -3,33 +3,49 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Inject, Injectable } from '@nestjs/common';
-import { FastifyRequest } from 'fastify';
-import Logger from '@/logger.js';
+import { Injectable } from '@nestjs/common';
+import { isAxiosError } from 'axios';
+import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { ApiError } from '@/server/api/error.js';
-import { EnvService } from '@/core/EnvService.js';
import { getBaseUrl } from '@/server/api/mastodon/MastodonClientService.js';
+import { AuthenticationError } from '@/server/api/AuthenticateService.js';
+import type { FastifyRequest } from 'fastify';
@Injectable()
export class MastodonLogger {
public readonly logger: Logger;
constructor(
- @Inject(EnvService)
- private readonly envService: EnvService,
-
loggerService: LoggerService,
) {
this.logger = loggerService.getLogger('masto-api');
}
public error(request: FastifyRequest, error: MastodonError, status: number): void {
- if ((status < 400 && status > 499) || this.envService.env.NODE_ENV === 'development') {
- const path = new URL(request.url, getBaseUrl(request)).pathname;
+ const path = getPath(request);
+
+ if (status >= 400 && status <= 499) { // Client errors
+ this.logger.debug(`Error in mastodon endpoint ${request.method} ${path}:`, error);
+ } else { // Server errors
this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, error);
}
}
+
+ public exception(request: FastifyRequest, ex: Error): void {
+ const path = getPath(request);
+
+ // Exceptions are always server errors, and should therefore always be logged.
+ this.logger.error(`Exception in mastodon endpoint ${request.method} ${path}:`, ex);
+ }
+}
+
+function getPath(request: FastifyRequest): string {
+ try {
+ return new URL(request.url, getBaseUrl(request)).pathname;
+ } catch {
+ return request.url;
+ }
}
// TODO move elsewhere
@@ -38,6 +54,43 @@ export interface MastodonError {
error_description?: string;
}
+export function getErrorException(error: unknown): Error | null {
+ if (!(error instanceof Error)) {
+ return null;
+ }
+
+ // AxiosErrors need special decoding
+ if (isAxiosError(error)) {
+ // Axios errors with a response are from the remote
+ if (error.response) {
+ return null;
+ }
+
+ // This is the inner exception, basically
+ if (error.cause && !isAxiosError(error.cause)) {
+ if (!error.cause.stack) {
+ error.cause.stack = error.stack;
+ }
+
+ return error.cause;
+ }
+
+ const ex = new Error();
+ ex.name = error.name;
+ ex.stack = error.stack;
+ ex.message = error.message;
+ ex.cause = error.cause;
+ return ex;
+ }
+
+ // AuthenticationError is a client error
+ if (error instanceof AuthenticationError) {
+ return null;
+ }
+
+ return error;
+}
+
export function getErrorData(error: unknown): MastodonError {
// Axios wraps errors from the backend
error = unpackAxiosError(error);
@@ -59,17 +112,33 @@ export function getErrorData(error: unknown): MastodonError {
}
}
+ if ('error' in error && typeof (error.error) === 'string') {
+ if ('message' in error && typeof (error.message) === 'string') {
+ return convertErrorMessageError(error as { error: string, message: string });
+ }
+ }
+
if (error instanceof Error) {
return convertGenericError(error);
}
- return convertUnknownError(error);
+ if ('error' in error && typeof(error.error) === 'string') {
+ // "error_description" is string, undefined, or not present.
+ if (!('error_description' in error) || typeof(error.error_description) === 'string' || typeof(error.error_description) === 'undefined') {
+ return convertMastodonError(error as MastodonError);
+ }
+ }
+
+ return {
+ error: 'INTERNAL_ERROR',
+ error_description: 'Internal error occurred. Please contact us if the error persists.',
+ };
}
function unpackAxiosError(error: unknown): unknown {
- if (error && typeof(error) === 'object') {
- if ('response' in error && error.response && typeof (error.response) === 'object') {
- if ('data' in error.response && error.response.data && typeof (error.response.data) === 'object') {
+ if (isAxiosError(error)) {
+ if (error.response) {
+ if (error.response.data && typeof(error.response.data) === 'object') {
if ('error' in error.response.data && error.response.data.error && typeof(error.response.data.error) === 'object') {
return error.response.data.error;
}
@@ -80,46 +149,48 @@ function unpackAxiosError(error: unknown): unknown {
// No data - this is a fallback to avoid leaking request/response details in the error
return undefined;
}
+
+ if (error.cause && !isAxiosError(error.cause)) {
+ if (!error.cause.stack) {
+ error.cause.stack = error.stack;
+ }
+
+ return error.cause;
+ }
+
+ // No data - this is a fallback to avoid leaking request/response details in the error
+ return String(error);
}
return error;
}
function convertApiError(apiError: ApiError): MastodonError {
- const mastoError: MastodonError & Partial<ApiError> = {
+ return {
error: apiError.code,
error_description: apiError.message,
- ...apiError,
};
-
- delete mastoError.code;
- delete mastoError.message;
- delete mastoError.httpStatusCode;
-
- return mastoError;
}
-function convertUnknownError(data: object = {}): MastodonError {
- return Object.assign({}, data, {
- error: 'INTERNAL_ERROR',
- error_description: 'Internal error occurred. Please contact us if the error persists.',
- id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
- kind: 'server',
- });
+function convertErrorMessageError(error: { error: string, message: string }): MastodonError {
+ return {
+ error: error.error,
+ error_description: error.message,
+ };
}
function convertGenericError(error: Error): MastodonError {
- const mastoError: MastodonError & Partial<Error> = {
+ return {
error: 'INTERNAL_ERROR',
error_description: String(error),
- ...error,
};
+}
- delete mastoError.name;
- delete mastoError.message;
- delete mastoError.stack;
-
- return mastoError;
+function convertMastodonError(error: MastodonError): MastodonError {
+ return {
+ error: error.error,
+ error_description: error.error_description,
+ };
}
export function getErrorStatus(error: unknown): number {
@@ -134,6 +205,10 @@ export function getErrorStatus(error: unknown): number {
if ('httpStatusCode' in error && typeof(error.httpStatusCode) === 'number') {
return error.httpStatusCode;
}
+
+ if ('statusCode' in error && typeof(error.statusCode) === 'number') {
+ return error.statusCode;
+ }
}
return 500;
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 8bc3c14c15..6a1af62be7 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -11,7 +11,6 @@ import { DI } from '@/di-symbols.js';
import type { AccessTokensRepository, UserProfilesRepository } from '@/models/_.js';
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
import { MastodonConverters, convertRelationship, convertFeaturedTag, convertList } from '../MastodonConverters.js';
-import type multer from 'fastify-multer';
import type { FastifyInstance } from 'fastify';
interface ApiAccountMastodonRoute {
@@ -34,7 +33,7 @@ export class ApiAccountMastodon {
private readonly driveService: DriveService,
) {}
- public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
+ public register(fastify: FastifyInstance): void {
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.verifyAccountCredentials();
@@ -48,7 +47,7 @@ export class ApiAccountMastodon {
language: '',
},
});
- reply.send(response);
+ return reply.send(response);
});
fastify.patch<{
@@ -70,60 +69,50 @@ export class ApiAccountMastodon {
value: string,
}[],
},
- }>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
+ }>('/v1/accounts/update_credentials', async (_request, reply) => {
const accessTokens = _request.headers.authorization;
const client = this.clientService.getClient(_request);
// Check if there is a Header or Avatar being uploaded, if there is proceed to upload it to the drive of the user and then set it.
- if (_request.files.length > 0 && accessTokens) {
+ if (_request.savedRequestFiles?.length && accessTokens) {
const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const avatar = (_request.files as any).find((obj: any) => {
+ const avatar = _request.savedRequestFiles.find(obj => {
return obj.fieldname === 'avatar';
});
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const header = (_request.files as any).find((obj: any) => {
+ const header = _request.savedRequestFiles.find(obj => {
return obj.fieldname === 'header';
});
if (tokeninfo && avatar) {
const upload = await this.driveService.addFile({
user: { id: tokeninfo.userId, host: null },
- path: avatar.path,
- name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined,
+ path: avatar.filepath,
+ name: avatar.filename && avatar.filename !== 'file' ? avatar.filename : undefined,
sensitive: false,
});
if (upload.type.startsWith('image/')) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (_request.body as any).avatar = upload.id;
+ _request.body.avatar = upload.id;
}
} else if (tokeninfo && header) {
const upload = await this.driveService.addFile({
user: { id: tokeninfo.userId, host: null },
- path: header.path,
- name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined,
+ path: header.filepath,
+ name: header.filename && header.filename !== 'file' ? header.filename : undefined,
sensitive: false,
});
if (upload.type.startsWith('image/')) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (_request.body as any).header = upload.id;
+ _request.body.header = upload.id;
}
}
}
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((_request.body as any).fields_attributes) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const fields = (_request.body as any).fields_attributes.map((field: any) => {
+ if (_request.body.fields_attributes) {
+ for (const field of _request.body.fields_attributes) {
if (!(field.name.trim() === '' && field.value.trim() === '')) {
if (field.name.trim() === '') return reply.code(400).send('Field name can not be empty');
if (field.value.trim() === '') return reply.code(400).send('Field value can not be empty');
}
- return {
- ...field,
- };
- });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
+ }
+ _request.body.fields_attributes = _request.body.fields_attributes.filter(field => field.name.trim().length > 0 && field.value.length > 0);
}
const options = {
@@ -139,7 +128,7 @@ export class ApiAccountMastodon {
const data = await client.updateCredentials(options);
const response = await this.mastoConverters.convertAccount(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: { acct?: string } }>('/v1/accounts/lookup', async (_request, reply) => {
@@ -151,7 +140,7 @@ export class ApiAccountMastodon {
data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
const response = await this.mastoConverters.convertAccount(data.data.accounts[0]);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[] } }>('/v1/accounts/relationships', async (_request, reply) => {
@@ -161,7 +150,7 @@ export class ApiAccountMastodon {
const data = await client.getRelationships(_request.query.id);
const response = data.data.map(relationship => convertRelationship(relationship));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
@@ -171,7 +160,7 @@ export class ApiAccountMastodon {
const data = await client.getAccount(_request.params.id);
const account = await this.mastoConverters.convertAccount(data.data);
- reply.send(account);
+ return reply.send(account);
});
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (request, reply) => {
@@ -183,7 +172,7 @@ export class ApiAccountMastodon {
const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
@@ -193,7 +182,7 @@ export class ApiAccountMastodon {
const data = await client.getFeaturedTags();
const response = data.data.map((tag) => convertFeaturedTag(tag));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (request, reply) => {
@@ -207,7 +196,7 @@ export class ApiAccountMastodon {
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (request, reply) => {
@@ -221,7 +210,7 @@ export class ApiAccountMastodon {
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
@@ -231,10 +220,10 @@ export class ApiAccountMastodon {
const data = await client.getAccountLists(_request.params.id);
const response = data.data.map((list) => convertList(list));
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
@@ -242,10 +231,10 @@ export class ApiAccountMastodon {
const acct = convertRelationship(data.data);
acct.following = true; // TODO this is wrong, follow may not have processed immediately
- reply.send(acct);
+ return reply.send(acct);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
@@ -253,20 +242,20 @@ export class ApiAccountMastodon {
const acct = convertRelationship(data.data);
acct.following = false;
- reply.send(acct);
+ return reply.send(acct);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
const data = await client.blockAccount(_request.params.id);
const response = convertRelationship(data.data);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
@@ -276,7 +265,7 @@ export class ApiAccountMastodon {
return reply.send(response);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
@@ -286,17 +275,17 @@ export class ApiAccountMastodon {
);
const response = convertRelationship(data.data);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
const data = await client.unmuteAccount(_request.params.id);
const response = convertRelationship(data.data);
- reply.send(response);
+ return reply.send(response);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/apps.ts b/packages/backend/src/server/api/mastodon/endpoints/apps.ts
index dbef3b7d35..72b520c74a 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/apps.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/apps.ts
@@ -5,8 +5,8 @@
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';
-import type multer from 'fastify-multer';
const readScope = [
'read:account',
@@ -48,9 +48,9 @@ const writeScope = [
export interface AuthPayload {
scopes?: string | string[],
- redirect_uris?: string,
- client_name?: string,
- website?: string,
+ redirect_uris?: string | string[],
+ client_name?: string | string[],
+ website?: string | string[],
}
// Not entirely right, but it gets TypeScript to work so *shrug*
@@ -60,14 +60,18 @@ type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
export class ApiAppsMastodon {
constructor(
private readonly clientService: MastodonClientService,
+ private readonly mastoConverters: MastodonConverters,
) {}
- public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
- fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
+ public register(fastify: FastifyInstance): void {
+ fastify.post<AuthMastodonRoute>('/v1/apps', async (_request, reply) => {
const body = _request.body ?? _request.query;
if (!body.scopes) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "scopes"' });
if (!body.redirect_uris) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "redirect_uris"' });
+ if (Array.isArray(body.redirect_uris)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "redirect_uris": only one value is allowed' });
if (!body.client_name) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "client_name"' });
+ if (Array.isArray(body.client_name)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "client_name": only one value is allowed' });
+ if (Array.isArray(body.website)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "website": only one value is allowed' });
let scope = body.scopes;
if (typeof scope === 'string') {
@@ -88,12 +92,10 @@ export class ApiAppsMastodon {
}
}
- const red = body.redirect_uris;
-
const client = this.clientService.getClient(_request);
const appData = await client.registerApp(body.client_name, {
scopes: Array.from(pushScope),
- redirect_uris: red,
+ redirect_uri: body.redirect_uris,
website: body.website,
});
@@ -101,12 +103,19 @@ export class ApiAppsMastodon {
id: Math.floor(Math.random() * 100).toString(),
name: appData.name,
website: body.website,
- redirect_uri: red,
+ redirect_uri: body.redirect_uris,
client_id: Buffer.from(appData.url || '').toString('base64'),
client_secret: appData.clientSecret,
};
- reply.send(response);
+ 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/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index deac1e9aad..f2bd0052d5 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -8,7 +8,6 @@ import { toBoolean } from '@/server/api/mastodon/argsUtils.js';
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import { convertFilter } from '../MastodonConverters.js';
import type { FastifyInstance } from 'fastify';
-import type multer from 'fastify-multer';
interface ApiFilterMastodonRoute {
Params: {
@@ -29,14 +28,14 @@ export class ApiFilterMastodon {
private readonly clientService: MastodonClientService,
) {}
- public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
+ public register(fastify: FastifyInstance): void {
fastify.get('/v1/filters', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.getFilters();
const response = data.data.map((filter) => convertFilter(filter));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
@@ -46,10 +45,10 @@ export class ApiFilterMastodon {
const data = await client.getFilter(_request.params.id);
const response = convertFilter(data.data);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiFilterMastodonRoute>('/v1/filters', async (_request, reply) => {
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
@@ -65,10 +64,10 @@ export class ApiFilterMastodon {
const data = await client.createFilter(_request.body.phrase, _request.body.context, options);
const response = convertFilter(data.data);
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
@@ -85,7 +84,7 @@ export class ApiFilterMastodon {
const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options);
const response = convertFilter(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
@@ -94,7 +93,7 @@ export class ApiFilterMastodon {
const client = this.clientService.getClient(_request);
const data = await client.deleteFilter(_request.params.id);
- reply.send(data.data);
+ return reply.send(data.data);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/instance.ts b/packages/backend/src/server/api/mastodon/endpoints/instance.ts
index a168339ac6..cfca5b1350 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/instance.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/instance.ts
@@ -87,7 +87,7 @@ export class ApiInstanceMastodon {
rules: instance.rules ?? [],
};
- reply.send(response);
+ return reply.send(response);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index ee6c990fd1..f6cc59e782 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -10,7 +10,6 @@ import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js'
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
import { MastodonClientService } from '../MastodonClientService.js';
import type { FastifyInstance } from 'fastify';
-import type multer from 'fastify-multer';
interface ApiNotifyMastodonRoute {
Params: {
@@ -26,7 +25,7 @@ export class ApiNotificationsMastodon {
private readonly clientService: MastodonClientService,
) {}
- public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
+ public register(fastify: FastifyInstance): void {
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (request, reply) => {
const { client, me } = await this.clientService.getAuthClient(request);
const data = await client.getNotifications(parseTimelineArgs(request.query));
@@ -46,7 +45,7 @@ export class ApiNotificationsMastodon {
}
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
@@ -63,23 +62,23 @@ export class ApiNotificationsMastodon {
});
}
- reply.send(response);
+ return reply.send(response);
});
- fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
const client = this.clientService.getClient(_request);
const data = await client.dismissNotification(_request.params.id);
- reply.send(data.data);
+ return reply.send(data.data);
});
- fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
+ fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', async (_request, reply) => {
const client = this.clientService.getClient(_request);
const data = await client.dismissNotifications();
- reply.send(data.data);
+ return reply.send(data.data);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index 33bfa87e5f..f58f21966c 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -62,7 +62,7 @@ export class ApiSearchMastodon {
attachMinMaxPagination(request, reply, response[type]);
}
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiSearchMastodonRoute>('/v2/search', async (request, reply) => {
@@ -103,7 +103,7 @@ export class ApiSearchMastodon {
// Offset pagination is the only possible option
attachOffsetPagination(request, reply, longestResult);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (request, reply) => {
@@ -126,7 +126,7 @@ export class ApiSearchMastodon {
const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (request, reply) => {
@@ -158,7 +158,7 @@ export class ApiSearchMastodon {
}));
attachOffsetPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index ec31e0cc46..22b8a911ca 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -38,7 +38,7 @@ export class ApiStatusMastodon {
response.media_attachments = [];
}
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
@@ -47,7 +47,7 @@ export class ApiStatusMastodon {
const client = this.clientService.getClient(_request);
const data = await client.getStatusSource(_request.params.id);
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
@@ -59,7 +59,7 @@ export class ApiStatusMastodon {
const descendants = await Promise.all(data.descendants.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
const response = { ancestors, descendants };
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
@@ -68,7 +68,7 @@ export class ApiStatusMastodon {
const user = await this.clientService.getAuth(_request);
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
- reply.send(edits);
+ return reply.send(edits);
});
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
@@ -78,7 +78,7 @@ export class ApiStatusMastodon {
const data = await client.getStatusRebloggedBy(_request.params.id);
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
@@ -88,7 +88,7 @@ export class ApiStatusMastodon {
const data = await client.getStatusFavouritedBy(_request.params.id);
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
@@ -98,7 +98,7 @@ export class ApiStatusMastodon {
const data = await client.getMedia(_request.params.id);
const response = convertAttachment(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
@@ -108,7 +108,7 @@ export class ApiStatusMastodon {
const data = await client.getPoll(_request.params.id);
const response = convertPoll(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
@@ -119,7 +119,7 @@ export class ApiStatusMastodon {
const data = await client.votePoll(_request.params.id, _request.body.choices);
const response = convertPoll(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{
@@ -161,14 +161,14 @@ export class ApiStatusMastodon {
body.in_reply_to_id,
removed,
);
- reply.send(a.data);
+ return reply.send(a.data);
}
if (body.in_reply_to_id && removed === '/unreact') {
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
const react = post.data.emoji_reactions.filter((e: Entity.Emoji) => e.me)[0].name;
const data = await client.deleteEmojiReaction(id, react);
- reply.send(data.data);
+ return reply.send(data.data);
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
@@ -194,7 +194,7 @@ export class ApiStatusMastodon {
const data = await client.postStatus(text, options);
const response = await this.mastoConverters.convertStatus(data.data as Entity.Status, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.put<{
@@ -233,7 +233,7 @@ export class ApiStatusMastodon {
const data = await client.editStatus(_request.params.id, options);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
@@ -243,7 +243,7 @@ export class ApiStatusMastodon {
const data = await client.createEmojiReaction(_request.params.id, '❤');
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
@@ -253,7 +253,7 @@ export class ApiStatusMastodon {
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
@@ -263,7 +263,7 @@ export class ApiStatusMastodon {
const data = await client.reblogStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
@@ -273,7 +273,7 @@ export class ApiStatusMastodon {
const data = await client.unreblogStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
@@ -283,7 +283,7 @@ export class ApiStatusMastodon {
const data = await client.bookmarkStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
@@ -293,7 +293,7 @@ export class ApiStatusMastodon {
const data = await client.unbookmarkStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
@@ -302,7 +302,7 @@ export class ApiStatusMastodon {
const data = await client.pinStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
@@ -312,7 +312,7 @@ export class ApiStatusMastodon {
const data = await client.unpinStatus(_request.params.id);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
@@ -323,7 +323,7 @@ export class ApiStatusMastodon {
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
@@ -334,7 +334,7 @@ export class ApiStatusMastodon {
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
const response = await this.mastoConverters.convertStatus(data.data, me);
- reply.send(response);
+ return reply.send(response);
});
fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
@@ -343,7 +343,7 @@ export class ApiStatusMastodon {
const client = this.clientService.getClient(_request);
const data = await client.deleteStatus(_request.params.id);
- reply.send(data.data);
+ return reply.send(data.data);
});
}
}
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index b6162d9eb2..b2f7b18dc9 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -28,7 +28,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (request, reply) => {
@@ -38,7 +38,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (request, reply) => {
@@ -50,7 +50,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (request, reply) => {
@@ -62,7 +62,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (request, reply) => {
@@ -72,7 +72,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
@@ -82,7 +82,7 @@ export class ApiTimelineMastodon {
const data = await client.getList(_request.params.id);
const response = convertList(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.get('/v1/lists', async (request, reply) => {
@@ -91,7 +91,7 @@ export class ApiTimelineMastodon {
const response = data.data.map((list: Entity.List) => convertList(list));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/lists/:id/accounts', async (request, reply) => {
@@ -102,7 +102,7 @@ export class ApiTimelineMastodon {
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
attachMinMaxPagination(request, reply, response);
- reply.send(response);
+ return reply.send(response);
});
fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
@@ -112,7 +112,7 @@ export class ApiTimelineMastodon {
const client = this.clientService.getClient(_request);
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
@@ -122,7 +122,7 @@ export class ApiTimelineMastodon {
const client = this.clientService.getClient(_request);
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
- reply.send(data.data);
+ return reply.send(data.data);
});
fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
@@ -132,7 +132,7 @@ export class ApiTimelineMastodon {
const data = await client.createList(_request.body.title);
const response = convertList(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
@@ -143,7 +143,7 @@ export class ApiTimelineMastodon {
const data = await client.updateList(_request.params.id, _request.body.title);
const response = convertList(data.data);
- reply.send(response);
+ return reply.send(response);
});
fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
@@ -152,7 +152,7 @@ export class ApiTimelineMastodon {
const client = this.clientService.getClient(_request);
await client.deleteList(_request.params.id);
- reply.send({});
+ return reply.send({});
});
}
}
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index a65acb7c9b..01ee451297 100644
--- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts
+++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
@@ -3,15 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import querystring from 'querystring';
import { Inject, Injectable } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
-import multer from 'fastify-multer';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import { getErrorData } from '@/server/api/mastodon/MastodonLogger.js';
+import { ServerUtilityService } from '@/server/ServerUtilityService.js';
import type { FastifyInstance } from 'fastify';
const kinds = [
@@ -56,6 +55,7 @@ export class OAuth2ProviderService {
private config: Config,
private readonly mastodonClientService: MastodonClientService,
+ private readonly serverUtilityService: ServerUtilityService,
) { }
// https://datatracker.ietf.org/doc/html/rfc8414.html
@@ -92,36 +92,10 @@ export class OAuth2ProviderService {
});
}); */
- const upload = multer({
- storage: multer.diskStorage({}),
- limits: {
- fileSize: this.config.maxFileSize || 262144000,
- files: 1,
- },
- });
-
- fastify.addHook('onRequest', (request, reply, done) => {
- reply.header('Access-Control-Allow-Origin', '*');
- done();
- });
-
- fastify.addContentTypeParser('application/x-www-form-urlencoded', (request, payload, done) => {
- let body = '';
- payload.on('data', (data) => {
- body += data;
- });
- payload.on('end', () => {
- try {
- const parsed = querystring.parse(body);
- done(null, parsed);
- } catch (e: unknown) {
- done(e instanceof Error ? e : new Error(String(e)));
- }
- });
- payload.on('error', done);
- });
-
- fastify.register(multer.contentParser);
+ this.serverUtilityService.addMultipartFormDataContentType(fastify);
+ this.serverUtilityService.addFormUrlEncodedContentType(fastify);
+ this.serverUtilityService.addCORS(fastify);
+ this.serverUtilityService.addFlattenedQueryType(fastify);
for (const url of ['/authorize', '/authorize/']) {
fastify.get<{ Querystring: Record<string, string | string[] | undefined> }>(url, async (request, reply) => {
@@ -132,11 +106,11 @@ export class OAuth2ProviderService {
if (request.query.state) redirectUri.searchParams.set('state', String(request.query.state));
if (request.query.redirect_uri) redirectUri.searchParams.set('redirect_uri', String(request.query.redirect_uri));
- reply.redirect(redirectUri.toString());
+ return reply.redirect(redirectUri.toString());
});
}
- fastify.post<{ Body?: Record<string, string | string[] | undefined>, Querystring: Record<string, string | string[] | undefined> }>('/token', { preHandler: upload.none() }, async (request, reply) => {
+ fastify.post<{ Body?: Record<string, string | string[] | undefined>, Querystring: Record<string, string | string[] | undefined> }>('/token', async (request, reply) => {
const body = request.body ?? request.query;
if (body.grant_type === 'client_credentials') {
@@ -146,7 +120,7 @@ export class OAuth2ProviderService {
scope: 'read',
created_at: Math.floor(new Date().getTime() / 1000),
};
- reply.send(ret);
+ return reply.send(ret);
}
try {
@@ -163,13 +137,13 @@ export class OAuth2ProviderService {
const ret = {
access_token: atData.accessToken,
token_type: 'Bearer',
- scope: body.scope || 'read write follow push',
- created_at: Math.floor(new Date().getTime() / 1000),
+ scope: atData.scope || body.scope || 'read write follow push',
+ created_at: atData.createdAt || Math.floor(new Date().getTime() / 1000),
};
- reply.send(ret);
+ return reply.send(ret);
} catch (e: unknown) {
const data = getErrorData(e);
- reply.code(401).send(data);
+ return reply.code(401).send(data);
}
});
}