mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor: 🔥 Remove dead code
This commit is contained in:
parent
592f7c0ac2
commit
7b05a34cce
|
|
@ -1,90 +0,0 @@
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
|
||||||
import type * as Lysand from "lysand-types";
|
|
||||||
import { db } from "~drizzle/db";
|
|
||||||
import { LysandObjects } from "~drizzle/schema";
|
|
||||||
import { findFirstUser } from "./User";
|
|
||||||
|
|
||||||
export type LysandObject = InferSelectModel<typeof LysandObjects>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a Lysand object in the database.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const createFromObject = async (
|
|
||||||
object: Lysand.Entity,
|
|
||||||
authorUri: string,
|
|
||||||
) => {
|
|
||||||
const foundObject = await db.query.LysandObjects.findFirst({
|
|
||||||
where: (o, { eq }) => eq(o.remoteId, object.id),
|
|
||||||
with: {
|
|
||||||
author: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (foundObject) {
|
|
||||||
return foundObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = await findFirstUser({
|
|
||||||
where: (user, { eq }) => eq(user.uri, authorUri),
|
|
||||||
});
|
|
||||||
|
|
||||||
return await db.insert(LysandObjects).values({
|
|
||||||
authorId: author?.id,
|
|
||||||
createdAt: new Date(object.created_at).toISOString(),
|
|
||||||
extensions: object.extensions,
|
|
||||||
remoteId: object.id,
|
|
||||||
type: object.type,
|
|
||||||
uri: object.uri,
|
|
||||||
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
|
||||||
extraData: Object.fromEntries(
|
|
||||||
Object.entries(object).filter(
|
|
||||||
([key]) =>
|
|
||||||
![
|
|
||||||
"id",
|
|
||||||
"author",
|
|
||||||
"created_at",
|
|
||||||
"extensions",
|
|
||||||
"type",
|
|
||||||
"uri",
|
|
||||||
].includes(key),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toLysand = (lyObject: LysandObject): Lysand.Entity => {
|
|
||||||
return {
|
|
||||||
id: lyObject.remoteId || lyObject.id,
|
|
||||||
created_at: new Date(lyObject.createdAt).toISOString(),
|
|
||||||
type: lyObject.type,
|
|
||||||
uri: lyObject.uri,
|
|
||||||
...(lyObject.extraData as object),
|
|
||||||
// @ts-expect-error Assume stored JSON is valid
|
|
||||||
extensions: lyObject.extensions as object,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isPublication = (lyObject: LysandObject): boolean => {
|
|
||||||
return lyObject.type === "Note" || lyObject.type === "Patch";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isAction = (lyObject: LysandObject): boolean => {
|
|
||||||
return [
|
|
||||||
"Like",
|
|
||||||
"Follow",
|
|
||||||
"Dislike",
|
|
||||||
"FollowAccept",
|
|
||||||
"FollowReject",
|
|
||||||
"Undo",
|
|
||||||
"Announce",
|
|
||||||
].includes(lyObject.type);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isActor = (lyObject: LysandObject): boolean => {
|
|
||||||
return lyObject.type === "User";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isExtension = (lyObject: LysandObject): boolean => {
|
|
||||||
return lyObject.type === "Extension";
|
|
||||||
};
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import { config } from "config-manager";
|
|
||||||
// import { Worker } from "bullmq";
|
|
||||||
|
|
||||||
/* export const federationWorker = new Worker(
|
|
||||||
"federation",
|
|
||||||
async job => {
|
|
||||||
await job.updateProgress(0);
|
|
||||||
|
|
||||||
switch (job.name) {
|
|
||||||
case "federation": {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
const statusId = job.data.id as string;
|
|
||||||
|
|
||||||
const status = await client.status.findUnique({
|
|
||||||
where: { id: statusId },
|
|
||||||
include: statusAndUserRelations,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!status) return;
|
|
||||||
|
|
||||||
// Only get remote users that follow the author of the status, and the remote mentioned users
|
|
||||||
const peopleToSendTo = await client.user.findMany({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
["public", "unlisted", "private"].includes(
|
|
||||||
status.visibility
|
|
||||||
)
|
|
||||||
? {
|
|
||||||
relationships: {
|
|
||||||
some: {
|
|
||||||
subjectId: status.authorId,
|
|
||||||
following: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
instanceId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
// Mentioned users
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
in: status.mentions.map(m => m.id),
|
|
||||||
},
|
|
||||||
instanceId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let peopleDone = 0;
|
|
||||||
|
|
||||||
// Spawn sendToServer job for each user
|
|
||||||
for (const person of peopleToSendTo) {
|
|
||||||
await federationQueue.add("sendToServer", {
|
|
||||||
id: statusId,
|
|
||||||
user: person,
|
|
||||||
});
|
|
||||||
|
|
||||||
peopleDone++;
|
|
||||||
|
|
||||||
await job.updateProgress(
|
|
||||||
Math.round((peopleDone / peopleToSendTo.length) * 100)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "sendToServer": {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
const statusId = job.data.id as string;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
||||||
const user = job.data.user as User;
|
|
||||||
|
|
||||||
const status = await client.status.findUnique({
|
|
||||||
where: { id: statusId },
|
|
||||||
include: statusAndUserRelations,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!status) return;
|
|
||||||
|
|
||||||
const response = await federateStatusTo(
|
|
||||||
status,
|
|
||||||
status.author,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(
|
|
||||||
`Federation error: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await job.updateProgress(100);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
connection: {
|
|
||||||
host: config.redis.queue.host,
|
|
||||||
port: config.redis.queue.port,
|
|
||||||
password: config.redis.queue.password,
|
|
||||||
db: config.redis.queue.database || undefined,
|
|
||||||
},
|
|
||||||
removeOnComplete: {
|
|
||||||
count: 400,
|
|
||||||
},
|
|
||||||
removeOnFail: {
|
|
||||||
count: 3000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
); */
|
|
||||||
|
|
||||||
export const addStatusFederationJob = async (statusId: string) => {
|
|
||||||
/* await federationQueue.add("federation", {
|
|
||||||
id: statusId,
|
|
||||||
}); */
|
|
||||||
};
|
|
||||||
|
|
@ -647,10 +647,3 @@ export const federateNote = async (note: Note) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFavouritedBy = async (status: Status, user: UserType) => {
|
|
||||||
return !!(await db.query.Likes.findFirst({
|
|
||||||
where: (like, { and, eq }) =>
|
|
||||||
and(eq(like.likerId, user.id), eq(like.likedId, status.id)),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,6 @@ export type UserWithRelations = UserType & {
|
||||||
statusCount: number;
|
statusCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserWithRelationsAndRelationships = UserWithRelations & {
|
|
||||||
relationships: InferSelectModel<typeof Relationships>[];
|
|
||||||
relationshipSubjects: InferSelectModel<typeof Relationships>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userRelations: {
|
export const userRelations: {
|
||||||
instance: true;
|
instance: true;
|
||||||
emojis: {
|
emojis: {
|
||||||
|
|
@ -99,16 +94,6 @@ export interface AuthData {
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
|
||||||
// Check auth token
|
|
||||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
|
||||||
|
|
||||||
const { user, application } =
|
|
||||||
await retrieveUserAndApplicationFromToken(token);
|
|
||||||
|
|
||||||
return { user, token, application };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFromHeader = async (value: string): Promise<AuthData> => {
|
export const getFromHeader = async (value: string): Promise<AuthData> => {
|
||||||
const token = value.split(" ")[1];
|
const token = value.split(" ")[1];
|
||||||
|
|
||||||
|
|
@ -388,15 +373,6 @@ export const resolveWebFinger = async (
|
||||||
return User.resolve(relevantLink.href);
|
return User.resolve(relevantLink.href);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses mentions from a list of URIs
|
|
||||||
*/
|
|
||||||
export const parseMentionsUris = async (
|
|
||||||
mentions: string[],
|
|
||||||
): Promise<User[]> => {
|
|
||||||
return await User.manyFromSql(inArray(Users.uri, mentions));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a user from a token.
|
* Retrieves a user from a token.
|
||||||
* @param access_token The access token to retrieve the user from.
|
* @param access_token The access token to retrieve the user from.
|
||||||
|
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import type {
|
|
||||||
BuildQueryResult,
|
|
||||||
DBQueryConfig,
|
|
||||||
ExtractTablesWithRelations,
|
|
||||||
} from "drizzle-orm";
|
|
||||||
|
|
||||||
import type * as schema from "./schema";
|
|
||||||
|
|
||||||
type Schema = typeof schema;
|
|
||||||
type TablesWithRelations = ExtractTablesWithRelations<Schema>;
|
|
||||||
|
|
||||||
export type IncludeRelation<TableName extends keyof TablesWithRelations> =
|
|
||||||
DBQueryConfig<
|
|
||||||
"one" | "many",
|
|
||||||
boolean,
|
|
||||||
TablesWithRelations,
|
|
||||||
TablesWithRelations[TableName]
|
|
||||||
>["with"];
|
|
||||||
|
|
||||||
export type IncludeColumns<TableName extends keyof TablesWithRelations> =
|
|
||||||
DBQueryConfig<
|
|
||||||
"one" | "many",
|
|
||||||
boolean,
|
|
||||||
TablesWithRelations,
|
|
||||||
TablesWithRelations[TableName]
|
|
||||||
>["columns"];
|
|
||||||
|
|
||||||
export type InferQueryModel<
|
|
||||||
TableName extends keyof TablesWithRelations,
|
|
||||||
Columns extends IncludeColumns<TableName> | undefined = undefined,
|
|
||||||
With extends IncludeRelation<TableName> | undefined = undefined,
|
|
||||||
> = BuildQueryResult<
|
|
||||||
TablesWithRelations,
|
|
||||||
TablesWithRelations[TableName],
|
|
||||||
{
|
|
||||||
columns: Columns;
|
|
||||||
with: With;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
2
index.ts
2
index.ts
|
|
@ -11,9 +11,9 @@ import { ipBans } from "~middlewares/ip-bans";
|
||||||
import { logger } from "~middlewares/logger";
|
import { logger } from "~middlewares/logger";
|
||||||
import { Note } from "~packages/database-interface/note";
|
import { Note } from "~packages/database-interface/note";
|
||||||
import { handleGlitchRequest } from "~packages/glitch-server/main";
|
import { handleGlitchRequest } from "~packages/glitch-server/main";
|
||||||
import type { APIRouteExports } from "~packages/server-handler";
|
|
||||||
import { routes } from "~routes";
|
import { routes } from "~routes";
|
||||||
import { createServer } from "~server";
|
import { createServer } from "~server";
|
||||||
|
import type { APIRouteExports } from "~types/api";
|
||||||
|
|
||||||
const timeAtStart = performance.now();
|
const timeAtStart = performance.now();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,11 @@
|
||||||
"dev": "bun run --hot index.ts",
|
"dev": "bun run --hot index.ts",
|
||||||
"start": "NODE_ENV=production bun run dist/index.js --prod",
|
"start": "NODE_ENV=production bun run dist/index.js --prod",
|
||||||
"lint": "bunx @biomejs/biome check .",
|
"lint": "bunx @biomejs/biome check .",
|
||||||
"prod-build": "bun run build.ts",
|
"build": "bun run build.ts",
|
||||||
"benchmark:timeline": "bun run benchmarks/timelines.ts",
|
|
||||||
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem",
|
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem",
|
||||||
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
||||||
"cli": "bun run cli.ts"
|
"cli": "bun run cli.ts",
|
||||||
|
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@biomejs/biome",
|
"@biomejs/biome",
|
||||||
|
|
@ -62,6 +62,7 @@
|
||||||
"@types/pg": "^8.11.5",
|
"@types/pg": "^8.11.5",
|
||||||
"bun-types": "latest",
|
"bun-types": "latest",
|
||||||
"drizzle-kit": "^0.20.14",
|
"drizzle-kit": "^0.20.14",
|
||||||
|
"ts-prune": "^0.10.3",
|
||||||
"typescript": "latest"
|
"typescript": "latest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
@ -71,7 +72,6 @@
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||||
"@hono/zod-validator": "^0.2.1",
|
"@hono/zod-validator": "^0.2.1",
|
||||||
"@json2csv/plainjs": "^7.0.6",
|
"@json2csv/plainjs": "^7.0.6",
|
||||||
"@shikijs/markdown-it": "^1.3.0",
|
|
||||||
"@tufjs/canonical-json": "^2.0.0",
|
"@tufjs/canonical-json": "^2.0.0",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"bullmq": "^5.7.1",
|
"bullmq": "^5.7.1",
|
||||||
|
|
@ -102,7 +102,6 @@
|
||||||
"oauth4webapi": "^2.4.0",
|
"oauth4webapi": "^2.4.0",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"qs": "^6.12.1",
|
"qs": "^6.12.1",
|
||||||
"request-parser": "workspace:*",
|
|
||||||
"sharp": "^0.33.3",
|
"sharp": "^0.33.3",
|
||||||
"string-comparison": "^1.3.0",
|
"string-comparison": "^1.3.0",
|
||||||
"stringify-entities": "^4.0.4",
|
"stringify-entities": "^4.0.4",
|
||||||
|
|
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
import { parse } from "qs";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RequestParser
|
|
||||||
* Parses Request object into a JavaScript object
|
|
||||||
* based on the Content-Type header
|
|
||||||
* @param request Request object
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
*/
|
|
||||||
export class RequestParser {
|
|
||||||
constructor(public request: Request) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse request body into a JavaScript object
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
*/
|
|
||||||
async toObject<T>() {
|
|
||||||
switch (await this.determineContentType()) {
|
|
||||||
case "application/json":
|
|
||||||
return {
|
|
||||||
...(await this.parseJson<T>()),
|
|
||||||
...this.parseQuery<T>(),
|
|
||||||
};
|
|
||||||
case "application/x-www-form-urlencoded":
|
|
||||||
return {
|
|
||||||
...(await this.parseFormUrlencoded<T>()),
|
|
||||||
...this.parseQuery<T>(),
|
|
||||||
};
|
|
||||||
case "multipart/form-data":
|
|
||||||
return {
|
|
||||||
...(await this.parseFormData<T>()),
|
|
||||||
...this.parseQuery<T>(),
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return { ...this.parseQuery() } as T;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine body content type
|
|
||||||
* If there is no Content-Type header, automatically
|
|
||||||
* guess content type. Cuts off after ";" character
|
|
||||||
* @returns Content-Type header value, or empty string if there is no body
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async determineContentType() {
|
|
||||||
const content_type = this.request.headers.get("Content-Type");
|
|
||||||
|
|
||||||
if (content_type?.startsWith("application/json")) {
|
|
||||||
return "application/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type?.startsWith("application/x-www-form-urlencoded")) {
|
|
||||||
return "application/x-www-form-urlencoded";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type?.startsWith("multipart/form-data")) {
|
|
||||||
return "multipart/form-data";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if body is valid JSON
|
|
||||||
try {
|
|
||||||
await this.request.clone().json();
|
|
||||||
return "application/json";
|
|
||||||
} catch {
|
|
||||||
// This is not JSON
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if body is valid FormData
|
|
||||||
try {
|
|
||||||
await this.request.clone().formData();
|
|
||||||
return "multipart/form-data";
|
|
||||||
} catch {
|
|
||||||
// This is not FormData
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type) {
|
|
||||||
return content_type.split(";")[0] ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.request.body) {
|
|
||||||
throw new Error("Invalid body");
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is no body, return query parameters
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse FormData body into a JavaScript object
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
* @private
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
*/
|
|
||||||
private async parseFormData<T>(): Promise<Partial<T>> {
|
|
||||||
const formData = await this.request.clone().formData();
|
|
||||||
const result: Partial<T> = {};
|
|
||||||
|
|
||||||
// Extract the files from the FormData
|
|
||||||
for (const [key, value] of formData.entries()) {
|
|
||||||
if (value instanceof Blob) {
|
|
||||||
result[key as keyof T] = value as T[keyof T];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formDataWithoutFiles = new FormData();
|
|
||||||
for (const [key, value] of formData.entries()) {
|
|
||||||
if (!(value instanceof Blob)) {
|
|
||||||
formDataWithoutFiles.append(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to URLSearchParams and parse as query
|
|
||||||
const searchParams = new URLSearchParams([
|
|
||||||
...formDataWithoutFiles.entries(),
|
|
||||||
] as [string, string][]);
|
|
||||||
|
|
||||||
const parsed = parse(searchParams.toString(), {
|
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const casted = castBooleanObject(
|
|
||||||
parsed as PossiblyRecursiveObject,
|
|
||||||
) as Partial<T>;
|
|
||||||
|
|
||||||
return { ...result, ...casted };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse application/x-www-form-urlencoded body into a JavaScript object
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
* @private
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
*/
|
|
||||||
private async parseFormUrlencoded<T>(): Promise<Partial<T>> {
|
|
||||||
const parsed = parse(await this.request.text(), {
|
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
return castBooleanObject(
|
|
||||||
parsed as PossiblyRecursiveObject,
|
|
||||||
) as Partial<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse JSON body into a JavaScript object
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
* @private
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
*/
|
|
||||||
private async parseJson<T>(): Promise<T> {
|
|
||||||
return (await this.request.json()) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse query parameters into a JavaScript object
|
|
||||||
* @private
|
|
||||||
* @throws Error if body is invalid
|
|
||||||
* @returns JavaScript object of type T
|
|
||||||
*/
|
|
||||||
parseQuery<T>(): Partial<T> {
|
|
||||||
const parsed = parse(
|
|
||||||
new URL(this.request.url).searchParams.toString(),
|
|
||||||
{
|
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return castBooleanObject(
|
|
||||||
parsed as PossiblyRecursiveObject,
|
|
||||||
) as Partial<T>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PossiblyRecursiveObject {
|
|
||||||
[key: string]:
|
|
||||||
| PossiblyRecursiveObject[]
|
|
||||||
| PossiblyRecursiveObject
|
|
||||||
| string
|
|
||||||
| string[]
|
|
||||||
| boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive
|
|
||||||
const castBooleanObject = (value: PossiblyRecursiveObject | string) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return castBoolean(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in value) {
|
|
||||||
const child = value[key];
|
|
||||||
if (Array.isArray(child)) {
|
|
||||||
value[key] = child.map((v) => castBooleanObject(v)) as string[];
|
|
||||||
} else if (typeof child === "object") {
|
|
||||||
value[key] = castBooleanObject(child);
|
|
||||||
} else {
|
|
||||||
value[key] = castBoolean(child as string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const castBoolean = (value: string) => {
|
|
||||||
if (["true"].includes(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (["false"].includes(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"name": "request-parser",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "index.ts",
|
|
||||||
"dependencies": { "qs": "^6.12.1" },
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/qs": "^6.9.15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
import { describe, expect, it, test } from "bun:test";
|
|
||||||
import { RequestParser } from "..";
|
|
||||||
|
|
||||||
describe("RequestParser", () => {
|
|
||||||
describe("Should parse query parameters correctly", () => {
|
|
||||||
test("With text parameters", async () => {
|
|
||||||
const request = new Request(
|
|
||||||
"http://localhost?param1=value1¶m2=value2",
|
|
||||||
);
|
|
||||||
const result = await new RequestParser(request).parseQuery<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With Array", async () => {
|
|
||||||
const request = new Request(
|
|
||||||
"http://localhost?test[]=value1&test[]=value2",
|
|
||||||
);
|
|
||||||
const result = await new RequestParser(request).parseQuery<{
|
|
||||||
test: string[];
|
|
||||||
}>();
|
|
||||||
expect(result?.test).toEqual(["value1", "value2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With Array of objects", async () => {
|
|
||||||
const request = new Request(
|
|
||||||
"http://localhost?test[][key]=value1&test[][value]=value2",
|
|
||||||
);
|
|
||||||
const result = await new RequestParser(request).parseQuery<{
|
|
||||||
test: { key: string; value: string }[];
|
|
||||||
}>();
|
|
||||||
expect(result?.test).toEqual([{ key: "value1", value: "value2" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With Array of multiple objects", async () => {
|
|
||||||
const request = new Request(
|
|
||||||
"http://localhost?test[][key]=value1&test[][value]=value2&test[][key]=value3&test[][value]=value4",
|
|
||||||
);
|
|
||||||
const result = await new RequestParser(request).parseQuery<{
|
|
||||||
test: { key: string[]; value: string[] }[];
|
|
||||||
}>();
|
|
||||||
expect(result?.test).toEqual([
|
|
||||||
{ key: ["value1", "value3"], value: ["value2", "value4"] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With both at once", async () => {
|
|
||||||
const request = new Request(
|
|
||||||
"http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2",
|
|
||||||
);
|
|
||||||
const result = await new RequestParser(request).parseQuery<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
test: string[];
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({
|
|
||||||
param1: "value1",
|
|
||||||
param2: "value2",
|
|
||||||
test: ["value1", "value2"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse JSON body correctly", async () => {
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ param1: "value1", param2: "value2" }),
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle invalid JSON body", async () => {
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: "invalid json",
|
|
||||||
});
|
|
||||||
const result = new RequestParser(request).toObject<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
}>();
|
|
||||||
expect(result).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("should parse form data correctly", () => {
|
|
||||||
test("With basic text parameters", async () => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("param1", "value1");
|
|
||||||
formData.append("param2", "value2");
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With File object", async () => {
|
|
||||||
const file = new File(["content"], "filename.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
file: File;
|
|
||||||
}>();
|
|
||||||
expect(result?.file).toBeInstanceOf(File);
|
|
||||||
expect(await result?.file?.text()).toEqual("content");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With Array", async () => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("test[]", "value1");
|
|
||||||
formData.append("test[]", "value2");
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
test: string[];
|
|
||||||
}>();
|
|
||||||
expect(result?.test).toEqual(["value1", "value2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("With all three at once", async () => {
|
|
||||||
const file = new File(["content"], "filename.txt", {
|
|
||||||
type: "text/plain",
|
|
||||||
});
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("param1", "value1");
|
|
||||||
formData.append("param2", "value2");
|
|
||||||
formData.append("file", file);
|
|
||||||
formData.append("test[]", "value1");
|
|
||||||
formData.append("test[]", "value2");
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
method: "POST",
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
file: File;
|
|
||||||
test: string[];
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({
|
|
||||||
param1: "value1",
|
|
||||||
param2: "value2",
|
|
||||||
file: file,
|
|
||||||
test: ["value1", "value2"],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("URL Encoded", async () => {
|
|
||||||
const request = new Request("http://localhost", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
body: "param1=value1¶m2=value2",
|
|
||||||
});
|
|
||||||
const result = await new RequestParser(request).toObject<{
|
|
||||||
param1: string;
|
|
||||||
param2: string;
|
|
||||||
}>();
|
|
||||||
expect(result).toEqual({ param1: "value1", param2: "value2" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
import { dualLogger } from "@loggers";
|
|
||||||
import { errorResponse, jsonResponse, response } from "@response";
|
|
||||||
import type { MatchedRoute } from "bun";
|
|
||||||
import { type Config, config } from "config-manager";
|
|
||||||
import type { Hono } from "hono";
|
|
||||||
import type { RouterRoute } from "hono/types";
|
|
||||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
|
||||||
import { RequestParser } from "request-parser";
|
|
||||||
import type { ZodType, z } from "zod";
|
|
||||||
import { fromZodError } from "zod-validation-error";
|
|
||||||
import type { Application } from "~database/entities/Application";
|
|
||||||
import { type AuthData, getFromRequest } from "~database/entities/User";
|
|
||||||
import type { User } from "~packages/database-interface/user";
|
|
||||||
|
|
||||||
type MaybePromise<T> = T | Promise<T>;
|
|
||||||
export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
|
||||||
|
|
||||||
export type RouteHandler<
|
|
||||||
RouteMeta extends APIRouteMetadata,
|
|
||||||
ZodSchema extends ZodType,
|
|
||||||
> = (
|
|
||||||
req: Request,
|
|
||||||
matchedRoute: MatchedRoute,
|
|
||||||
extraData: {
|
|
||||||
auth: {
|
|
||||||
// If the route doesn't require authentication, set the type to User | null
|
|
||||||
// Otherwise set to User
|
|
||||||
user: RouteMeta["auth"]["required"] extends true
|
|
||||||
? User
|
|
||||||
: User | null;
|
|
||||||
token: RouteMeta["auth"]["required"] extends true
|
|
||||||
? string
|
|
||||||
: string | null;
|
|
||||||
application: Application | null;
|
|
||||||
};
|
|
||||||
parsedRequest: z.infer<ZodSchema>;
|
|
||||||
configManager: {
|
|
||||||
getConfig: () => Promise<Config>;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
) => MaybePromise<Response> | MaybePromise<object>;
|
|
||||||
|
|
||||||
export interface APIRouteMetadata {
|
|
||||||
allowedMethods: HttpVerb[];
|
|
||||||
ratelimits: {
|
|
||||||
max: number;
|
|
||||||
duration: number;
|
|
||||||
};
|
|
||||||
route: string;
|
|
||||||
auth: {
|
|
||||||
required: boolean;
|
|
||||||
requiredOnMethods?: HttpVerb[];
|
|
||||||
oauthPermissions?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APIRouteExports {
|
|
||||||
meta: APIRouteMetadata;
|
|
||||||
schemas?: {
|
|
||||||
query?: z.AnyZodObject;
|
|
||||||
body?: z.AnyZodObject;
|
|
||||||
};
|
|
||||||
default: (app: Hono) => RouterRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const processRoute = async (
|
|
||||||
matchedRoute: MatchedRoute,
|
|
||||||
request: Request,
|
|
||||||
logger: LogManager | MultiLogManager,
|
|
||||||
): Promise<Response> => {
|
|
||||||
if (request.method === "OPTIONS") {
|
|
||||||
return response();
|
|
||||||
}
|
|
||||||
|
|
||||||
const route: APIRouteExports | null = await import(
|
|
||||||
matchedRoute.filePath
|
|
||||||
).catch((e) => {
|
|
||||||
dualLogger.logError(LogLevel.ERROR, "Server.RouteImport", e as Error);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!route?.meta) {
|
|
||||||
return errorResponse("Route not found", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the request method is allowed
|
|
||||||
if (!route.meta.allowedMethods.includes(request.method as HttpVerb)) {
|
|
||||||
return errorResponse("Method not allowed", 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth: AuthData = await getFromRequest(request);
|
|
||||||
|
|
||||||
if (
|
|
||||||
route.meta.auth.required ||
|
|
||||||
route.meta.auth.requiredOnMethods?.includes(request.method as HttpVerb)
|
|
||||||
) {
|
|
||||||
if (!auth.user) {
|
|
||||||
return errorResponse(
|
|
||||||
"Unauthorized: access to this method requires an authenticated user",
|
|
||||||
401,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Content-Type header is missing if there is a body
|
|
||||||
if (request.clone().body) {
|
|
||||||
if (!request.headers.has("Content-Type")) {
|
|
||||||
return errorResponse(
|
|
||||||
`Content-Type header is missing but required on method ${request.method}`,
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedRequest = await new RequestParser(request.clone())
|
|
||||||
.toObject()
|
|
||||||
.catch(async (err) => {
|
|
||||||
console.log(err);
|
|
||||||
await logger.logError(
|
|
||||||
LogLevel.ERROR,
|
|
||||||
"Server.RouteRequestParser",
|
|
||||||
err as Error,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parsedRequest) {
|
|
||||||
return errorResponse(
|
|
||||||
"The request could not be parsed, it may be malformed",
|
|
||||||
400,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsingResult = route.schema?.safeParse(parsedRequest);
|
|
||||||
|
|
||||||
if (parsingResult && !parsingResult.success) {
|
|
||||||
// Return a 422 error with the first error message
|
|
||||||
return errorResponse(fromZodError(parsingResult.error).toString(), 422);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const output = await route.default(request, matchedRoute, {
|
|
||||||
auth: {
|
|
||||||
token: auth?.token ?? null,
|
|
||||||
user: auth?.user ?? null,
|
|
||||||
application: auth?.application ?? null,
|
|
||||||
},
|
|
||||||
parsedRequest: parsingResult
|
|
||||||
? (parsingResult.data as z.infer<typeof route.schema>)
|
|
||||||
: parsedRequest,
|
|
||||||
configManager: {
|
|
||||||
getConfig: async () => config as Config,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// If the output is a normal JS object and not a Response, convert it to a jsonResponse
|
|
||||||
if (!(output instanceof Response)) {
|
|
||||||
return jsonResponse(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
} catch (err) {
|
|
||||||
await logger.log(
|
|
||||||
LogLevel.DEBUG,
|
|
||||||
"Server.RouteHandler",
|
|
||||||
(err as Error).toString(),
|
|
||||||
);
|
|
||||||
await logger.logError(
|
|
||||||
LogLevel.ERROR,
|
|
||||||
"Server.RouteHandler",
|
|
||||||
err as Error,
|
|
||||||
);
|
|
||||||
|
|
||||||
return errorResponse(
|
|
||||||
`A server error occured: ${(err as Error).message}`,
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"name": "server-handler",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "index.ts",
|
|
||||||
"dependencies": { "zod": "^3.22.4", "zod-validation-error": "^3.2.0" }
|
|
||||||
}
|
|
||||||
|
|
@ -1,340 +0,0 @@
|
||||||
import { afterAll, describe, expect, it, mock } from "bun:test";
|
|
||||||
import type { MatchedRoute } from "bun";
|
|
||||||
import { LogManager } from "log-manager";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { getTestUsers } from "~tests/utils";
|
|
||||||
import { type APIRouteExports, processRoute } from ".";
|
|
||||||
|
|
||||||
describe("Route Processor", () => {
|
|
||||||
it("should return a Response", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "GET",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output).toBeInstanceOf(Response);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 404 when the route does not exist", async () => {
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./nonexistent-route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/nonexistent-route"),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 405 when the request method is not allowed", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["POST"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "GET",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(405);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 401 when the route requires authentication but no user is authenticated", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["POST"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "POST",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(401);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 400 when the Content-Type header is missing but there is a body", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["POST", "PUT", "PATCH"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "POST",
|
|
||||||
body: "test",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 400 when the request could not be parsed", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["POST"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: "invalid-json",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(400);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return a 422 when the request does not match the schema", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response(),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["POST"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({
|
|
||||||
foo: z.string(),
|
|
||||||
}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ bar: "baz" }),
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(422);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert any JS objects returned by the route to a Response", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => ({ status: 200 }),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "GET",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle route errors", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => {
|
|
||||||
throw new Error("Route error");
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "GET",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(500);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return the route output when everything is valid", async () => {
|
|
||||||
mock.module(
|
|
||||||
"./route",
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
default: async () => new Response("OK"),
|
|
||||||
meta: {
|
|
||||||
allowedMethods: ["GET"],
|
|
||||||
ratelimits: {
|
|
||||||
max: 100,
|
|
||||||
duration: 60,
|
|
||||||
},
|
|
||||||
route: "/route",
|
|
||||||
auth: {
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: z.object({}),
|
|
||||||
}) as APIRouteExports,
|
|
||||||
);
|
|
||||||
|
|
||||||
const output = await processRoute(
|
|
||||||
{
|
|
||||||
filePath: "./route",
|
|
||||||
} as MatchedRoute,
|
|
||||||
new Request("https://test.com/route", {
|
|
||||||
method: "GET",
|
|
||||||
}),
|
|
||||||
new LogManager(Bun.file("/dev/null")),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(output.status).toBe(200);
|
|
||||||
expect(await output.text()).toBe("OK");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -21,9 +21,3 @@ for (const [route, path] of Object.entries(routes)) {
|
||||||
routes = Object.fromEntries(Object.entries(routes).reverse());
|
routes = Object.fromEntries(Object.entries(routes).reverse());
|
||||||
|
|
||||||
export { routes };
|
export { routes };
|
||||||
|
|
||||||
export const matchRoute = (request: Request) => {
|
|
||||||
const route = routeMatcher.match(request);
|
|
||||||
|
|
||||||
return route ?? null;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError } from "@api";
|
import { applyConfig, auth, handleZodError } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { jsonResponse, response } from "@response";
|
import { jsonResponse, response } from "@response";
|
||||||
import { tempmailDomains } from "@tempmail";
|
import { tempmailDomains } from "@tempmail";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError, idValidator } from "@api";
|
import { applyConfig, auth, handleZodError, idValidator } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { fetchTimeline } from "@timelines";
|
import { fetchTimeline } from "@timelines";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, applyConfig, handleZodError } from "@api";
|
import { applyConfig, handleZodError } from "@api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
export type APActivityPubContext =
|
|
||||||
| "https://www.w3.org/ns/activitystreams"
|
|
||||||
| {
|
|
||||||
ostatus: string;
|
|
||||||
atomUri: string;
|
|
||||||
inReplyToAtomUri: string;
|
|
||||||
conversation: string;
|
|
||||||
sensitive: string;
|
|
||||||
toot: string;
|
|
||||||
votersCount: string;
|
|
||||||
litepub: string;
|
|
||||||
directMessage: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface APActivityPubObject {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
summary?: string;
|
|
||||||
inReplyTo?: string;
|
|
||||||
published: string;
|
|
||||||
url: string;
|
|
||||||
attributedTo: string;
|
|
||||||
to: string[];
|
|
||||||
cc: string[];
|
|
||||||
sensitive?: boolean;
|
|
||||||
atomUri: string;
|
|
||||||
inReplyToAtomUri?: string;
|
|
||||||
conversation: string;
|
|
||||||
content: string;
|
|
||||||
contentMap: Record<string, string>;
|
|
||||||
attachment: APActivityPubAttachment[];
|
|
||||||
tag: APTag[];
|
|
||||||
context?: string;
|
|
||||||
quoteUri?: string;
|
|
||||||
quoteUrl?: string;
|
|
||||||
source?: {
|
|
||||||
content: string;
|
|
||||||
mediaType: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActivityPubAttachment {
|
|
||||||
type?: string;
|
|
||||||
mediaType?: string;
|
|
||||||
url?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActivityPubCollection<T> {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
first?: {
|
|
||||||
type: string;
|
|
||||||
next: string;
|
|
||||||
partOf: string;
|
|
||||||
items: T[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActivityPubNote extends APActivityPubObject {
|
|
||||||
type: "Note";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActivityPubActivity {
|
|
||||||
"@context": APActivityPubContext[];
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
actor: string;
|
|
||||||
published: string;
|
|
||||||
to: string[];
|
|
||||||
cc: string[];
|
|
||||||
object: APActivityPubNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type APActorContext =
|
|
||||||
| "https://www.w3.org/ns/activitystreams"
|
|
||||||
| "https://w3id.org/security/v1"
|
|
||||||
| Record<
|
|
||||||
string,
|
|
||||||
| string
|
|
||||||
| { "@id": string; "@type": string }
|
|
||||||
| { "@container": string; "@id": string }
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface APActorPublicKey {
|
|
||||||
id: string;
|
|
||||||
owner: string;
|
|
||||||
publicKeyPem: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActorEndpoints {
|
|
||||||
sharedInbox: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActorIcon {
|
|
||||||
type: string;
|
|
||||||
mediaType: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APActor {
|
|
||||||
"@context": APActorContext[];
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
following: string;
|
|
||||||
followers: string;
|
|
||||||
inbox: string;
|
|
||||||
outbox: string;
|
|
||||||
featured: string;
|
|
||||||
featuredTags: string;
|
|
||||||
preferredUsername: string;
|
|
||||||
name: string;
|
|
||||||
summary: string;
|
|
||||||
url: string;
|
|
||||||
manuallyApprovesFollowers: boolean;
|
|
||||||
discoverable: boolean;
|
|
||||||
indexable: boolean;
|
|
||||||
published: string;
|
|
||||||
memorial: boolean;
|
|
||||||
devices: string;
|
|
||||||
publicKey: APActorPublicKey;
|
|
||||||
tag: APTag[];
|
|
||||||
attachment: APAttachment[];
|
|
||||||
endpoints: APActorEndpoints;
|
|
||||||
icon: APActorIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APTag {
|
|
||||||
type: string;
|
|
||||||
href: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APAttachment {
|
|
||||||
type: string;
|
|
||||||
mediaType: string;
|
|
||||||
url: string;
|
|
||||||
name?: string;
|
|
||||||
blurhash?: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
20
types/api.ts
20
types/api.ts
|
|
@ -1,5 +1,10 @@
|
||||||
export interface APIRouteMeta {
|
import type { Hono } from "hono";
|
||||||
allowedMethods: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
|
import type { RouterRoute } from "hono/types";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
|
||||||
|
export interface APIRouteMetadata {
|
||||||
|
allowedMethods: HttpVerb[];
|
||||||
ratelimits: {
|
ratelimits: {
|
||||||
max: number;
|
max: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
|
@ -7,7 +12,16 @@ export interface APIRouteMeta {
|
||||||
route: string;
|
route: string;
|
||||||
auth: {
|
auth: {
|
||||||
required: boolean;
|
required: boolean;
|
||||||
requiredOnMethods?: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
|
requiredOnMethods?: HttpVerb[];
|
||||||
oauthPermissions?: string[];
|
oauthPermissions?: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface APIRouteExports {
|
||||||
|
meta: APIRouteMetadata;
|
||||||
|
schemas?: {
|
||||||
|
query?: z.AnyZodObject;
|
||||||
|
body?: z.AnyZodObject;
|
||||||
|
};
|
||||||
|
default: (app: Hono) => RouterRoute;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import type { LysandObjectType } from "./Object";
|
|
||||||
|
|
||||||
export interface ExtensionType extends LysandObjectType {
|
|
||||||
type: "Extension";
|
|
||||||
extension_type: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,177 +0,0 @@
|
||||||
import type { Emoji } from "./extensions/org.lysand/custom_emojis";
|
|
||||||
|
|
||||||
export interface LysandObjectType {
|
|
||||||
type: string;
|
|
||||||
id: string; // Either a UUID or some kind of time-based UUID-compatible system
|
|
||||||
uri: string; // URI to the note
|
|
||||||
created_at: string;
|
|
||||||
extensions?: {
|
|
||||||
// Should be in the format
|
|
||||||
// "organization:extension_name": value
|
|
||||||
// Example: "org.joinmastodon:spoiler_text": "This is a spoiler!"
|
|
||||||
"org.lysand:custom_emojis"?: {
|
|
||||||
emojis: Emoji[];
|
|
||||||
};
|
|
||||||
"org.lysand:reactions"?: {
|
|
||||||
reactions: string;
|
|
||||||
};
|
|
||||||
"org.lysand:polls"?: {
|
|
||||||
poll: {
|
|
||||||
options: ContentFormat[][];
|
|
||||||
votes: number[];
|
|
||||||
expires_at: string;
|
|
||||||
multiple_choice: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
[key: string]: Record<string, unknown> | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActorPublicKeyData {
|
|
||||||
public_key: string;
|
|
||||||
actor: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Collection<T> {
|
|
||||||
first: string;
|
|
||||||
last: string;
|
|
||||||
next?: string;
|
|
||||||
prev?: string;
|
|
||||||
items: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LysandUser extends LysandObjectType {
|
|
||||||
type: "User";
|
|
||||||
bio: ContentFormat[];
|
|
||||||
|
|
||||||
inbox: string;
|
|
||||||
outbox: string;
|
|
||||||
followers: string;
|
|
||||||
following: string;
|
|
||||||
liked: string;
|
|
||||||
disliked: string;
|
|
||||||
featured: string;
|
|
||||||
|
|
||||||
indexable: boolean;
|
|
||||||
fields?: {
|
|
||||||
key: ContentFormat[];
|
|
||||||
value: ContentFormat[];
|
|
||||||
}[];
|
|
||||||
display_name?: string;
|
|
||||||
public_key?: ActorPublicKeyData;
|
|
||||||
username: string;
|
|
||||||
avatar?: ContentFormat[];
|
|
||||||
header?: ContentFormat[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LysandPublication extends LysandObjectType {
|
|
||||||
type: "Note" | "Patch";
|
|
||||||
author: string;
|
|
||||||
contents: ContentFormat[];
|
|
||||||
mentions: string[];
|
|
||||||
replies_to: string[];
|
|
||||||
quotes: string[];
|
|
||||||
is_sensitive: boolean;
|
|
||||||
subject: string;
|
|
||||||
attachments: ContentFormat[][];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LysandAction extends LysandObjectType {
|
|
||||||
type:
|
|
||||||
| "Like"
|
|
||||||
| "Dislike"
|
|
||||||
| "Follow"
|
|
||||||
| "FollowAccept"
|
|
||||||
| "FollowReject"
|
|
||||||
| "Announce"
|
|
||||||
| "Undo"
|
|
||||||
| "Extension";
|
|
||||||
author: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Note is a publication on the network, such as a post or comment
|
|
||||||
*/
|
|
||||||
export interface Note extends LysandPublication {
|
|
||||||
type: "Note";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Patch is an edit to a Note
|
|
||||||
*/
|
|
||||||
export interface Patch extends LysandPublication {
|
|
||||||
type: "Patch";
|
|
||||||
patched_id: string;
|
|
||||||
patched_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Like extends LysandAction {
|
|
||||||
type: "Like";
|
|
||||||
object: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Dislike extends LysandAction {
|
|
||||||
type: "Dislike";
|
|
||||||
object: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Announce extends LysandAction {
|
|
||||||
type: "Announce";
|
|
||||||
object: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Undo extends LysandAction {
|
|
||||||
type: "Undo";
|
|
||||||
object: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Follow extends LysandAction {
|
|
||||||
type: "Follow";
|
|
||||||
followee: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FollowAccept extends LysandAction {
|
|
||||||
type: "FollowAccept";
|
|
||||||
follower: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FollowReject extends LysandAction {
|
|
||||||
type: "FollowReject";
|
|
||||||
follower: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServerMetadata extends LysandObjectType {
|
|
||||||
type: "ServerMetadata";
|
|
||||||
name: string;
|
|
||||||
version?: string;
|
|
||||||
description?: string;
|
|
||||||
website?: string;
|
|
||||||
moderators?: string[];
|
|
||||||
admins?: string[];
|
|
||||||
logo?: ContentFormat[];
|
|
||||||
banner?: ContentFormat[];
|
|
||||||
supported_extensions?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Content format is an array of objects that contain the content and the content type.
|
|
||||||
*/
|
|
||||||
export interface ContentFormat {
|
|
||||||
content: string;
|
|
||||||
content_type: string;
|
|
||||||
description?: string;
|
|
||||||
size?: number;
|
|
||||||
hash?: {
|
|
||||||
md5?: string;
|
|
||||||
sha1?: string;
|
|
||||||
sha256?: string;
|
|
||||||
sha512?: string;
|
|
||||||
[key: string]: string | undefined;
|
|
||||||
};
|
|
||||||
blurhash?: string;
|
|
||||||
fps?: number;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import type { ContentFormat } from "../../Object";
|
|
||||||
|
|
||||||
export interface Emoji {
|
|
||||||
name: string;
|
|
||||||
url: ContentFormat[];
|
|
||||||
alt?: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { ExtensionType } from "../../Extension";
|
|
||||||
|
|
||||||
export interface OrgLysandPollsVoteType extends ExtensionType {
|
|
||||||
extension_type: "org.lysand:polls/Vote";
|
|
||||||
author: string;
|
|
||||||
poll: string;
|
|
||||||
option: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OrgLysandPollsVoteResultType extends ExtensionType {
|
|
||||||
extension_type: "org.lysand:polls/VoteResult";
|
|
||||||
poll: string;
|
|
||||||
votes: number[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import type { ExtensionType } from "../../Extension";
|
|
||||||
|
|
||||||
export interface OrgLysandReactionsType extends ExtensionType {
|
|
||||||
extension_type: "org.lysand:reactions/Reaction";
|
|
||||||
author: string;
|
|
||||||
object: string;
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
18
utils/api.ts
18
utils/api.ts
|
|
@ -13,17 +13,12 @@ import {
|
||||||
exactly,
|
exactly,
|
||||||
} from "magic-regexp";
|
} from "magic-regexp";
|
||||||
import { parse } from "qs";
|
import { parse } from "qs";
|
||||||
import type {
|
|
||||||
APIRouteExports,
|
|
||||||
APIRouteMetadata,
|
|
||||||
HttpVerb,
|
|
||||||
RouteHandler,
|
|
||||||
} from "server-handler";
|
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import type { Application } from "~database/entities/Application";
|
import type { Application } from "~database/entities/Application";
|
||||||
import { getFromHeader, getFromRequest } from "~database/entities/User";
|
import { getFromHeader } from "~database/entities/User";
|
||||||
import type { User } from "~packages/database-interface/user";
|
import type { User } from "~packages/database-interface/user";
|
||||||
|
import type { APIRouteMetadata, HttpVerb } from "~types/api";
|
||||||
|
|
||||||
export const applyConfig = (routeMeta: APIRouteMetadata) => {
|
export const applyConfig = (routeMeta: APIRouteMetadata) => {
|
||||||
const newMeta = routeMeta;
|
const newMeta = routeMeta;
|
||||||
|
|
@ -39,15 +34,6 @@ export const applyConfig = (routeMeta: APIRouteMetadata) => {
|
||||||
return newMeta;
|
return newMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const apiRoute = <
|
|
||||||
Metadata extends APIRouteMetadata,
|
|
||||||
ZodSchema extends Zod.AnyZodObject,
|
|
||||||
>(
|
|
||||||
routeFunction: APIRouteExports["default"],
|
|
||||||
) => {
|
|
||||||
return routeFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const idValidator = createRegExp(
|
export const idValidator = createRegExp(
|
||||||
anyOf(digit, charIn("ABCDEF")).times(8),
|
anyOf(digit, charIn("ABCDEF")).times(8),
|
||||||
exactly("-"),
|
exactly("-"),
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import { config } from "config-manager";
|
||||||
import { count } from "drizzle-orm";
|
import { count } from "drizzle-orm";
|
||||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||||
import { Meilisearch } from "meilisearch";
|
import { Meilisearch } from "meilisearch";
|
||||||
import type { Status } from "~database/entities/Status";
|
|
||||||
import type { UserType } from "~database/entities/User";
|
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { Notes, Users } from "~drizzle/schema";
|
import { Notes, Users } from "~drizzle/schema";
|
||||||
import type { User } from "~packages/database-interface/user";
|
import type { User } from "~packages/database-interface/user";
|
||||||
|
|
@ -54,18 +52,6 @@ export enum MeiliIndexType {
|
||||||
Statuses = "statuses",
|
Statuses = "statuses",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addStausToMeilisearch = async (status: Status) => {
|
|
||||||
if (!config.meilisearch.enabled) return;
|
|
||||||
|
|
||||||
await meilisearch.index(MeiliIndexType.Statuses).addDocuments([
|
|
||||||
{
|
|
||||||
id: status.id,
|
|
||||||
content: status.content,
|
|
||||||
createdAt: status.createdAt,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addUserToMeilisearch = async (user: User) => {
|
export const addUserToMeilisearch = async (user: User) => {
|
||||||
if (!config.meilisearch.enabled) return;
|
if (!config.meilisearch.enabled) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
export const deepMerge = (
|
|
||||||
target: Record<string, unknown>,
|
|
||||||
source: Record<string, unknown>,
|
|
||||||
) => {
|
|
||||||
const result = { ...target, ...source };
|
|
||||||
for (const key of Object.keys(result)) {
|
|
||||||
result[key] =
|
|
||||||
typeof target[key] === "object" && typeof source[key] === "object"
|
|
||||||
? // @ts-expect-error deepMerge is recursive
|
|
||||||
deepMerge(target[key], source[key])
|
|
||||||
: structuredClone(result[key]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deepMergeArray = (array: Record<string, unknown>[]) =>
|
|
||||||
array.reduce((ci, ni) => deepMerge(ci, ni), {});
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether a module is the entry point for the running node process.
|
|
||||||
* This works for both CommonJS and ES6 environments.
|
|
||||||
*
|
|
||||||
* ### CommonJS
|
|
||||||
* ```js
|
|
||||||
* if (moduleIsEntry(module)) {
|
|
||||||
* console.log('WOO HOO!!!');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ### ES6
|
|
||||||
* ```js
|
|
||||||
* if (moduleIsEntry(import.meta.url)) {
|
|
||||||
* console.log('WOO HOO!!!');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const moduleIsEntry = (moduleOrImportMetaUrl: NodeModule | string) => {
|
|
||||||
if (typeof moduleOrImportMetaUrl === "string") {
|
|
||||||
return process.argv[1] === fileURLToPath(moduleOrImportMetaUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof require !== "undefined" && "exports" in moduleOrImportMetaUrl) {
|
|
||||||
return require.main === moduleOrImportMetaUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
@ -59,5 +59,3 @@ export const checkIfOauthIsValid = (
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const oauthCodeVerifiers: Record<string, string> = {};
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { exists, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
|
||||||
|
|
||||||
export const writeToTempDirectory = async (filename: string, data: string) => {
|
|
||||||
const tempDir = join("/tmp/", "lysand");
|
|
||||||
if (!(await exists(tempDir))) await mkdir(tempDir);
|
|
||||||
|
|
||||||
const tempFile = join(tempDir, filename);
|
|
||||||
await writeFile(tempFile, data);
|
|
||||||
|
|
||||||
return tempFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const readFromTempDirectory = async (filename: string) => {
|
|
||||||
const tempDir = join("/tmp/", "lysand");
|
|
||||||
if (!(await exists(tempDir))) await mkdir(tempDir);
|
|
||||||
|
|
||||||
const tempFile = join(tempDir, filename);
|
|
||||||
return readFile(tempFile, "utf-8");
|
|
||||||
};
|
|
||||||
Loading…
Reference in a new issue