From 1027eada7c41571b51186f674092213de1b9c7ff Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 12 Sep 2023 17:06:47 -1000 Subject: [PATCH] Add new inbox endpoint --- database/entities/RawActivity.ts | 27 ++++++++ server/api/@[username]/inbox/index.ts | 69 +++++++++++++++++++++ server/api/@[username]/outbox.json/index.ts | 19 +++--- server/api/object/[uuid]/index.ts | 4 +- utils/response.ts | 7 ++- 5 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 database/entities/RawActivity.ts create mode 100644 server/api/@[username]/inbox/index.ts diff --git a/database/entities/RawActivity.ts b/database/entities/RawActivity.ts new file mode 100644 index 00000000..125116f1 --- /dev/null +++ b/database/entities/RawActivity.ts @@ -0,0 +1,27 @@ +import { + BaseEntity, + Column, + Entity, + ManyToMany, + PrimaryGeneratedColumn, +} from "typeorm"; +import { APActivity } from "activitypub-types"; +import { RawObject } from "./RawObject"; + +/** + * Stores an ActivityPub activity as raw JSON-LD data + */ +@Entity({ + name: "activities", +}) +export class RawActivity extends BaseEntity { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column("json") + data!: APActivity; + + // Any associated objects (there is typically only one) + @ManyToMany(() => RawObject, object => object.id) + objects!: RawObject[]; +} diff --git a/server/api/@[username]/inbox/index.ts b/server/api/@[username]/inbox/index.ts new file mode 100644 index 00000000..dcc4e9e0 --- /dev/null +++ b/server/api/@[username]/inbox/index.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { errorResponse, jsonResponse } from "@response"; +import { APActivity, APCreate, APObject } from "activitypub-types"; +import { MatchedRoute } from "bun"; +import { RawActivity } from "~database/entities/RawActivity"; +import { RawObject } from "~database/entities/RawObject"; + +/** + * ActivityPub user inbox endpoint + */ +export default async ( + req: Request, + matchedRoute: MatchedRoute +): Promise => { + // Check if POST request + if (req.method !== "POST") { + return errorResponse("Method not allowed", 405); + } + + // Process request body + const body: APActivity = await req.json(); + + // Get the object's ActivityPub type + const type = body.type; + + switch (type) { + case "Create" as APCreate: { + // Body is an APCreate object + // Store the Create object in database + + // Check is Activity already exists + const exists = await RawActivity.findOneBy({ + data: { + id: body.id, + }, + }); + + if (exists) return errorResponse("Activity already exists", 409); + + // Check if object already exists + const objectExists = await RawObject.findOneBy({ + data: { + id: (body.object as APObject).id, + }, + }); + + if (objectExists) + return errorResponse("Object already exists", 409); + + const activity = new RawActivity(); + const object = new RawObject(); + + activity.data = { + ...body, + object: undefined, + }; + object.data = body.object as APObject; + + activity.objects = [object]; + + // Save the new object and activity + await object.save(); + await activity.save(); + break; + } + } + + return jsonResponse({}); +}; diff --git a/server/api/@[username]/outbox.json/index.ts b/server/api/@[username]/outbox.json/index.ts index fde4224c..d5030f73 100644 --- a/server/api/@[username]/outbox.json/index.ts +++ b/server/api/@[username]/outbox.json/index.ts @@ -3,20 +3,15 @@ import { MatchedRoute } from "bun"; import { User } from "~database/entities/User"; import { getHost } from "@config"; import { NodeObject, compact } from "jsonld"; -import { RawObject } from "~database/entities/RawObject"; +import { RawActivity } from "~database/entities/RawActivity"; /** - * ActivityPub user inbox endpoint + * ActivityPub user outbox endpoint */ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - // Check if POST request - if (req.method !== "POST") { - return errorResponse("Method not allowed", 405); - } - const username = matchedRoute.params.username; const page = Boolean(matchedRoute.query.page || "false"); const min_id = matchedRoute.query.min_id || false; @@ -29,7 +24,7 @@ export default async ( } // Get the user's corresponding ActivityPub notes - const count = await RawObject.count({ + const count = await RawActivity.count({ where: { data: { attributedTo: `${getHost()}/@${user.username}`, @@ -43,7 +38,7 @@ export default async ( }); const lastPost = ( - await RawObject.find({ + await RawActivity.find({ where: { data: { attributedTo: `${getHost()}/@${user.username}`, @@ -75,10 +70,10 @@ export default async ( }) ); else { - let posts: RawObject[] = []; + let posts: RawActivity[] = []; if (min_id) { - posts = await RawObject.find({ + posts = await RawActivity.find({ where: { data: { attributedTo: `${getHost()}/@${user.username}`, @@ -93,7 +88,7 @@ export default async ( take: 11, // Take one extra to have the ID of the next post }); } else if (max_id) { - posts = await RawObject.find({ + posts = await RawActivity.find({ where: { data: { attributedTo: `${getHost()}/@${user.username}`, diff --git a/server/api/object/[uuid]/index.ts b/server/api/object/[uuid]/index.ts index bcf6e206..4b15c5de 100644 --- a/server/api/object/[uuid]/index.ts +++ b/server/api/object/[uuid]/index.ts @@ -1,6 +1,6 @@ import { errorResponse, jsonLdResponse } from "@response"; import { MatchedRoute } from "bun"; -import { RawObject } from "~database/entities/RawObject"; +import { RawActivity } from "~database/entities/RawActivity"; /** * Fetch a user @@ -9,7 +9,7 @@ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - const object = await RawObject.findOneBy({ + const object = await RawActivity.findOneBy({ id: matchedRoute.params.id, }); diff --git a/utils/response.ts b/utils/response.ts index 17e29fcf..1ebd09e0 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -1,4 +1,4 @@ -import { APObject } from "activitypub-types"; +import { APActivity, APObject } from "activitypub-types"; import { NodeObject } from "jsonld"; export const jsonResponse = (data: object, status = 200) => { @@ -10,7 +10,10 @@ export const jsonResponse = (data: object, status = 200) => { }); }; -export const jsonLdResponse = (data: NodeObject | APObject, status = 200) => { +export const jsonLdResponse = ( + data: NodeObject | APActivity | APObject, + status = 200 +) => { return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/activity+json",