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 { RawObject } from "./RawObject";
|
||||||
import { RawActor } from "./RawActor";
|
import { RawActor } from "./RawActor";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
|
import { errorResponse } from "@response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an ActivityPub activity as raw JSON-LD data
|
* 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
|
// Check if object body contains any filtered terms
|
||||||
if (await rawObject.isObjectFiltered())
|
if (await rawObject.isObjectFiltered())
|
||||||
return new Error("Object filtered");
|
return errorResponse("Object filtered", 409);
|
||||||
|
|
||||||
await rawObject.save();
|
await rawObject.save();
|
||||||
return rawObject;
|
return rawObject;
|
||||||
} else {
|
} else {
|
||||||
return new Error("Object does not exist");
|
return errorResponse("Object does not exist", 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deleteObjectIfExists(object: APObject) {
|
static async deleteObjectIfExists(object: APObject) {
|
||||||
const dbObject = await RawObject.getById(object.id ?? "");
|
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();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -139,7 +140,7 @@ export class RawActivity extends BaseEntity {
|
||||||
activity.actor as APActor
|
activity.actor as APActor
|
||||||
);
|
);
|
||||||
|
|
||||||
if (actor instanceof Error) {
|
if (actor instanceof Response) {
|
||||||
return actor;
|
return actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,14 +148,14 @@ export class RawActivity extends BaseEntity {
|
||||||
activity.object as APObject
|
activity.object as APObject
|
||||||
);
|
);
|
||||||
|
|
||||||
if (object instanceof Error) {
|
if (object instanceof Response) {
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
await rawActivity.save();
|
await rawActivity.save();
|
||||||
return rawActivity;
|
return rawActivity;
|
||||||
} else {
|
} 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
|
// Check if object body contains any filtered terms
|
||||||
if (await rawObject.isObjectFiltered())
|
if (await rawObject.isObjectFiltered())
|
||||||
return new Error("Object filtered");
|
return errorResponse("Object filtered", 409);
|
||||||
|
|
||||||
await rawObject.save();
|
await rawObject.save();
|
||||||
|
|
||||||
|
|
@ -173,7 +174,7 @@ export class RawActivity extends BaseEntity {
|
||||||
|
|
||||||
return rawObject;
|
return rawObject;
|
||||||
} else {
|
} 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()) {
|
if (await rawActor.isObjectFiltered()) {
|
||||||
return new Error("Actor filtered");
|
return errorResponse("Actor filtered", 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
await rawActor.save();
|
await rawActor.save();
|
||||||
|
|
@ -210,7 +211,7 @@ export class RawActivity extends BaseEntity {
|
||||||
|
|
||||||
return rawActor;
|
return rawActor;
|
||||||
} else {
|
} 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 { APActor } from "activitypub-types";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
import { appendFile } from "fs/promises";
|
||||||
|
import { errorResponse } from "@response";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an ActivityPub actor as raw JSON-LD data
|
* Stores an ActivityPub actor as raw JSON-LD data
|
||||||
|
|
@ -24,6 +25,40 @@ export class RawActor extends BaseEntity {
|
||||||
.getOne();
|
.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() {
|
async isObjectFiltered() {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,15 @@ import {
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
|
import { RawActor } from "./RawActor";
|
||||||
|
import { APActor } from "activitypub-types";
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -42,7 +47,18 @@ export class User extends BaseEntity {
|
||||||
@Column("varchar", {
|
@Column("varchar", {
|
||||||
default: "",
|
default: "",
|
||||||
})
|
})
|
||||||
bio!: string;
|
note!: string;
|
||||||
|
|
||||||
|
@Column("boolean", {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
is_admin!: boolean;
|
||||||
|
|
||||||
|
@Column("varchar")
|
||||||
|
avatar!: string;
|
||||||
|
|
||||||
|
@Column("varchar")
|
||||||
|
header!: string;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
created_at!: Date;
|
created_at!: Date;
|
||||||
|
|
@ -56,6 +72,95 @@ export class User extends BaseEntity {
|
||||||
@Column("varchar")
|
@Column("varchar")
|
||||||
private_key!: string;
|
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> {
|
async generateKeys(): Promise<void> {
|
||||||
// openssl genrsa -out private.pem 2048
|
// openssl genrsa -out private.pem 2048
|
||||||
// openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
// openssl rsa -in private.pem -outform PEM -pubout -out public.pem
|
||||||
|
|
@ -110,7 +215,7 @@ export class User extends BaseEntity {
|
||||||
locked: false,
|
locked: false,
|
||||||
moved: null,
|
moved: null,
|
||||||
noindex: false,
|
noindex: false,
|
||||||
note: this.bio,
|
note: this.note,
|
||||||
suspended: false,
|
suspended: false,
|
||||||
url: `${config.http.base_url}/@${this.username}`,
|
url: `${config.http.base_url}/@${this.username}`,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export default async (
|
||||||
type: "Person",
|
type: "Person",
|
||||||
preferredUsername: user.username, // TODO: Add user display name
|
preferredUsername: user.username, // TODO: Add user display name
|
||||||
name: user.username,
|
name: user.username,
|
||||||
summary: user.bio,
|
summary: user.note,
|
||||||
icon: /*{
|
icon: /*{
|
||||||
type: "Image",
|
type: "Image",
|
||||||
url: user.avatar,
|
url: user.avatar,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { errorResponse, jsonResponse } from "@response";
|
||||||
import {
|
import {
|
||||||
APAccept,
|
APAccept,
|
||||||
APActivity,
|
APActivity,
|
||||||
|
APActor,
|
||||||
APCreate,
|
APCreate,
|
||||||
APDelete,
|
APDelete,
|
||||||
APFollow,
|
APFollow,
|
||||||
|
|
@ -14,6 +15,8 @@ import {
|
||||||
} from "activitypub-types";
|
} from "activitypub-types";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
import { RawActivity } from "~database/entities/RawActivity";
|
import { RawActivity } from "~database/entities/RawActivity";
|
||||||
|
import { RawActor } from "~database/entities/RawActor";
|
||||||
|
import { User } from "~database/entities/User";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub user inbox endpoint
|
* ActivityPub user inbox endpoint
|
||||||
|
|
@ -44,8 +47,8 @@ export default async (
|
||||||
// Check is Activity already exists
|
// Check is Activity already exists
|
||||||
const activity = await RawActivity.addIfNotExists(body);
|
const activity = await RawActivity.addIfNotExists(body);
|
||||||
|
|
||||||
if (activity instanceof Error) {
|
if (activity instanceof Response) {
|
||||||
return errorResponse(activity.message, 409);
|
return activity;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -58,14 +61,14 @@ export default async (
|
||||||
body.object as APObject
|
body.object as APObject
|
||||||
);
|
);
|
||||||
|
|
||||||
if (object instanceof Error) {
|
if (object instanceof Response) {
|
||||||
return errorResponse(object.message, 409);
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activity = await RawActivity.addIfNotExists(body);
|
const activity = await RawActivity.addIfNotExists(body);
|
||||||
|
|
||||||
if (activity instanceof Error) {
|
if (activity instanceof Response) {
|
||||||
return errorResponse(activity.message, 409);
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -78,19 +81,67 @@ export default async (
|
||||||
await RawActivity.deleteObjectIfExists(body.object as APObject);
|
await RawActivity.deleteObjectIfExists(body.object as APObject);
|
||||||
|
|
||||||
// Store the Delete event in the database
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
case "Accept" as APAccept: {
|
case "Accept" as APAccept: {
|
||||||
// Body is an APAccept object
|
// Body is an APAccept object
|
||||||
// Add the actor to the object actor's followers list
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
case "Reject" as APReject: {
|
case "Reject" as APReject: {
|
||||||
// Body is an APReject object
|
// Body is an APReject object
|
||||||
// Mark the follow request as not pending
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.bio = note;
|
user.note = note;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ beforeAll(async () => {
|
||||||
user.username = "test";
|
user.username = "test";
|
||||||
user.password = await Bun.password.hash("test");
|
user.password = await Bun.password.hash("test");
|
||||||
user.display_name = "";
|
user.display_name = "";
|
||||||
user.bio = "";
|
user.note = "";
|
||||||
|
|
||||||
await user.generateKeys();
|
await user.generateKeys();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ beforeAll(async () => {
|
||||||
user.username = "test";
|
user.username = "test";
|
||||||
user.password = await Bun.password.hash("test");
|
user.password = await Bun.password.hash("test");
|
||||||
user.display_name = "";
|
user.display_name = "";
|
||||||
user.bio = "";
|
user.note = "";
|
||||||
|
|
||||||
await user.generateKeys();
|
await user.generateKeys();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ export interface ConfigType {
|
||||||
url_scheme_whitelist: string[];
|
url_scheme_whitelist: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
visibility: string;
|
||||||
|
language: string;
|
||||||
|
avatar: string;
|
||||||
|
header: string;
|
||||||
|
};
|
||||||
|
|
||||||
activitypub: {
|
activitypub: {
|
||||||
use_tombstones: boolean;
|
use_tombstones: boolean;
|
||||||
reject_activities: string[];
|
reject_activities: string[];
|
||||||
|
|
@ -134,6 +141,12 @@ export const configDefaults: ConfigType = {
|
||||||
"ssb",
|
"ssb",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
defaults: {
|
||||||
|
visibility: "public",
|
||||||
|
language: "en",
|
||||||
|
avatar: "",
|
||||||
|
header: "",
|
||||||
|
},
|
||||||
activitypub: {
|
activitypub: {
|
||||||
use_tombstones: true,
|
use_tombstones: true,
|
||||||
reject_activities: [],
|
reject_activities: [],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue