summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-05-06 12:55:51 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-05-08 11:23:20 -0400
commitcd4fbc851b0fc766c93552971cb916e4ccd1ef55 (patch)
tree4ff9de42a5f63e66496833e7684b5437411876c7 /packages
parentmerge: Add missing paused state (!992) (diff)
downloadsharkey-cd4fbc851b0fc766c93552971cb916e4ccd1ef55.tar.gz
sharkey-cd4fbc851b0fc766c93552971cb916e4ccd1ef55.tar.bz2
sharkey-cd4fbc851b0fc766c93552971cb916e4ccd1ef55.zip
improve compatibility with multipart/form-data mastodon API requests
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/package.json1
-rw-r--r--packages/backend/src/server/ServerModule.ts2
-rw-r--r--packages/backend/src/server/ServerUtilityService.ts141
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts132
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/account.ts53
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/apps.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/filter.ts7
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/notifications.ts7
-rw-r--r--packages/backend/src/server/oauth/OAuth2ProviderService.ts40
-rw-r--r--packages/megalodon/src/misskey.ts6
10 files changed, 202 insertions, 192 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 9aa26033d0..4a9560e833 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -115,7 +115,6 @@
"deep-email-validator": "0.1.21",
"fast-xml-parser": "4.4.1",
"fastify": "5.3.2",
- "fastify-multer": "^2.0.3",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
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..f2900fad4f
--- /dev/null
+++ b/packages/backend/src/server/ServerUtilityService.ts
@@ -0,0 +1,141 @@
+/*
+ * 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';
+
+@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> }>('onRequest', async request => {
+ if (request.isMultipart()) {
+ const body = request.body ??= {};
+
+ // Save upload to temp directory.
+ // These are attached to request.savedRequestFiles
+ await request.saveRequestFiles();
+
+ // Copy fields to body
+ const formData = await request.formData();
+ formData.forEach((v, k) => {
+ // This can be string or File, and we handle files above.
+ if (typeof(v) === 'string') {
+ // This is just progressive conversion from undefined -> string -> string[]
+ if (body[k]) {
+ if (Array.isArray(body[k])) {
+ body[k].push(v);
+ } else {
+ body[k] = [body[k], v];
+ }
+ } else {
+ body[k] = v;
+ }
+ }
+ });
+ }
+ });
+ }
+
+ 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('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();
+ });
+ }
+
+ 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/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index 59ab3b71aa..757610450a 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -3,12 +3,8 @@
* 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 { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.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,97 +36,15 @@ 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);
fastify.setErrorHandler((error, request, reply) => {
const data = getErrorData(error);
@@ -143,14 +55,12 @@ export class MastodonApiServerService {
reply.code(status).send(data);
});
- fastify.register(multer.contentParser);
-
// 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);
@@ -178,11 +88,10 @@ export class MastodonApiServerService {
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);
@@ -192,11 +101,10 @@ export class MastodonApiServerService {
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);
@@ -294,7 +202,7 @@ export class MastodonApiServerService {
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);
@@ -304,7 +212,7 @@ export class MastodonApiServerService {
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);
@@ -325,7 +233,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 = {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 8bc3c14c15..b4ce56408e 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();
@@ -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 = {
@@ -234,7 +223,7 @@ export class ApiAccountMastodon {
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);
@@ -245,7 +234,7 @@ export class ApiAccountMastodon {
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);
@@ -256,7 +245,7 @@ export class ApiAccountMastodon {
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);
@@ -266,7 +255,7 @@ export class ApiAccountMastodon {
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);
@@ -289,7 +278,7 @@ export class ApiAccountMastodon {
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);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/apps.ts b/packages/backend/src/server/api/mastodon/endpoints/apps.ts
index dbef3b7d35..ec08600e53 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/apps.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/apps.ts
@@ -6,7 +6,6 @@
import { Injectable } from '@nestjs/common';
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import type { FastifyInstance } from 'fastify';
-import type multer from 'fastify-multer';
const readScope = [
'read:account',
@@ -62,8 +61,8 @@ export class ApiAppsMastodon {
private readonly clientService: MastodonClientService,
) {}
- 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"' });
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index deac1e9aad..242f068b99 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,7 +28,7 @@ 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);
@@ -49,7 +48,7 @@ export class ApiFilterMastodon {
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"' });
@@ -68,7 +67,7 @@ export class ApiFilterMastodon {
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"' });
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index ee6c990fd1..75512c2efc 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));
@@ -66,7 +65,7 @@ export class ApiNotificationsMastodon {
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);
@@ -75,7 +74,7 @@ export class ApiNotificationsMastodon {
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();
diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts
index a65acb7c9b..e1f39dd9b6 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) => {
@@ -136,7 +110,7 @@ export class OAuth2ProviderService {
});
}
- 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') {
diff --git a/packages/megalodon/src/misskey.ts b/packages/megalodon/src/misskey.ts
index 670b31e838..a7d604de26 100644
--- a/packages/megalodon/src/misskey.ts
+++ b/packages/megalodon/src/misskey.ts
@@ -1502,13 +1502,13 @@ export default class Misskey implements MegalodonInterface {
/**
* POST /api/drive/files/create
*/
- public async uploadMedia(file: any, _options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
+ public async uploadMedia(file: { filepath: fs.PathLike, mimetype: string, filename: string }, _options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
const formData = new FormData()
- formData.append('file', fs.createReadStream(file.path), {
+ formData.append('file', fs.createReadStream(file.filepath), {
contentType: file.mimetype,
});
- if (file.originalname != null && file.originalname !== "file") formData.append("name", file.originalname);
+ if (file.filename && file.filename !== "file") formData.append("name", file.filename);
if (_options?.description != null) formData.append("comment", _options.description);
let headers: { [key: string]: string } = {}