New API route format to make code cleaner

This commit is contained in:
Jesse Wierzbinski 2023-10-15 17:51:29 -10:00
parent c7b2f5b741
commit ca7d325cb1
36 changed files with 600 additions and 237 deletions

15
.dockerignore Normal file
View file

@ -0,0 +1,15 @@
node_modules
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode
Makefile
helm-charts
.env
.editorconfig
.idea
coverage*

View file

@ -14,5 +14,7 @@ module.exports = {
root: true,
rules: {
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-explicit-any": "off"
},
};

BIN
bun.lockb

Binary file not shown.

View file

@ -193,7 +193,7 @@ export class RawActivity extends BaseEntity {
* Returns the ActivityPub representation of the activity.
* @returns The ActivityPub representation of the activity.
*/
makeActivityPubRepresentation() {
getActivityPubRepresentation() {
return {
...this.data,
object: this.objects[0].data,

View file

@ -156,6 +156,13 @@ export class User extends BaseEntity {
@JoinTable()
pinned_notes!: RawObject[];
static async getFromRequest(req: Request) {
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
return { user: await User.retrieveFromToken(token), token };
}
/**
* Update this user data from its actor
* @returns The updated user.

132
index.ts
View file

@ -1,8 +1,12 @@
import { getConfig } from "@config";
import { jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { appendFile } from "fs/promises";
import { matches } from "ip-matching";
import "reflect-metadata";
import { AppDataSource } from "~database/datasource";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
const router = new Bun.FileSystemRouter({
style: "nextjs",
@ -25,38 +29,25 @@ Bun.serve({
port: config.http.bind_port,
hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0"
async fetch(req) {
if (config.logging.log_requests_verbose) {
await appendFile(
`${process.cwd()}/logs/requests.log`,
`[${new Date().toISOString()}] ${req.method} ${
req.url
}\n\tHeaders:\n`
);
/* Check for banned IPs */
const request_ip = this.requestIP(req)?.address ?? "";
// Add headers
const headers = req.headers.entries();
for (const [key, value] of headers) {
await appendFile(
`${process.cwd()}/logs/requests.log`,
`\t\t${key}: ${value}\n`
);
for (const ip of config.http.banned_ips) {
try {
if (matches(ip, request_ip)) {
return new Response(undefined, {
status: 403,
statusText: "Forbidden",
});
}
} catch (e) {
console.error(`[-] Error while parsing banned IP "${ip}" `);
throw e;
}
const body = await req.clone().text();
await appendFile(
`${process.cwd()}/logs/requests.log`,
`\tBody:\n\t${body}\n`
);
} else if (config.logging.log_requests) {
await appendFile(
process.cwd() + "/logs/requests.log",
`[${new Date().toISOString()}] ${req.method} ${req.url}\n`
);
}
await logRequest(req);
if (req.method === "OPTIONS") {
return jsonResponse({});
}
@ -64,11 +55,52 @@ Bun.serve({
const matchedRoute = router.match(req);
if (matchedRoute) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
return (await import(matchedRoute.filePath)).default(
req,
matchedRoute
) as Response | Promise<Response>;
const file: {
meta: APIRouteMeta;
default: (
req: Request,
matchedRoute: MatchedRoute
) => Response | Promise<Response>;
} = await import(matchedRoute.filePath);
const meta = file.meta;
// Check for allowed requests
if (!meta.allowedMethods.includes(req.method as any)) {
return new Response(undefined, {
status: 405,
statusText: `Method not allowed: allowed methods are: ${meta.allowedMethods.join(
", "
)}`,
});
}
// TODO: Check for ratelimits
// Check for authentication if required
if (meta.auth.required) {
const { user } = await User.getFromRequest(req);
if (!user) {
return new Response(undefined, {
status: 401,
statusText: "Unauthorized",
});
}
} else if (
(meta.auth.requiredOnMethods ?? []).includes(req.method as any)
) {
const { user } = await User.getFromRequest(req);
if (!user) {
return new Response(undefined, {
status: 401,
statusText: "Unauthorized",
});
}
}
return file.default(req, matchedRoute);
} else {
return new Response(undefined, {
status: 404,
@ -78,4 +110,38 @@ Bun.serve({
},
});
const logRequest = async (req: Request) => {
if (config.logging.log_requests_verbose) {
await appendFile(
`${process.cwd()}/logs/requests.log`,
`[${new Date().toISOString()}] ${req.method} ${
req.url
}\n\tHeaders:\n`
);
// Add headers
const headers = req.headers.entries();
for (const [key, value] of headers) {
await appendFile(
`${process.cwd()}/logs/requests.log`,
`\t\t${key}: ${value}\n`
);
}
const body = await req.clone().text();
await appendFile(
`${process.cwd()}/logs/requests.log`,
`\tBody:\n\t${body}\n`
);
} else if (config.logging.log_requests) {
await appendFile(
process.cwd() + "/logs/requests.log",
`[${new Date().toISOString()}] ${req.method} ${req.url}\n`
);
}
};
console.log("[+] Lysand started!");

View file

@ -1,64 +1,65 @@
{
"name": "lysand",
"module": "index.ts",
"type": "module",
"version": "0.0.1",
"description": "A project to build a federated social network",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"bugs": {
"url": "https://github.com/CPlusPatch/lysand/issues"
},
"icon": "https://github.com/CPlusPatch/lysand",
"license": "AGPL-3.0",
"keywords": [
"federated",
"activitypub",
"bun"
],
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/CPlusPatch/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run index.ts",
"start": "bun run index.ts"
},
"devDependencies": {
"@julr/unocss-preset-forms": "^0.0.5",
"@types/jsonld": "^1.5.9",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@unocss/cli": "^0.55.7",
"activitypub-types": "^1.0.3",
"bun-types": "latest",
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.0.0",
"eslint-formatter-pretty": "^5.0.0",
"eslint-formatter-summary": "^1.1.0",
"eslint-plugin-prettier": "^5.0.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"unocss": "^0.55.7"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"jsonld": "^8.3.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.17"
}
"name": "lysand",
"module": "index.ts",
"type": "module",
"version": "0.0.1",
"description": "A project to build a federated social network",
"author": {
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
},
"bugs": {
"url": "https://github.com/CPlusPatch/lysand/issues"
},
"icon": "https://github.com/CPlusPatch/lysand",
"license": "AGPL-3.0",
"keywords": [
"federated",
"activitypub",
"bun"
],
"maintainers": [
{
"email": "contact@cpluspatch.com",
"name": "CPlusPatch",
"url": "https://cpluspatch.com"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/CPlusPatch/lysand.git"
},
"private": true,
"scripts": {
"dev": "bun run index.ts",
"start": "bun run index.ts"
},
"devDependencies": {
"@julr/unocss-preset-forms": "^0.0.5",
"@types/jsonld": "^1.5.9",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"@unocss/cli": "^0.55.7",
"activitypub-types": "^1.0.3",
"bun-types": "latest",
"eslint": "^8.49.0",
"eslint-config-prettier": "^9.0.0",
"eslint-formatter-pretty": "^5.0.0",
"eslint-formatter-summary": "^1.1.0",
"eslint-plugin-prettier": "^5.0.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"unocss": "^0.55.7"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"ip-matching": "^2.1.2",
"jsonld": "^8.3.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.17"
}
}

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/block",
auth: {
required: true,
},
});
/**
* Blocks a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/follow",
auth: {
required: true,
},
});
/**
* Follow a user
@ -13,13 +26,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,6 +1,19 @@
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id",
auth: {
required: true,
},
});
/**
* Fetch a user
@ -11,13 +24,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1];
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user } = await User.getFromRequest(req);
let foundUser: User | null;
try {

View file

@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/mute",
auth: {
required: true,
},
});
/**
* Mute a user
@ -13,13 +26,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -3,6 +3,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/note",
auth: {
required: true,
},
});
/**
* Sets a user note
@ -13,13 +26,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/pin",
auth: {
required: true,
},
});
/**
* Pin a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/remove_from_followers",
auth: {
required: true,
},
});
/**
* Removes an account from your followers list
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["GET"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/statuses",
auth: {
required: false,
},
});
/**
* Fetch all statuses for a user

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/unblock",
auth: {
required: true,
},
});
/**
* Blocks a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/unfollow",
auth: {
required: true,
},
});
/**
* Unfollows a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/unmute",
auth: {
required: true,
},
});
/**
* Unmute a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,6 +2,19 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 30,
duration: 60,
},
route: "/accounts/:id/unpin",
auth: {
required: true,
},
});
/**
* Unpin a user
@ -12,13 +25,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,18 +2,25 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User";
import { APIAccount } from "~types/entities/account";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/accounts/familiar_followers",
ratelimits: {
max: 30,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Find familiar followers (followers of a user that you also follow)
*/
export default async (req: Request): Promise<Response> => {
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -3,6 +3,19 @@ import { parseRequest } from "@request";
import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["POST"],
route: "/api/v1/accounts",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Creates a new user

View file

@ -2,18 +2,25 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/accounts/relationships",
ratelimits: {
max: 30,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Find relationships
*/
export default async (req: Request): Promise<Response> => {
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const self = await User.retrieveFromToken(token);
const { user: self } = await User.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401);

View file

@ -2,22 +2,25 @@ import { getConfig } from "@config";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["PATCH"],
route: "/api/v1/accounts/update_credentials",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Patches a user
*/
export default async (req: Request): Promise<Response> => {
// Check if request is a PATCH request
if (req.method !== "PATCH")
return errorResponse("This method requires a PATCH request", 405);
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user } = await User.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,22 +1,23 @@
import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User";
import { applyConfig } from "@api";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/accounts/verify_credentials",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Patches a user
*/
export default async (req: Request): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts
// Check if request is a PATCH request
if (req.method !== "GET")
return errorResponse("This method requires a GET request", 405);
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user } = await User.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,8 +1,21 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application";
export const meta = applyConfig({
allowedMethods: ["POST"],
route: "/api/v1/apps",
ratelimits: {
max: 2,
duration: 60,
},
auth: {
required: false,
},
});
/**
* Creates a new application to obtain OAuth 2 credentials
*/

View file

@ -1,18 +1,25 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { Application } from "~database/entities/Application";
import { User } from "~database/entities/User";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/apps/verify_credentials",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: true,
},
});
/**
* Returns OAuth2 credentials
*/
export default async (req: Request): Promise<Response> => {
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user, token } = await User.getFromRequest(req);
const application = await Application.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,9 +1,22 @@
import { applyConfig } from "@api";
import { jsonResponse } from "@response";
import { IsNull } from "typeorm";
import { Emoji } from "~database/entities/Emoji";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/custom_emojis",
ratelimits: {
max: 100,
duration: 60,
},
auth: {
required: false,
},
});
/**
* Creates a new user
* S
*/
// eslint-disable-next-line @typescript-eslint/require-await
export default async (): Promise<Response> => {

View file

@ -1,8 +1,21 @@
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { jsonResponse } from "@response";
import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
export const meta = applyConfig({
allowedMethods: ["GET"],
route: "/api/v1/instance",
ratelimits: {
max: 300,
duration: 60,
},
auth: {
required: false,
},
});
/**
* Creates a new user
*/

View file

@ -1,8 +1,23 @@
import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { RawObject } from "~database/entities/RawObject";
import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["GET", "DELETE"],
ratelimits: {
max: 100,
duration: 60,
},
route: "/api/v1/statuses/:id",
auth: {
required: false,
requiredOnMethods: ["DELETE"],
},
});
/**
* Fetch a user
@ -13,13 +28,7 @@ export default async (
): Promise<Response> => {
const id = matchedRoute.params.id;
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user } = await User.getFromRequest(req);
// TODO: Add checks for user's permissions to view this status

View file

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unused-vars */
import { applyConfig } from "@api";
import { getConfig } from "@config";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
@ -9,22 +10,25 @@ import { Application } from "~database/entities/Application";
import { RawObject } from "~database/entities/RawObject";
import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 300,
duration: 60,
},
route: "/api/v1/statuses",
auth: {
required: true,
},
});
/**
* Post new status
*/
export default async (req: Request): Promise<Response> => {
// Check if request is a PATCH request
if (req.method !== "POST")
return errorResponse("This method requires a POST request", 405);
// Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
const { user, token } = await User.getFromRequest(req);
const application = await Application.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,6 +1,20 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { RawObject } from "~database/entities/RawObject";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["GET"],
ratelimits: {
max: 200,
duration: 60,
},
route: "/api/v1/timelines/home",
auth: {
required: true,
},
});
/**
* Fetch home timeline statuses

View file

@ -1,6 +1,20 @@
import { applyConfig } from "@api";
import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response";
import { RawObject } from "~database/entities/RawObject";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["GET"],
ratelimits: {
max: 200,
duration: 60,
},
route: "/api/v1/timelines/public",
auth: {
required: false,
},
});
/**
* Fetch public timeline statuses

View file

@ -1,9 +1,23 @@
import { applyConfig } from "@api";
import { errorResponse } from "@response";
import { MatchedRoute } from "bun";
import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application";
import { Token } from "~database/entities/Token";
import { User } from "~database/entities/User";
import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({
allowedMethods: ["POST"],
ratelimits: {
max: 4,
duration: 60,
},
route: "/auth/login",
auth: {
required: false,
},
});
/**
* OAuth Code flow

12
types/api.ts Normal file
View file

@ -0,0 +1,12 @@
export interface APIRouteMeta {
allowedMethods: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
ratelimits: {
max: number;
duration: number;
};
route: string;
auth: {
required: boolean;
requiredOnMethods?: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[];
};
}

18
utils/api.ts Normal file
View file

@ -0,0 +1,18 @@
import { getConfig } from "@config";
import { APIRouteMeta } from "~types/api";
export const applyConfig = (routeMeta: APIRouteMeta) => {
const config = getConfig();
const newMeta = routeMeta;
// Apply ratelimits from config
newMeta.ratelimits.duration *= config.ratelimits.duration_coeff;
newMeta.ratelimits.max *= config.ratelimits.max_coeff;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (config.custom_ratelimits[routeMeta.route]) {
newMeta.ratelimits = config.custom_ratelimits[routeMeta.route];
}
return newMeta;
};

View file

@ -13,6 +13,7 @@ export interface ConfigType {
base_url: string;
bind: string;
bind_port: string;
banned_ips: string[];
};
validation: {
@ -67,6 +68,19 @@ export interface ConfigType {
log_requests_verbose: boolean;
log_filters: boolean;
};
ratelimits: {
duration_coeff: number;
max_coeff: number;
};
custom_ratelimits: Record<
string,
{
duration: number;
max: number;
}
>;
[key: string]: unknown;
}
@ -75,6 +89,7 @@ export const configDefaults: ConfigType = {
bind: "http://0.0.0.0",
bind_port: "8000",
base_url: "http://fediproject.localhost:8000",
banned_ips: [],
},
database: {
host: "localhost",
@ -178,6 +193,11 @@ export const configDefaults: ConfigType = {
log_requests_verbose: false,
log_filters: true,
},
ratelimits: {
duration_coeff: 1,
max_coeff: 1,
},
custom_ratelimits: {},
};
export const getConfig = () => {