Refactors, bugfixing

This commit is contained in:
Jesse Wierzbinski 2024-04-07 17:28:18 -10:00
parent 5812618170
commit e26d604a54
No known key found for this signature in database
42 changed files with 370 additions and 376 deletions

View file

@ -18,7 +18,7 @@ if (!token) {
} }
const fetchTimeline = () => const fetchTimeline = () =>
fetch(`${config.http.base_url}/api/v1/timelines/home`, { fetch(new URL("/api/v1/timelines/home", config.http.base_url), {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },

1
cli.ts
View file

@ -388,7 +388,6 @@ const cliBuilder = new CliBuilder([
for (const key of keys) { for (const key of keys) {
if (!args.fields.includes(key)) { if (!args.fields.includes(key)) {
// @ts-expect-error This is fine // @ts-expect-error This is fine
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete data[key]; delete data[key];
} }
} }

View file

@ -58,13 +58,11 @@ export const attachmentToAPI = (
}; };
export const getUrl = (name: string, config: Config) => { export const getUrl = (name: string, config: Config) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
if (config.media.backend === MediaBackendType.LOCAL) { if (config.media.backend === MediaBackendType.LOCAL) {
return `${config.http.base_url}/media/${name}`; return new URL(`/media/${name}`, config.http.base_url).toString();
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
} }
if (config.media.backend === MediaBackendType.S3) { if (config.media.backend === MediaBackendType.S3) {
return `${config.s3.public_url}/${name}`; return new URL(`/${name}`, config.s3.public_url).toString();
} }
return ""; return "";
}; };

View file

@ -1,7 +1,6 @@
import type { Like, Prisma } from "@prisma/client"; import type { Like } from "@prisma/client";
import { config } from "config-manager"; import { config } from "config-manager";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { Like as LysandLike } from "~types/lysand/Object"; import type { Like as LysandLike } from "~types/lysand/Object";
import type { StatusWithRelations } from "./Status"; import type { StatusWithRelations } from "./Status";
import type { UserWithRelations } from "./User"; import type { UserWithRelations } from "./User";
@ -18,7 +17,7 @@ export const toLysand = (like: Like): LysandLike => {
created_at: new Date(like.createdAt).toISOString(), created_at: new Date(like.createdAt).toISOString(),
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten // biome-ignore lint/suspicious/noExplicitAny: to be rewritten
object: (like as any).liked?.uri, 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(),
}; };
}; };

View file

@ -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 type { LysandObject } from "@prisma/client";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import type { LysandObjectType } from "~types/lysand/Object"; import type { LysandObjectType } from "~types/lysand/Object";

View file

@ -149,7 +149,6 @@ export const federateStatusTo = async (
new TextEncoder().encode("request_body"), new TextEncoder().encode("request_body"),
); );
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const userInbox = new URL(user.endpoints.inbox); const userInbox = new URL(user.endpoints.inbox);
const date = new Date(); const date = new Date();

View file

@ -21,7 +21,6 @@ import type { LysandPublication, Note } from "~types/lysand/Object";
import { applicationToAPI } from "./Application"; import { applicationToAPI } from "./Application";
import { attachmentToAPI } from "./Attachment"; import { attachmentToAPI } from "./Attachment";
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji"; import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { UserWithRelations } from "./User"; import type { UserWithRelations } from "./User";
import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User"; import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User";
import { statusAndUserRelations, userRelations } from "./relations"; import { statusAndUserRelations, userRelations } from "./relations";
@ -118,7 +117,6 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
/** /**
* Return all the ancestors of this post, * Return all the ancestors of this post,
*/ */
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
export const getAncestors = async ( export const getAncestors = async (
status: StatusWithRelations, status: StatusWithRelations,
fetcher: UserWithRelations | null, fetcher: UserWithRelations | null,
@ -154,7 +152,6 @@ export const getAncestors = async (
* Return all the descendants of this post (recursive) * Return all the descendants of this post (recursive)
* Temporary implementation, will be replaced with a recursive SQL query when Prisma adds support for it * 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 ( export const getDescendants = async (
status: StatusWithRelations, status: StatusWithRelations,
fetcher: UserWithRelations | null, fetcher: UserWithRelations | null,
@ -295,7 +292,10 @@ export const createNewStatus = async (data: {
isReblog: false, isReblog: false,
uri: uri:
data.uri || data.uri ||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`, new URL(
`/statuses/FAKE-${crypto.randomUUID()}`,
config.http.base_url,
).toString(),
mentions: { mentions: {
connect: mentions.map((mention) => { connect: mentions.map((mention) => {
return { return {
@ -313,7 +313,12 @@ export const createNewStatus = async (data: {
id: status.id, id: status.id,
}, },
data: { 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, include: statusAndUserRelations,
}); });
@ -467,13 +472,10 @@ export const statusToAPI = async (
card: null, card: null,
content: status.content, content: status.content,
emojis: status.emojis.map((emoji) => emojiToAPI(emoji)), emojis: status.emojis.map((emoji) => emojiToAPI(emoji)),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
favourited: !!(status.likes ?? []).find( favourited: !!(status.likes ?? []).find(
(like) => like.likerId === user?.id, (like) => like.likerId === user?.id,
), ),
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
favourites_count: (status.likes ?? []).length, favourites_count: (status.likes ?? []).length,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
media_attachments: (status.attachments ?? []).map( media_attachments: (status.attachments ?? []).map(
(a) => attachmentToAPI(a) as APIAttachment, (a) => attachmentToAPI(a) as APIAttachment,
), ),
@ -485,7 +487,7 @@ export const statusToAPI = async (
?.muting || false ?.muting || false
: false, : false,
pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false, pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false,
// TODO: Add pols // TODO: Add polls
poll: null, poll: null,
reblog: status.reblog reblog: status.reblog
? await statusToAPI(status.reblog as unknown as StatusWithRelations) ? await statusToAPI(status.reblog as unknown as StatusWithRelations)
@ -501,9 +503,9 @@ export const statusToAPI = async (
sensitive: status.sensitive, sensitive: status.sensitive,
spoiler_text: status.spoilerText, spoiler_text: status.spoilerText,
tags: [], tags: [],
uri: `${config.http.base_url}/statuses/${status.id}`, uri: new URL(`/statuses/${status.id}`, config.http.base_url).toString(),
visibility: "public", visibility: "public",
url: `${config.http.base_url}/statuses/${status.id}`, url: new URL(`/statuses/${status.id}`, config.http.base_url).toString(),
bookmarked: false, bookmarked: false,
quote: status.quotingPost quote: status.quotingPost
? await statusToAPI( ? await statusToAPI(
@ -567,7 +569,7 @@ export const statusToLysand = (status: StatusWithRelations): Note => {
created_at: new Date(status.createdAt).toISOString(), created_at: new Date(status.createdAt).toISOString(),
id: status.id, id: status.id,
author: status.authorId, 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: [ contents: [
{ {
content: status.content, content: status.content,

View file

@ -4,13 +4,13 @@ import { Prisma } from "@prisma/client";
import { type Config, config } from "config-manager"; import { type Config, config } from "config-manager";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { MediaBackendType } from "~packages/media-manager";
import type { APIAccount } from "~types/entities/account"; import type { APIAccount } from "~types/entities/account";
import type { APISource } from "~types/entities/source"; import type { APISource } from "~types/entities/source";
import type { LysandUser } from "~types/lysand/Object"; import type { LysandUser } from "~types/lysand/Object";
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji"; import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
import { addInstanceIfNotExists } from "./Instance"; import { addInstanceIfNotExists } from "./Instance";
import { userRelations } from "./relations"; import { userRelations } from "./relations";
import { getUrl } from "./Attachment";
export interface AuthData { export interface AuthData {
user: UserWithRelations | null; user: UserWithRelations | null;
@ -35,14 +35,7 @@ export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
*/ */
export const getAvatarUrl = (user: User, config: Config) => { export const getAvatarUrl = (user: User, config: Config) => {
if (!user.avatar) return config.defaults.avatar; if (!user.avatar) return config.defaults.avatar;
if (config.media.backend === MediaBackendType.LOCAL) { return getUrl(user.avatar, config);
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 "";
}; };
/** /**
@ -52,14 +45,7 @@ export const getAvatarUrl = (user: User, config: Config) => {
*/ */
export const getHeaderUrl = (user: User, config: Config) => { export const getHeaderUrl = (user: User, config: Config) => {
if (!user.header) return config.defaults.header; if (!user.header) return config.defaults.header;
if (config.media.backend === MediaBackendType.LOCAL) { return getUrl(user.header, config);
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 "";
}; };
export const getFromRequest = async (req: Request): Promise<AuthData> => { export const getFromRequest = async (req: Request): Promise<AuthData> => {
@ -224,16 +210,7 @@ export const createNewLocalUser = async (data: {
id: user.id, id: user.id,
}, },
data: { data: {
uri: `${config.http.base_url}/users/${user.id}`, uri: new URL(`/users/${user.id}`, config.http.base_url).toString(),
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`,
},
}, },
include: userRelations, include: userRelations,
}); });
@ -399,13 +376,35 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
}, },
], ],
created_at: new Date(user.createdAt).toISOString(), created_at: new Date(user.createdAt).toISOString(),
disliked: `${user.uri}/disliked`,
featured: `${user.uri}/featured`, disliked: new URL(
liked: `${user.uri}/liked`, `/users/${user.id}/disliked`,
followers: `${user.uri}/followers`, config.http.base_url,
following: `${user.uri}/following`, ).toString(),
inbox: `${user.uri}/inbox`, featured: new URL(
outbox: `${user.uri}/outbox`, `/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, indexable: false,
username: user.username, username: user.username,
avatar: [ avatar: [
@ -444,7 +443,10 @@ export const userToLysand = (user: UserWithRelations): LysandUser => {
], ],
})), })),
public_key: { 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, public_key: user.publicKey,
}, },
extensions: { extensions: {

View file

@ -174,7 +174,6 @@ export class CliBuilder {
// Split the command into parts and iterate over them // Split the command into parts and iterate over them
for (const part of command.categories) { 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) // 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 (!currentLevel[part] && part !== "__proto__") {
// If this is the last part of the command, add the command itself // If this is the last part of the command, add the command itself
if ( if (
@ -297,7 +296,6 @@ export class CliBuilder {
type ExecuteFunction<T> = ( type ExecuteFunction<T> = (
instance: CliCommand, instance: CliCommand,
args: Partial<T>, args: Partial<T>,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => Promise<number> | Promise<void> | number | void; ) => Promise<number> | Promise<void> | number | void;
/** /**

View file

@ -64,9 +64,7 @@ export class MediaBackend {
* @returns The file as a File object * @returns The file as a File object
*/ */
public getFileByHash( public getFileByHash(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
file: string, file: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
databaseHashFetcher: (sha256: string) => Promise<string>, databaseHashFetcher: (sha256: string) => Promise<string>,
): Promise<File | null> { ): Promise<File | null> {
return Promise.reject( return Promise.reject(
@ -79,7 +77,6 @@ export class MediaBackend {
* @param filename File name * @param filename File name
* @returns The file as a File object * @returns The file as a File object
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getFile(filename: string): Promise<File | null> { public getFile(filename: string): Promise<File | null> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),
@ -91,7 +88,6 @@ export class MediaBackend {
* @param file File to add * @param file File to add
* @returns Metadata about the uploaded file * @returns Metadata about the uploaded file
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public addFile(file: File): Promise<UploadedFileMetadata> { public addFile(file: File): Promise<UploadedFileMetadata> {
return Promise.reject( return Promise.reject(
new Error("Do not call MediaBackend directly: use a subclass"), new Error("Do not call MediaBackend directly: use a subclass"),

View file

@ -20,7 +20,6 @@ export default defineConfig({
}, },
}, },
define: { define: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
__VERSION__: JSON.stringify(pkg.version), __VERSION__: JSON.stringify(pkg.version),
}, },
ssr: { ssr: {

View file

@ -1,4 +1,9 @@
import { errorResponse, jsonResponse } from "@response"; import {
clientResponse,
errorResponse,
jsonResponse,
response,
} from "@response";
import type { Config } from "config-manager"; import type { Config } from "config-manager";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import type { LogManager, MultiLogManager } from "log-manager"; import type { LogManager, MultiLogManager } from "log-manager";
@ -22,10 +27,7 @@ export const createServer = (
for (const ip of config.http.banned_ips) { for (const ip of config.http.banned_ips) {
try { try {
if (matches(ip, request_ip)) { if (matches(ip, request_ip)) {
return new Response(undefined, { return errorResponse("Forbidden", 403);
status: 403,
statusText: "Forbidden",
});
} }
} catch (e) { } catch (e) {
console.error(`[-] Error while parsing banned IP "${ip}" `); console.error(`[-] Error while parsing banned IP "${ip}" `);
@ -38,10 +40,7 @@ export const createServer = (
for (const agent of config.http.banned_user_agents) { for (const agent of config.http.banned_user_agents) {
if (new RegExp(agent).test(ua)) { if (new RegExp(agent).test(ua)) {
return new Response(undefined, { return errorResponse("Forbidden", 403);
status: 403,
statusText: "Forbidden",
});
} }
} }
@ -56,7 +55,7 @@ export const createServer = (
); );
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return response(file);
} }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
@ -81,7 +80,7 @@ export const createServer = (
); );
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return response(file);
} }
await logger.log( await logger.log(
LogLevel.ERROR, LogLevel.ERROR,
@ -126,12 +125,12 @@ export const createServer = (
// Check for allowed requests // Check for allowed requests
// @ts-expect-error Stupid error // @ts-expect-error Stupid error
if (!meta.allowedMethods.includes(req.method as string)) { if (!meta.allowedMethods.includes(req.method as string)) {
return new Response(undefined, { return errorResponse(
status: 405, `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
", ", ", ",
)}`, )}`,
}); 405,
);
} }
// TODO: Check for ratelimits // TODO: Check for ratelimits
@ -140,20 +139,14 @@ export const createServer = (
// Check for authentication if required // Check for authentication if required
if (meta.auth.required) { if (meta.auth.required) {
if (!auth.user) { if (!auth.user) {
return new Response(undefined, { return errorResponse("Unauthorized", 401);
status: 401,
statusText: "Unauthorized",
});
} }
} else if ( } else if (
// @ts-expect-error Stupid error // @ts-expect-error Stupid error
(meta.auth.requiredOnMethods ?? []).includes(req.method) (meta.auth.requiredOnMethods ?? []).includes(req.method)
) { ) {
if (!auth.user) { if (!auth.user) {
return new Response(undefined, { return errorResponse("Unauthorized", 401);
status: 401,
statusText: "Unauthorized",
});
} }
} }
@ -167,10 +160,7 @@ export const createServer = (
"Server.RouteRequestParser", "Server.RouteRequestParser",
e as Error, e as Error,
); );
return new Response(undefined, { return errorResponse("Bad request", 400);
status: 400,
statusText: "Bad request",
});
} }
return await file.default(req.clone(), matchedRoute, { return await file.default(req.clone(), matchedRoute, {
@ -196,7 +186,7 @@ export const createServer = (
// Serve from pages/dist/assets // Serve from pages/dist/assets
if (await file.exists()) { if (await file.exists()) {
return new Response(file); return clientResponse(file);
} }
return errorResponse("Asset not found", 404); return errorResponse("Asset not found", 404);
} }
@ -207,7 +197,7 @@ export const createServer = (
const file = Bun.file("./pages/dist/index.html"); const file = Bun.file("./pages/dist/index.html");
// Serve from pages/dist // Serve from pages/dist
return new Response(file); return clientResponse(file);
} }
const proxy = await fetch( const proxy = await fetch(
req.url.replace( req.url.replace(

View file

@ -19,7 +19,10 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return xmlResponse(` return xmlResponse(`
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" template="${config.http.base_url}/.well-known/webfinger?resource={uri}"/> <Link rel="lrdd" template="${new URL(
"/.well-known/webfinger",
config.http.base_url,
).toString()}?resource={uri}"/>
</XRD> </XRD>
`); `);
}); });

View file

@ -1,4 +1,5 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { redirect } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -15,10 +16,8 @@ export const meta = applyConfig({
export default apiRoute(async (req, matchedRoute, extraData) => { export default apiRoute(async (req, matchedRoute, extraData) => {
const config = await extraData.configManager.getConfig(); const config = await extraData.configManager.getConfig();
return new Response("", { return redirect(
status: 301, new URL("/.well-known/nodeinfo/2.0", config.http.base_url),
headers: { 301,
Location: `${config.http.base_url}/.well-known/nodeinfo/2.0`, );
},
});
}); });

View file

@ -41,18 +41,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
links: [ links: [
{ {
rel: "self", rel: "self",
type: "application/activity+json", type: "application/json",
href: `${config.http.base_url}/users/${user.username}/actor`, href: new URL(
}, `/users/${user.id}`,
{ config.http.base_url,
rel: "https://webfinger.net/rel/profile-page", ).toString(),
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`,
}, },
], ],
}); });

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { userToAPI } from "~database/entities/User"; import { userToAPI } from "~database/entities/User";

View file

@ -33,7 +33,6 @@ export default apiRoute<{
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { notifications, duration } = extraData.parsedRequest; const { notifications, duration } = extraData.parsedRequest;
const user = await client.user.findUnique({ const user = await client.user.findUnique({

View file

@ -1,6 +1,4 @@
import { apiRoute, applyConfig } from "@api"; 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 { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines"; import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse, response } from "@response";
import { tempmailDomains } from "@tempmail"; import { tempmailDomains } from "@tempmail";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
@ -200,7 +200,5 @@ export default apiRoute<{
email: body.email ?? "", email: body.email ?? "",
}); });
return new Response("", { return response(null, 200);
status: 200,
});
}); });

View file

@ -1,7 +1,8 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -13,15 +14,25 @@ export const meta = applyConfig({
}, },
auth: { auth: {
required: true, 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; const { user } = extraData.auth;
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const blocks = await client.user.findMany({ const { max_id, since_id, limit = 40 } = extraData.parsedRequest;
const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
client.user,
{
where: { where: {
relationshipSubjects: { relationshipSubjects: {
some: { some: {
@ -29,9 +40,22 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
blocking: true, blocking: true,
}, },
}, },
id: {
lt: max_id,
gte: since_id,
},
}, },
include: userRelations, include: userRelations,
}); take: Number(limit),
},
req,
);
return jsonResponse(blocks.map((u) => userToAPI(u))); return jsonResponse(
blocks.map((u) => userToAPI(u)),
200,
{
Link: link,
},
);
}); });

View file

@ -1,7 +1,11 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { statusAndUserRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -32,7 +36,9 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const objects = await client.status.findMany({ const { objects, link } = await fetchTimeline<StatusWithRelations>(
client.status,
{
where: { where: {
id: { id: {
lt: max_id ?? undefined, lt: max_id ?? undefined,
@ -50,17 +56,9 @@ export default apiRoute<{
orderBy: { orderBy: {
id: "desc", id: "desc",
}, },
}); },
req,
// 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"`,
); );
}
return jsonResponse( return jsonResponse(
await Promise.all( await Promise.all(
@ -68,7 +66,7 @@ export default apiRoute<{
), ),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,7 +1,8 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -32,7 +33,9 @@ export default apiRoute<{
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const objects = await client.user.findMany({ const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user,
{
where: { where: {
id: { id: {
lt: max_id ?? undefined, lt: max_id ?? undefined,
@ -51,23 +54,15 @@ export default apiRoute<{
orderBy: { orderBy: {
id: "desc", id: "desc",
}, },
}); },
req,
// 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"`,
); );
}
return jsonResponse( return jsonResponse(
objects.map((user) => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse, response } from "@response";
import type { MediaBackend } from "media-manager"; import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager"; import { MediaBackendType } from "media-manager";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
@ -52,9 +52,7 @@ export default apiRoute<{
if (attachment.url) { if (attachment.url) {
return jsonResponse(attachmentToAPI(attachment)); return jsonResponse(attachmentToAPI(attachment));
} }
return new Response(null, { return response(null, 206);
status: 206,
});
} }
case "PUT": { case "PUT": {
const { description, thumbnail } = extraData.parsedRequest; const { description, thumbnail } = extraData.parsedRequest;

View file

@ -1,7 +1,8 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; 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"; import { userRelations } from "~database/entities/relations";
export const meta = applyConfig({ export const meta = applyConfig({
@ -13,15 +14,23 @@ export const meta = applyConfig({
}, },
auth: { auth: {
required: true, 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 { user } = extraData.auth;
const { max_id, since_id, limit = 40 } = extraData.parsedRequest;
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const blocks = await client.user.findMany({ const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
client.user,
{
where: { where: {
relationshipSubjects: { relationshipSubjects: {
some: { some: {
@ -29,9 +38,16 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
muting: true, muting: true,
}, },
}, },
id: {
lt: max_id,
gte: since_id,
},
}, },
include: userRelations, include: userRelations,
}); take: Number(limit),
},
req,
);
return jsonResponse(blocks.map((u) => userToAPI(u))); return jsonResponse(blocks.map((u) => userToAPI(u)));
}); });

View file

@ -1,5 +1,7 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import type { Prisma } from "@prisma/client";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { notificationToAPI } from "~database/entities/Notification"; import { notificationToAPI } from "~database/entities/Notification";
import { import {
@ -50,17 +52,25 @@ export default apiRoute<{
return errorResponse("Can't use both types and exclude_types", 400); return errorResponse("Can't use both types and exclude_types", 400);
} }
const objects = await client.notification.findMany({ const { objects, link } = await fetchTimeline<
Prisma.NotificationGetPayload<{
include: {
account: {
include: typeof userRelations;
};
status: {
include: typeof statusAndUserRelations;
};
};
}>
>(
client.notification,
{
where: { where: {
notifiedId: user.id,
id: { id: {
lt: max_id, lt: max_id ?? undefined,
gt: min_id, gte: since_id ?? undefined,
gte: since_id, gt: min_id ?? undefined,
},
type: {
in: types,
notIn: exclude_types,
}, },
accountId: account_id, accountId: account_id,
}, },
@ -76,27 +86,15 @@ export default apiRoute<{
id: "desc", id: "desc",
}, },
take: Number(limit), take: Number(limit),
}); },
req,
// 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"`,
);
}
return jsonResponse( return jsonResponse(
await Promise.all(objects.map((n) => notificationToAPI(n))), await Promise.all(objects.map((n) => notificationToAPI(n))),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { isViewableByUser } from "~database/entities/Status"; import { isViewableByUser } from "~database/entities/Status";
import { userToAPI } from "~database/entities/User"; import { userToAPI, type UserWithRelations } from "~database/entities/User";
import { import {
statusAndUserRelations, statusAndUserRelations,
userRelations, userRelations,
@ -42,18 +43,15 @@ export default apiRoute<{
if (!status || !isViewableByUser(status, user)) if (!status || !isViewableByUser(status, user))
return errorResponse("Record not found", 404); return errorResponse("Record not found", 404);
const { const { max_id, min_id, since_id, limit = 40 } = extraData.parsedRequest;
max_id = null,
min_id = null,
since_id = null,
limit = 40,
} = extraData.parsedRequest;
// Check for limit limits // Check for limit limits
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400); if (limit < 1) return errorResponse("Invalid limit", 400);
const objects = await client.user.findMany({ const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user,
{
where: { where: {
likes: { likes: {
some: { some: {
@ -61,9 +59,9 @@ export default apiRoute<{
}, },
}, },
id: { id: {
lt: max_id ?? undefined, lt: max_id,
gte: since_id ?? undefined, gte: since_id,
gt: min_id ?? undefined, gt: min_id,
}, },
}, },
include: { include: {
@ -78,27 +76,15 @@ export default apiRoute<{
orderBy: { orderBy: {
id: "desc", id: "desc",
}, },
}); },
req,
// 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"`,
);
}
return jsonResponse( return jsonResponse(
objects.map((user) => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
@ -58,7 +57,10 @@ export default apiRoute<{
authorId: user.id, authorId: user.id,
reblogId: status.id, reblogId: status.id,
isReblog: true, isReblog: true,
uri: `${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`, uri: new URL(
`/statuses/FAKE-${crypto.randomUUID()}`,
config.http.base_url,
).toString(),
visibility, visibility,
sensitive: false, sensitive: false,
}, },
@ -68,7 +70,10 @@ export default apiRoute<{
await client.status.update({ await client.status.update({
where: { id: newReblog.id }, where: { id: newReblog.id },
data: { data: {
uri: `${config.http.base_url}/statuses/${newReblog.id}`, uri: new URL(
`/statuses/${newReblog.id}`,
config.http.base_url,
).toString(),
}, },
include: statusAndUserRelations, include: statusAndUserRelations,
}); });
@ -89,7 +94,10 @@ export default apiRoute<{
await statusToAPI( await statusToAPI(
{ {
...newReblog, ...newReblog,
uri: `${config.http.base_url}/statuses/${newReblog.id}`, uri: new URL(
`/statuses/${newReblog.id}`,
config.http.base_url,
).toString(),
}, },
user, user,
), ),

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { fetchTimeline } from "@timelines";
import { client } from "~database/datasource"; import { client } from "~database/datasource";
import { isViewableByUser } from "~database/entities/Status"; import { isViewableByUser } from "~database/entities/Status";
import { userToAPI } from "~database/entities/User"; import { type UserWithRelations, userToAPI } from "~database/entities/User";
import { import {
statusAndUserRelations, statusAndUserRelations,
userRelations, userRelations,
@ -53,7 +54,9 @@ export default apiRoute<{
if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400); if (limit > 80) return errorResponse("Invalid limit (maximum is 80)", 400);
if (limit < 1) return errorResponse("Invalid limit", 400); if (limit < 1) return errorResponse("Invalid limit", 400);
const objects = await client.user.findMany({ const { objects, link } = await fetchTimeline<UserWithRelations>(
client.user,
{
where: { where: {
statuses: { statuses: {
some: { some: {
@ -79,27 +82,15 @@ export default apiRoute<{
orderBy: { orderBy: {
id: "desc", id: "desc",
}, },
}); },
req,
// 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"`,
);
}
return jsonResponse( return jsonResponse(
objects.map((user) => userToAPI(user)), objects.map((user) => userToAPI(user)),
200, 200,
{ {
Link: linkHeader.join(", "), Link: link,
}, },
); );
}); });

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { client } from "~database/datasource"; import { client } from "~database/datasource";

View file

@ -1,9 +1,9 @@
import { apiRoute, applyConfig } from "@api"; import { apiRoute, applyConfig } from "@api";
import { errorResponse } from "@response"; import { errorResponse, response } from "@response";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
route: "/media/:id", route: "/api/v1/media/:id",
ratelimits: { ratelimits: {
max: 100, max: 100,
duration: 60, duration: 60,
@ -35,11 +35,9 @@ export default apiRoute(async (req, matchedRoute) => {
if (!(await file.exists())) return errorResponse("File not found", 404); if (!(await file.exists())) return errorResponse("File not found", 404);
// Can't directly copy file into Response because this crashes Bun for now // Can't directly copy file into Response because this crashes Bun for now
return new Response(buffer, { return response(buffer, 200, {
headers: {
"Content-Type": file.type || "application/octet-stream", "Content-Type": file.type || "application/octet-stream",
"Content-Length": `${file.size - start}`, "Content-Length": `${file.size - start}`,
"Content-Range": `bytes ${start}-${end}/${file.size}`, "Content-Range": `bytes ${start}-${end}/${file.size}`,
},
}); });
}); });

View file

@ -157,7 +157,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return redirectToLogin("No user found with that account"); 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"); if (!flow.application) return redirectToLogin("Invalid client_id");
const code = randomBytes(32).toString("hex"); const code = randomBytes(32).toString("hex");

View file

@ -51,14 +51,11 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
last: `${host}/users/${uuid}/outbox?page=1`, last: `${host}/users/${uuid}/outbox?page=1`,
total_items: totalStatuses, total_items: totalStatuses,
// Server actor // Server actor
author: `${config.http.base_url}/users/actor`, author: new URL("/users/actor", config.http.base_url).toString(),
next: next: statuses.length === 20
statuses.length === 20 ? new URL(`/users/${uuid}/outbox?page=${pageNumber + 1}`, config.http.base_url).toString(),
? `${host}/users/${uuid}/outbox?page=${pageNumber + 1}` prev: pageNumber > 1
: undefined, ? new URL(`/users/${uuid}/outbox?page=${pageNumber - 1}`, config.http.base_url).toString()
prev:
pageNumber > 1
? `${host}/users/${uuid}/outbox?page=${pageNumber - 1}`
: undefined, : undefined,
items: statuses.map((s) => statusToLysand(s)), items: statuses.map((s) => statusToLysand(s)),
}); });

View file

@ -172,7 +172,7 @@ describe("API Tests", () => {
expect(account.statuses_count).toBe(0); expect(account.statuses_count).toBe(0);
expect(account.note).toBe(""); expect(account.note).toBe("");
expect(account.url).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).toBeDefined();
expect(account.avatar_static).toBeDefined(); expect(account.avatar_static).toBeDefined();

View file

@ -136,7 +136,6 @@ describe("API Tests", () => {
"application/json", "application/json",
); );
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
status = (await response.json()) as APIStatus; status = (await response.json()) as APIStatus;
expect(status.content).toContain("Hello, world!"); expect(status.content).toContain("Hello, world!");
expect(status.visibility).toBe("public"); expect(status.visibility).toBe("public");
@ -184,7 +183,6 @@ describe("API Tests", () => {
"application/json", "application/json",
); );
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
status2 = (await response.json()) as APIStatus; status2 = (await response.json()) as APIStatus;
expect(status2.content).toContain("This is a reply!"); expect(status2.content).toContain("This is a reply!");
expect(status2.visibility).toBe("public"); expect(status2.visibility).toBe("public");

View file

@ -40,7 +40,6 @@ describe("POST /api/v1/apps/", () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json"); expect(response.headers.get("content-type")).toBe("application/json");
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const json = await response.json(); const json = await response.json();
expect(json).toEqual({ expect(json).toEqual({
@ -53,9 +52,7 @@ describe("POST /api/v1/apps/", () => {
vapid_link: null, vapid_link: null,
}); });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
client_id = json.client_id; 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; 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(); const json = await response.json();
expect(response.status).toBe(200); expect(response.status).toBe(200);
@ -123,7 +119,6 @@ describe("POST /oauth/token/", () => {
created_at: expect.any(Number), created_at: expect.any(Number),
}); });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
token = json; token = json;
}); });
}); });

View file

@ -9,7 +9,6 @@ export const applyConfig = (routeMeta: APIRouteMeta) => {
newMeta.ratelimits.duration *= config.ratelimits.duration_coeff; newMeta.ratelimits.duration *= config.ratelimits.duration_coeff;
newMeta.ratelimits.max *= config.ratelimits.max_coeff; newMeta.ratelimits.max *= config.ratelimits.max_coeff;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (config.custom_ratelimits[routeMeta.route]) { if (config.custom_ratelimits[routeMeta.route]) {
newMeta.ratelimits = config.custom_ratelimits[routeMeta.route]; newMeta.ratelimits = config.custom_ratelimits[routeMeta.route];
} }

View file

@ -1,4 +1,4 @@
import { config } from "config-manager"; import { config } from "config-manager";
export const oauthRedirectUri = (issuer: string) => export const oauthRedirectUri = (issuer: string) =>
`${config.http.base_url}/oauth/callback/${issuer}`; new URL(`/oauth/callback/${issuer}`, config.http.base_url).toString();

View file

@ -1,12 +1,12 @@
import type { APActivity, APObject } from "activitypub-types"; import type { APActivity, APObject } from "activitypub-types";
import type { NodeObject } from "jsonld"; import type { NodeObject } from "jsonld";
export const jsonResponse = ( export const response = (
data: object, data: BodyInit | null = null,
status = 200, status = 200,
headers: Record<string, string> = {}, headers: Record<string, string> = {},
) => { ) => {
return new Response(JSON.stringify(data), { return new Response(data, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Frame-Options": "DENY", "X-Frame-Options": "DENY",
@ -27,12 +27,32 @@ export const jsonResponse = (
}); });
}; };
export const clientResponse = (
data: BodyInit | null = null,
status = 200,
headers: Record<string, string> = {},
) => {
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<string, string> = {},
) => {
return response(JSON.stringify(data), status, {
"Content-Type": "application/json",
...headers,
});
};
export const xmlResponse = (data: string, status = 200) => { export const xmlResponse = (data: string, status = 200) => {
return new Response(data, { return response(data, status, {
headers: {
"Content-Type": "application/xml", "Content-Type": "application/xml",
},
status,
}); });
}; };
@ -40,11 +60,8 @@ export const jsonLdResponse = (
data: NodeObject | APActivity | APObject, data: NodeObject | APActivity | APObject,
status = 200, status = 200,
) => { ) => {
return new Response(JSON.stringify(data), { return response(JSON.stringify(data), status, {
headers: {
"Content-Type": "application/activity+json", "Content-Type": "application/activity+json",
},
status,
}); });
}; };
@ -56,3 +73,9 @@ export const errorResponse = (error: string, status = 500) => {
status, status,
); );
}; };
export const redirect = (url: string | URL, status = 302) => {
return response(null, status, {
Location: url.toString(),
});
};

View file

@ -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<T extends User | Status>( export async function fetchTimeline<T extends User | Status | Notification>(
model: Prisma.StatusDelegate | Prisma.UserDelegate, model:
args: Prisma.StatusFindManyArgs | Prisma.UserFindManyArgs, | Prisma.StatusDelegate
| Prisma.UserDelegate
| Prisma.NotificationDelegate,
args:
| Prisma.StatusFindManyArgs
| Prisma.UserFindManyArgs
| Prisma.NotificationFindManyArgs,
req: Request, req: Request,
) { ) {
// BEFORE: Before in a top-to-bottom order, so the most recent posts // BEFORE: Before in a top-to-bottom order, so the most recent posts