Add more utilities, implement Accept and Reject objects

This commit is contained in:
Jesse Wierzbinski 2023-09-18 10:29:56 -10:00
parent 4d0283caf0
commit 1a1bee83a7
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
9 changed files with 230 additions and 25 deletions

View file

@ -10,6 +10,7 @@ import { APActivity, APActor, APObject, APTombstone } from "activitypub-types";
import { RawObject } from "./RawObject";
import { RawActor } from "./RawActor";
import { getConfig } from "@config";
import { errorResponse } from "@response";
/**
* Stores an ActivityPub activity as raw JSON-LD data
@ -81,19 +82,19 @@ export class RawActivity extends BaseEntity {
// Check if object body contains any filtered terms
if (await rawObject.isObjectFiltered())
return new Error("Object filtered");
return errorResponse("Object filtered", 409);
await rawObject.save();
return rawObject;
} else {
return new Error("Object does not exist");
return errorResponse("Object does not exist", 404);
}
}
static async deleteObjectIfExists(object: APObject) {
const dbObject = await RawObject.getById(object.id ?? "");
if (!dbObject) return new Error("Object not found");
if (!dbObject) return errorResponse("Object does not exist", 404);
const config = getConfig();
@ -139,7 +140,7 @@ export class RawActivity extends BaseEntity {
activity.actor as APActor
);
if (actor instanceof Error) {
if (actor instanceof Response) {
return actor;
}
@ -147,14 +148,14 @@ export class RawActivity extends BaseEntity {
activity.object as APObject
);
if (object instanceof Error) {
if (object instanceof Response) {
return object;
}
await rawActivity.save();
return rawActivity;
} else {
return new Error("Activity already exists");
return errorResponse("Activity already exists", 409);
}
}
@ -165,7 +166,7 @@ export class RawActivity extends BaseEntity {
// Check if object body contains any filtered terms
if (await rawObject.isObjectFiltered())
return new Error("Object filtered");
return errorResponse("Object filtered", 409);
await rawObject.save();
@ -173,7 +174,7 @@ export class RawActivity extends BaseEntity {
return rawObject;
} else {
return new Error("Object already exists");
return errorResponse("Object already exists", 409);
}
}
@ -201,7 +202,7 @@ export class RawActivity extends BaseEntity {
}
if (await rawActor.isObjectFiltered()) {
return new Error("Actor filtered");
return errorResponse("Actor filtered", 409);
}
await rawActor.save();
@ -210,7 +211,7 @@ export class RawActivity extends BaseEntity {
return rawActor;
} else {
return new Error("Actor already exists");
return errorResponse("Actor already exists", 409);
}
}
}

View file

@ -2,6 +2,7 @@ import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { APActor } from "activitypub-types";
import { getConfig } from "@config";
import { appendFile } from "fs/promises";
import { errorResponse } from "@response";
/**
* Stores an ActivityPub actor as raw JSON-LD data
@ -24,6 +25,40 @@ export class RawActor extends BaseEntity {
.getOne();
}
static async addIfNotExists(data: APActor) {
if (!(await RawActor.exists(data.id ?? ""))) {
const actor = new RawActor();
actor.data = data;
const config = getConfig();
if (
config.activitypub.discard_avatars.find(instance =>
actor.id.includes(instance)
)
) {
actor.data.icon = undefined;
}
if (
config.activitypub.discard_banners.find(instance =>
actor.id.includes(instance)
)
) {
actor.data.image = undefined;
}
if (await actor.isObjectFiltered()) {
return errorResponse("Actor filtered", 409);
}
await actor.save();
return actor;
}
return errorResponse("Actor already exists", 409);
}
async isObjectFiltered() {
const config = getConfig();

View file

@ -4,10 +4,15 @@ import {
Column,
CreateDateColumn,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
import { APIAccount } from "~types/entities/account";
import { RawActor } from "./RawActor";
import { APActor } from "activitypub-types";
const config = getConfig();
@ -42,7 +47,18 @@ export class User extends BaseEntity {
@Column("varchar", {
default: "",
})
bio!: string;
note!: string;
@Column("boolean", {
default: false,
})
is_admin!: boolean;
@Column("varchar")
avatar!: string;
@Column("varchar")
header!: string;
@CreateDateColumn()
created_at!: Date;
@ -56,6 +72,95 @@ export class User extends BaseEntity {
@Column("varchar")
private_key!: string;
@ManyToOne(() => RawActor, actor => actor.id)
actor!: RawActor;
@ManyToMany(() => RawActor, actor => actor.id)
@JoinTable()
following!: RawActor[];
static async getByActorId(id: string) {
return await User.createQueryBuilder("user")
// Objects is a many-to-many relationship
.leftJoinAndSelect("user.actor", "actor")
.leftJoinAndSelect("user.following", "following")
.where("actor.data @> :data", {
data: JSON.stringify({
id,
}),
})
.getOne();
}
static async createNew(data: {
username: string;
display_name?: string;
password: string;
email: string;
bio?: string;
avatar?: string;
header?: string;
}) {
const config = getConfig();
const user = new User();
user.username = data.username;
user.display_name = data.display_name ?? data.username;
user.password = await Bun.password.hash(data.password);
user.email = data.email;
user.note = data.bio ?? "";
user.avatar = data.avatar ?? config.defaults.avatar;
user.header = data.header ?? config.defaults.avatar;
await user.generateKeys();
await user.updateActor();
await user.save();
return user;
}
async updateActor() {
// Check if actor exists
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const actor = this.actor ? this.actor : new RawActor();
actor.data = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
],
id: `${config.http.base_url}/@${this.username}`,
type: "Person",
preferredUsername: this.username,
name: this.display_name,
inbox: `${config.http.base_url}/@${this.username}/inbox`,
outbox: `${config.http.base_url}/@${this.username}/outbox`,
followers: `${config.http.base_url}/@${this.username}/followers`,
following: `${config.http.base_url}/@${this.username}/following`,
manuallyApprovesFollowers: false,
summary: this.note,
icon: {
type: "Image",
url: this.avatar,
},
image: {
type: "Image",
url: this.header,
},
publicKey: {
id: `${config.http.base_url}/@${this.username}/actor#main-key`,
owner: `${config.http.base_url}/@${this.username}/actor`,
publicKeyPem: this.public_key,
},
} as APActor;
await actor.save();
this.actor = actor;
await this.save();
return actor;
}
async generateKeys(): Promise<void> {
// openssl genrsa -out private.pem 2048
// openssl rsa -in private.pem -outform PEM -pubout -out public.pem
@ -110,7 +215,7 @@ export class User extends BaseEntity {
locked: false,
moved: null,
noindex: false,
note: this.bio,
note: this.note,
suspended: false,
url: `${config.http.base_url}/@${this.username}`,
username: this.username,

View file

@ -86,7 +86,7 @@ export default async (
type: "Person",
preferredUsername: user.username, // TODO: Add user display name
name: user.username,
summary: user.bio,
summary: user.note,
icon: /*{
type: "Image",
url: user.avatar,

View file

@ -4,6 +4,7 @@ import { errorResponse, jsonResponse } from "@response";
import {
APAccept,
APActivity,
APActor,
APCreate,
APDelete,
APFollow,
@ -14,6 +15,8 @@ import {
} from "activitypub-types";
import { MatchedRoute } from "bun";
import { RawActivity } from "~database/entities/RawActivity";
import { RawActor } from "~database/entities/RawActor";
import { User } from "~database/entities/User";
/**
* ActivityPub user inbox endpoint
@ -44,8 +47,8 @@ export default async (
// Check is Activity already exists
const activity = await RawActivity.addIfNotExists(body);
if (activity instanceof Error) {
return errorResponse(activity.message, 409);
if (activity instanceof Response) {
return activity;
}
break;
}
@ -58,14 +61,14 @@ export default async (
body.object as APObject
);
if (object instanceof Error) {
return errorResponse(object.message, 409);
if (object instanceof Response) {
return object;
}
const activity = await RawActivity.addIfNotExists(body);
if (activity instanceof Error) {
return errorResponse(activity.message, 409);
if (activity instanceof Response) {
return activity;
}
break;
@ -78,19 +81,67 @@ export default async (
await RawActivity.deleteObjectIfExists(body.object as APObject);
// Store the Delete event in the database
const activity = RawActivity.addIfNotExists(body);
const activity = await RawActivity.addIfNotExists(body);
if (activity instanceof Response) {
return activity;
}
break;
}
case "Accept" as APAccept: {
// Body is an APAccept object
// Add the actor to the object actor's followers list
// TODO: Add actor to object actor's followers list
if ((body.object as APFollow).type === "Follow") {
const user = await User.getByActorId(
((body.object as APFollow).actor as APActor).id ?? ""
);
if (!user) {
return errorResponse("User not found", 404);
}
const actor = await RawActor.addIfNotExists(
body.actor as APActor
);
if (actor instanceof Response) {
return actor;
}
user.following.push(actor);
await user.save();
}
break;
}
case "Reject" as APReject: {
// Body is an APReject object
// Mark the follow request as not pending
// TODO: Implement
if ((body.object as APFollow).type === "Follow") {
const user = await User.getByActorId(
((body.object as APFollow).actor as APActor).id ?? ""
);
if (!user) {
return errorResponse("User not found", 404);
}
const actor = await RawActor.addIfNotExists(
body.actor as APActor
);
if (actor instanceof Response) {
return actor;
}
user.following = user.following.filter(
following => following.id !== actor.id
);
await user.save();
}
break;
}
}

View file

@ -63,7 +63,7 @@ export default async (req: Request): Promise<Response> => {
);
}
user.bio = note;
user.note = note;
}
if (avatar) {

View file

@ -17,7 +17,7 @@ beforeAll(async () => {
user.username = "test";
user.password = await Bun.password.hash("test");
user.display_name = "";
user.bio = "";
user.note = "";
await user.generateKeys();

View file

@ -21,7 +21,7 @@ beforeAll(async () => {
user.username = "test";
user.password = await Bun.password.hash("test");
user.display_name = "";
user.bio = "";
user.note = "";
await user.generateKeys();

View file

@ -31,6 +31,13 @@ export interface ConfigType {
url_scheme_whitelist: string[];
};
defaults: {
visibility: string;
language: string;
avatar: string;
header: string;
};
activitypub: {
use_tombstones: boolean;
reject_activities: string[];
@ -134,6 +141,12 @@ export const configDefaults: ConfigType = {
"ssb",
],
},
defaults: {
visibility: "public",
language: "en",
avatar: "",
header: "",
},
activitypub: {
use_tombstones: true,
reject_activities: [],