mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Add more utilities, implement Accept and Reject objects
This commit is contained in:
parent
4d0283caf0
commit
1a1bee83a7
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
);
|
||||
}
|
||||
|
||||
user.bio = note;
|
||||
user.note = note;
|
||||
}
|
||||
|
||||
if (avatar) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue