From 0c1d093c4040c34ae758bf2eb83f351d2125c535 Mon Sep 17 00:00:00 2001 From: Moon Man Date: Wed, 27 Dec 2023 09:18:26 -0500 Subject: [PATCH] tons of additions. --- migrations/20231225130928_init.js | 3 + module.d.ts | 5 + src/activity.ts | 94 +++++++-- src/activitypub_types.ts | 308 ++++++++++++++++++++++++++++++ src/collection.ts | 54 ++++-- src/follower.ts | 62 ++++++ src/index.ts | 65 +++++-- src/net.ts | 92 ++++++++- src/outbox.ts | 3 +- src/router.ts | 12 +- src/user.ts | 10 +- 11 files changed, 654 insertions(+), 54 deletions(-) create mode 100644 src/activitypub_types.ts create mode 100644 src/follower.ts diff --git a/migrations/20231225130928_init.js b/migrations/20231225130928_init.js index 08bb4e1..4082402 100644 --- a/migrations/20231225130928_init.js +++ b/migrations/20231225130928_init.js @@ -41,6 +41,9 @@ export const up = async function (knex) { table.index(["collection_types_id", "articles_id"]); table.index(["collection_types_id", "users_id"]); + + table.unique(["collection_types_id", "articles_id", "value"]); + table.unique(["collection_types_id", "users_id", "value"]); }) .createTable("outboxes", (table) => { table.increments("id"); diff --git a/module.d.ts b/module.d.ts index a360a9b..adb9a07 100644 --- a/module.d.ts +++ b/module.d.ts @@ -3,10 +3,15 @@ declare module "activitypub-express" { export default ActivitypubExpress; } +// Only enough here for what I need. declare module "activitypub-http-signatures" { export class Sha256Signer { constructor(options: { publicKeyId: string, privateKey: string }); sign: (options: { url: string, method: string, headers: any[] }) => string; }; + + export const parse: (params: { url: string, method: string, headers: Record }) => { + verify: (publicKey: string) => boolean; + }; } diff --git a/src/activity.ts b/src/activity.ts index 17b328a..64479f7 100644 --- a/src/activity.ts +++ b/src/activity.ts @@ -3,16 +3,21 @@ import { Article } from "./article.js"; import { User, getByActor } from "./user.js"; import { fillRoute } from "./router.js"; import { streamToString, hashDigest } from "./util.js"; -import { signedFetch } from "./net.js"; +import { signedFetch, SignedInit, getActor} from "./net.js"; +import { getById as getUserById, getKeyId } from "./user.js"; +import { parse as parseSignedRequest } from "activitypub-http-signatures"; +import type { Request, Response } from "express"; +import { addFollower } from "./follower.js"; export const CONTEXT = "https://www.w3.org/ns/activitystreams"; export const PUBLIC = CONTEXT + "#Public"; export const TYPE = "application/activity+json"; -export const handleInboxPost = async (req: Request) => { +export const handleInboxPost = async (req: Request, res: Response) => { try { if (!req.body) { console.warn("no body"); + res.status(403).end(); return; } @@ -23,6 +28,7 @@ export const handleInboxPost = async (req: Request) => { } catch (e) { console.warn("body json parse failed"); + res.status(403).end(); return; } @@ -30,10 +36,53 @@ export const handleInboxPost = async (req: Request) => { const actor = await getByActor(activity.object); if (actor) { - const follower = activity.actor; + const followerUrl: string = activity.actor; + + const signer = await getUserById(1); + + if (!signer) { + res.status(500).end(); + return; + } + + const init: SignedInit = { + keyId: getKeyId(signer.nickname), + privateKey: signer.private_key + }; + + const follower = await getActor(followerUrl, init); + + if (!follower || !follower.publicKey?.publicKeyPem) { + console.warn("No public key for follow requester:", followerUrl); + res.status(403).send("no public key found for follow requester"); + return; + } + + // OK validate request signature + const signature = parseSignedRequest({ + url: req.originalUrl, + method: "POST", + headers: req.headers as Record + }); + + const validSignature = signature.verify(follower.publicKey.publicKeyPem); + + if (!validSignature) { + console.warn("Signature validation failed."); + res.status(403).send("signature validation failed"); + return; + } + + await addFollower(actor.id, follower.id, follower.preferredUsername, follower.name, follower.inbox, follower.endpoints?.sharedInbox); + + console.log("Done handling inbox POST follow request"); + res.status(200); + return; } else { console.warn("follow attempt on unknown user"); + res.status(403).end(); + return; } } else if (activity.type === "Undo") { @@ -41,9 +90,13 @@ export const handleInboxPost = async (req: Request) => { } else { console.warn("unsupported activity type"); + res.status(200).end(); } } - finally { } + catch(e) { + console.warn("Failed to handle inbox POST request.", e); + res.status(500).end(); + } }; export const deleteArticleActivity = (article: Article, user: User) => { @@ -104,6 +157,32 @@ export const createArticleActivity = (article: Article, user: User) => { return activity; }; +export const createArticleObject = (article: Article, nickname: string) => { + const actor = fillRoute("actor", nickname); + const context = fillRoute("context", article.id); + const objectId = fillRoute("object", article.id); + const content = readFileSync(article.file as string, "utf-8"); + const published = typeof article.created_at === "number" ? new Date(article.created_at) : article.created_at; + const canonicalUrl = `https://${process.env.blog_host}/${article.slug}.html`; + const followers = fillRoute("followers", nickname); + + const obj: Record = { + id: objectId, + actor, + attributedTo: actor, + type: "Article", + context, + content, + to: [PUBLIC], + cc: [followers], + url: canonicalUrl, + mediaType: "text/markdown", + published + } + + return obj; +}; + export const sendAll = async (keyId: string, privateKey: string, activity: Record | string, inboxes: string[]) => { activity = typeof activity === "string" ? activity @@ -124,13 +203,6 @@ export const sendAll = async (keyId: string, privateKey: string, activity: Recor return !!!errors.length; }; -export const orderedCollection = (id: string, orderedItems: (string | Record)[]) => ({ - type: "OrderedCollectionPage", - context: CONTEXT, - id, - orderedItems -}); - export const userToPerson = (user: User) => { const id = fillRoute("actor", user.nickname); diff --git a/src/activitypub_types.ts b/src/activitypub_types.ts new file mode 100644 index 0000000..e4c2681 --- /dev/null +++ b/src/activitypub_types.ts @@ -0,0 +1,308 @@ +// Shamelessly lifted from: https://gitlab.com/soapbox-pub/ditto/-/blob/main/src/schemas/activitypub.ts + +import { z } from "zod"; + +const apId = z.string().url(); +const recipients = z.array(z.string()).catch([]); +const published = () => z.string().datetime().catch(new Date().toISOString()); + +/** Validates individual items in an array, dropping any that aren"t valid. */ +function filteredArray(schema: T) { + return z.any().array() + .transform((arr) => ( + arr.map((item) => { + const parsed = schema.safeParse(item); + return parsed.success ? parsed.data : undefined; + }).filter((item): item is z.infer => Boolean(item)) + )); +} + +const imageSchema = z.object({ + type: z.literal("Image").catch("Image"), + url: z.string().url(), +}); + +const attachmentSchema = z.object({ + type: z.literal("Document").catch("Document"), + mediaType: z.string().optional().catch(undefined), + url: z.string().url(), +}); + +const mentionSchema = z.object({ + type: z.literal("Mention"), + href: z.string().url(), + name: z.string().optional().catch(undefined), +}); + +const hashtagSchema = z.object({ + type: z.literal("Hashtag"), + href: z.string().url(), + name: z.string(), +}); + +const emojiSchema = z.object({ + type: z.literal("Emoji"), + icon: imageSchema, + name: z.string(), +}); + +const tagSchema = z.discriminatedUnion("type", [ + mentionSchema, + hashtagSchema, + emojiSchema, +]); + +const propertyValueSchema = z.object({ + type: z.literal("PropertyValue"), + name: z.string(), + value: z.string(), + verified_at: z.string().nullish(), +}); + +/** https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-fffd.md */ +const proxySchema = z.object({ + protocol: z.string().url(), + proxied: z.string(), + authoritative: z.boolean().optional().catch(undefined), +}); + +const personSchema = z.object({ + type: z.literal("Person"), + id: apId, + icon: imageSchema.optional().catch(undefined), + image: imageSchema.optional().catch(undefined), + name: z.string().catch(""), + preferredUsername: z.string(), + inbox: apId, + followers: apId.optional().catch(undefined), + following: apId.optional().catch(undefined), + outbox: apId.optional().catch(undefined), + summary: z.string().catch(""), + attachment: filteredArray(propertyValueSchema).catch([]), + tag: filteredArray(emojiSchema).catch([]), + endpoints: z.object({ + sharedInbox: apId.optional(), + }).optional().catch({}), + publicKey: z.object({ + id: apId, + owner: apId, + publicKeyPem: z.string(), + }).optional().catch(undefined), + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const applicationSchema = personSchema.merge(z.object({ type: z.literal("Application") })); +const groupSchema = personSchema.merge(z.object({ type: z.literal("Group") })); +const organizationSchema = personSchema.merge(z.object({ type: z.literal("Organization") })); +const serviceSchema = personSchema.merge(z.object({ type: z.literal("Service") })); + +const actorSchema = z.discriminatedUnion("type", [ + personSchema, + applicationSchema, + groupSchema, + organizationSchema, + serviceSchema, +]); + +const noteSchema = z.object({ + type: z.literal("Note"), + id: apId, + to: recipients, + cc: recipients, + content: z.string(), + attachment: z.array(attachmentSchema).optional().catch(undefined), + tag: filteredArray(tagSchema).catch([]), + inReplyTo: apId.optional().catch(undefined), + attributedTo: apId, + published: published(), + sensitive: z.boolean().optional().catch(undefined), + summary: z.string().nullish().catch(undefined), + quoteUrl: apId.optional().catch(undefined), + source: z.object({ + content: z.string(), + mediaType: z.literal("text/markdown"), + }).optional().catch(undefined), + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const flexibleNoteSchema = noteSchema.extend({ + quoteURL: apId.optional().catch(undefined), + quoteUri: apId.optional().catch(undefined), + _misskey_quote: apId.optional().catch(undefined), +}).transform((note) => { + const { quoteUrl, quoteUri, quoteURL, _misskey_quote, ...rest } = note; + return { + quoteUrl: quoteUrl || quoteUri || quoteURL || _misskey_quote, + ...rest, + }; +}); + +// https://github.com/colinhacks/zod/discussions/2100#discussioncomment-5109781 +const objectSchema = z.union([ + flexibleNoteSchema, + personSchema, + applicationSchema, + groupSchema, + organizationSchema, + serviceSchema, +]).pipe( + z.discriminatedUnion("type", [ + noteSchema, + personSchema, + applicationSchema, + groupSchema, + organizationSchema, + serviceSchema, + ]), +); + +const createNoteSchema = z.object({ + type: z.literal("Create"), + id: apId, + to: recipients, + cc: recipients, + actor: apId, + object: noteSchema, + published: published(), + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const announceNoteSchema = z.object({ + type: z.literal("Announce"), + id: apId, + to: recipients, + cc: recipients, + actor: apId, + object: apId.or(noteSchema), + published: published(), + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const followSchema = z.object({ + type: z.literal("Follow"), + id: apId, + to: recipients, + cc: recipients, + actor: apId, + object: apId, + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const acceptSchema = z.object({ + type: z.literal("Accept"), + id: apId, + actor: apId, + to: recipients, + cc: recipients, + object: apId.or(followSchema), +}); + +const likeSchema = z.object({ + type: z.literal("Like"), + id: apId, + actor: apId, + object: apId, + to: recipients, + cc: recipients, + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const emojiReactSchema = z.object({ + type: z.literal("EmojiReact"), + id: apId, + actor: apId, + object: apId, + content: z.string().refine((v) => /\p{Extended_Pictographic}/u.test(v)), + to: recipients, + cc: recipients, + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const deleteSchema = z.object({ + type: z.literal("Delete"), + id: apId, + actor: apId, + object: apId, + to: recipients, + cc: recipients, + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const updateActorSchema = z.object({ + type: z.literal("Update"), + id: apId, + actor: apId, + to: recipients, + cc: recipients, + object: actorSchema, + proxyOf: z.array(proxySchema).optional().catch(undefined), +}); + +const activitySchema = z.discriminatedUnion("type", [ + followSchema, + acceptSchema, + createNoteSchema, + announceNoteSchema, + updateActorSchema, + likeSchema, + emojiReactSchema, + deleteSchema, +]).refine((activity) => { + const ids: string[] = [activity.id]; + + if (activity.type === "Create") { + ids.push( + activity.object.id, + activity.object.attributedTo, + ); + } + + if (activity.type === "Update") { + ids.push(activity.object.id); + } + + const { origin: actorOrigin } = new URL(activity.actor); + + // Object containment + return ids.every((id) => { + const { origin: idOrigin } = new URL(id); + return idOrigin === actorOrigin; + }); +}); + +type Activity = z.infer; +type CreateNote = z.infer; +type Announce = z.infer; +type Update = z.infer; +type Object = z.infer; +type Follow = z.infer; +type Accept = z.infer; +type Actor = z.infer; +type Note = z.infer; +type Mention = z.infer; +type Hashtag = z.infer; +type Emoji = z.infer; +type Like = z.infer; +type EmojiReact = z.infer; +type Delete = z.infer; +type Proxy = z.infer; + +export { acceptSchema, activitySchema, actorSchema, emojiSchema, followSchema, imageSchema, noteSchema, objectSchema }; +export type { + Accept, + Activity, + Actor, + Announce, + CreateNote, + Delete, + Emoji, + EmojiReact, + Follow, + Hashtag, + Like, + Mention, + Note, + Object, + Proxy, + Update, +}; \ No newline at end of file diff --git a/src/collection.ts b/src/collection.ts index 7d2acfa..d48ce53 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -1,3 +1,4 @@ +import { CONTEXT } from "./activity.js"; import db from "./db.js"; import { z } from "zod"; @@ -11,19 +12,44 @@ export const zCollection = z.object({ export type CollectionEntry = z.infer; -export interface Follower { - actor: string, - nickname: string, - name: string, - inbox: string | null, - shared_inbox: string | null +export const orderedCollection = (id: string, orderedItems: (string | Record)[], paged = false) => { + const collection: Record = { + type: "OrderedCollection", + "@context": CONTEXT, + id, + totalItems: orderedItems.length + }; + + if (paged) { + collection.first = { + id: `${id}?page=1`, + next: `${id}?page=2`, + orderedItems, + partOf: id, + totalItems: orderedItems.length, + type: "OrderedCollectionPage" + }; + } + else { + collection.orderedItems = orderedItems; + } + + return collection; }; -export const getFollowers = async (userId: number): Promise => - db("collections") - .select("remote_users.*", "collections.value") - .join("remote_users", "remote_users.actor", "=", "collections.value") - .where("collection_types_id", 0) - .where("users_id", userId) - .orderBy("collections.created_at", "desc") - ; +export const orderedCollectionPage = (collectionId: string, orderedItems: (string | Record)[], page: number, pageLength: number, totalItems: number) => { + const collection: Record = { + id: `${collectionId}?page=${page}`, + type: "OrderedCollectionPage", + "@context": CONTEXT, + partOf: collectionId, + totalItems, + orderedItems + }; + + const hasNext = totalItems > (((page - 1) * pageLength) + orderedItems.length); + + if (hasNext) collection.next = `${collectionId}?page=${page + 1}`; + + return collection; +}; diff --git a/src/follower.ts b/src/follower.ts new file mode 100644 index 0000000..6b65f7d --- /dev/null +++ b/src/follower.ts @@ -0,0 +1,62 @@ +import { TYPE } from "./activity.js"; +import { CollectionEntry, orderedCollection } from "./collection.js"; +import db from "./db.js"; +import { fillRoute } from "./router.js"; +import { get as getUserByNickname } from "./user.js"; +import { Request, Response } from "express"; + +export interface Follower { + actor: string, + nickname: string, + name: string, + inbox: string | null, + shared_inbox: string | null +}; + +export const getFollowers = async (userId: number): Promise => + db("collections") + .select("remote_users.*", "collections.value") + .join("remote_users", "remote_users.actor", "=", "collections.value") + .where("collection_types_id", 0) + .where("users_id", userId) + .orderBy("collections.created_at", "desc") + ; + +export const addFollower = async (followeeUserId: number, actor: string, nickname: string, name: string, inbox?: string, sharedInbox?: string) => { + await db("remote_users") + .insert({ + actor, + nickname, + name, + inbox, + shared_inbox: sharedInbox + }) + .onConflict().ignore(); + ; + + await db("collections") + .insert({ + collection_types_id: 0, + users_id: followeeUserId, + value: actor, + created_at: new Date() + }) + .onConflict().ignore() + ; + + console.log("Follower added"); +}; + +export const handleFollowerGet = async (req: Request<{ actor: string}>, res: Response) => { + const user = await getUserByNickname(req.params.actor); + + if (user) { + const followers = await getFollowers(user.id); + const payload = orderedCollection(fillRoute("followers", user.nickname), followers); + res.set("Content-Type", TYPE); + res.send(JSON.stringify(payload, null, 4)); + } + else { + res.status(403).end(); + } +}; diff --git a/src/index.ts b/src/index.ts index 3af7348..523d792 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,12 @@ import express from "express"; import ActivitypubExpress from "activitypub-express"; import { get as getOutbox } from "./outbox.js"; -import { get as getUserByNickname, getId as getUserId } from "./user.js"; +import { getById as getUserById, getNickname, get as getUserByNickname, getId as getUserId, User } from "./user.js"; import { Routes } from "./router.js"; -import { getBySlug as getArticleBySlug } from "./article.js"; -import { userToPerson, TYPE as ACTIVITYPUB_TYPE } from "./activity.js"; +import { getBySlug as getArticleBySlug, getById } from "./article.js"; +import { userToPerson, TYPE as ACTIVITYPUB_TYPE, handleInboxPost, createArticleObject, CONTEXT, createArticleActivity } from "./activity.js"; +import { handleFollowerGet } from "./follower.js"; +import { handleWebfingerGet } from "./net.js"; const port = parseInt(process.env.port || "8080"); const app = express(); @@ -25,18 +27,51 @@ app.use( SITE, ); -app.route(Routes.inbox).get(SITE.net.inbox.get).post(SITE.net.inbox.post); -app.get(Routes.followers, SITE.net.followers.get); -app.get(Routes.following, SITE.net.following.get); -app.get(Routes.liked, SITE.net.liked.get); -app.get(Routes.object, SITE.net.object.get); -app.get(Routes.activity, SITE.net.activityStream.get); -app.get(Routes.shares, SITE.net.shares.get); -app.get(Routes.likes, SITE.net.likes.get); -app.get("/.well-known/webfinger", SITE.net.webfinger.get); -app.get("/.well-known/nodeinfo", SITE.net.nodeInfoLocation.get); -app.get("/nodeinfo/:version", SITE.net.nodeInfo.get); -app.post("/proxy", SITE.net.proxy.post); +app.post(Routes.inbox, handleInboxPost); +app.get(Routes.followers, handleFollowerGet); + +app.get(Routes.object, async (req, res) => { + const article = await getById(parseInt(req.params.id)); + + if (article) { + const nickname = await getNickname(article.users_id) as string; + + const obj = createArticleObject(article, nickname); + obj["@context"] = CONTEXT; + + res.append("Content-Type", ACTIVITYPUB_TYPE); + res.send(JSON.stringify(obj, null, 4)); + } + else { + res.status(404).end(); + } +}); + +app.get(Routes.activity, async (req, res) => { + let id = parseInt(req.params.id); + if (id >= 1_000_000_000) { + // it's a delete. TODO: implement. + res.status(501).end(); + } + else { + const article = await getById(id); + + if (article) { + const user = await getUserById(article.users_id) as User; + const activity = createArticleActivity(article, user); + + res.append("Content-Type", ACTIVITYPUB_TYPE); + res.send(JSON.stringify(activity, null, 4)); + } + else { + res.status(404).end(); + } + } +}); + +app.get("/.well-known/webfinger", handleWebfingerGet); +// app.get("/.well-known/nodeinfo", SITE.net.nodeInfoLocation.get); +// app.get("/nodeinfo/:version", SITE.net.nodeInfo.get); app.get(Routes.outbox, async (req, res) => { const userId = await getUserId(req.params.actor); diff --git a/src/net.ts b/src/net.ts index b802bd8..d2369a5 100644 --- a/src/net.ts +++ b/src/net.ts @@ -1,6 +1,12 @@ -import { CONTEXT } from "./activity.js"; -import { hashDigest } from "./util.js"; +import { z } from "zod"; +import { CONTEXT, TYPE } from "./activity.js"; +import { hashDigest, streamToString } from "./util.js"; import { Sha256Signer } from "activitypub-http-signatures"; +import { actorSchema } from "./activitypub_types.js"; +import type { Actor } from "./activitypub_types.js"; +import type { Request, Response } from "express"; +import { fillRoute } from "./router.js"; +import { get as getUserByNickname } from "./user.js"; export const flattenHeaders = (headers: Record) => Object.entries(headers).map(([key, value]) => `${key}: ${value}`); @@ -11,6 +17,7 @@ export interface SignedInit { digest?: string; }; +// TODO: Handle redirects. export const signedFetch = async (url: string, init: RequestInit, signedInit: SignedInit) => { const signedHeaders: HeadersInit = [ ["Date", new Date().toUTCString()], @@ -59,3 +66,84 @@ export const signedFetch = async (url: string, init: RequestInit, signedInit: Si return fetch(url, init); }; + +export const getActor = async (actorUrl: string, signedInit: SignedInit): Promise => { + const init: RequestInit = { + method: "GET", + headers: [["Accept", TYPE]] + }; + + try { + const res = await signedFetch(actorUrl, init, signedInit); + + if (res.status == 200 && res.body) { + const body = await streamToString(res.body); + const person = JSON.parse(body); + + // this will throw if it's not valid + actorSchema.parse(person); + + return person; + } + else return null; + } + catch (e) { + console.warn("Failed to get remote actor:", e); + return null; + } +}; + +export const getPublicKey = async (actorUrl: string, signedInit: SignedInit): Promise => { + const actor = await getActor(actorUrl, signedInit); + if (actor?.type === "Person") { + return actor.publicKey?.publicKeyPem || null; + } + else return null; +}; + +const domainStripper = new RegExp("/@" + process.env.blog_url?.replace(".", "\\.") + "$/i"); + +export const handleWebfingerGet = async (req: Request, res: Response) => { + let nickname = req.query.resource; + + if (nickname && typeof nickname === "string") { + nickname = nickname.replace(/^acct:/, "").replace(domainStripper, ""); + + const user = await getUserByNickname(nickname); + + if (user) { + const actor = fillRoute("actor", nickname); + + const payload = { + "subject": `acct:${nickname}@${process.env.blog_host}`, + "aliases": [ + actor + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": actor + }, + { + "rel": "self", + "type": "application/activity+json", + "href": actor + } + ] + }; + + res.set("Content-Type", `application/jrd+json; charset=utf-8`); + res.send(JSON.stringify(payload, null, 4)); + return; + } + else { + res.status(404).end(); + return; + } + } + else { + res.status(404).end(); + return; + } +}; diff --git a/src/outbox.ts b/src/outbox.ts index a4a041f..1f2f440 100644 --- a/src/outbox.ts +++ b/src/outbox.ts @@ -3,7 +3,8 @@ import { z } from "zod"; import { get as getUserByNickname } from "./user.js"; import { fillRoute } from "./router.js"; import { getByUserId as getArticlesByUserId, getById as getArticleById, Article } from "./article.js"; -import { createArticleActivity, deleteArticleActivity, orderedCollection } from "./activity.js"; +import { createArticleActivity, deleteArticleActivity } from "./activity.js"; +import { orderedCollection } from "./collection.js"; export const zRawOutboxRecord = z.object({ diff --git a/src/router.ts b/src/router.ts index 3e86a63..3769b4b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,19 +1,11 @@ export const Routes = { actor: "/author/:actor", - object: "/o/:id", - activity: "/a/:id", inbox: "/author/:actor/inbox", outbox: "/author/:actor/outbox", followers: "/author/:actor/followers", - following: "/author/:actor/following", - liked: "/author/:actor/liked", - collections: "/author/:actor/c/:id", - blocked: "/author/:actor/blocked", - rejections: "/author/:actor/rejections", - rejected: "/author/:actor/rejected", - shares: "/a/:id/shares", - likes: "/a/:id/likes", context: "/c/:id", + object: "/o/:id", + activity: "/a/:id", } as const; export type Route = keyof typeof Routes; diff --git a/src/user.ts b/src/user.ts index e89a6ef..c0bbdeb 100644 --- a/src/user.ts +++ b/src/user.ts @@ -1,7 +1,7 @@ import { generateKeyPair } from "node:crypto"; import { z } from "zod"; import db from "./db.js"; -import { getFollowers } from "./collection.js"; +import { getFollowers } from "./follower.js"; import { fillRoute } from "./router.js"; export const nicknameRegex = /^[a-zA-Z0-9_]+$/; @@ -26,6 +26,12 @@ export const get = async (nickname: string) => .first() ; +export const getById = async (id: number) => + db("users") + .where("id", id) + .first() + ; + const EXTRACT_NICKNAME = new RegExp("/" + fillRoute("actor", "(.+)").replaceAll("/", "\\/") + "/"); export const getByActor = async (actor: string) => { @@ -98,3 +104,5 @@ export const getFollowerInboxes = async (userId: number): Promise => return [...inboxes]; }); + +export const getKeyId = (nickname: string) => fillRoute("actor", nickname) + "#main-key";