Small refactors

This commit is contained in:
Jesse Wierzbinski 2023-09-26 12:33:43 -10:00
parent 2cadb68a56
commit 3b452d66aa
22 changed files with 165 additions and 76 deletions

View file

@ -12,6 +12,9 @@ import { RawActor } from "./RawActor";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { errorResponse } from "@response"; import { errorResponse } from "@response";
/**
* Represents a raw activity entity in the database.
*/
@Entity({ @Entity({
name: "activities", name: "activities",
}) })
@ -30,6 +33,11 @@ export class RawActivity extends BaseEntity {
@JoinTable() @JoinTable()
actors!: RawActor[]; actors!: RawActor[];
/**
* Retrieves all activities that contain an object with the given ID.
* @param id The ID of the object to search for.
* @returns A promise that resolves to an array of matching activities.
*/
static async getByObjectId(id: string) { static async getByObjectId(id: string) {
return await RawActivity.createQueryBuilder("activity") return await RawActivity.createQueryBuilder("activity")
.leftJoinAndSelect("activity.objects", "objects") .leftJoinAndSelect("activity.objects", "objects")
@ -38,6 +46,11 @@ export class RawActivity extends BaseEntity {
.getMany(); .getMany();
} }
/**
* Retrieves the activity with the given ID.
* @param id The ID of the activity to retrieve.
* @returns A promise that resolves to the matching activity, or undefined if not found.
*/
static async getById(id: string) { static async getById(id: string) {
return await RawActivity.createQueryBuilder("activity") return await RawActivity.createQueryBuilder("activity")
.leftJoinAndSelect("activity.objects", "objects") .leftJoinAndSelect("activity.objects", "objects")
@ -46,6 +59,11 @@ export class RawActivity extends BaseEntity {
.getOne(); .getOne();
} }
/**
* Retrieves the latest activity with the given ID.
* @param id The ID of the activity to retrieve.
* @returns A promise that resolves to the latest matching activity, or undefined if not found.
*/
static async getLatestById(id: string) { static async getLatestById(id: string) {
return await RawActivity.createQueryBuilder("activity") return await RawActivity.createQueryBuilder("activity")
.where("activity.data->>'id' = :id", { id }) .where("activity.data->>'id' = :id", { id })
@ -55,10 +73,20 @@ export class RawActivity extends BaseEntity {
.getOne(); .getOne();
} }
/**
* Checks if an activity with the given ID exists.
* @param id The ID of the activity to check for.
* @returns A promise that resolves to true if the activity exists, false otherwise.
*/
static async exists(id: string) { static async exists(id: string) {
return !!(await RawActivity.getById(id)); return !!(await RawActivity.getById(id));
} }
/**
* Updates an object in the database if it exists.
* @param object The object to update.
* @returns A promise that resolves to the updated object, or an error response if the object does not exist or is filtered.
*/
static async updateObjectIfExists(object: APObject) { static async updateObjectIfExists(object: APObject) {
const rawObject = await RawObject.getById(object.id ?? ""); const rawObject = await RawObject.getById(object.id ?? "");
@ -76,6 +104,11 @@ export class RawActivity extends BaseEntity {
return rawObject; return rawObject;
} }
/**
* Deletes an object from the database if it exists.
* @param object The object to delete.
* @returns A promise that resolves to the deleted object, or an error response if the object does not exist.
*/
static async deleteObjectIfExists(object: APObject) { static async deleteObjectIfExists(object: APObject) {
const dbObject = await RawObject.getById(object.id ?? ""); const dbObject = await RawObject.getById(object.id ?? "");
@ -110,7 +143,16 @@ export class RawActivity extends BaseEntity {
return dbObject; return dbObject;
} }
static async addIfNotExists(activity: APActivity, addObject?: RawObject) { /**
* Adds an activity to the database if it does not already exist.
* @param activity The activity to add.
* @param addObject An optional object to add to the activity.
* @returns A promise that resolves to the added activity, or an error response if the activity already exists or is filtered.
*/
static async createIfNotExists(
activity: APActivity,
addObject?: RawObject
) {
if (await RawActivity.exists(activity.id ?? "")) { if (await RawActivity.exists(activity.id ?? "")) {
return errorResponse("Activity already exists", 409); return errorResponse("Activity already exists", 409);
} }
@ -144,6 +186,10 @@ export class RawActivity extends BaseEntity {
return rawActivity; return rawActivity;
} }
/**
* Returns the ActivityPub representation of the activity.
* @returns The ActivityPub representation of the activity.
*/
makeActivityPubRepresentation() { makeActivityPubRepresentation() {
return { return {
...this.data, ...this.data,
@ -152,6 +198,11 @@ export class RawActivity extends BaseEntity {
}; };
} }
/**
* Adds an object to the activity if it does not already exist.
* @param object The object to add.
* @returns A promise that resolves to the added object, or an error response if the object already exists or is filtered.
*/
async addObjectIfNotExists(object: APObject) { async addObjectIfNotExists(object: APObject) {
if (this.objects.some(o => o.data.id === object.id)) { if (this.objects.some(o => o.data.id === object.id)) {
return errorResponse("Object already exists", 409); return errorResponse("Object already exists", 409);
@ -169,6 +220,11 @@ export class RawActivity extends BaseEntity {
return rawObject; return rawObject;
} }
/**
* Adds an actor to the activity if it does not already exist.
* @param actor The actor to add.
* @returns A promise that resolves to the added actor, or an error response if the actor already exists or is filtered.
*/
async addActorIfNotExists(actor: APActor) { async addActorIfNotExists(actor: APActor) {
const dbActor = await RawActor.getByActorId(actor.id ?? ""); const dbActor = await RawActor.getByActorId(actor.id ?? "");

View file

@ -139,6 +139,25 @@ export class User extends BaseEntity {
return user; return user;
} }
static async retrieveFromToken(access_token: string) {
if (!access_token) return null;
const token = await Token.findOne({
where: {
access_token,
},
relations: {
user: {
relationships: true,
},
},
});
if (!token) return null;
return token.user;
}
async getRelationshipToOtherUser(other: User) { async getRelationshipToOtherUser(other: User) {
const relationship = await Relationship.findOne({ const relationship = await Relationship.findOne({
where: { where: {

View file

@ -45,7 +45,7 @@ export default async (
// TODO: Add authentication // TODO: Add authentication
// Check is Activity already exists // Check is Activity already exists
const activity = await RawActivity.addIfNotExists(body); const activity = await RawActivity.createIfNotExists(body);
if (activity instanceof Response) { if (activity instanceof Response) {
return activity; return activity;
@ -65,7 +65,7 @@ export default async (
return object; return object;
} }
const activity = await RawActivity.addIfNotExists(body, object); const activity = await RawActivity.createIfNotExists(body, object);
if (activity instanceof Response) { if (activity instanceof Response) {
return activity; return activity;
@ -87,7 +87,7 @@ export default async (
} }
// Store the Delete event in the database // Store the Delete event in the database
const activity = await RawActivity.addIfNotExists(body); const activity = await RawActivity.createIfNotExists(body);
if (activity instanceof Response) { if (activity instanceof Response) {
return activity; return activity;

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
@ -20,7 +19,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,7 +1,7 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { RawActor } from "~database/entities/RawActor"; import { RawActor } from "~database/entities/RawActor";
import { User } from "~database/entities/User";
/** /**
* Fetch a user * Fetch a user
@ -13,8 +13,12 @@ export default async (
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
// Check auth token // Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null; const token = req.headers.get("Authorization")?.split(" ")[1];
const user = await getUserByToken(token);
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
let foundUser: RawActor | null; let foundUser: RawActor | null;
try { try {

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
@ -20,7 +19,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
@ -20,7 +19,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -19,7 +18,7 @@ export default async (
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User"; import { User } from "~database/entities/User";
@ -14,7 +13,7 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,4 +1,3 @@
import { getUserByToken } from "@auth";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
@ -14,7 +13,7 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const self = await getUserByToken(token); const self = await User.retrieveFromToken(token);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);

View file

@ -1,7 +1,7 @@
import { getUserByToken } from "@auth";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User";
/** /**
* Patches a user * Patches a user
@ -17,27 +17,35 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const user = await getUserByToken(token); const user = await User.retrieveFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const config = getConfig(); const config = getConfig();
const { display_name, note, avatar, header, locked, bot, discoverable } = const {
await parseRequest<{ display_name,
display_name: string; note,
note: string; avatar,
avatar: File; header,
header: File; locked,
locked: string; bot,
bot: string; discoverable,
discoverable: string; "source[privacy]": source_privacy,
}>(req); "source[sensitive]": source_sensitive,
"source[language]": source_language,
// TODO: Implement other options like field or source } = await parseRequest<{
// const source_privacy = body.get("source[privacy]")?.toString() || null; display_name: string;
// const source_sensitive = body.get("source[sensitive]")?.toString() || null; note: string;
// const source_language = body.get("source[language]")?.toString() || null; avatar: File;
header: File;
locked: string;
bot: string;
discoverable: string;
"source[privacy]": string;
"source[sensitive]": string;
"source[language]": string;
}>(req);
if (display_name) { if (display_name) {
// Check if within allowed display name lengths // Check if within allowed display name lengths
@ -66,6 +74,36 @@ export default async (req: Request): Promise<Response> => {
user.note = note; user.note = note;
} }
if (source_privacy) {
// Check if within allowed privacy values
if (
!["public", "unlisted", "private", "direct"].includes(
source_privacy
)
) {
return errorResponse(
"Privacy must be one of public, unlisted, private, or direct",
422
);
}
user.source.privacy = source_privacy;
}
if (source_sensitive) {
// Check if within allowed sensitive values
if (source_sensitive !== "true" && source_sensitive !== "false") {
return errorResponse("Sensitive must be a boolean", 422);
}
user.source.sensitive = source_sensitive === "true";
}
if (source_language) {
// TODO: Check if proper ISO code
user.source.language = source_language;
}
if (avatar) { if (avatar) {
// Check if within allowed avatar length (avatar is an image) // Check if within allowed avatar length (avatar is an image)
if (avatar.size > config.validation.max_avatar_size) { if (avatar.size > config.validation.max_avatar_size) {

View file

@ -1,5 +1,5 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User";
/** /**
* Patches a user * Patches a user
@ -16,7 +16,7 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const user = await getUserByToken(token); const user = await User.retrieveFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,6 +1,6 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Application } from "~database/entities/Application"; import { Application } from "~database/entities/Application";
import { User } from "~database/entities/User";
/** /**
* Returns OAuth2 credentials * Returns OAuth2 credentials
@ -12,7 +12,7 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const user = await getUserByToken(token); const user = await User.retrieveFromToken(token);
const application = await Application.getFromToken(token); const application = await Application.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,8 +1,8 @@
import { getUserByToken } from "@auth";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { RawObject } from "~database/entities/RawObject"; import { RawObject } from "~database/entities/RawObject";
import { Status } from "~database/entities/Status"; import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
/** /**
* Fetch a user * Fetch a user
@ -15,7 +15,11 @@ export default async (
// Check auth token // Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || null; const token = req.headers.get("Authorization")?.split(" ")[1] || null;
const user = await getUserByToken(token);
if (!token)
return errorResponse("This method requires an authenticated user", 422);
const user = await User.retrieveFromToken(token);
// TODO: Add checks for user's permissions to view this status // TODO: Add checks for user's permissions to view this status

View file

@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { getUserByToken } from "@auth";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Application } from "~database/entities/Application"; import { Application } from "~database/entities/Application";
import { Status } from "~database/entities/Status"; import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User";
/** /**
* Post new status * Post new status
@ -20,7 +20,7 @@ export default async (req: Request): Promise<Response> => {
if (!token) if (!token)
return errorResponse("This method requires an authenticated user", 422); return errorResponse("This method requires an authenticated user", 422);
const user = await getUserByToken(token); const user = await User.retrieveFromToken(token);
const application = await Application.getFromToken(token); const application = await Application.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -1,20 +0,0 @@
import { Token } from "~database/entities/Token";
export const getUserByToken = async (access_token: string | null) => {
if (!access_token) return null;
const token = await Token.findOne({
where: {
access_token,
},
relations: {
user: {
relationships: true,
},
},
});
if (!token) return null;
return token.user;
};