2023-09-13 05:06:47 +02:00
|
|
|
import {
|
|
|
|
|
BaseEntity,
|
|
|
|
|
Column,
|
|
|
|
|
Entity,
|
2023-10-08 22:20:42 +02:00
|
|
|
Index,
|
2023-09-14 05:39:11 +02:00
|
|
|
JoinTable,
|
2023-09-13 05:06:47 +02:00
|
|
|
ManyToMany,
|
|
|
|
|
PrimaryGeneratedColumn,
|
|
|
|
|
} from "typeorm";
|
2023-09-18 07:38:08 +02:00
|
|
|
import { APActivity, APActor, APObject, APTombstone } from "activitypub-types";
|
2023-09-13 05:06:47 +02:00
|
|
|
import { RawObject } from "./RawObject";
|
2023-09-18 07:38:08 +02:00
|
|
|
import { RawActor } from "./RawActor";
|
|
|
|
|
import { getConfig } from "@config";
|
2023-09-18 22:29:56 +02:00
|
|
|
import { errorResponse } from "@response";
|
2023-09-13 05:06:47 +02:00
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* Represents a raw activity entity in the database.
|
|
|
|
|
*/
|
2023-09-13 05:06:47 +02:00
|
|
|
@Entity({
|
|
|
|
|
name: "activities",
|
|
|
|
|
})
|
|
|
|
|
export class RawActivity extends BaseEntity {
|
|
|
|
|
@PrimaryGeneratedColumn("uuid")
|
|
|
|
|
id!: string;
|
|
|
|
|
|
2023-09-14 05:39:11 +02:00
|
|
|
@Column("jsonb")
|
2023-10-08 22:20:42 +02:00
|
|
|
// Index ID for faster lookups
|
|
|
|
|
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
2023-09-13 05:06:47 +02:00
|
|
|
data!: APActivity;
|
|
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
@ManyToMany(() => RawObject)
|
2023-09-14 05:39:11 +02:00
|
|
|
@JoinTable()
|
2023-09-13 05:06:47 +02:00
|
|
|
objects!: RawObject[];
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
@ManyToMany(() => RawActor)
|
2023-09-18 07:38:08 +02:00
|
|
|
@JoinTable()
|
|
|
|
|
actors!: RawActor[];
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async getByObjectId(id: string) {
|
|
|
|
|
return await RawActivity.createQueryBuilder("activity")
|
|
|
|
|
.leftJoinAndSelect("activity.objects", "objects")
|
|
|
|
|
.leftJoinAndSelect("activity.actors", "actors")
|
2023-09-22 00:42:59 +02:00
|
|
|
.where("objects.data @> :data", { data: JSON.stringify({ id }) })
|
2023-09-18 07:38:08 +02:00
|
|
|
.getMany();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async getById(id: string) {
|
|
|
|
|
return await RawActivity.createQueryBuilder("activity")
|
|
|
|
|
.leftJoinAndSelect("activity.objects", "objects")
|
|
|
|
|
.leftJoinAndSelect("activity.actors", "actors")
|
2023-09-22 00:42:59 +02:00
|
|
|
.where("activity.data->>'id' = :id", { id })
|
2023-09-18 07:38:08 +02:00
|
|
|
.getOne();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async getLatestById(id: string) {
|
|
|
|
|
return await RawActivity.createQueryBuilder("activity")
|
2023-09-22 00:42:59 +02:00
|
|
|
.where("activity.data->>'id' = :id", { id })
|
2023-09-18 07:38:08 +02:00
|
|
|
.leftJoinAndSelect("activity.objects", "objects")
|
|
|
|
|
.leftJoinAndSelect("activity.actors", "actors")
|
|
|
|
|
.orderBy("activity.data->>'published'", "DESC")
|
|
|
|
|
.getOne();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async exists(id: string) {
|
|
|
|
|
return !!(await RawActivity.getById(id));
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async updateObjectIfExists(object: APObject) {
|
|
|
|
|
const rawObject = await RawObject.getById(object.id ?? "");
|
|
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (!rawObject) {
|
|
|
|
|
return errorResponse("Object does not exist", 404);
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
rawObject.data = object;
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (await rawObject.isObjectFiltered()) {
|
|
|
|
|
return errorResponse("Object filtered", 409);
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
2023-09-22 00:42:59 +02:00
|
|
|
|
|
|
|
|
await rawObject.save();
|
|
|
|
|
return rawObject;
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async deleteObjectIfExists(object: APObject) {
|
|
|
|
|
const dbObject = await RawObject.getById(object.id ?? "");
|
|
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (!dbObject) {
|
|
|
|
|
return errorResponse("Object does not exist", 404);
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
|
|
|
|
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 ?? "");
|
|
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
for (const activity of activities) {
|
|
|
|
|
activity.objects = activity.objects.filter(
|
|
|
|
|
o => o.id !== object.id
|
|
|
|
|
);
|
|
|
|
|
await activity.save();
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
|
|
|
|
await dbObject.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dbObject;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
) {
|
2023-09-22 00:42:59 +02:00
|
|
|
if (await RawActivity.exists(activity.id ?? "")) {
|
|
|
|
|
return errorResponse("Activity already exists", 409);
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (addObject) {
|
|
|
|
|
rawActivity.objects.push(addObject);
|
|
|
|
|
} else {
|
2023-09-18 07:38:08 +02:00
|
|
|
const object = await rawActivity.addObjectIfNotExists(
|
|
|
|
|
activity.object as APObject
|
|
|
|
|
);
|
|
|
|
|
|
2023-09-18 22:29:56 +02:00
|
|
|
if (object instanceof Response) {
|
2023-09-18 07:38:08 +02:00
|
|
|
return object;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
await rawActivity.save();
|
|
|
|
|
return rawActivity;
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* Returns the ActivityPub representation of the activity.
|
|
|
|
|
* @returns The ActivityPub representation of the activity.
|
|
|
|
|
*/
|
2023-10-16 05:51:29 +02:00
|
|
|
getActivityPubRepresentation() {
|
2023-09-22 00:42:59 +02:00
|
|
|
return {
|
|
|
|
|
...this.data,
|
|
|
|
|
object: this.objects[0].data,
|
|
|
|
|
actor: this.actors[0].data,
|
|
|
|
|
};
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-22 00:42:59 +02:00
|
|
|
async addObjectIfNotExists(object: APObject) {
|
|
|
|
|
if (this.objects.some(o => o.data.id === object.id)) {
|
|
|
|
|
return errorResponse("Object already exists", 409);
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
const rawObject = new RawObject();
|
|
|
|
|
rawObject.data = object;
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (await rawObject.isObjectFiltered()) {
|
|
|
|
|
return errorResponse("Object filtered", 409);
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
2023-09-22 00:42:59 +02:00
|
|
|
|
|
|
|
|
await rawObject.save();
|
|
|
|
|
this.objects.push(rawObject);
|
|
|
|
|
return rawObject;
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:33:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
async addActorIfNotExists(actor: APActor) {
|
2023-09-22 00:42:59 +02:00
|
|
|
const dbActor = await RawActor.getByActorId(actor.id ?? "");
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (dbActor) {
|
|
|
|
|
this.actors.push(dbActor);
|
|
|
|
|
return dbActor;
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (this.actors.some(a => a.data.id === actor.id)) {
|
|
|
|
|
return errorResponse("Actor already exists", 409);
|
|
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
const rawActor = new RawActor();
|
|
|
|
|
rawActor.data = actor;
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
const config = getConfig();
|
2023-09-18 07:38:08 +02:00
|
|
|
|
2023-09-22 00:42:59 +02:00
|
|
|
if (
|
|
|
|
|
config.activitypub.discard_avatars.find(
|
|
|
|
|
instance => actor.id?.includes(instance)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
rawActor.data.icon = undefined;
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
2023-09-22 00:42:59 +02:00
|
|
|
|
|
|
|
|
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;
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
2023-09-13 05:06:47 +02:00
|
|
|
}
|