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 }>('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 }>('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; - - 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): void { + public register(fastify: FastifyInstance): void { fastify.get('/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('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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): void { - fastify.post('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => { + public register(fastify: FastifyInstance): void { + fastify.post('/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): 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('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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): void { + public register(fastify: FastifyInstance): void { fastify.get('/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('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => { + fastify.post('/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 }>(url, async (request, reply) => { @@ -136,7 +110,7 @@ export class OAuth2ProviderService { }); } - fastify.post<{ Body?: Record, Querystring: Record }>('/token', { preHandler: upload.none() }, async (request, reply) => { + fastify.post<{ Body?: Record, Querystring: Record }>('/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> { + public async uploadMedia(file: { filepath: fs.PathLike, mimetype: string, filename: string }, _options?: { description?: string; focus?: string }): Promise> { 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 } = {} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 691069f563..4eb78db1b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,9 +233,6 @@ importers: fastify: specifier: 5.3.2 version: 5.3.2 - fastify-multer: - specifier: ^2.0.3 - version: 2.0.3 fastify-raw-body: specifier: 5.0.0 version: 5.0.0 @@ -2274,10 +2271,6 @@ packages: '@fastify/ajv-compiler@4.0.1': resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==} - '@fastify/busboy@1.2.1': - resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} - engines: {node: '>=14'} - '@fastify/busboy@2.1.0': resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} engines: {node: '>=14'} @@ -5455,10 +5448,6 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -6313,13 +6302,6 @@ packages: resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} hasBin: true - fastify-multer@2.0.3: - resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==} - engines: {node: '>=10.17.0'} - - fastify-plugin@2.3.4: - resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==} - fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} @@ -9960,9 +9942,6 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-decoding@1.0.0: - resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} - textarea-caret@3.1.0: resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==} @@ -12060,10 +12039,6 @@ snapshots: ajv-formats: 3.0.1(ajv@8.17.1) fast-uri: 3.0.1 - '@fastify/busboy@1.2.1': - dependencies: - text-decoding: 1.0.0 - '@fastify/busboy@2.1.0': {} '@fastify/busboy@3.0.0': {} @@ -15990,13 +15965,6 @@ snapshots: readable-stream: 2.3.7 typedarray: 0.0.6 - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.0 - typedarray: 0.0.6 - config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -17122,21 +17090,6 @@ snapshots: dependencies: strnum: 1.0.5 - fastify-multer@2.0.3: - dependencies: - '@fastify/busboy': 1.2.1 - append-field: 1.0.0 - concat-stream: 2.0.0 - fastify-plugin: 2.3.4 - mkdirp: 1.0.4 - on-finished: 2.4.1 - type-is: 1.6.18 - xtend: 4.0.2 - - fastify-plugin@2.3.4: - dependencies: - semver: 7.6.0 - fastify-plugin@4.5.1: {} fastify-plugin@5.0.1: {} @@ -21438,8 +21391,6 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 - text-decoding@1.0.0: {} - textarea-caret@3.1.0: {} thenify-all@1.6.0: