From e26d604a54ba07a5ac3db21b1193462389fa7819 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 7 Apr 2024 17:28:18 -1000 Subject: [PATCH] Refactors, bugfixing --- benchmarks/timelines.ts | 2 +- cli.ts | 1 - database/entities/Attachment.ts | 6 +- database/entities/Like.ts | 5 +- database/entities/Object.ts | 2 - database/entities/Queue.ts | 1 - database/entities/Status.ts | 26 ++++--- database/entities/User.ts | 72 +++++++++--------- packages/cli-parser/index.ts | 2 - packages/media-manager/index.ts | 4 - pages/vite.config.ts | 1 - server.ts | 48 +++++------- server/api/.well-known/host-meta/index.ts | 5 +- server/api/.well-known/nodeinfo/index.ts | 11 ++- server/api/.well-known/webfinger/index.ts | 17 ++--- server/api/api/v1/accounts/[id]/followers.ts | 1 - server/api/api/v1/accounts/[id]/following.ts | 1 - server/api/api/v1/accounts/[id]/mute.ts | 1 - server/api/api/v1/accounts/[id]/statuses.ts | 2 - server/api/api/v1/accounts/index.ts | 6 +- server/api/api/v1/blocks/index.ts | 46 ++++++++--- server/api/api/v1/favourites/index.ts | 54 +++++++------ server/api/api/v1/follow_requests/index.ts | 53 ++++++------- server/api/api/v1/media/[id]/index.ts | 6 +- server/api/api/v1/mutes/index.ts | 36 ++++++--- server/api/api/v1/notifications/index.ts | 76 +++++++++---------- server/api/api/v1/statuses/[id]/favourite.ts | 1 - .../api/api/v1/statuses/[id]/favourited_by.ts | 72 +++++++----------- server/api/api/v1/statuses/[id]/pin.ts | 1 - server/api/api/v1/statuses/[id]/reblog.ts | 16 +++- .../api/api/v1/statuses/[id]/reblogged_by.ts | 67 +++++++--------- .../api/api/v1/statuses/[id]/unfavourite.ts | 1 - server/api/media/[id]/index.ts | 14 ++-- server/api/oauth/callback/[issuer]/index.ts | 1 - server/api/users/[uuid]/outbox/index.ts | 13 ++-- tests/api/accounts.test.ts | 2 +- tests/api/statuses.test.ts | 2 - tests/oauth.test.ts | 5 -- utils/api.ts | 1 - utils/constants.ts | 2 +- utils/response.ts | 49 ++++++++---- utils/timelines.ts | 14 +++- 42 files changed, 370 insertions(+), 376 deletions(-) diff --git a/benchmarks/timelines.ts b/benchmarks/timelines.ts index ac8e8958..9e00fc2c 100644 --- a/benchmarks/timelines.ts +++ b/benchmarks/timelines.ts @@ -18,7 +18,7 @@ if (!token) { } const fetchTimeline = () => - fetch(`${config.http.base_url}/api/v1/timelines/home`, { + fetch(new URL("/api/v1/timelines/home", config.http.base_url), { headers: { Authorization: `Bearer ${token}`, }, diff --git a/cli.ts b/cli.ts index 28ac6210..4af4ecf6 100644 --- a/cli.ts +++ b/cli.ts @@ -388,7 +388,6 @@ const cliBuilder = new CliBuilder([ for (const key of keys) { if (!args.fields.includes(key)) { // @ts-expect-error This is fine - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete data[key]; } } diff --git a/database/entities/Attachment.ts b/database/entities/Attachment.ts index c2452bd3..e5749359 100644 --- a/database/entities/Attachment.ts +++ b/database/entities/Attachment.ts @@ -58,13 +58,11 @@ export const attachmentToAPI = ( }; export const getUrl = (name: string, config: Config) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (config.media.backend === MediaBackendType.LOCAL) { - return `${config.http.base_url}/media/${name}`; - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition + return new URL(`/media/${name}`, config.http.base_url).toString(); } if (config.media.backend === MediaBackendType.S3) { - return `${config.s3.public_url}/${name}`; + return new URL(`/${name}`, config.s3.public_url).toString(); } return ""; }; diff --git a/database/entities/Like.ts b/database/entities/Like.ts index 00935004..675c2335 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -1,7 +1,6 @@ -import type { Like, Prisma } from "@prisma/client"; +import type { Like } from "@prisma/client"; import { config } from "config-manager"; import { client } from "~database/datasource"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type { Like as LysandLike } from "~types/lysand/Object"; import type { StatusWithRelations } from "./Status"; import type { UserWithRelations } from "./User"; @@ -18,7 +17,7 @@ export const toLysand = (like: Like): LysandLike => { created_at: new Date(like.createdAt).toISOString(), // biome-ignore lint/suspicious/noExplicitAny: to be rewritten object: (like as any).liked?.uri, - uri: `${config.http.base_url}/actions/${like.id}`, + uri: new URL(`/actions/${like.id}`, config.http.base_url).toString(), }; }; diff --git a/database/entities/Object.ts b/database/entities/Object.ts index 9e176f31..9a54b8c5 100644 --- a/database/entities/Object.ts +++ b/database/entities/Object.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type { LysandObject } from "@prisma/client"; import { client } from "~database/datasource"; import type { LysandObjectType } from "~types/lysand/Object"; diff --git a/database/entities/Queue.ts b/database/entities/Queue.ts index a5e399a5..d804e41e 100644 --- a/database/entities/Queue.ts +++ b/database/entities/Queue.ts @@ -149,7 +149,6 @@ export const federateStatusTo = async ( new TextEncoder().encode("request_body"), ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const userInbox = new URL(user.endpoints.inbox); const date = new Date(); diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 1d8e4d95..c82dad63 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -21,7 +21,6 @@ import type { LysandPublication, Note } from "~types/lysand/Object"; import { applicationToAPI } from "./Application"; import { attachmentToAPI } from "./Attachment"; import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type { UserWithRelations } from "./User"; import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User"; import { statusAndUserRelations, userRelations } from "./relations"; @@ -118,7 +117,6 @@ export const fetchFromRemote = async (uri: string): Promise => { /** * Return all the ancestors of this post, */ -// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars export const getAncestors = async ( status: StatusWithRelations, fetcher: UserWithRelations | null, @@ -154,7 +152,6 @@ export const getAncestors = async ( * Return all the descendants of this post (recursive) * Temporary implementation, will be replaced with a recursive SQL query when Prisma adds support for it */ -// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars export const getDescendants = async ( status: StatusWithRelations, fetcher: UserWithRelations | null, @@ -295,7 +292,10 @@ export const createNewStatus = async (data: { isReblog: false, uri: data.uri || - `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`, + new URL( + `/statuses/FAKE-${crypto.randomUUID()}`, + config.http.base_url, + ).toString(), mentions: { connect: mentions.map((mention) => { return { @@ -313,7 +313,12 @@ export const createNewStatus = async (data: { id: status.id, }, data: { - uri: data.uri || `${config.http.base_url}/statuses/${status.id}`, + uri: + data.uri || + new URL( + `/statuses/${status.id}`, + config.http.base_url, + ).toString(), }, include: statusAndUserRelations, }); @@ -467,13 +472,10 @@ export const statusToAPI = async ( card: null, content: status.content, emojis: status.emojis.map((emoji) => emojiToAPI(emoji)), - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition favourited: !!(status.likes ?? []).find( (like) => like.likerId === user?.id, ), - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition favourites_count: (status.likes ?? []).length, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition media_attachments: (status.attachments ?? []).map( (a) => attachmentToAPI(a) as APIAttachment, ), @@ -485,7 +487,7 @@ export const statusToAPI = async ( ?.muting || false : false, pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false, - // TODO: Add pols + // TODO: Add polls poll: null, reblog: status.reblog ? await statusToAPI(status.reblog as unknown as StatusWithRelations) @@ -501,9 +503,9 @@ export const statusToAPI = async ( sensitive: status.sensitive, spoiler_text: status.spoilerText, tags: [], - uri: `${config.http.base_url}/statuses/${status.id}`, + uri: new URL(`/statuses/${status.id}`, config.http.base_url).toString(), visibility: "public", - url: `${config.http.base_url}/statuses/${status.id}`, + url: new URL(`/statuses/${status.id}`, config.http.base_url).toString(), bookmarked: false, quote: status.quotingPost ? await statusToAPI( @@ -567,7 +569,7 @@ export const statusToLysand = (status: StatusWithRelations): Note => { created_at: new Date(status.createdAt).toISOString(), id: status.id, author: status.authorId, - uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`, + uri: new URL(`/statuses/${status.id}`, config.http.base_url).toString(), contents: [ { content: status.content, diff --git a/database/entities/User.ts b/database/entities/User.ts index 0879eb98..3f3aa4e7 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -4,13 +4,13 @@ import { Prisma } from "@prisma/client"; import { type Config, config } from "config-manager"; import { htmlToText } from "html-to-text"; import { client } from "~database/datasource"; -import { MediaBackendType } from "~packages/media-manager"; import type { APIAccount } from "~types/entities/account"; import type { APISource } from "~types/entities/source"; import type { LysandUser } from "~types/lysand/Object"; import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji"; import { addInstanceIfNotExists } from "./Instance"; import { userRelations } from "./relations"; +import { getUrl } from "./Attachment"; export interface AuthData { user: UserWithRelations | null; @@ -35,14 +35,7 @@ export type UserWithRelations = Prisma.UserGetPayload; */ export const getAvatarUrl = (user: User, config: Config) => { if (!user.avatar) return config.defaults.avatar; - if (config.media.backend === MediaBackendType.LOCAL) { - return `${config.http.base_url}/media/${user.avatar}`; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } - if (config.media.backend === MediaBackendType.S3) { - return `${config.s3.public_url}/${user.avatar}`; - } - return ""; + return getUrl(user.avatar, config); }; /** @@ -52,14 +45,7 @@ export const getAvatarUrl = (user: User, config: Config) => { */ export const getHeaderUrl = (user: User, config: Config) => { if (!user.header) return config.defaults.header; - if (config.media.backend === MediaBackendType.LOCAL) { - return `${config.http.base_url}/media/${user.header}`; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } - if (config.media.backend === MediaBackendType.S3) { - return `${config.s3.public_url}/${user.header}`; - } - return ""; + return getUrl(user.header, config); }; export const getFromRequest = async (req: Request): Promise => { @@ -224,16 +210,7 @@ export const createNewLocalUser = async (data: { id: user.id, }, data: { - uri: `${config.http.base_url}/users/${user.id}`, - endpoints: { - disliked: `${config.http.base_url}/users/${user.id}/disliked`, - featured: `${config.http.base_url}/users/${user.id}/featured`, - liked: `${config.http.base_url}/users/${user.id}/liked`, - followers: `${config.http.base_url}/users/${user.id}/followers`, - following: `${config.http.base_url}/users/${user.id}/following`, - inbox: `${config.http.base_url}/users/${user.id}/inbox`, - outbox: `${config.http.base_url}/users/${user.id}/outbox`, - }, + uri: new URL(`/users/${user.id}`, config.http.base_url).toString(), }, include: userRelations, }); @@ -399,13 +376,35 @@ export const userToLysand = (user: UserWithRelations): LysandUser => { }, ], created_at: new Date(user.createdAt).toISOString(), - disliked: `${user.uri}/disliked`, - featured: `${user.uri}/featured`, - liked: `${user.uri}/liked`, - followers: `${user.uri}/followers`, - following: `${user.uri}/following`, - inbox: `${user.uri}/inbox`, - outbox: `${user.uri}/outbox`, + + disliked: new URL( + `/users/${user.id}/disliked`, + config.http.base_url, + ).toString(), + featured: new URL( + `/users/${user.id}/featured`, + config.http.base_url, + ).toString(), + liked: new URL( + `/users/${user.id}/liked`, + config.http.base_url, + ).toString(), + followers: new URL( + `/users/${user.id}/followers`, + config.http.base_url, + ).toString(), + following: new URL( + `/users/${user.id}/following`, + config.http.base_url, + ).toString(), + inbox: new URL( + `/users/${user.id}/inbox`, + config.http.base_url, + ).toString(), + outbox: new URL( + `/users/${user.id}/outbox`, + config.http.base_url, + ).toString(), indexable: false, username: user.username, avatar: [ @@ -444,7 +443,10 @@ export const userToLysand = (user: UserWithRelations): LysandUser => { ], })), public_key: { - actor: `${config.http.base_url}/users/${user.id}`, + actor: new URL( + `/users/${user.id}`, + config.http.base_url, + ).toString(), public_key: user.publicKey, }, extensions: { diff --git a/packages/cli-parser/index.ts b/packages/cli-parser/index.ts index 2dbb65c9..be0aabea 100644 --- a/packages/cli-parser/index.ts +++ b/packages/cli-parser/index.ts @@ -174,7 +174,6 @@ export class CliBuilder { // Split the command into parts and iterate over them for (const part of command.categories) { // If this part doesn't exist in the current level of the tree, add it (__proto__ check to prevent prototype pollution) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!currentLevel[part] && part !== "__proto__") { // If this is the last part of the command, add the command itself if ( @@ -297,7 +296,6 @@ export class CliBuilder { type ExecuteFunction = ( instance: CliCommand, args: Partial, - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ) => Promise | Promise | number | void; /** diff --git a/packages/media-manager/index.ts b/packages/media-manager/index.ts index 9f871534..9303980d 100644 --- a/packages/media-manager/index.ts +++ b/packages/media-manager/index.ts @@ -64,9 +64,7 @@ export class MediaBackend { * @returns The file as a File object */ public getFileByHash( - // eslint-disable-next-line @typescript-eslint/no-unused-vars file: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars databaseHashFetcher: (sha256: string) => Promise, ): Promise { return Promise.reject( @@ -79,7 +77,6 @@ export class MediaBackend { * @param filename File name * @returns The file as a File object */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars public getFile(filename: string): Promise { return Promise.reject( new Error("Do not call MediaBackend directly: use a subclass"), @@ -91,7 +88,6 @@ export class MediaBackend { * @param file File to add * @returns Metadata about the uploaded file */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars public addFile(file: File): Promise { return Promise.reject( new Error("Do not call MediaBackend directly: use a subclass"), diff --git a/pages/vite.config.ts b/pages/vite.config.ts index eaf65bed..8d70508b 100644 --- a/pages/vite.config.ts +++ b/pages/vite.config.ts @@ -20,7 +20,6 @@ export default defineConfig({ }, }, define: { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access __VERSION__: JSON.stringify(pkg.version), }, ssr: { diff --git a/server.ts b/server.ts index cbb86544..6aaec938 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,9 @@ -import { errorResponse, jsonResponse } from "@response"; +import { + clientResponse, + errorResponse, + jsonResponse, + response, +} from "@response"; import type { Config } from "config-manager"; import { matches } from "ip-matching"; import type { LogManager, MultiLogManager } from "log-manager"; @@ -22,10 +27,7 @@ export const createServer = ( for (const ip of config.http.banned_ips) { try { if (matches(ip, request_ip)) { - return new Response(undefined, { - status: 403, - statusText: "Forbidden", - }); + return errorResponse("Forbidden", 403); } } catch (e) { console.error(`[-] Error while parsing banned IP "${ip}" `); @@ -38,10 +40,7 @@ export const createServer = ( for (const agent of config.http.banned_user_agents) { if (new RegExp(agent).test(ua)) { - return new Response(undefined, { - status: 403, - statusText: "Forbidden", - }); + return errorResponse("Forbidden", 403); } } @@ -56,7 +55,7 @@ export const createServer = ( ); if (await file.exists()) { - return new Response(file); + return response(file); } await logger.log( LogLevel.ERROR, @@ -81,7 +80,7 @@ export const createServer = ( ); if (await file.exists()) { - return new Response(file); + return response(file); } await logger.log( LogLevel.ERROR, @@ -126,12 +125,12 @@ export const createServer = ( // Check for allowed requests // @ts-expect-error Stupid error if (!meta.allowedMethods.includes(req.method as string)) { - return new Response(undefined, { - status: 405, - statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join( + return errorResponse( + `Method not allowed: allowed methods are: ${meta.allowedMethods.join( ", ", )}`, - }); + 405, + ); } // TODO: Check for ratelimits @@ -140,20 +139,14 @@ export const createServer = ( // Check for authentication if required if (meta.auth.required) { if (!auth.user) { - return new Response(undefined, { - status: 401, - statusText: "Unauthorized", - }); + return errorResponse("Unauthorized", 401); } } else if ( // @ts-expect-error Stupid error (meta.auth.requiredOnMethods ?? []).includes(req.method) ) { if (!auth.user) { - return new Response(undefined, { - status: 401, - statusText: "Unauthorized", - }); + return errorResponse("Unauthorized", 401); } } @@ -167,10 +160,7 @@ export const createServer = ( "Server.RouteRequestParser", e as Error, ); - return new Response(undefined, { - status: 400, - statusText: "Bad request", - }); + return errorResponse("Bad request", 400); } return await file.default(req.clone(), matchedRoute, { @@ -196,7 +186,7 @@ export const createServer = ( // Serve from pages/dist/assets if (await file.exists()) { - return new Response(file); + return clientResponse(file); } return errorResponse("Asset not found", 404); } @@ -207,7 +197,7 @@ export const createServer = ( const file = Bun.file("./pages/dist/index.html"); // Serve from pages/dist - return new Response(file); + return clientResponse(file); } const proxy = await fetch( req.url.replace( diff --git a/server/api/.well-known/host-meta/index.ts b/server/api/.well-known/host-meta/index.ts index ecabdfe1..723a99bd 100644 --- a/server/api/.well-known/host-meta/index.ts +++ b/server/api/.well-known/host-meta/index.ts @@ -19,7 +19,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return xmlResponse(` - + `); }); diff --git a/server/api/.well-known/nodeinfo/index.ts b/server/api/.well-known/nodeinfo/index.ts index 68b910f8..0aee0bc1 100644 --- a/server/api/.well-known/nodeinfo/index.ts +++ b/server/api/.well-known/nodeinfo/index.ts @@ -1,4 +1,5 @@ import { apiRoute, applyConfig } from "@api"; +import { redirect } from "@response"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -15,10 +16,8 @@ export const meta = applyConfig({ export default apiRoute(async (req, matchedRoute, extraData) => { const config = await extraData.configManager.getConfig(); - return new Response("", { - status: 301, - headers: { - Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`, - }, - }); + return redirect( + new URL("/.well-known/nodeinfo/2.0", config.http.base_url), + 301, + ); }); diff --git a/server/api/.well-known/webfinger/index.ts b/server/api/.well-known/webfinger/index.ts index 558fd415..1fdd5c59 100644 --- a/server/api/.well-known/webfinger/index.ts +++ b/server/api/.well-known/webfinger/index.ts @@ -41,18 +41,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => { links: [ { rel: "self", - type: "application/activity+json", - href: `${config.http.base_url}/users/${user.username}/actor`, - }, - { - rel: "https://webfinger.net/rel/profile-page", - type: "text/html", - href: `${config.http.base_url}/users/${user.username}`, - }, - { - rel: "self", - type: 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"', - href: `${config.http.base_url}/users/${user.username}/actor`, + type: "application/json", + href: new URL( + `/users/${user.id}`, + config.http.base_url, + ).toString(), }, ], }); diff --git a/server/api/api/v1/accounts/[id]/followers.ts b/server/api/api/v1/accounts/[id]/followers.ts index 4d768394..39dcf03f 100644 --- a/server/api/api/v1/accounts/[id]/followers.ts +++ b/server/api/api/v1/accounts/[id]/followers.ts @@ -1,5 +1,4 @@ import { apiRoute, applyConfig } from "@api"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { userToAPI } from "~database/entities/User"; diff --git a/server/api/api/v1/accounts/[id]/following.ts b/server/api/api/v1/accounts/[id]/following.ts index de0913cc..9a3ca34d 100644 --- a/server/api/api/v1/accounts/[id]/following.ts +++ b/server/api/api/v1/accounts/[id]/following.ts @@ -1,5 +1,4 @@ import { apiRoute, applyConfig } from "@api"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; import { userToAPI } from "~database/entities/User"; diff --git a/server/api/api/v1/accounts/[id]/mute.ts b/server/api/api/v1/accounts/[id]/mute.ts index d272fb48..89063c79 100644 --- a/server/api/api/v1/accounts/[id]/mute.ts +++ b/server/api/api/v1/accounts/[id]/mute.ts @@ -33,7 +33,6 @@ export default apiRoute<{ if (!self) return errorResponse("Unauthorized", 401); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const { notifications, duration } = extraData.parsedRequest; const user = await client.user.findUnique({ diff --git a/server/api/api/v1/accounts/[id]/statuses.ts b/server/api/api/v1/accounts/[id]/statuses.ts index 48e95a3e..269413c0 100644 --- a/server/api/api/v1/accounts/[id]/statuses.ts +++ b/server/api/api/v1/accounts/[id]/statuses.ts @@ -1,6 +1,4 @@ import { apiRoute, applyConfig } from "@api"; -import type { Prisma, Status, User } from "@prisma/client"; -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; diff --git a/server/api/api/v1/accounts/index.ts b/server/api/api/v1/accounts/index.ts index e721697e..09cde002 100644 --- a/server/api/api/v1/accounts/index.ts +++ b/server/api/api/v1/accounts/index.ts @@ -1,5 +1,5 @@ import { apiRoute, applyConfig } from "@api"; -import { jsonResponse } from "@response"; +import { jsonResponse, response } from "@response"; import { tempmailDomains } from "@tempmail"; import ISO6391 from "iso-639-1"; import { client } from "~database/datasource"; @@ -200,7 +200,5 @@ export default apiRoute<{ email: body.email ?? "", }); - return new Response("", { - status: 200, - }); + return response(null, 200); }); diff --git a/server/api/api/v1/blocks/index.ts b/server/api/api/v1/blocks/index.ts index e1b8240a..79e115dc 100644 --- a/server/api/api/v1/blocks/index.ts +++ b/server/api/api/v1/blocks/index.ts @@ -1,7 +1,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { userToAPI } from "~database/entities/User"; +import { userToAPI, type UserWithRelations } from "~database/entities/User"; import { userRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -13,25 +14,48 @@ export const meta = applyConfig({ }, auth: { required: true, + oauthPermissions: ["read:blocks"], }, }); -export default apiRoute(async (req, matchedRoute, extraData) => { +export default apiRoute<{ + max_id?: string; + since_id?: string; + min_id?: string; + limit?: number; +}>(async (req, matchedRoute, extraData) => { const { user } = extraData.auth; if (!user) return errorResponse("Unauthorized", 401); - const blocks = await client.user.findMany({ - where: { - relationshipSubjects: { - some: { - ownerId: user.id, - blocking: true, + const { max_id, since_id, limit = 40 } = extraData.parsedRequest; + + const { objects: blocks, link } = await fetchTimeline( + client.user, + { + where: { + relationshipSubjects: { + some: { + ownerId: user.id, + blocking: true, + }, + }, + id: { + lt: max_id, + gte: since_id, }, }, + include: userRelations, + take: Number(limit), }, - include: userRelations, - }); + req, + ); - return jsonResponse(blocks.map((u) => userToAPI(u))); + return jsonResponse( + blocks.map((u) => userToAPI(u)), + 200, + { + Link: link, + }, + ); }); diff --git a/server/api/api/v1/favourites/index.ts b/server/api/api/v1/favourites/index.ts index 19a6d628..a4c7470c 100644 --- a/server/api/api/v1/favourites/index.ts +++ b/server/api/api/v1/favourites/index.ts @@ -1,7 +1,11 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { statusToAPI } from "~database/entities/Status"; +import { + statusToAPI, + type StatusWithRelations, +} from "~database/entities/Status"; import { statusAndUserRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -32,35 +36,29 @@ export default apiRoute<{ if (!user) return errorResponse("Unauthorized", 401); - const objects = await client.status.findMany({ - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - likes: { - some: { - likerId: user.id, + const { objects, link } = await fetchTimeline( + client.status, + { + where: { + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + likes: { + some: { + likerId: user.id, + }, }, }, + include: statusAndUserRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, }, - include: statusAndUserRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( await Promise.all( @@ -68,7 +66,7 @@ export default apiRoute<{ ), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/follow_requests/index.ts b/server/api/api/v1/follow_requests/index.ts index a7c20f9b..30373108 100644 --- a/server/api/api/v1/follow_requests/index.ts +++ b/server/api/api/v1/follow_requests/index.ts @@ -1,7 +1,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { userToAPI } from "~database/entities/User"; +import { userToAPI, type UserWithRelations } from "~database/entities/User"; import { userRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -32,42 +33,36 @@ export default apiRoute<{ if (!user) return errorResponse("Unauthorized", 401); - const objects = await client.user.findMany({ - where: { - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - relationships: { - some: { - subjectId: user.id, - requested: true, + const { objects, link } = await fetchTimeline( + client.user, + { + where: { + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + relationships: { + some: { + subjectId: user.id, + requested: true, + }, }, }, + include: userRelations, + take: Number(limit), + orderBy: { + id: "desc", + }, }, - include: userRelations, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects.at(-1)?.id}>; rel="next"`, - `<${urlWithoutQuery}?min_id=${objects[0].id}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( objects.map((user) => userToAPI(user)), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/media/[id]/index.ts b/server/api/api/v1/media/[id]/index.ts index 14f21878..4d5e5ebe 100644 --- a/server/api/api/v1/media/[id]/index.ts +++ b/server/api/api/v1/media/[id]/index.ts @@ -1,5 +1,5 @@ import { apiRoute, applyConfig } from "@api"; -import { errorResponse, jsonResponse } from "@response"; +import { errorResponse, jsonResponse, response } from "@response"; import type { MediaBackend } from "media-manager"; import { MediaBackendType } from "media-manager"; import { client } from "~database/datasource"; @@ -52,9 +52,7 @@ export default apiRoute<{ if (attachment.url) { return jsonResponse(attachmentToAPI(attachment)); } - return new Response(null, { - status: 206, - }); + return response(null, 206); } case "PUT": { const { description, thumbnail } = extraData.parsedRequest; diff --git a/server/api/api/v1/mutes/index.ts b/server/api/api/v1/mutes/index.ts index 7d6bd590..64be2713 100644 --- a/server/api/api/v1/mutes/index.ts +++ b/server/api/api/v1/mutes/index.ts @@ -1,7 +1,8 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; -import { userToAPI } from "~database/entities/User"; +import { userToAPI, type UserWithRelations } from "~database/entities/User"; import { userRelations } from "~database/entities/relations"; export const meta = applyConfig({ @@ -13,25 +14,40 @@ export const meta = applyConfig({ }, auth: { required: true, + oauthPermissions: ["read:mutes"], }, }); -export default apiRoute(async (req, matchedRoute, extraData) => { +export default apiRoute<{ + max_id?: string; + since_id?: string; + limit?: number; +}>(async (req, matchedRoute, extraData) => { const { user } = extraData.auth; + const { max_id, since_id, limit = 40 } = extraData.parsedRequest; if (!user) return errorResponse("Unauthorized", 401); - const blocks = await client.user.findMany({ - where: { - relationshipSubjects: { - some: { - ownerId: user.id, - muting: true, + const { objects: blocks, link } = await fetchTimeline( + client.user, + { + where: { + relationshipSubjects: { + some: { + ownerId: user.id, + muting: true, + }, + }, + id: { + lt: max_id, + gte: since_id, }, }, + include: userRelations, + take: Number(limit), }, - include: userRelations, - }); + req, + ); return jsonResponse(blocks.map((u) => userToAPI(u))); }); diff --git a/server/api/api/v1/notifications/index.ts b/server/api/api/v1/notifications/index.ts index 754fa201..1c846145 100644 --- a/server/api/api/v1/notifications/index.ts +++ b/server/api/api/v1/notifications/index.ts @@ -1,5 +1,7 @@ import { apiRoute, applyConfig } from "@api"; +import type { Prisma } from "@prisma/client"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; import { notificationToAPI } from "~database/entities/Notification"; import { @@ -50,53 +52,49 @@ export default apiRoute<{ return errorResponse("Can't use both types and exclude_types", 400); } - const objects = await client.notification.findMany({ - where: { - notifiedId: user.id, - id: { - lt: max_id, - gt: min_id, - gte: since_id, + const { objects, link } = await fetchTimeline< + Prisma.NotificationGetPayload<{ + include: { + account: { + include: typeof userRelations; + }; + status: { + include: typeof statusAndUserRelations; + }; + }; + }> + >( + client.notification, + { + where: { + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, + }, + accountId: account_id, }, - type: { - in: types, - notIn: exclude_types, + include: { + account: { + include: userRelations, + }, + status: { + include: statusAndUserRelations, + }, }, - accountId: account_id, + orderBy: { + id: "desc", + }, + take: Number(limit), }, - include: { - account: { - include: userRelations, - }, - status: { - include: statusAndUserRelations, - }, - }, - orderBy: { - id: "desc", - }, - take: Number(limit), - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`, - ); - linkHeader.push( - `<${urlWithoutQuery}?since_id=${ - objects.at(-1)?.id - }&limit=${limit}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( await Promise.all(objects.map((n) => notificationToAPI(n))), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/statuses/[id]/favourite.ts b/server/api/api/v1/statuses/[id]/favourite.ts index 51838305..7028bf7a 100644 --- a/server/api/api/v1/statuses/[id]/favourite.ts +++ b/server/api/api/v1/statuses/[id]/favourite.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; diff --git a/server/api/api/v1/statuses/[id]/favourited_by.ts b/server/api/api/v1/statuses/[id]/favourited_by.ts index 6d20de6a..7c554e5e 100644 --- a/server/api/api/v1/statuses/[id]/favourited_by.ts +++ b/server/api/api/v1/statuses/[id]/favourited_by.ts @@ -1,8 +1,9 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; import { isViewableByUser } from "~database/entities/Status"; -import { userToAPI } from "~database/entities/User"; +import { userToAPI, type UserWithRelations } from "~database/entities/User"; import { statusAndUserRelations, userRelations, @@ -42,63 +43,48 @@ export default apiRoute<{ if (!status || !isViewableByUser(status, user)) return errorResponse("Record not found", 404); - const { - max_id = null, - min_id = null, - since_id = null, - limit = 40, - } = extraData.parsedRequest; + const { max_id, min_id, since_id, limit = 40 } = extraData.parsedRequest; // Check for limit limits if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit < 1) return errorResponse("Invalid limit", 400); - const objects = await client.user.findMany({ - where: { - likes: { - some: { - likedId: status.id, + const { objects, link } = await fetchTimeline( + client.user, + { + where: { + likes: { + some: { + likedId: status.id, + }, + }, + id: { + lt: max_id, + gte: since_id, + gt: min_id, }, }, - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - }, - include: { - ...userRelations, - likes: { - where: { - likedId: status.id, + include: { + ...userRelations, + likes: { + where: { + likedId: status.id, + }, }, }, + take: Number(limit), + orderBy: { + id: "desc", + }, }, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`, - ); - linkHeader.push( - `<${urlWithoutQuery}?since_id=${ - objects[objects.length - 1].id - }&limit=${limit}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( objects.map((user) => userToAPI(user)), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/statuses/[id]/pin.ts b/server/api/api/v1/statuses/[id]/pin.ts index 481ec6f8..21bf22bd 100644 --- a/server/api/api/v1/statuses/[id]/pin.ts +++ b/server/api/api/v1/statuses/[id]/pin.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; diff --git a/server/api/api/v1/statuses/[id]/reblog.ts b/server/api/api/v1/statuses/[id]/reblog.ts index e4565428..65bd2a84 100644 --- a/server/api/api/v1/statuses/[id]/reblog.ts +++ b/server/api/api/v1/statuses/[id]/reblog.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; @@ -58,7 +57,10 @@ export default apiRoute<{ authorId: user.id, reblogId: status.id, isReblog: true, - uri: `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`, + uri: new URL( + `/statuses/FAKE-${crypto.randomUUID()}`, + config.http.base_url, + ).toString(), visibility, sensitive: false, }, @@ -68,7 +70,10 @@ export default apiRoute<{ await client.status.update({ where: { id: newReblog.id }, data: { - uri: `${config.http.base_url}/statuses/${newReblog.id}`, + uri: new URL( + `/statuses/${newReblog.id}`, + config.http.base_url, + ).toString(), }, include: statusAndUserRelations, }); @@ -89,7 +94,10 @@ export default apiRoute<{ await statusToAPI( { ...newReblog, - uri: `${config.http.base_url}/statuses/${newReblog.id}`, + uri: new URL( + `/statuses/${newReblog.id}`, + config.http.base_url, + ).toString(), }, user, ), diff --git a/server/api/api/v1/statuses/[id]/reblogged_by.ts b/server/api/api/v1/statuses/[id]/reblogged_by.ts index 50c109f8..0a6087c1 100644 --- a/server/api/api/v1/statuses/[id]/reblogged_by.ts +++ b/server/api/api/v1/statuses/[id]/reblogged_by.ts @@ -1,8 +1,9 @@ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; +import { fetchTimeline } from "@timelines"; import { client } from "~database/datasource"; import { isViewableByUser } from "~database/entities/Status"; -import { userToAPI } from "~database/entities/User"; +import { type UserWithRelations, userToAPI } from "~database/entities/User"; import { statusAndUserRelations, userRelations, @@ -53,53 +54,43 @@ export default apiRoute<{ if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit < 1) return errorResponse("Invalid limit", 400); - const objects = await client.user.findMany({ - where: { - statuses: { - some: { - reblogId: status.id, + const { objects, link } = await fetchTimeline( + client.user, + { + where: { + statuses: { + some: { + reblogId: status.id, + }, + }, + id: { + lt: max_id ?? undefined, + gte: since_id ?? undefined, + gt: min_id ?? undefined, }, }, - id: { - lt: max_id ?? undefined, - gte: since_id ?? undefined, - gt: min_id ?? undefined, - }, - }, - include: { - ...userRelations, - statuses: { - where: { - reblogId: status.id, + include: { + ...userRelations, + statuses: { + where: { + reblogId: status.id, + }, + include: statusAndUserRelations, }, - include: statusAndUserRelations, + }, + take: Number(limit), + orderBy: { + id: "desc", }, }, - take: Number(limit), - orderBy: { - id: "desc", - }, - }); - - // Constuct HTTP Link header (next and prev) - const linkHeader = []; - if (objects.length > 0) { - const urlWithoutQuery = req.url.split("?")[0]; - linkHeader.push( - `<${urlWithoutQuery}?max_id=${objects[0].id}&limit=${limit}>; rel="next"`, - ); - linkHeader.push( - `<${urlWithoutQuery}?since_id=${ - objects[objects.length - 1].id - }&limit=${limit}>; rel="prev"`, - ); - } + req, + ); return jsonResponse( objects.map((user) => userToAPI(user)), 200, { - Link: linkHeader.join(", "), + Link: link, }, ); }); diff --git a/server/api/api/v1/statuses/[id]/unfavourite.ts b/server/api/api/v1/statuses/[id]/unfavourite.ts index 718cfc1e..6ec2c9d5 100644 --- a/server/api/api/v1/statuses/[id]/unfavourite.ts +++ b/server/api/api/v1/statuses/[id]/unfavourite.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { apiRoute, applyConfig } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { client } from "~database/datasource"; diff --git a/server/api/media/[id]/index.ts b/server/api/media/[id]/index.ts index 3e9f6318..efe78af8 100644 --- a/server/api/media/[id]/index.ts +++ b/server/api/media/[id]/index.ts @@ -1,9 +1,9 @@ import { apiRoute, applyConfig } from "@api"; -import { errorResponse } from "@response"; +import { errorResponse, response } from "@response"; export const meta = applyConfig({ allowedMethods: ["GET"], - route: "/media/:id", + route: "/api/v1/media/:id", ratelimits: { max: 100, duration: 60, @@ -35,11 +35,9 @@ export default apiRoute(async (req, matchedRoute) => { if (!(await file.exists())) return errorResponse("File not found", 404); // Can't directly copy file into Response because this crashes Bun for now - return new Response(buffer, { - headers: { - "Content-Type": file.type || "application/octet-stream", - "Content-Length": `${file.size - start}`, - "Content-Range": `bytes ${start}-${end}/${file.size}`, - }, + return response(buffer, 200, { + "Content-Type": file.type || "application/octet-stream", + "Content-Length": `${file.size - start}`, + "Content-Range": `bytes ${start}-${end}/${file.size}`, }); }); diff --git a/server/api/oauth/callback/[issuer]/index.ts b/server/api/oauth/callback/[issuer]/index.ts index 6b40f265..7c590896 100644 --- a/server/api/oauth/callback/[issuer]/index.ts +++ b/server/api/oauth/callback/[issuer]/index.ts @@ -157,7 +157,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => { return redirectToLogin("No user found with that account"); } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!flow.application) return redirectToLogin("Invalid client_id"); const code = randomBytes(32).toString("hex"); diff --git a/server/api/users/[uuid]/outbox/index.ts b/server/api/users/[uuid]/outbox/index.ts index ec0e97f7..4e7a3cc4 100644 --- a/server/api/users/[uuid]/outbox/index.ts +++ b/server/api/users/[uuid]/outbox/index.ts @@ -51,14 +51,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => { last: `${host}/users/${uuid}/outbox?page=1`, total_items: totalStatuses, // Server actor - author: `${config.http.base_url}/users/actor`, - next: - statuses.length === 20 - ? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}` - : undefined, - prev: - pageNumber > 1 - ? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}` + author: new URL("/users/actor", config.http.base_url).toString(), + next: statuses.length === 20 + ? new URL(`/users/${uuid}/outbox?page=${pageNumber + 1}`, config.http.base_url).toString(), + prev: pageNumber > 1 + ? new URL(`/users/${uuid}/outbox?page=${pageNumber - 1}`, config.http.base_url).toString() : undefined, items: statuses.map((s) => statusToLysand(s)), }); diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index 32aaea3f..5a50076f 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -172,7 +172,7 @@ describe("API Tests", () => { expect(account.statuses_count).toBe(0); expect(account.note).toBe(""); expect(account.url).toBe( - `${config.http.base_url}/users/${user.id}`, + new URL(`/users/${user.id}`, config.http.base_url).toString(), ); expect(account.avatar).toBeDefined(); expect(account.avatar_static).toBeDefined(); diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index b6a34569..a6b5dd7b 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -136,7 +136,6 @@ describe("API Tests", () => { "application/json", ); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion status = (await response.json()) as APIStatus; expect(status.content).toContain("Hello, world!"); expect(status.visibility).toBe("public"); @@ -184,7 +183,6 @@ describe("API Tests", () => { "application/json", ); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion status2 = (await response.json()) as APIStatus; expect(status2.content).toContain("This is a reply!"); expect(status2.visibility).toBe("public"); diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index 18d14411..1911d637 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -40,7 +40,6 @@ describe("POST /api/v1/apps/", () => { expect(response.status).toBe(200); expect(response.headers.get("content-type")).toBe("application/json"); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const json = await response.json(); expect(json).toEqual({ @@ -53,9 +52,7 @@ describe("POST /api/v1/apps/", () => { vapid_link: null, }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access client_id = json.client_id; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access client_secret = json.client_secret; }); }); @@ -111,7 +108,6 @@ describe("POST /oauth/token/", () => { }), ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const json = await response.json(); expect(response.status).toBe(200); @@ -123,7 +119,6 @@ describe("POST /oauth/token/", () => { created_at: expect.any(Number), }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access token = json; }); }); diff --git a/utils/api.ts b/utils/api.ts index 55aa682b..70a378f8 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -9,7 +9,6 @@ export const applyConfig = (routeMeta: APIRouteMeta) => { newMeta.ratelimits.duration *= config.ratelimits.duration_coeff; newMeta.ratelimits.max *= config.ratelimits.max_coeff; - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (config.custom_ratelimits[routeMeta.route]) { newMeta.ratelimits = config.custom_ratelimits[routeMeta.route]; } diff --git a/utils/constants.ts b/utils/constants.ts index 96bd661f..76522986 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,4 +1,4 @@ import { config } from "config-manager"; export const oauthRedirectUri = (issuer: string) => - `${config.http.base_url}/oauth/callback/${issuer}`; + new URL(`/oauth/callback/${issuer}`, config.http.base_url).toString(); diff --git a/utils/response.ts b/utils/response.ts index 69fc1e51..9da48c4c 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -1,12 +1,12 @@ import type { APActivity, APObject } from "activitypub-types"; import type { NodeObject } from "jsonld"; -export const jsonResponse = ( - data: object, +export const response = ( + data: BodyInit | null = null, status = 200, headers: Record = {}, ) => { - return new Response(JSON.stringify(data), { + return new Response(data, { headers: { "Content-Type": "application/json", "X-Frame-Options": "DENY", @@ -27,12 +27,32 @@ export const jsonResponse = ( }); }; +export const clientResponse = ( + data: BodyInit | null = null, + status = 200, + headers: Record = {}, +) => { + return response(data, status, { + ...headers, + "Content-Security-Policy": + "default-src 'none'; frame-ancestors 'none'; form-action 'none'; connect-src 'self' blob: https: wss:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; media-src 'self'; frame-src 'none'; worker-src 'self'; manifest-src 'self'; prefetch-src 'self'; base-uri 'none';", + }); +}; + +export const jsonResponse = ( + data: object, + status = 200, + headers: Record = {}, +) => { + return response(JSON.stringify(data), status, { + "Content-Type": "application/json", + ...headers, + }); +}; + export const xmlResponse = (data: string, status = 200) => { - return new Response(data, { - headers: { - "Content-Type": "application/xml", - }, - status, + return response(data, status, { + "Content-Type": "application/xml", }); }; @@ -40,11 +60,8 @@ export const jsonLdResponse = ( data: NodeObject | APActivity | APObject, status = 200, ) => { - return new Response(JSON.stringify(data), { - headers: { - "Content-Type": "application/activity+json", - }, - status, + return response(JSON.stringify(data), status, { + "Content-Type": "application/activity+json", }); }; @@ -56,3 +73,9 @@ export const errorResponse = (error: string, status = 500) => { status, ); }; + +export const redirect = (url: string | URL, status = 302) => { + return response(null, status, { + Location: url.toString(), + }); +}; diff --git a/utils/timelines.ts b/utils/timelines.ts index ea0f638a..a58632e0 100644 --- a/utils/timelines.ts +++ b/utils/timelines.ts @@ -1,8 +1,14 @@ -import type { Status, User, Prisma } from "@prisma/client"; +import type { Status, User, Prisma, Notification } from "@prisma/client"; -export async function fetchTimeline( - model: Prisma.StatusDelegate | Prisma.UserDelegate, - args: Prisma.StatusFindManyArgs | Prisma.UserFindManyArgs, +export async function fetchTimeline( + model: + | Prisma.StatusDelegate + | Prisma.UserDelegate + | Prisma.NotificationDelegate, + args: + | Prisma.StatusFindManyArgs + | Prisma.UserFindManyArgs + | Prisma.NotificationFindManyArgs, req: Request, ) { // BEFORE: Before in a top-to-bottom order, so the most recent posts