diff --git a/database/entities/RawActivity.ts b/database/entities/RawActivity.ts deleted file mode 100644 index e5337a6d..00000000 --- a/database/entities/RawActivity.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { - BaseEntity, - Column, - Entity, - Index, - JoinTable, - ManyToMany, - PrimaryGeneratedColumn, -} from "typeorm"; -import { APActivity, APActor, APObject, APTombstone } from "activitypub-types"; -import { RawObject } from "./RawObject"; -import { RawActor } from "./RawActor"; -import { getConfig } from "@config"; -import { errorResponse } from "@response"; - -/** - * Represents a raw activity entity in the database. - */ -@Entity({ - name: "activities", -}) -export class RawActivity extends BaseEntity { - @PrimaryGeneratedColumn("uuid") - id!: string; - - @Column("jsonb") - // Index ID for faster lookups - @Index({ unique: true, where: "(data->>'id') IS NOT NULL" }) - data!: APActivity; - - @ManyToMany(() => RawObject) - @JoinTable() - objects!: RawObject[]; - - @ManyToMany(() => RawActor) - @JoinTable() - 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) { - return await RawActivity.createQueryBuilder("activity") - .leftJoinAndSelect("activity.objects", "objects") - .leftJoinAndSelect("activity.actors", "actors") - .where("objects.data @> :data", { data: JSON.stringify({ id }) }) - .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) { - return await RawActivity.createQueryBuilder("activity") - .leftJoinAndSelect("activity.objects", "objects") - .leftJoinAndSelect("activity.actors", "actors") - .where("activity.data->>'id' = :id", { id }) - .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) { - return await RawActivity.createQueryBuilder("activity") - .where("activity.data->>'id' = :id", { id }) - .leftJoinAndSelect("activity.objects", "objects") - .leftJoinAndSelect("activity.actors", "actors") - .orderBy("activity.data->>'published'", "DESC") - .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) { - 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) { - const rawObject = await RawObject.getById(object.id ?? ""); - - if (!rawObject) { - return errorResponse("Object does not exist", 404); - } - - rawObject.data = object; - - if (await rawObject.isObjectFiltered()) { - return errorResponse("Object filtered", 409); - } - - await rawObject.save(); - 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) { - const dbObject = await RawObject.getById(object.id ?? ""); - - if (!dbObject) { - return errorResponse("Object does not exist", 404); - } - - const config = getConfig(); - - if (config.activitypub.use_tombstones) { - dbObject.data = { - ...dbObject.data, - type: "Tombstone", - deleted: new Date(), - formerType: dbObject.data.type, - } as APTombstone; - - await dbObject.save(); - } else { - const activities = await RawActivity.getByObjectId(object.id ?? ""); - - for (const activity of activities) { - activity.objects = activity.objects.filter( - o => o.id !== object.id - ); - await activity.save(); - } - - await dbObject.remove(); - } - - return dbObject; - } - - /** - * 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 ?? "")) { - return errorResponse("Activity already exists", 409); - } - - const rawActivity = new RawActivity(); - rawActivity.data = { ...activity, object: undefined, actor: undefined }; - rawActivity.actors = []; - rawActivity.objects = []; - - const actor = await rawActivity.addActorIfNotExists( - activity.actor as APActor - ); - - if (actor instanceof Response) { - return actor; - } - - if (addObject) { - rawActivity.objects.push(addObject); - } else { - const object = await rawActivity.addObjectIfNotExists( - activity.object as APObject - ); - - if (object instanceof Response) { - return object; - } - } - - await rawActivity.save(); - return rawActivity; - } - - /** - * Returns the ActivityPub representation of the activity. - * @returns The ActivityPub representation of the activity. - */ - getActivityPubRepresentation() { - return { - ...this.data, - object: this.objects[0].data, - actor: this.actors[0].data, - }; - } - - /** - * 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) { - if (this.objects.some(o => o.data.id === object.id)) { - return errorResponse("Object already exists", 409); - } - - const rawObject = new RawObject(); - rawObject.data = object; - - if (await rawObject.isObjectFiltered()) { - return errorResponse("Object filtered", 409); - } - - await rawObject.save(); - this.objects.push(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) { - const dbActor = await RawActor.getByActorId(actor.id ?? ""); - - if (dbActor) { - this.actors.push(dbActor); - return dbActor; - } - - if (this.actors.some(a => a.data.id === actor.id)) { - return errorResponse("Actor already exists", 409); - } - - const rawActor = new RawActor(); - rawActor.data = actor; - - const config = getConfig(); - - if ( - config.activitypub.discard_avatars.find( - instance => actor.id?.includes(instance) - ) - ) { - rawActor.data.icon = undefined; - } - - if ( - config.activitypub.discard_banners.find( - instance => actor.id?.includes(instance) - ) - ) { - rawActor.data.image = undefined; - } - - if (await rawActor.isObjectFiltered()) { - return errorResponse("Actor filtered", 409); - } - - await rawActor.save(); - this.actors.push(rawActor); - return rawActor; - } -} diff --git a/database/entities/RawActor.ts b/database/entities/RawActor.ts deleted file mode 100644 index 47095d1a..00000000 --- a/database/entities/RawActor.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* eslint-disable @typescript-eslint/require-await */ -import { - BaseEntity, - Column, - Entity, - Index, - PrimaryGeneratedColumn, -} from "typeorm"; -import { APActor, APImage } from "activitypub-types"; -import { getConfig, getHost } from "@config"; -import { appendFile } from "fs/promises"; -import { errorResponse } from "@response"; -import { APIAccount } from "~types/entities/account"; -import { RawActivity } from "./RawActivity"; -/** - * Represents a raw actor entity in the database. - */ -@Entity({ name: "actors" }) -export class RawActor extends BaseEntity { - /** - * The unique identifier of the actor. - */ - @PrimaryGeneratedColumn("uuid") - id!: string; - - /** - * The ActivityPub actor data associated with the actor. - */ - @Column("jsonb") - @Index({ unique: true, where: "(data->>'id') IS NOT NULL" }) - data!: APActor; - - /** - * Retrieves a RawActor entity by actor ID. - * @param id The ID of the actor to retrieve. - * @returns The RawActor entity with the specified ID, or undefined if not found. - */ - static async getByActorId(id: string) { - return await RawActor.createQueryBuilder("actor") - .where("actor.data->>'id' = :id", { id }) - .getOne(); - } - - /** - * Adds a new RawActor entity to the database if an actor with the same ID does not already exist. - * @param data The ActivityPub actor data to add. - * @returns The newly created RawActor entity, or an error response if the actor already exists or is filtered. - */ - static async addIfNotExists(data: APActor) { - // TODO: Also add corresponding user - if (await RawActor.exists(data.id ?? "")) { - return errorResponse("Actor already exists", 409); - } - - const actor = new RawActor(); - actor.data = data; - - const config = getConfig(); - - if ( - config.activitypub.discard_avatars.some(instance => - actor.id.includes(instance) - ) - ) { - actor.data.icon = undefined; - } - - if ( - config.activitypub.discard_banners.some(instance => - actor.id.includes(instance) - ) - ) { - actor.data.image = undefined; - } - - if (await actor.isObjectFiltered()) { - return errorResponse("Actor filtered", 409); - } - - await actor.save(); - - return actor; - } - - /** - * Retrieves the domain of the instance associated with the actor. - * @returns The domain of the instance associated with the actor. - */ - getInstanceDomain() { - return new URL(this.data.id ?? "").host; - } - - /** - * Converts the RawActor entity to an API account object. - * @param isOwnAccount Whether the account is the user's own account. - * @returns The API account object representing the RawActor entity. - */ - async toAPIAccount(isOwnAccount = false): Promise { - const config = getConfig(); - const { preferredUsername, name, summary, published, icon, image } = - this.data; - - const statusCount = await RawActivity.createQueryBuilder("activity") - .leftJoinAndSelect("activity.actors", "actors") - .where("actors.data @> :data", { - data: JSON.stringify({ - id: this.data.id, - }), - }) - .getCount(); - - const isLocalUser = this.getInstanceDomain() == getHost(); - - return { - id: this.id, - username: preferredUsername ?? "", - display_name: name ?? preferredUsername ?? "", - note: summary ?? "", - url: `${config.http.base_url}/users/${preferredUsername}${ - isLocalUser ? "" : `@${this.getInstanceDomain()}` - }`, - avatar: - ((icon as APImage).url as string | undefined) || - config.defaults.avatar, - header: - ((image as APImage).url as string | undefined) || - config.defaults.header, - locked: false, - created_at: new Date(published ?? 0).toISOString(), - followers_count: 0, - following_count: 0, - statuses_count: statusCount, - emojis: [], - fields: [], - bot: false, - source: isOwnAccount - ? { - privacy: "public", - sensitive: false, - language: "en", - note: "", - fields: [], - } - : undefined, - avatar_static: "", - header_static: "", - acct: - this.getInstanceDomain() == getHost() - ? `${preferredUsername}` - : `${preferredUsername}@${this.getInstanceDomain()}`, - limited: false, - moved: null, - noindex: false, - suspended: false, - discoverable: undefined, - mute_expires_at: undefined, - group: false, - role: undefined, - }; - } - - /** - * Determines whether the actor is filtered based on the instance's filter rules. - * @returns Whether the actor is filtered. - */ - async isObjectFiltered() { - const config = getConfig(); - const { type, preferredUsername, name, id } = this.data; - - const usernameFilterResult = await Promise.all( - config.filters.username_filters.map(async filter => { - if (type === "Person" && preferredUsername?.match(filter)) { - if (config.logging.log_filters) { - await appendFile( - process.cwd() + "/logs/filters.log", - `${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n` - ); - } - return true; - } - }) - ); - - const displayNameFilterResult = await Promise.all( - config.filters.displayname_filters.map(async filter => { - if (type === "Person" && name?.match(filter)) { - if (config.logging.log_filters) { - await appendFile( - process.cwd() + "/logs/filters.log", - `${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n` - ); - } - return true; - } - }) - ); - - return ( - usernameFilterResult.includes(true) || - displayNameFilterResult.includes(true) - ); - } - - /** - * Determines whether an actor with the specified ID exists in the database. - * @param id The ID of the actor to check for. - * @returns Whether an actor with the specified ID exists in the database. - */ - static async exists(id: string) { - return !!(await RawActor.getByActorId(id)); - } -} diff --git a/database/entities/RawObject.ts b/database/entities/RawObject.ts deleted file mode 100644 index 120ffb32..00000000 --- a/database/entities/RawObject.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { - BaseEntity, - Column, - Entity, - Index, - PrimaryGeneratedColumn, -} from "typeorm"; -import { APImage, APObject, DateTime } from "activitypub-types"; -import { ConfigType, getConfig } from "@config"; -import { appendFile } from "fs/promises"; -import { APIStatus } from "~types/entities/status"; -import { RawActor } from "./RawActor"; -import { APIAccount } from "~types/entities/account"; -import { APIEmoji } from "~types/entities/emoji"; -import { User } from "./User"; -import { Status } from "./Status"; - -/** - * Represents a raw ActivityPub object in the database. - */ -@Entity({ - name: "objects", -}) -export class RawObject extends BaseEntity { - /** - * The unique identifier of the object. - */ - @PrimaryGeneratedColumn("uuid") - id!: string; - - /** - * The data associated with the object. - */ - @Column("jsonb") - // Index ID, attributedTo, to and published for faster lookups - @Index({ unique: true, where: "(data->>'id') IS NOT NULL" }) - @Index({ where: "(data->>'attributedTo') IS NOT NULL" }) - @Index({ where: "(data->>'to') IS NOT NULL" }) - @Index({ where: "(data->>'published') IS NOT NULL" }) - data!: APObject; - - /** - * Retrieves a RawObject instance by its ID. - * @param id The ID of the RawObject to retrieve. - * @returns A Promise that resolves to the RawObject instance, or undefined if not found. - */ - static async getById(id: string) { - return await RawObject.createQueryBuilder("object") - .where("object.data->>'id' = :id", { - id, - }) - .getOne(); - } - - /** - * Parses the emojis associated with the object. - * @returns A Promise that resolves to an array of APIEmoji objects. - */ - // eslint-disable-next-line @typescript-eslint/require-await - async parseEmojis() { - const emojis = this.data.tag as { - id: string; - type: string; - name: string; - updated: string; - icon: { - type: "Image"; - mediaType: string; - url: string; - }; - }[]; - - return emojis.map(emoji => ({ - shortcode: emoji.name, - static_url: (emoji.icon as APImage).url, - url: (emoji.icon as APImage).url, - visible_in_picker: true, - category: "custom", - })) as APIEmoji[]; - } - - /** - * Converts the RawObject instance to an APIStatus object. - * @returns A Promise that resolves to the APIStatus object. - */ - async toAPI(): Promise { - const mentions = ( - await Promise.all( - (this.data.to as string[]).map( - async person => await RawActor.getByActorId(person) - ) - ) - ).filter(m => m) as RawActor[]; - - return { - account: - (await ( - await User.getByActorId(this.data.attributedTo as string) - )?.toAPI()) ?? (null as unknown as APIAccount), - created_at: new Date(this.data.published as DateTime).toISOString(), - id: this.id, - in_reply_to_id: null, - application: null, - card: null, - content: this.data.content as string, - emojis: await this.parseEmojis(), - favourited: false, - favourites_count: 0, - media_attachments: [], - mentions: await Promise.all( - mentions.map(async m => await m.toAPIAccount()) - ), - in_reply_to_account_id: null, - language: null, - muted: false, - pinned: false, - poll: null, - reblog: null, - reblogged: false, - reblogs_count: 0, - replies_count: 0, - sensitive: false, - spoiler_text: "", - tags: [], - uri: this.data.id as string, - visibility: "public", - url: this.data.id as string, - bookmarked: false, - quote: null, - quote_id: undefined, - }; - } - - /** - * Determines whether the object is filtered based on the note filters in the configuration. - * @returns A Promise that resolves to a boolean indicating whether the object is filtered. - */ - async isObjectFiltered() { - const config = getConfig(); - - const filter_result = await Promise.all( - config.filters.note_filters.map(async filter => { - if ( - this.data.type === "Note" && - this.data.content?.match(filter) - ) { - // Log filter - - if (config.logging.log_filters) - await appendFile( - process.cwd() + "/logs/filters.log", - `${new Date().toISOString()} Filtered note content: "${this.data.content.replaceAll( - "\n", - " " - )}" (ID: ${ - this.data.id - }) based on rule: ${filter}\n` - ); - return true; - } - }) - ); - - return filter_result.includes(true); - } - - /** - * Determines whether a RawObject instance with the given ID exists in the database. - * @param id The ID of the RawObject to check for existence. - * @returns A Promise that resolves to a boolean indicating whether the RawObject exists. - */ - static async exists(id: string) { - return !!(await RawObject.getById(id)); - } - - /** - * Creates a RawObject instance from a Status object. - * DOES NOT SAVE THE OBJECT TO THE DATABASE. - * @param status The Status object to create the RawObject from. - * @returns A Promise that resolves to the RawObject instance. - */ - static createFromStatus(status: Status, config: ConfigType) { - const object = new RawObject(); - - object.data = { - id: `${config.http.base_url}/users/${status.account.username}/statuses/${status.id}`, - type: "Note", - summary: status.spoiler_text, - content: status.content, - inReplyTo: status.in_reply_to_post?.object.data.id, - published: new Date().toISOString(), - tag: [], - attributedTo: `${config.http.base_url}/users/${status.account.username}`, - }; - - // Map status mentions to ActivityPub Actor IDs - const mentionedUsers = status.mentions.map( - user => user.actor.data.id as string - ); - - object.data.to = mentionedUsers; - - if (status.visibility === "private") { - object.data.cc = [ - `${config.http.base_url}/users/${status.account.username}/followers`, - ]; - } else if (status.visibility === "direct") { - // Add nothing else - } else if (status.visibility === "public") { - object.data.to = [ - ...object.data.to, - "https://www.w3.org/ns/activitystreams#Public", - ]; - object.data.cc = [ - `${config.http.base_url}/users/${status.account.username}/followers`, - ]; - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - else if (status.visibility === "unlisted") { - object.data.to = [ - ...object.data.to, - "https://www.w3.org/ns/activitystreams#Public", - ]; - } - - return object; - } -} diff --git a/database/entities/Status.ts b/database/entities/Status.ts index fd82a7cf..ab595371 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -18,8 +18,6 @@ import { APIStatus } from "~types/entities/status"; import { User, userRelations } from "./User"; import { Application } from "./Application"; import { Emoji } from "./Emoji"; -import { RawActivity } from "./RawActivity"; -import { RawObject } from "./RawObject"; import { Instance } from "./Instance"; import { Like } from "./Like"; import { AppDataSource } from "~database/datasource"; @@ -29,25 +27,17 @@ const config = getConfig(); export const statusRelations = [ "account", "reblog", - "object", "in_reply_to_post", "instance", "in_reply_to_post.account", "application", "emojis", "mentions", - "likes", - "announces", ]; export const statusAndUserRelations = [ ...statusRelations, - ...[ - "account.actor", - "account.relationships", - "account.pinned_notes", - "account.instance", - ], + ...["account.relationships", "account.pinned_notes", "account.instance"], ]; /** @@ -91,15 +81,6 @@ export class Status extends BaseEntity { }) reblog?: Status | null; - /** - * The raw object associated with this status. - */ - @ManyToOne(() => RawObject, { - nullable: true, - onDelete: "SET NULL", - }) - object!: RawObject; - /** * Whether this status is a reblog. */ @@ -175,20 +156,6 @@ export class Status extends BaseEntity { @JoinTable() mentions!: User[]; - /** - * The activities that have liked this status. - */ - @ManyToMany(() => RawActivity, activity => activity.id) - @JoinTable() - likes!: RawActivity[]; - - /** - * The activities that have announced this status. - */ - @ManyToMany(() => RawActivity, activity => activity.id) - @JoinTable() - announces!: RawActivity[]; - /** * Removes this status from the database. * @param options The options for removing this status. @@ -196,8 +163,6 @@ export class Status extends BaseEntity { */ async remove(options?: RemoveOptions | undefined) { // Delete object - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (this.object) await this.object.remove(options); // Get all associated Likes and remove them as well await Like.delete({ @@ -338,10 +303,7 @@ export class Status extends BaseEntity { newStatus.sensitive = data.sensitive; newStatus.spoiler_text = data.spoiler_text; newStatus.emojis = data.emojis; - newStatus.likes = []; - newStatus.announces = []; newStatus.isReblog = false; - newStatus.announces = []; newStatus.mentions = []; newStatus.instance = data.account.instance; @@ -391,10 +353,6 @@ export class Status extends BaseEntity { }) ); - const object = RawObject.createFromStatus(newStatus, config); - - newStatus.object = object; - await newStatus.object.save(); await newStatus.save(); return newStatus; } diff --git a/database/entities/User.ts b/database/entities/User.ts index fb42427d..abf4aab2 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -13,24 +13,13 @@ import { UpdateDateColumn, } from "typeorm"; import { APIAccount } from "~types/entities/account"; -import { RawActor } from "./RawActor"; -import { - APActor, - APCollectionPage, - APOrderedCollectionPage, -} from "activitypub-types"; import { Token } from "./Token"; import { Status, statusRelations } from "./Status"; import { APISource } from "~types/entities/source"; import { Relationship } from "./Relationship"; import { Instance } from "./Instance"; -export const userRelations = [ - "actor", - "relationships", - "pinned_notes", - "instance", -]; +export const userRelations = ["relationships", "pinned_notes", "instance"]; /** * Represents a user in the database. @@ -151,14 +140,6 @@ export class User extends BaseEntity { }) instance!: Instance | null; - /** */ - - /** - * The actor for the user. - */ - @ManyToOne(() => RawActor, actor => actor.id) - actor!: RawActor; - /** * The pinned notes for the user. */ @@ -199,53 +180,11 @@ export class User extends BaseEntity { return { user: await User.retrieveFromToken(token), token }; } - /** - * Update this user data from its actor - * @returns The updated user. - */ - async updateFromActor() { - const actor = await this.actor.toAPIAccount(); - - this.username = actor.username; - this.display_name = actor.display_name; - this.note = actor.note; - this.avatar = actor.avatar; - this.header = actor.header; - this.avatar = actor.avatar; - - return await this.save(); - } - /** * Fetches the list of followers associated with the actor and updates the user's followers */ async fetchFollowers() { - const config = getConfig(); - - let followers: APOrderedCollectionPage = await fetch( - `${this.actor.data.followers?.toString() ?? ""}?page=1`, - { - headers: { Accept: "application/activity+json" }, - } - ); - - let followersList = followers.orderedItems ?? []; - - while (followers.type === "OrderedCollectionPage" && followers.next) { - followers = await fetch((followers.next as string).toString(), { - headers: { Accept: "application/activity+json" }, - }).then(res => res.json() as APCollectionPage); - - followersList = { - ...followersList, - ...(followers.orderedItems ?? []), - }; - } - - if (config.activitypub.fetch_all_collection_members) { - // Loop through followers list and retrieve each actor individually - // TODO: Implement - } + // } /** @@ -306,7 +245,6 @@ export class User extends BaseEntity { await user.generateKeys(); await user.save(); - await user.updateActor(); return user; } @@ -406,64 +344,6 @@ export class User extends BaseEntity { return relationships; } - /** - * Updates the actor for the user. - * @returns The updated actor. - */ - async updateActor() { - const config = getConfig(); - - // Check if actor exists - const actorExists = await RawActor.getByActorId( - `${config.http.base_url}/users/${this.username}` - ); - - let actor: RawActor; - - if (actorExists) { - actor = actorExists; - } else { - actor = new RawActor(); - } - - actor.data = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - id: `${config.http.base_url}/users/${this.username}`, - type: "Person", - preferredUsername: this.username, - name: this.display_name, - inbox: `${config.http.base_url}/users/${this.username}/inbox`, - outbox: `${config.http.base_url}/users/${this.username}/outbox`, - followers: `${config.http.base_url}/users/${this.username}/followers`, - following: `${config.http.base_url}/users/${this.username}/following`, - published: new Date(this.created_at).toISOString(), - manuallyApprovesFollowers: false, - summary: this.note, - icon: { - type: "Image", - url: this.getAvatarUrl(config), - }, - image: { - type: "Image", - url: this.getHeaderUrl(config), - }, - publicKey: { - id: `${config.http.base_url}/users/${this.username}/actor#main-key`, - owner: `${config.http.base_url}/users/${this.username}/actor`, - publicKeyPem: this.public_key, - }, - } as APActor; - - await actor.save(); - - this.actor = actor; - await this.save(); - return actor; - } - /** * Generates keys for the user. */ diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 76fb4e90..a88bb82a 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -81,7 +81,6 @@ export default async (req: Request): Promise => { return errorResponse("Display name contains blocked words", 422); } - user.actor.data.name = sanitizedDisplayName; user.display_name = sanitizedDisplayName; } @@ -103,7 +102,6 @@ export default async (req: Request): Promise => { return errorResponse("Bio contains blocked words", 422); } - user.actor.data.summary = sanitizedNote; user.note = sanitizedNote; } @@ -196,7 +194,6 @@ export default async (req: Request): Promise => { } await user.save(); - await user.updateActor(); return jsonResponse(await user.toAPI()); }; diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 97bdba08..d84623b5 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -6,11 +6,8 @@ import { getConfig } from "@config"; import { parseRequest } from "@request"; import { errorResponse, jsonResponse } from "@response"; import { sanitizeHtml } from "@sanitization"; -import { APActor } from "activitypub-types"; -import { sanitize } from "isomorphic-dompurify"; import { parse } from "marked"; import { Application } from "~database/entities/Application"; -import { RawObject } from "~database/entities/RawObject"; import { Status, statusRelations } from "~database/entities/Status"; import { User } from "~database/entities/User"; import { APIRouteMeta } from "~types/api"; diff --git a/server/api/object/[uuid]/index.ts b/server/api/object/[uuid]/index.ts index c72239f6..902f9ecc 100644 --- a/server/api/object/[uuid]/index.ts +++ b/server/api/object/[uuid]/index.ts @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { applyConfig } from "@api"; -import { errorResponse, jsonLdResponse } from "@response"; +import { jsonResponse } from "@response"; import { MatchedRoute } from "bun"; -import { RawActivity } from "~database/entities/RawActivity"; export const meta = applyConfig({ allowedMethods: ["GET"], @@ -22,11 +23,5 @@ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - const object = await RawActivity.findOneBy({ - id: matchedRoute.params.id, - }); - - if (!object) return errorResponse("Object not found", 404); - - return jsonLdResponse(object); + return jsonResponse({}); }; diff --git a/tests/actor.test.ts b/tests/actor.test.ts index 1455a2d3..fab17ac7 100644 --- a/tests/actor.test.ts +++ b/tests/actor.test.ts @@ -1,97 +1 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { getConfig } from "@config"; -import { APActor } from "activitypub-types"; -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { RawActivity } from "~database/entities/RawActivity"; -import { User, userRelations } from "~database/entities/User"; - -const config = getConfig(); - -beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - - // Initialize test user - await User.createNewLocal({ - email: "test@test.com", - username: "test", - password: "test", - display_name: "", - }); -}); - -describe("POST /@test/actor", () => { - test("should return a valid ActivityPub Actor when querying an existing user", async () => { - const response = await fetch( - `${config.http.base_url}/users/test/actor`, - { - method: "GET", - headers: { - Accept: "application/activity+json", - }, - } - ); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe( - "application/activity+json" - ); - - const actor = (await response.json()) as APActor; - - expect(actor.type).toBe("Person"); - expect(actor.id).toBe(`${config.http.base_url}/users/test`); - expect(actor.preferredUsername).toBe("test"); - expect(actor.inbox).toBe(`${config.http.base_url}/users/test/inbox`); - expect(actor.outbox).toBe(`${config.http.base_url}/users/test/outbox`); - expect(actor.followers).toBe( - `${config.http.base_url}/users/test/followers` - ); - expect(actor.following).toBe( - `${config.http.base_url}/users/test/following` - ); - expect((actor as any).publicKey).toBeDefined(); - expect((actor as any).publicKey.id).toBeDefined(); - expect((actor as any).publicKey.owner).toBe( - `${config.http.base_url}/users/test` - ); - expect((actor as any).publicKey.publicKeyPem).toBeDefined(); - expect((actor as any).publicKey.publicKeyPem).toMatch( - /(-----BEGIN PUBLIC KEY-----(\n|\r|\r\n)([0-9a-zA-Z+/=]{64}(\n|\r|\r\n))*([0-9a-zA-Z+/=]{1,63}(\n|\r|\r\n))?-----END PUBLIC KEY-----)|(-----BEGIN PRIVATE KEY-----(\n|\r|\r\n)([0-9a-zA-Z+/=]{64}(\n|\r|\r\n))*([0-9a-zA-Z+/=]{1,63}(\n|\r|\r\n))?-----END PRIVATE KEY-----)/ - ); - }); -}); - -afterAll(async () => { - // Clean up user - const user = await User.findOne({ - where: { - username: "test", - }, - relations: userRelations, - }); - - const activities = await RawActivity.createQueryBuilder("activity") - .where("activity.data->>'actor' = :actor", { - actor: `${config.http.base_url}/users/test`, - }) - .leftJoinAndSelect("activity.objects", "objects") - .getMany(); - - // Delete all created objects and activities as part of testing - await Promise.all( - activities.map(async activity => { - await Promise.all( - activity.objects.map(async object => await object.remove()) - ); - await activity.remove(); - }) - ); - - if (user) { - await user.remove(); - } - - await AppDataSource.destroy(); -}); +// Empty file diff --git a/tests/api.test.ts b/tests/api.test.ts index 0cc98df1..c70164b3 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -5,7 +5,6 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { AppDataSource } from "~database/datasource"; import { Application } from "~database/entities/Application"; import { Emoji } from "~database/entities/Emoji"; -import { RawActivity } from "~database/entities/RawActivity"; import { Token, TokenType } from "~database/entities/Token"; import { User } from "~database/entities/User"; import { APIEmoji } from "~types/entities/emoji"; @@ -63,21 +62,6 @@ describe("API Tests", () => { }); afterAll(async () => { - const activities = await RawActivity.createQueryBuilder("activity") - .where("activity.data->>'actor' = :actor", { - actor: `${config.http.base_url}/users/test`, - }) - .leftJoinAndSelect("activity.objects", "objects") - .getMany(); - - // Delete all created objects and activities as part of testing - for (const activity of activities) { - for (const object of activity.objects) { - await object.remove(); - } - await activity.remove(); - } - await user.remove(); await user2.remove(); diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index 47dfa1e3..83d86029 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -4,7 +4,6 @@ import { getConfig } from "@config"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { AppDataSource } from "~database/datasource"; import { Application } from "~database/entities/Application"; -import { RawActivity } from "~database/entities/RawActivity"; import { Token, TokenType } from "~database/entities/Token"; import { User } from "~database/entities/User"; import { APIAccount } from "~types/entities/account"; @@ -63,21 +62,6 @@ describe("API Tests", () => { }); afterAll(async () => { - const activities = await RawActivity.createQueryBuilder("activity") - .where("activity.data->>'actor' = :actor", { - actor: `${config.http.base_url}/users/test`, - }) - .leftJoinAndSelect("activity.objects", "objects") - .getMany(); - - // Delete all created objects and activities as part of testing - for (const activity of activities) { - for (const object of activity.objects) { - await object.remove(); - } - await activity.remove(); - } - await user.remove(); await user2.remove(); diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index c4db675b..03f31ee8 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -4,7 +4,6 @@ import { getConfig } from "@config"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { AppDataSource } from "~database/datasource"; import { Application } from "~database/entities/Application"; -import { RawActivity } from "~database/entities/RawActivity"; import { Token, TokenType } from "~database/entities/Token"; import { User } from "~database/entities/User"; import { APIContext } from "~types/entities/context"; @@ -64,21 +63,6 @@ describe("API Tests", () => { }); afterAll(async () => { - const activities = await RawActivity.createQueryBuilder("activity") - .where("activity.data->>'actor' = :actor", { - actor: `${config.http.base_url}/users/test`, - }) - .leftJoinAndSelect("activity.objects", "objects") - .getMany(); - - // Delete all created objects and activities as part of testing - for (const activity of activities) { - for (const object of activity.objects) { - await object.remove(); - } - await activity.remove(); - } - await user.remove(); await user2.remove(); diff --git a/tests/inbox.test.ts b/tests/inbox.test.ts index d397febd..fab17ac7 100644 --- a/tests/inbox.test.ts +++ b/tests/inbox.test.ts @@ -1,320 +1 @@ -import { getConfig } from "@config"; -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { AppDataSource } from "~database/datasource"; -import { RawActivity } from "~database/entities/RawActivity"; -import { Token } from "~database/entities/Token"; -import { User, userRelations } from "~database/entities/User"; - -const config = getConfig(); - -beforeAll(async () => { - if (!AppDataSource.isInitialized) await AppDataSource.initialize(); - - // Initialize test user - await User.createNewLocal({ - email: "test@test.com", - username: "test", - password: "test", - display_name: "", - }); -}); - -describe("POST /@test/inbox", () => { - test("should store a new Note object", async () => { - const activityId = `https://example.com/objects/${crypto.randomUUID()}`; - - const response = await fetch( - `${config.http.base_url}/users/test/inbox/`, - { - method: "POST", - headers: { - "Content-Type": "application/activity+json", - Origin: "http://lysand-test.localhost", - }, - body: JSON.stringify({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Create", - id: activityId, - actor: { - id: `${config.http.base_url}/users/test`, - type: "Person", - preferredUsername: "test", - }, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-01T00:00:00.000Z", - object: { - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/1", - type: "Note", - content: "Hello, world!", - summary: null, - inReplyTo: null, - published: "2021-01-01T00:00:00.000Z", - }, - }), - } - ); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); - - const activity = await RawActivity.getLatestById(activityId); - - expect(activity).not.toBeUndefined(); - expect(activity?.data).toEqual({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Create", - id: activityId, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-01T00:00:00.000Z", - }); - - expect(activity?.objects).toHaveLength(1); - expect(activity?.objects[0].data).toEqual({ - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/1", - type: "Note", - content: "Hello, world!", - summary: null, - inReplyTo: null, - published: "2021-01-01T00:00:00.000Z", - }); - }); - - test("should try to update that Note object", async () => { - const activityId = `https://example.com/objects/${crypto.randomUUID()}`; - - const response = await fetch( - `${config.http.base_url}/users/test/inbox/`, - { - method: "POST", - headers: { - "Content-Type": "application/activity+json", - Origin: "http://lysand-test.localhost", - }, - body: JSON.stringify({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Update", - id: activityId, - actor: { - id: `${config.http.base_url}/users/test`, - type: "Person", - preferredUsername: "test", - }, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-02T00:00:00.000Z", - object: { - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/1", - type: "Note", - content: "This note has been edited!", - summary: null, - inReplyTo: null, - published: "2021-01-01T00:00:00.000Z", - }, - }), - } - ); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); - - const activity = await RawActivity.getLatestById(activityId); - - expect(activity).not.toBeUndefined(); - expect(activity?.data).toEqual({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Update", - id: activityId, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-02T00:00:00.000Z", - }); - - expect(activity?.objects).toHaveLength(1); - expect(activity?.objects[0].data).toEqual({ - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/1", - type: "Note", - content: "This note has been edited!", - summary: null, - inReplyTo: null, - published: "2021-01-01T00:00:00.000Z", - }); - }); - - test("should delete the Note object", async () => { - const activityId = `https://example.com/objects/${crypto.randomUUID()}`; - const response = await fetch( - `${config.http.base_url}/users/test/inbox/`, - { - method: "POST", - headers: { - "Content-Type": "application/activity+json", - Origin: "http://lysand-test.localhost", - }, - body: JSON.stringify({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Delete", - id: activityId, - actor: { - id: `${config.http.base_url}/users/test`, - type: "Person", - preferredUsername: "test", - }, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-03T00:00:00.000Z", - object: { - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/1", - type: "Note", - content: "This note has been edited!", - summary: null, - inReplyTo: null, - published: "2021-01-01T00:00:00.000Z", - }, - }), - } - ); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); - - const activity = await RawActivity.getLatestById(activityId); - - expect(activity).not.toBeUndefined(); - expect(activity?.data).toEqual({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Delete", - id: activityId, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-03T00:00:00.000Z", - }); - - expect(activity?.actors).toHaveLength(1); - expect(activity?.actors[0].data).toEqual({ - preferredUsername: "test", - id: `${config.http.base_url}/users/test`, - summary: "", - publicKey: { - id: `${config.http.base_url}/users/test/actor#main-key`, - owner: `${config.http.base_url}/users/test/actor`, - publicKeyPem: expect.any(String), - }, - outbox: `${config.http.base_url}/users/test/outbox`, - manuallyApprovesFollowers: false, - followers: `${config.http.base_url}/users/test/followers`, - following: `${config.http.base_url}/users/test/following`, - published: expect.any(String), - name: "", - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - icon: { - type: "Image", - url: expect.any(String), - }, - image: { - type: "Image", - url: expect.any(String), - }, - inbox: `${config.http.base_url}/users/test/inbox`, - type: "Person", - }); - - // Can be 0 or 1 length depending on whether config.activitypub.use_tombstone is true or false - if (config.activitypub.use_tombstones) { - expect(activity?.objects).toHaveLength(1); - } else { - expect(activity?.objects).toHaveLength(0); - } - }); - - test("should return a 404 error when trying to delete a non-existent Note object", async () => { - const activityId = `https://example.com/objects/${crypto.randomUUID()}`; - - const response = await fetch( - `${config.http.base_url}/users/test/inbox/`, - { - method: "POST", - headers: { - "Content-Type": "application/activity+json", - Origin: "http://lysand-test.localhost", - }, - body: JSON.stringify({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "Delete", - id: activityId, - actor: { - id: `${config.http.base_url}/users/test`, - type: "Person", - preferredUsername: "test", - }, - to: ["https://www.w3.org/ns/activitystreams#Public"], - cc: [], - published: "2021-01-03T00:00:00.000Z", - object: { - "@context": "https://www.w3.org/ns/activitystreams", - id: "https://example.com/notes/2345678909876543", - type: "Note", - }, - }), - } - ); - - expect(response.status).toBe(404); - expect(response.headers.get("content-type")).toBe("application/json"); - }); -}); - -afterAll(async () => { - // Clean up user - const user = await User.findOne({ - where: { - username: "test", - }, - relations: userRelations, - }); - - // Clean up tokens - const tokens = await Token.findBy({ - user: { - username: "test", - }, - }); - - const activities = await RawActivity.createQueryBuilder("activity") - // Join objects - .leftJoinAndSelect("activity.objects", "objects") - .leftJoinAndSelect("activity.actors", "actors") - // activity.actors is a many-to-many relationship with Actor objects (it is an array of Actor objects) - // Get the actors of the activity that have data.id as `${config.http.base_url}/users/test` - .where("actors.data @> :data", { - data: JSON.stringify({ - id: `${config.http.base_url}/users/test`, - }), - }) - .getMany(); - - // Delete all created objects and activities as part of testing - await Promise.all( - activities.map(async activity => { - await Promise.all( - activity.objects.map(async object => await object.remove()) - ); - await activity.remove(); - }) - ); - - await Promise.all(tokens.map(async token => await token.remove())); - - if (user) await user.remove(); - - await AppDataSource.destroy(); -}); +// Empty file