From 6d77c8edc7c372be26a51f781c59ddc6871ff762 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 12 Sep 2023 12:50:51 -1000 Subject: [PATCH] Add user outbox --- .../{actor => actor.json}/index.ts | 11 +- server/api/@[username]/inbox/index.ts | 26 ---- server/api/@[username]/outbox.json/index.ts | 132 ++++++++++++++++++ 3 files changed, 141 insertions(+), 28 deletions(-) rename server/api/@[username]/{actor => actor.json}/index.ts (69%) delete mode 100644 server/api/@[username]/inbox/index.ts create mode 100644 server/api/@[username]/outbox.json/index.ts diff --git a/server/api/@[username]/actor/index.ts b/server/api/@[username]/actor.json/index.ts similarity index 69% rename from server/api/@[username]/actor/index.ts rename to server/api/@[username]/actor.json/index.ts index 35a73426..6ead31d2 100644 --- a/server/api/@[username]/actor/index.ts +++ b/server/api/@[username]/actor.json/index.ts @@ -11,7 +11,6 @@ export default async ( req: Request, matchedRoute: MatchedRoute ): Promise => { - // In the format acct:name@example.com const username = matchedRoute.params.username; const user = await User.findOneBy({ username }); @@ -28,9 +27,17 @@ export default async ( ], id: `${getHost()}/@${user.username}/actor`, type: "Person", - preferredUsername: user.username, + preferredUsername: user.username, // TODO: Add user display name + name: user.username, summary: user.bio, + icon: [ + // TODO: Add user avatar + ], inbox: `${getHost()}/@${user.username}/inbox`, + outbox: `${getHost()}/@${user.username}/outbox`, + followers: `${getHost()}/@${user.username}/followers`, + following: `${getHost()}/@${user.username}/following`, + liked: `${getHost()}/@${user.username}/liked`, }) ); }; diff --git a/server/api/@[username]/inbox/index.ts b/server/api/@[username]/inbox/index.ts deleted file mode 100644 index 070d97f0..00000000 --- a/server/api/@[username]/inbox/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { errorResponse, jsonResponse } from "@response"; -import { MatchedRoute } from "bun"; -import { User } from "~database/entities/User"; -import { getHost } from "@config"; -import { compact } from "jsonld"; - -/** - * ActivityPub user actor endpoinmt - */ -export default async ( - req: Request, - matchedRoute: MatchedRoute -): Promise => { - // In the format acct:name@example.com - const username = matchedRoute.params.username; - - const user = await User.findOneBy({ username }); - - if (!user) { - return errorResponse("User not found", 404); - } - - return jsonResponse({ - - }) -}; diff --git a/server/api/@[username]/outbox.json/index.ts b/server/api/@[username]/outbox.json/index.ts new file mode 100644 index 00000000..fa4ce3f0 --- /dev/null +++ b/server/api/@[username]/outbox.json/index.ts @@ -0,0 +1,132 @@ +import { errorResponse, jsonResponse } from "@response"; +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"; + +/** + * 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); + } + + const username = matchedRoute.params.username; + const page = Boolean(matchedRoute.query.page || "false"); + const min_id = matchedRoute.query.min_id || false; + const max_id = matchedRoute.query.max_id || false; + + const user = await User.findOneBy({ username }); + + if (!user) { + return errorResponse("User not found", 404); + } + + // Get the user's corresponding ActivityPub notes + const count = await RawObject.count({ + where: { + data: { + attributedTo: `${getHost()}/@${user.username}`, + }, + }, + order: { + data: { + published: "DESC", + }, + }, + }); + + const lastPost = (await RawObject.find({ + where: { + data: { + attributedTo: `${getHost()}/@${user.username}`, + }, + }, + order: { + data: { + published: "ASC", + }, + }, + take: 1, + }))[0]; + + if (!page) + return jsonResponse( + await compact({ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + id: `${getHost()}/@${user.username}/inbox`, + type: "OrderedCollection", + totalItems: count, + first: `${getHost()}/@${user.username}/outbox?page=true`, + last: `${getHost()}/@${ + user.username + }/outbox?min_id=${lastPost.id}&page=true`, + }) + ); + else { + let posts: RawObject[] = [] + + if (min_id) { + posts = await RawObject.find({ + where: { + data: { + attributedTo: `${getHost()}/@${user.username}`, + id: min_id, + }, + }, + order: { + data: { + published: "DESC", + }, + }, + take: 11, // Take one extra to have the ID of the next post + }); + } else if (max_id) { + posts = await RawObject.find({ + where: { + data: { + attributedTo: `${getHost()}/@${user.username}`, + id: max_id, + }, + }, + order: { + data: { + published: "ASC", + }, + }, + take: 10, + }); + } + + + + return jsonResponse(await compact({ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + id: `${getHost()}/@${user.username}/inbox`, + type: "OrderedCollectionPage", + totalItems: count, + partOf: `${getHost()}/@${user.username}/inbox`, + // Next is less recent posts chronologically, uses min_id + next: `${getHost()}/@${user.username}/outbox?min_id=${ + posts[posts.length - 1].id + }&page=true`, + // Prev is more recent posts chronologically, uses max_id + prev: `${getHost()}/@${user.username}/outbox?max_id=${ + posts[0].id + }&page=true`, + orderedItems: posts.slice(0, 10).map(post => post.data) as NodeObject[] + })); + } +};