From da25595ba306f1767883c9c3949dea446343def5 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Fri, 21 Mar 2025 20:38:28 -0400 Subject: [PATCH] de-duplicate mastodon API logging --- .../api/mastodon/MastodonApiServerService.ts | 266 ++++------ .../src/server/api/mastodon/MastodonLogger.ts | 124 ++++- .../server/api/mastodon/endpoints/account.ts | 426 ++++++---------- .../src/server/api/mastodon/endpoints/apps.ts | 90 ++-- .../server/api/mastodon/endpoints/filter.ts | 112 ++--- .../server/api/mastodon/endpoints/instance.ts | 130 +++-- .../api/mastodon/endpoints/notifications.ts | 78 +-- .../server/api/mastodon/endpoints/search.ts | 140 +++--- .../server/api/mastodon/endpoints/status.ts | 473 +++++++----------- .../server/api/mastodon/endpoints/timeline.ts | 217 +++----- 10 files changed, 827 insertions(+), 1229 deletions(-) diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts index 7a4611fd74..17f706e617 100644 --- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts +++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts @@ -9,7 +9,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Config } from '@/config.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.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'; import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js'; @@ -74,6 +74,15 @@ export class MastodonApiServerService { payload.on('error', done); }); + fastify.setErrorHandler((error, request, reply) => { + const data = getErrorData(error); + const status = getErrorStatus(error); + + this.logger.error(request, data, status); + + reply.code(status).send(data); + }); + fastify.register(multer.contentParser); // External endpoints @@ -87,98 +96,56 @@ export class MastodonApiServerService { this.apiTimelineMastodon.register(fastify); fastify.get('/v1/custom_emojis', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getInstanceCustomEmojis(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/custom_emojis', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getInstanceCustomEmojis(); + reply.send(data.data); }); fastify.get('/v1/announcements', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getInstanceAnnouncements(); - reply.send(data.data.map((announcement) => convertAnnouncement(announcement))); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/announcements', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getInstanceAnnouncements(); + reply.send(data.data.map((announcement) => convertAnnouncement(announcement))); }); fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => { - try { - if (!_request.body.id) return reply.code(400).send({ error: 'Missing required payload "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.dismissInstanceAnnouncement(_request.body['id']); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/announcements/${_request.body.id}/dismiss`, data); - reply.code(401).send(data); - } + if (!_request.body.id) return reply.code(400).send({ error: 'Missing required payload "id"' }); + const client = this.clientService.getClient(_request); + const data = await client.dismissInstanceAnnouncement(_request.body['id']); + reply.send(data.data); }); fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => { - try { - const multipartData = await _request.file(); - if (!multipartData) { - reply.code(401).send({ error: 'No image' }); - return; - } - const client = this.clientService.getClient(_request); - const data = await client.uploadMedia(multipartData); - reply.send(convertAttachment(data.data as Entity.Attachment)); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v1/media', data); - reply.code(401).send(data); + const multipartData = await _request.file(); + if (!multipartData) { + reply.code(401).send({ error: 'No image' }); + return; } + const client = this.clientService.getClient(_request); + const data = await client.uploadMedia(multipartData); + reply.send(convertAttachment(data.data as Entity.Attachment)); }); fastify.post<{ Body: { description?: string; focus?: string }}>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => { - try { - const multipartData = await _request.file(); - if (!multipartData) { - reply.code(401).send({ error: 'No image' }); - return; - } - const client = this.clientService.getClient(_request); - const data = await client.uploadMedia(multipartData, _request.body); - reply.send(convertAttachment(data.data as Entity.Attachment)); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v2/media', data); - reply.code(401).send(data); + const multipartData = await _request.file(); + if (!multipartData) { + reply.code(401).send({ error: 'No image' }); + return; } + const client = this.clientService.getClient(_request); + const data = await client.uploadMedia(multipartData, _request.body); + reply.send(convertAttachment(data.data as Entity.Attachment)); }); fastify.get('/v1/trends', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getInstanceTrends(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/trends', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getInstanceTrends(); + reply.send(data.data); }); fastify.get('/v1/trends/tags', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getInstanceTrends(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/trends/tags', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getInstanceTrends(); + reply.send(data.data); }); fastify.get('/v1/trends/links', async (_request, reply) => { @@ -187,132 +154,81 @@ export class MastodonApiServerService { }); fastify.get('/v1/preferences', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getPreferences(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/preferences', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getPreferences(); + reply.send(data.data); }); fastify.get('/v1/followed_tags', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getFollowedTags(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/followed_tags', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + const data = await client.getFollowedTags(); + reply.send(data.data); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); + const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getBookmarks(parseTimelineArgs(_request.query)); - const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me))); + 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); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/bookmarks', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); + const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getFavourites(parseTimelineArgs(_request.query)); - const response = Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me))); + const data = await client.getFavourites(parseTimelineArgs(_request.query)); + const response = Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/favourites', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); + const client = this.clientService.getClient(_request); - const data = await client.getMutes(parseTimelineArgs(_request.query)); - const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account))); + const data = await client.getMutes(parseTimelineArgs(_request.query)); + const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/mutes', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); + const client = this.clientService.getClient(_request); - const data = await client.getBlocks(parseTimelineArgs(_request.query)); - const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account))); + const data = await client.getBlocks(parseTimelineArgs(_request.query)); + const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/blocks', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: { limit?: string }}>('/v1/follow_requests', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const limit = _request.query.limit ? parseInt(_request.query.limit) : 20; - const data = await client.getFollowRequests(limit); - reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account as Entity.Account)))); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/follow_requests', data); - reply.code(401).send(data); - } + const client = this.clientService.getClient(_request); + + const limit = _request.query.limit ? parseInt(_request.query.limit) : 20; + 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); }); fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.acceptFollowRequest(_request.params.id); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.acceptFollowRequest(_request.params.id); + const response = convertRelationship(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/follow_requests/${_request.params.id}/authorize`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.rejectFollowRequest(_request.params.id); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.rejectFollowRequest(_request.params.id); + const response = convertRelationship(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/follow_requests/${_request.params.id}/reject`, data); - reply.code(401).send(data); - } + reply.send(response); }); //#endregion @@ -327,23 +243,17 @@ export class MastodonApiServerService { is_sensitive?: string, }, }>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const options = { - ..._request.body, - is_sensitive: toBoolean(_request.body.is_sensitive), - }; - const client = this.clientService.getClient(_request); - const data = await client.updateMedia(_request.params.id, options); - const response = convertAttachment(data.data); + const options = { + ..._request.body, + is_sensitive: toBoolean(_request.body.is_sensitive), + }; + const client = this.clientService.getClient(_request); + const data = await client.updateMedia(_request.params.id, options); + const response = convertAttachment(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`PUT /v1/media/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); done(); diff --git a/packages/backend/src/server/api/mastodon/MastodonLogger.ts b/packages/backend/src/server/api/mastodon/MastodonLogger.ts index bb844773c4..c7bca22922 100644 --- a/packages/backend/src/server/api/mastodon/MastodonLogger.ts +++ b/packages/backend/src/server/api/mastodon/MastodonLogger.ts @@ -3,37 +3,137 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; -import Logger, { Data } from '@/logger.js'; +import { Inject, Injectable } from '@nestjs/common'; +import Logger from '@/logger.js'; import { LoggerService } from '@/core/LoggerService.js'; +import { ApiError } from '@/server/api/error.js'; +import { EnvService } from '@/core/EnvService.js'; +import { FastifyRequest } from 'fastify'; @Injectable() export class MastodonLogger { public readonly logger: Logger; - constructor(loggerService: LoggerService) { + constructor( + @Inject(EnvService) + private readonly envService: EnvService, + + loggerService: LoggerService, + ) { this.logger = loggerService.getLogger('masto-api'); } - public error(endpoint: string, error: Data): void { - this.logger.error(`Error in mastodon API endpoint ${endpoint}:`, error); + public error(request: FastifyRequest, error: MastodonError, status: number): void { + if ((status < 400 && status > 499) || this.envService.env.NODE_ENV === 'development') { + this.logger.error(`Error in mastodon endpoint ${request.method} ${request.url}:`, error); + } } } -export function getErrorData(error: unknown): Data { - if (error == null) return {}; - if (typeof(error) === 'string') return error; - if (typeof(error) === 'object') { +// TODO move elsewhere +export interface MastodonError { + error: string; + error_description: string; +} + +export function getErrorData(error: unknown): MastodonError { + if (error && typeof(error) === 'object') { + // AxiosError, comes from the backend if ('response' in error) { if (typeof(error.response) === 'object' && error.response) { if ('data' in error.response) { if (typeof(error.response.data) === 'object' && error.response.data) { - return error.response.data as Record; + if ('error' in error.response.data) { + if (typeof(error.response.data.error) === 'object' && error.response.data.error) { + if ('code' in error.response.data.error) { + if (typeof(error.response.data.error.code) === 'string') { + return convertApiError(error.response.data.error as ApiError); + } + } + + return convertUnknownError(error.response.data.error); + } + } + + return convertUnknownError(error.response.data); + } + } + } + + // No data - this is a fallback to avoid leaking request/response details in the error + return convertUnknownError(); + } + + if (error instanceof ApiError) { + return convertApiError(error); + } + + if (error instanceof Error) { + return convertGenericError(error); + } + + return convertUnknownError(error); + } + + return { + error: 'UNKNOWN_ERROR', + error_description: String(error), + }; +} + +function convertApiError(apiError: ApiError): MastodonError { + const mastoError: MastodonError & Partial = { + error: apiError.code, + error_description: apiError.message, + ...apiError, + }; + + delete mastoError.code; + delete mastoError.message; + + 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 convertGenericError(error: Error): MastodonError { + const mastoError: MastodonError & Partial = { + error: 'INTERNAL_ERROR', + error_description: String(error), + ...error, + }; + + delete mastoError.name; + delete mastoError.message; + delete mastoError.stack; + + return mastoError; +} + +export function getErrorStatus(error: unknown): number { + // AxiosError, comes from the backend + if (typeof(error) === 'object' && error) { + if ('response' in error) { + if (typeof (error.response) === 'object' && error.response) { + if ('status' in error.response) { + if (typeof(error.response.status) === 'number') { + return error.response.status; } } } } - return error as Record; } - return { error }; + + if (error instanceof ApiError && error.httpStatusCode) { + return error.httpStatusCode; + } + + 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 a5d7d89f7d..6ae6ea7c6a 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/account.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { parseTimelineArgs, TimelineArgs, toBoolean } from '@/server/api/mastodon/argsUtils.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { DriveService } from '@/core/DriveService.js'; import { DI } from '@/di-symbols.js'; @@ -31,32 +30,25 @@ export class ApiAccountMastodon { private readonly clientService: MastodonClientService, private readonly mastoConverters: MastoConverters, - private readonly logger: MastodonLogger, private readonly driveService: DriveService, ) {} public register(fastify: FastifyInstance, upload: ReturnType): void { fastify.get('/v1/accounts/verify_credentials', async (_request, reply) => { - try { - const client = await this.clientService.getClient(_request); - const data = await client.verifyAccountCredentials(); - const acct = await this.mastoConverters.convertAccount(data.data); - const response = Object.assign({}, acct, { - source: { - // TODO move these into the convertAccount logic directly - note: acct.note, - fields: acct.fields, - privacy: '', - sensitive: false, - language: '', - }, - }); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/accounts/verify_credentials', data); - reply.code(401).send(data); - } + const client = await this.clientService.getClient(_request); + const data = await client.verifyAccountCredentials(); + const acct = await this.mastoConverters.convertAccount(data.data); + const response = Object.assign({}, acct, { + source: { + // TODO move these into the convertAccount logic directly + note: acct.note, + fields: acct.fields, + privacy: '', + sensitive: false, + language: '', + }, + }); + reply.send(response); }); fastify.patch<{ @@ -80,318 +72,230 @@ export class ApiAccountMastodon { }, }>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => { const accessTokens = _request.headers.authorization; - try { - 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) { - 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) => { - return obj.fieldname === 'avatar'; - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const header = (_request.files as any).find((obj: any) => { - return obj.fieldname === 'header'; - }); + 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) { + 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) => { + return obj.fieldname === 'avatar'; + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const header = (_request.files as any).find((obj: any) => { + 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, - sensitive: false, - }); - if (upload.type.startsWith('image/')) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_request.body as any).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, - sensitive: false, - }); - if (upload.type.startsWith('image/')) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (_request.body as any).header = upload.id; - } + 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, + sensitive: false, + }); + if (upload.type.startsWith('image/')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_request.body as any).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, + sensitive: false, + }); + if (upload.type.startsWith('image/')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (_request.body as any).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 (!(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); - } - - const options = { - ..._request.body, - discoverable: toBoolean(_request.body.discoverable), - bot: toBoolean(_request.body.bot), - locked: toBoolean(_request.body.locked), - source: _request.body.source ? { - ..._request.body.source, - sensitive: toBoolean(_request.body.source.sensitive), - } : undefined, - }; - const data = await client.updateCredentials(options); - reply.send(await this.mastoConverters.convertAccount(data.data)); - } catch (e) { - const data = getErrorData(e); - this.logger.error('PATCH /v1/accounts/update_credentials', data); - reply.code(401).send(data); } + + // 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 (!(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); + } + + const options = { + ..._request.body, + discoverable: toBoolean(_request.body.discoverable), + bot: toBoolean(_request.body.bot), + locked: toBoolean(_request.body.locked), + source: _request.body.source ? { + ..._request.body.source, + sensitive: toBoolean(_request.body.source.sensitive), + } : undefined, + }; + const data = await client.updateCredentials(options); + const response = await this.mastoConverters.convertAccount(data.data); + + reply.send(response); }); fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => { - try { - if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' }); + if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' }); - const client = this.clientService.getClient(_request); - const data = await client.search(_request.query.acct, { type: 'accounts' }); - const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id }); - data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? []; - const response = await this.mastoConverters.convertAccount(data.data.accounts[0]); + const client = this.clientService.getClient(_request); + const data = await client.search(_request.query.acct, { type: 'accounts' }); + const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id }); + 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); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/accounts/lookup', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/accounts/relationships', async (_request, reply) => { - try { - let ids = _request.query['id[]'] ?? _request.query['id'] ?? []; - if (typeof ids === 'string') { - ids = [ids]; - } - - const client = this.clientService.getClient(_request); - const data = await client.getRelationships(ids); - const response = data.data.map(relationship => convertRelationship(relationship)); - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/accounts/relationships', data); - reply.code(401).send(data); + let ids = _request.query['id[]'] ?? _request.query['id'] ?? []; + if (typeof ids === 'string') { + ids = [ids]; } + + const client = this.clientService.getClient(_request); + const data = await client.getRelationships(ids); + const response = data.data.map(relationship => convertRelationship(relationship)); + + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getAccount(_request.params.id); - const account = await this.mastoConverters.convertAccount(data.data); + const client = this.clientService.getClient(_request); + const data = await client.getAccount(_request.params.id); + const account = await this.mastoConverters.convertAccount(data.data); - reply.send(account); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(account); }); fastify.get('/v1/accounts/:id/statuses', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getAccountStatuses(_request.params.id, parseTimelineArgs(_request.query)); - const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me))); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.getAccountStatuses(_request.params.id, parseTimelineArgs(_request.query)); + const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}/statuses`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getFeaturedTags(); - const response = data.data.map((tag) => convertFeaturedTag(tag)); + const client = this.clientService.getClient(_request); + const data = await client.getFeaturedTags(); + const response = data.data.map((tag) => convertFeaturedTag(tag)); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}/featured_tags`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/accounts/:id/followers', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getAccountFollowers( - _request.params.id, - parseTimelineArgs(_request.query), - ); - const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account))); + const client = this.clientService.getClient(_request); + const data = await client.getAccountFollowers( + _request.params.id, + parseTimelineArgs(_request.query), + ); + const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}/followers`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/accounts/:id/following', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getAccountFollowing( - _request.params.id, - parseTimelineArgs(_request.query), - ); - const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account))); + const client = this.clientService.getClient(_request); + const data = await client.getAccountFollowing( + _request.params.id, + parseTimelineArgs(_request.query), + ); + const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}/following`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getAccountLists(_request.params.id); - const response = data.data.map((list) => convertList(list)); + const client = this.clientService.getClient(_request); + const data = await client.getAccountLists(_request.params.id); + const response = data.data.map((list) => convertList(list)); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/accounts/${_request.params.id}/lists`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.followAccount(_request.params.id); - const acct = convertRelationship(data.data); - acct.following = true; + const client = this.clientService.getClient(_request); + const data = await client.followAccount(_request.params.id); + const acct = convertRelationship(data.data); + acct.following = true; - reply.send(acct); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/follow`, data); - reply.code(401).send(data); - } + reply.send(acct); }); fastify.post('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.unfollowAccount(_request.params.id); - const acct = convertRelationship(data.data); - acct.following = false; + const client = this.clientService.getClient(_request); + const data = await client.unfollowAccount(_request.params.id); + const acct = convertRelationship(data.data); + acct.following = false; - reply.send(acct); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/unfollow`, data); - reply.code(401).send(data); - } + reply.send(acct); }); fastify.post('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.blockAccount(_request.params.id); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.blockAccount(_request.params.id); + const response = convertRelationship(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/block`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.unblockAccount(_request.params.id); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.unblockAccount(_request.params.id); + const response = convertRelationship(data.data); - return reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/unblock`, data); - reply.code(401).send(data); - } + return reply.send(response); }); fastify.post('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.muteAccount( - _request.params.id, - _request.body.notifications ?? true, - ); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.muteAccount( + _request.params.id, + _request.body.notifications ?? true, + ); + const response = convertRelationship(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/mute`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.unmuteAccount(_request.params.id); - const response = convertRelationship(data.data); + const client = this.clientService.getClient(_request); + const data = await client.unmuteAccount(_request.params.id); + const response = convertRelationship(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/accounts/${_request.params.id}/unmute`, data); - reply.code(401).send(data); - } + 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 17b9ba889d..e1c5f27739 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/apps.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/apps.ts @@ -4,7 +4,6 @@ */ import { Injectable } from '@nestjs/common'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import type { FastifyInstance } from 'fastify'; import type multer from 'fastify-multer'; @@ -61,60 +60,53 @@ type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload }; export class ApiAppsMastodon { constructor( private readonly clientService: MastodonClientService, - private readonly logger: MastodonLogger, ) {} public register(fastify: FastifyInstance, upload: ReturnType): void { fastify.post('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - const body = _request.body ?? _request.query; - if (!body.scopes) return reply.code(400).send({ error: 'Missing required payload "scopes"' }); - if (!body.redirect_uris) return reply.code(400).send({ error: 'Missing required payload "redirect_uris"' }); - if (!body.client_name) return reply.code(400).send({ error: 'Missing required payload "client_name"' }); + const body = _request.body ?? _request.query; + if (!body.scopes) return reply.code(400).send({ error: 'Missing required payload "scopes"' }); + if (!body.redirect_uris) return reply.code(400).send({ error: 'Missing required payload "redirect_uris"' }); + if (!body.client_name) return reply.code(400).send({ error: 'Missing required payload "client_name"' }); - let scope = body.scopes; - if (typeof scope === 'string') { - scope = scope.split(/[ +]/g); - } - - const pushScope = new Set(); - for (const s of scope) { - if (s.match(/^read/)) { - for (const r of readScope) { - pushScope.add(r); - } - } - if (s.match(/^write/)) { - for (const r of writeScope) { - pushScope.add(r); - } - } - } - - 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, - website: body.website, - }); - - const response = { - id: Math.floor(Math.random() * 100).toString(), - name: appData.name, - website: body.website, - redirect_uri: red, - client_id: Buffer.from(appData.url || '').toString('base64'), - client_secret: appData.clientSecret, - }; - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/apps', data); - reply.code(401).send(data); + let scope = body.scopes; + if (typeof scope === 'string') { + scope = scope.split(/[ +]/g); } + + const pushScope = new Set(); + for (const s of scope) { + if (s.match(/^read/)) { + for (const r of readScope) { + pushScope.add(r); + } + } + if (s.match(/^write/)) { + for (const r of writeScope) { + pushScope.add(r); + } + } + } + + 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, + website: body.website, + }); + + const response = { + id: Math.floor(Math.random() * 100).toString(), + name: appData.name, + website: body.website, + redirect_uri: red, + client_id: Buffer.from(appData.url || '').toString('base64'), + client_secret: appData.clientSecret, + }; + + 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 10353ff7af..7f986974fc 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts @@ -6,7 +6,6 @@ import { Injectable } from '@nestjs/common'; import { toBoolean } from '@/server/api/mastodon/argsUtils.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { convertFilter } from '../converters.js'; import type { FastifyInstance } from 'fastify'; import type multer from 'fastify-multer'; @@ -28,105 +27,74 @@ interface ApiFilterMastodonRoute { export class ApiFilterMastodon { constructor( private readonly clientService: MastodonClientService, - private readonly logger: MastodonLogger, ) {} public register(fastify: FastifyInstance, upload: ReturnType): void { fastify.get('/v1/filters', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); + const client = this.clientService.getClient(_request); - const data = await client.getFilters(); - const response = data.data.map((filter) => convertFilter(filter)); + const data = await client.getFilters(); + const response = data.data.map((filter) => convertFilter(filter)); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/filters', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/filters/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getFilter(_request.params.id); - const response = convertFilter(data.data); + const client = this.clientService.getClient(_request); + const data = await client.getFilter(_request.params.id); + const response = convertFilter(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/filters/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' }); - if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' }); + if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' }); + if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' }); - const options = { - phrase: _request.body.phrase, - context: _request.body.context, - irreversible: toBoolean(_request.body.irreversible), - whole_word: toBoolean(_request.body.whole_word), - expires_in: _request.body.expires_in, - }; + const options = { + phrase: _request.body.phrase, + context: _request.body.context, + irreversible: toBoolean(_request.body.irreversible), + whole_word: toBoolean(_request.body.whole_word), + expires_in: _request.body.expires_in, + }; - const client = this.clientService.getClient(_request); - const data = await client.createFilter(_request.body.phrase, _request.body.context, options); - const response = convertFilter(data.data); + const client = this.clientService.getClient(_request); + const data = await client.createFilter(_request.body.phrase, _request.body.context, options); + const response = convertFilter(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v1/filters', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' }); - if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' }); + if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' }); - const options = { - phrase: _request.body.phrase, - context: _request.body.context, - irreversible: toBoolean(_request.body.irreversible), - whole_word: toBoolean(_request.body.whole_word), - expires_in: _request.body.expires_in, - }; + const options = { + phrase: _request.body.phrase, + context: _request.body.context, + irreversible: toBoolean(_request.body.irreversible), + whole_word: toBoolean(_request.body.whole_word), + expires_in: _request.body.expires_in, + }; - const client = this.clientService.getClient(_request); - const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options); - const response = convertFilter(data.data); + const client = this.clientService.getClient(_request); + const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options); + const response = convertFilter(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/filters/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.delete('/v1/filters/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.deleteFilter(_request.params.id); + const client = this.clientService.getClient(_request); + const data = await client.deleteFilter(_request.params.id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`DELETE /v1/filters/${_request.params.id}`, data); - reply.code(401).send(data); - } + 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 ffffb5e537..bc7ef69100 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/instance.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/instance.ts @@ -10,12 +10,9 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import type { MiMeta, UsersRepository } from '@/models/_.js'; import { MastoConverters } from '@/server/api/mastodon/converters.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import type { FastifyInstance } from 'fastify'; -// TODO rename to ApiInstanceMastodon - @Injectable() export class ApiInstanceMastodon { constructor( @@ -29,82 +26,75 @@ export class ApiInstanceMastodon { private readonly config: Config, private readonly mastoConverters: MastoConverters, - private readonly logger: MastodonLogger, private readonly clientService: MastodonClientService, ) {} public register(fastify: FastifyInstance): void { fastify.get('/v1/instance', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getInstance(); - const instance = data.data; - const admin = await this.usersRepository.findOne({ - where: { - host: IsNull(), - isRoot: true, - isDeleted: false, - isSuspended: false, - }, - order: { id: 'ASC' }, - }); - const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data); + const client = this.clientService.getClient(_request); + const data = await client.getInstance(); + const instance = data.data; + const admin = await this.usersRepository.findOne({ + where: { + host: IsNull(), + isRoot: true, + isDeleted: false, + isSuspended: false, + }, + order: { id: 'ASC' }, + }); + const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data); - const response = { - uri: this.config.url, - title: this.meta.name || 'Sharkey', - short_description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.', - description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.', - email: instance.email || '', - version: `3.0.0 (compatible; Sharkey ${this.config.version})`, - urls: instance.urls, - stats: { - user_count: instance.stats.user_count, - status_count: instance.stats.status_count, - domain_count: instance.stats.domain_count, + const response = { + uri: this.config.url, + title: this.meta.name || 'Sharkey', + short_description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.', + description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.', + email: instance.email || '', + version: `3.0.0 (compatible; Sharkey ${this.config.version})`, + urls: instance.urls, + stats: { + user_count: instance.stats.user_count, + status_count: instance.stats.status_count, + domain_count: instance.stats.domain_count, + }, + thumbnail: this.meta.backgroundImageUrl || '/static-assets/transparent.png', + languages: this.meta.langs, + registrations: !this.meta.disableRegistration || instance.registrations, + approval_required: this.meta.approvalRequiredForSignup, + invites_enabled: instance.registrations, + configuration: { + accounts: { + max_featured_tags: 20, }, - thumbnail: this.meta.backgroundImageUrl || '/static-assets/transparent.png', - languages: this.meta.langs, - registrations: !this.meta.disableRegistration || instance.registrations, - approval_required: this.meta.approvalRequiredForSignup, - invites_enabled: instance.registrations, - configuration: { - accounts: { - max_featured_tags: 20, - }, - statuses: { - max_characters: this.config.maxNoteLength, - max_media_attachments: 16, - characters_reserved_per_url: instance.uri.length, - }, - media_attachments: { - supported_mime_types: FILE_TYPE_BROWSERSAFE, - image_size_limit: 10485760, - image_matrix_limit: 16777216, - video_size_limit: 41943040, - video_frame_rate_limit: 60, - video_matrix_limit: 2304000, - }, - polls: { - max_options: 10, - max_characters_per_option: 150, - min_expiration: 50, - max_expiration: 2629746, - }, - reactions: { - max_reactions: 1, - }, + statuses: { + max_characters: this.config.maxNoteLength, + max_media_attachments: 16, + characters_reserved_per_url: instance.uri.length, }, - contact_account: contact, - rules: [], - }; + media_attachments: { + supported_mime_types: FILE_TYPE_BROWSERSAFE, + image_size_limit: 10485760, + image_matrix_limit: 16777216, + video_size_limit: 41943040, + video_frame_rate_limit: 60, + video_matrix_limit: 2304000, + }, + polls: { + max_options: 10, + max_characters_per_option: 150, + min_expiration: 50, + max_expiration: 2629746, + }, + reactions: { + max_reactions: 1, + }, + }, + contact_account: contact, + rules: [], + }; - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/instance', data); - reply.code(401).send(data); - } + 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 0dba247e5f..27e6cbcd0d 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -6,7 +6,6 @@ import { Injectable } from '@nestjs/common'; import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/argsUtils.js'; import { MastoConverters } from '@/server/api/mastodon/converters.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '../MastodonClientService.js'; import type { FastifyInstance } from 'fastify'; import type multer from 'fastify-multer'; @@ -23,75 +22,50 @@ export class ApiNotificationsMastodon { constructor( private readonly mastoConverters: MastoConverters, private readonly clientService: MastodonClientService, - private readonly logger: MastodonLogger, ) {} public register(fastify: FastifyInstance, upload: ReturnType): void { fastify.get('/v1/notifications', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getNotifications(parseTimelineArgs(_request.query)); - const response = Promise.all(data.data.map(async n => { - const converted = await this.mastoConverters.convertNotification(n, me); - if (converted.type === 'reaction') { - converted.type = 'favourite'; - } - return converted; - })); - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/notifications', data); - reply.code(401).send(data); - } - }); - - fastify.get('/v1/notification/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getNotification(_request.params.id); - const converted = await this.mastoConverters.convertNotification(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.getNotifications(parseTimelineArgs(_request.query)); + const response = Promise.all(data.data.map(async n => { + const converted = await this.mastoConverters.convertNotification(n, me); if (converted.type === 'reaction') { converted.type = 'favourite'; } + return converted; + })); - reply.send(converted); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/notification/${_request.params.id}`, data); - reply.code(401).send(data); + reply.send(response); + }); + + fastify.get('/v1/notification/:id', async (_request, reply) => { + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.getNotification(_request.params.id); + const converted = await this.mastoConverters.convertNotification(data.data, me); + if (converted.type === 'reaction') { + converted.type = 'favourite'; } + + reply.send(converted); }); fastify.post('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.dismissNotification(_request.params.id); + const client = this.clientService.getClient(_request); + const data = await client.dismissNotification(_request.params.id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/notification/${_request.params.id}/dismiss`, data); - reply.code(401).send(data); - } + reply.send(data.data); }); fastify.post('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.dismissNotifications(); + const client = this.clientService.getClient(_request); + const data = await client.dismissNotifications(); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v1/notifications/clear', data); - reply.code(401).send(data); - } + 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 3b1c984c3e..814e2cf776 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/search.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts @@ -5,7 +5,6 @@ import { Injectable } from '@nestjs/common'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastoConverters } from '../converters.js'; import { parseTimelineArgs, TimelineArgs } from '../argsUtils.js'; import Account = Entity.Account; @@ -24,107 +23,82 @@ export class ApiSearchMastodon { constructor( private readonly mastoConverters: MastoConverters, private readonly clientService: MastodonClientService, - private readonly logger: MastodonLogger, ) {} public register(fastify: FastifyInstance): void { fastify.get('/v1/search', async (_request, reply) => { - try { - if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' }); + if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' }); - const query = parseTimelineArgs(_request.query); - const client = this.clientService.getClient(_request); - const data = await client.search(_request.query.q, { type: _request.query.type, ...query }); + const query = parseTimelineArgs(_request.query); + const client = this.clientService.getClient(_request); + const data = await client.search(_request.query.q, { type: _request.query.type, ...query }); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/search', data); - reply.code(401).send(data); - } + reply.send(data.data); }); fastify.get('/v2/search', async (_request, reply) => { - try { - if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' }); + if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' }); - const query = parseTimelineArgs(_request.query); - const type = _request.query.type; - const { client, me } = await this.clientService.getAuthClient(_request); - const acct = !type || type === 'accounts' ? await client.search(_request.query.q, { type: 'accounts', ...query }) : null; - const stat = !type || type === 'statuses' ? await client.search(_request.query.q, { type: 'statuses', ...query }) : null; - const tags = !type || type === 'hashtags' ? await client.search(_request.query.q, { type: 'hashtags', ...query }) : null; - const response = { - accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []), - statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, me)) ?? []), - hashtags: tags?.data.hashtags ?? [], - }; + const query = parseTimelineArgs(_request.query); + const type = _request.query.type; + const { client, me } = await this.clientService.getAuthClient(_request); + const acct = !type || type === 'accounts' ? await client.search(_request.query.q, { type: 'accounts', ...query }) : null; + const stat = !type || type === 'statuses' ? await client.search(_request.query.q, { type: 'statuses', ...query }) : null; + const tags = !type || type === 'hashtags' ? await client.search(_request.query.q, { type: 'hashtags', ...query }) : null; + const response = { + accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []), + statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, me)) ?? []), + hashtags: tags?.data.hashtags ?? [], + }; - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v2/search', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/trends/statuses', async (_request, reply) => { - try { - const baseUrl = this.clientService.getBaseUrl(_request); - const res = await fetch(`${baseUrl}/api/notes/featured`, - { - method: 'POST', - headers: { - ..._request.headers as HeadersInit, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: '{}', - }); - const data = await res.json() as Status[]; - const me = await this.clientService.getAuth(_request); - const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me))); + const baseUrl = this.clientService.getBaseUrl(_request); + const res = await fetch(`${baseUrl}/api/notes/featured`, + { + method: 'POST', + headers: { + ..._request.headers as HeadersInit, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: '{}', + }); + const data = await res.json() as Status[]; + const me = await this.clientService.getAuth(_request); + const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/trends/statuses', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v2/suggestions', async (_request, reply) => { - try { - const baseUrl = this.clientService.getBaseUrl(_request); - const res = await fetch(`${baseUrl}/api/users`, - { - method: 'POST', - headers: { - ..._request.headers as HeadersInit, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - limit: parseTimelineArgs(_request.query).limit ?? 20, - origin: 'local', - sort: '+follower', - state: 'alive', - }), - }); - const data = await res.json() as Account[]; - const response = await Promise.all(data.map(async entry => { - return { - source: 'global', - account: await this.mastoConverters.convertAccount(entry), - }; - })); + const baseUrl = this.clientService.getBaseUrl(_request); + const res = await fetch(`${baseUrl}/api/users`, + { + method: 'POST', + headers: { + ..._request.headers as HeadersInit, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + limit: parseTimelineArgs(_request.query).limit ?? 20, + origin: 'local', + sort: '+follower', + state: 'alive', + }), + }); + const data = await res.json() as Account[]; + const response = await Promise.all(data.map(async entry => { + return { + source: 'global', + account: await this.mastoConverters.convertAccount(entry), + }; + })); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v2/suggestions', data); - reply.code(401).send(data); - } + 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 ba61918b75..8b9ccf44b6 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/status.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts @@ -6,7 +6,6 @@ import querystring, { ParsedUrlQueryInput } from 'querystring'; import { Injectable } from '@nestjs/common'; import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/argsUtils.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { convertAttachment, convertPoll, MastoConverters } from '../converters.js'; @@ -22,154 +21,99 @@ function normalizeQuery(data: Record) { export class ApiStatusMastodon { constructor( private readonly mastoConverters: MastoConverters, - private readonly logger: MastodonLogger, private readonly clientService: MastodonClientService, ) {} public register(fastify: FastifyInstance): void { fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.getStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.getStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}`, data); - reply.code(_request.is404 ? 404 : 401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getStatusSource(_request.params.id); + const client = this.clientService.getClient(_request); + const data = await client.getStatusSource(_request.params.id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data); - reply.code(_request.is404 ? 404 : 401).send(data); - } + reply.send(data.data); }); fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query)); - const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me))); - const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me))); - const response = { ancestors, descendants }; + const { client, me } = await this.clientService.getAuthClient(_request); + const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query)); + const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me))); + const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me))); + const response = { ancestors, descendants }; - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data); - reply.code(_request.is404 ? 404 : 401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const user = await this.clientService.getAuth(_request); - const edits = await this.mastoConverters.getEdits(_request.params.id, user); + const user = await this.clientService.getAuth(_request); + const edits = await this.mastoConverters.getEdits(_request.params.id, user); - reply.send(edits); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data); - reply.code(401).send(data); - } + reply.send(edits); }); fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getStatusRebloggedBy(_request.params.id); - const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account))); + const client = this.clientService.getClient(_request); + 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); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getStatusFavouritedBy(_request.params.id); - const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account))); + const client = this.clientService.getClient(_request); + 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); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getMedia(_request.params.id); - const response = convertAttachment(data.data); + const client = this.clientService.getClient(_request); + const data = await client.getMedia(_request.params.id); + const response = convertAttachment(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/media/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getPoll(_request.params.id); - const response = convertPoll(data.data); + const client = this.clientService.getClient(_request); + const data = await client.getPoll(_request.params.id); + const response = convertPoll(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/polls/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' }); - const client = this.clientService.getClient(_request); - const data = await client.votePoll(_request.params.id, _request.body.choices); - const response = convertPoll(data.data); + const client = this.clientService.getClient(_request); + const data = await client.votePoll(_request.params.id, _request.body.choices); + const response = convertPoll(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ @@ -196,60 +140,55 @@ export class ApiStatusMastodon { } }>('/v1/statuses', async (_request, reply) => { let body = _request.body; - try { - const { client, me } = await this.clientService.getAuthClient(_request); - if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]']) - ) { - body = normalizeQuery(body); - } - const text = body.status ??= ' '; - const removed = text.replace(/@\S+/g, '').replace(/\s|/g, ''); - const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); - const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); - if ((body.in_reply_to_id && isDefaultEmoji) || (body.in_reply_to_id && isCustomEmoji)) { - const a = await client.createEmojiReaction( - body.in_reply_to_id, - removed, - ); - 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 => e.me)[0].name; - const data = await client.deleteEmojiReaction(id, react); - reply.send(data.data); - } - if (!body.media_ids) body.media_ids = undefined; - if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; - - if (body.poll && !body.poll.options) { - return reply.code(400).send({ error: 'Missing required payload "poll.options"' }); - } - if (body.poll && !body.poll.expires_in) { - return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' }); - } - - const options = { - ...body, - sensitive: toBoolean(body.sensitive), - poll: body.poll ? { - options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion - expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion - multiple: toBoolean(body.poll.multiple), - hide_totals: toBoolean(body.poll.hide_totals), - } : undefined, - }; - - const data = await client.postStatus(text, options); - const response = await this.mastoConverters.convertStatus(data.data as Entity.Status, me); - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v1/statuses', data); - reply.code(401).send(data); + if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]']) + ) { + body = normalizeQuery(body); } + const text = body.status ??= ' '; + const removed = text.replace(/@\S+/g, '').replace(/\s|/g, ''); + const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); + const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); + + const { client, me } = await this.clientService.getAuthClient(_request); + if ((body.in_reply_to_id && isDefaultEmoji) || (body.in_reply_to_id && isCustomEmoji)) { + const a = await client.createEmojiReaction( + body.in_reply_to_id, + removed, + ); + 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 => e.me)[0].name; + const data = await client.deleteEmojiReaction(id, react); + reply.send(data.data); + } + if (!body.media_ids) body.media_ids = undefined; + if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; + + if (body.poll && !body.poll.options) { + return reply.code(400).send({ error: 'Missing required payload "poll.options"' }); + } + if (body.poll && !body.poll.expires_in) { + return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' }); + } + + const options = { + ...body, + sensitive: toBoolean(body.sensitive), + poll: body.poll ? { + options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion + multiple: toBoolean(body.poll.multiple), + hide_totals: toBoolean(body.poll.hide_totals), + } : undefined, + }; + + const data = await client.postStatus(text, options); + const response = await this.mastoConverters.convertStatus(data.data as Entity.Status, me); + + reply.send(response); }); fastify.put<{ @@ -267,210 +206,138 @@ export class ApiStatusMastodon { }, } }>('/v1/statuses/:id', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); - const body = _request.body; + const { client, me } = await this.clientService.getAuthClient(_request); + const body = _request.body; - if (!body.media_ids || !body.media_ids.length) { - body.media_ids = undefined; - } - - const options = { - ...body, - sensitive: toBoolean(body.sensitive), - poll: body.poll ? { - options: body.poll.options, - expires_in: toInt(body.poll.expires_in), - multiple: toBoolean(body.poll.multiple), - hide_totals: toBoolean(body.poll.hide_totals), - } : undefined, - }; - - const data = await client.editStatus(_request.params.id, options); - const response = await this.mastoConverters.convertStatus(data.data, me); - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}`, data); - reply.code(401).send(data); + if (!body.media_ids || !body.media_ids.length) { + body.media_ids = undefined; } + + const options = { + ...body, + sensitive: toBoolean(body.sensitive), + poll: body.poll ? { + options: body.poll.options, + expires_in: toInt(body.poll.expires_in), + multiple: toBoolean(body.poll.multiple), + hide_totals: toBoolean(body.poll.hide_totals), + } : undefined, + }; + + const data = await client.editStatus(_request.params.id, options); + const response = await this.mastoConverters.convertStatus(data.data, me); + + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.createEmojiReaction(_request.params.id, '❤'); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.createEmojiReaction(_request.params.id, '❤'); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.deleteEmojiReaction(_request.params.id, '❤'); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.deleteEmojiReaction(_request.params.id, '❤'); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.reblogStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.reblogStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.unreblogStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.unreblogStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.bookmarkStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.bookmarkStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.unbookmarkStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.unbookmarkStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.pinStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.pinStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.unpinStatus(_request.params.id); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.unpinStatus(_request.params.id); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.createEmojiReaction(_request.params.id, _request.params.name); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.createEmojiReaction(_request.params.id, _request.params.name); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name); - const response = await this.mastoConverters.convertStatus(data.data, me); + const { client, me } = await this.clientService.getAuthClient(_request); + const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name); + const response = await this.mastoConverters.convertStatus(data.data, me); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.deleteStatus(_request.params.id); + const client = this.clientService.getClient(_request); + const data = await client.deleteStatus(_request.params.id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data); - reply.code(401).send(data); - } + 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 864fdc7691..7dee9a062c 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -4,7 +4,6 @@ */ import { Injectable } from '@nestjs/common'; -import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js'; import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js'; import { convertList, MastoConverters } from '../converters.js'; import { parseTimelineArgs, TimelineArgs, toBoolean } from '../argsUtils.js'; @@ -16,216 +15,136 @@ export class ApiTimelineMastodon { constructor( private readonly clientService: MastodonClientService, private readonly mastoConverters: MastoConverters, - private readonly logger: MastodonLogger, ) {} public register(fastify: FastifyInstance): void { fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); + const { client, me } = await this.clientService.getAuthClient(_request); + const query = parseTimelineArgs(_request.query); + const data = toBoolean(_request.query.local) + ? await client.getLocalTimeline(query) + : await client.getPublicTimeline(query); + const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); - const query = parseTimelineArgs(_request.query); - const data = toBoolean(_request.query.local) - ? await client.getLocalTimeline(query) - : await client.getPublicTimeline(query); - const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); - - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/timelines/public', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); - const query = parseTimelineArgs(_request.query); - const data = await client.getHomeTimeline(query); - const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); + const { client, me } = await this.clientService.getAuthClient(_request); + const query = parseTimelineArgs(_request.query); + const data = await client.getHomeTimeline(query); + const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/timelines/home', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => { - try { - if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' }); + if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const query = parseTimelineArgs(_request.query); - const data = await client.getTagTimeline(_request.params.hashtag, query); - const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); + const { client, me } = await this.clientService.getAuthClient(_request); + const query = parseTimelineArgs(_request.query); + const data = await client.getTagTimeline(_request.params.hashtag, query); + const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const { client, me } = await this.clientService.getAuthClient(_request); - const query = parseTimelineArgs(_request.query); - const data = await client.getListTimeline(_request.params.id, query); - const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))); + const { client, me } = await this.clientService.getAuthClient(_request); + const query = parseTimelineArgs(_request.query); + const data = await client.getListTimeline(_request.params.id, query); + const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => { - try { - const { client, me } = await this.clientService.getAuthClient(_request); - const query = parseTimelineArgs(_request.query); - const data = await client.getConversationTimeline(query); - const conversations = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me))); + const { client, me } = await this.clientService.getAuthClient(_request); + const query = parseTimelineArgs(_request.query); + const data = await client.getConversationTimeline(query); + const conversations = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me))); - reply.send(conversations); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/conversations', data); - reply.code(401).send(data); - } + reply.send(conversations); }); fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getList(_request.params.id); - const response = convertList(data.data); + const client = this.clientService.getClient(_request); + const data = await client.getList(_request.params.id); + const response = convertList(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/lists/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get('/v1/lists', async (_request, reply) => { - try { - const client = this.clientService.getClient(_request); - const data = await client.getLists(); - const response = data.data.map((list: Entity.List) => convertList(list)); + const client = this.clientService.getClient(_request); + const data = await client.getLists(); + const response = data.data.map((list: Entity.List) => convertList(list)); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('GET /v1/lists', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - const data = await client.getAccountsInList(_request.params.id, _request.query); - const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account))); + const client = this.clientService.getClient(_request); + const data = await client.getAccountsInList(_request.params.id, _request.query); + const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account))); - reply.send(accounts); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data); - reply.code(401).send(data); - } + reply.send(accounts); }); fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' }); - const client = this.clientService.getClient(_request); - const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id); + const client = this.clientService.getClient(_request); + const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data); - reply.code(401).send(data); - } + reply.send(data.data); }); fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' }); - const client = this.clientService.getClient(_request); - const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id); + const client = this.clientService.getClient(_request); + const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id); - reply.send(data.data); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data); - reply.code(401).send(data); - } + reply.send(data.data); }); fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => { - try { - if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' }); + if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' }); - const client = this.clientService.getClient(_request); - const data = await client.createList(_request.body.title); - const response = convertList(data.data); + const client = this.clientService.getClient(_request); + const data = await client.createList(_request.body.title); + const response = convertList(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error('POST /v1/lists', data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' }); - const client = this.clientService.getClient(_request); - const data = await client.updateList(_request.params.id, _request.body.title); - const response = convertList(data.data); + const client = this.clientService.getClient(_request); + const data = await client.updateList(_request.params.id, _request.body.title); + const response = convertList(data.data); - reply.send(response); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`PUT /v1/lists/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send(response); }); fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => { - try { - if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); + if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' }); - const client = this.clientService.getClient(_request); - await client.deleteList(_request.params.id); + const client = this.clientService.getClient(_request); + await client.deleteList(_request.params.id); - reply.send({}); - } catch (e) { - const data = getErrorData(e); - this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data); - reply.code(401).send(data); - } + reply.send({}); }); } }