From 6f2e122dd6e2086ce2e6eefc3bf7f0a6b0cd665c Mon Sep 17 00:00:00 2001 From: Gordon Pedersen Date: Thu, 28 Sep 2023 11:15:25 +1000 Subject: [PATCH] Refactored to store actor info in a separate file This makes it more easy to modify for future users --- actor.ts | 88 +++++++++++++++++++++++++++++++++++++ src/activitypub.ts | 106 +++++++++++++++++---------------------------- src/admin.ts | 45 +++++++++---------- src/db.ts | 11 ++--- src/env.ts | 86 ++++++++++++++++++++++-------------- src/inbox.ts | 16 +++---- src/index.ts | 23 +++++----- src/outbox.ts | 22 +++++----- src/request.ts | 11 ++--- 9 files changed, 247 insertions(+), 161 deletions(-) create mode 100644 actor.ts diff --git a/actor.ts b/actor.ts new file mode 100644 index 0000000..171e658 --- /dev/null +++ b/actor.ts @@ -0,0 +1,88 @@ +import { BASE_URL, PUBLIC_KEY } from "./src/env" + +// change "activitypub" to whatever you want your account name to be +export const preferredUsername:string = process.env.ACCOUNT || "activitypub" +export const name = process.env.REAL_NAME || preferredUsername +export const summary = "" + +// avatar image +const icon:{ type:"Image", mediaType:string, url:string } = { + type: "Image", + mediaType: "image/svg+xml", + url: BASE_URL + "/assets/img/avatar-tt.svg" +} + +// banner image +const image:{ type:"Image", mediaType:string, url:string } = { + type: "Image", + mediaType: "image/jpeg", + url: BASE_URL + "/assets/img/banner-1500x500.jpg" +} + +// This is a list of other actor ids you identify as +const alsoKnownAs:string[] = [] + +// I'm not exactly sure what this time represents. +// The date your website got published? +const published:string = "2023-09-14T00:00:00Z" + +// customize this to add or remove PropertyValues +// this is the table that appears on Mastodon, etc, profile pages +const attachment:{ type:"PropertyValue", name:string, value:string }[] = [ + { + type: "PropertyValue", + name: "Pronouns", + value: "they/them" + }, + { + type: "PropertyValue", + name: "Website", + value: `${BASE_URL}` + } +] + +export default { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + manuallyApprovesFollowers: "as:manuallyApprovesFollowers", + toot: "http://joinmastodon.org/ns#", + alsoKnownAs: { + "@id": "as:alsoKnownAs", + "@type": "@id" + }, + schema: "http://schema.org#", + PropertyValue: "schema:PropertyValue", + value: "schema:value", + discoverable: "toot:discoverable", + Ed25519Signature: "toot:Ed25519Signature", + Ed25519Key: "toot:Ed25519Key", + Curve25519Key: "toot:Curve25519Key", + EncryptedMessage: "toot:EncryptedMessage", + publicKeyBase64: "toot:publicKeyBase64" + } + ], + id: BASE_URL, + type: "Person", + following: `${BASE_URL}/following`, + followers: `${BASE_URL}/followers`, + inbox: `${BASE_URL}/inbox`, + outbox: `${BASE_URL}/outbox`, + preferredUsername, + name, + summary, + url: BASE_URL, + manuallyApprovesFollowers: false, + discoverable: true, + published, + alsoKnownAs, + publicKey: { + id: `${BASE_URL}#main-key`, + owner: BASE_URL, + publicKeyPem: PUBLIC_KEY, + }, + attachment, + icon, + image +} \ No newline at end of file diff --git a/src/activitypub.ts b/src/activitypub.ts index 4affea7..6e85e9e 100644 --- a/src/activitypub.ts +++ b/src/activitypub.ts @@ -1,33 +1,23 @@ -import { ACCOUNT, ACTOR, ACTOR_OBJ, HOSTNAME, PUBLIC_KEY } from "./env" import * as db from "./db" -import { reqIsActivityPub, send, verify } from "./request" +import { reqIsActivityPub, verify } from "./request" import outbox from "./outbox" import inbox from "./inbox" +import ACTOR from "../actor" export default (req: Request): Response | Promise | undefined => { const url = new URL(req.url) let match if(!reqIsActivityPub(req)) return undefined - - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/?$/i))) return getActor(req, match[1]) - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return getOutbox(req, match[1]) - // else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/inbox\/?$/i))) return postInbox(req, match[1]) - // else if(req.method == "POST" && (match = url.pathname.match(/^\/([^\/]+)\/outbox\/?$/i))) return postOutbox(req, match[1]) - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/followers\/?$/i))) return getFollowers(req, match[1]) - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/following\/?$/i))) return getFollowing(req, match[1]) - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1], match[2]) - // else if(req.method == "GET" && (match = url.pathname.match(/^\/([^\/]+)\/posts\/([^\/]+)\/activity\/?$/i))) return getActivity(req, match[1], match[2]) - - else if(req.method == "GET" && (match = url.pathname.match(/^\/?$/i))) return getActor(req, ACCOUNT) - else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/?$/i))) return getOutbox(req, ACCOUNT) - else if(req.method == "POST" && (match = url.pathname.match(/^\/inbox\/?$/i))) return postInbox(req, ACCOUNT) - else if(req.method == "POST" && (match = url.pathname.match(/^\/outbox\/?$/i))) return postOutbox(req, ACCOUNT) - else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req, ACCOUNT) - else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req, ACCOUNT) - else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, ACCOUNT, match[1]) - else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getOutboxActivity(req, ACCOUNT, match[1]) - else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/([^\/]+)\/?$/i))) return getOutboxActivity(req, ACCOUNT, match[1]) + else if(req.method == "GET" && (match = url.pathname.match(/^\/?$/i))) return getActor(req) + else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/?$/i))) return getOutbox(req) + else if(req.method == "POST" && (match = url.pathname.match(/^\/inbox\/?$/i))) return postInbox(req) + else if(req.method == "POST" && (match = url.pathname.match(/^\/outbox\/?$/i))) return postOutbox(req) + else if(req.method == "GET" && (match = url.pathname.match(/^\/followers\/?$/i))) return getFollowers(req) + else if(req.method == "GET" && (match = url.pathname.match(/^\/following\/?$/i))) return getFollowing(req) + else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/?$/i))) return getPost(req, match[1]) + else if(req.method == "GET" && (match = url.pathname.match(/^\/posts\/([^\/]+)\/activity\/?$/i))) return getOutboxActivity(req, match[1]) + else if(req.method == "GET" && (match = url.pathname.match(/^\/outbox\/([^\/]+)\/?$/i))) return getOutboxActivity(req, match[1]) return undefined } @@ -41,9 +31,8 @@ export function idsFromValue(value:any):string[] { return [] } -const postOutbox = async (req:Request, account:string):Promise => { - console.log("PostOutbox", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const postOutbox = async (req:Request):Promise => { + console.log("PostOutbox") const bodyText = await req.text() @@ -51,14 +40,13 @@ const postOutbox = async (req:Request, account:string):Promise => { const body = JSON.parse(bodyText) // ensure that the verified actor matches the actor in the request body - if (ACTOR !== body.actor) return new Response("", { status: 401 }) + if (ACTOR.id !== body.actor) return new Response("", { status: 401 }) return await outbox(body) } -const postInbox = async (req:Request, account:string):Promise => { - console.log("PostInbox", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const postInbox = async (req:Request):Promise => { + console.log("PostInbox") const bodyText = await req.text() @@ -77,40 +65,28 @@ const postInbox = async (req:Request, account:string):Promise => { if (from !== body.actor) return new Response("", { status: 401 }) return await inbox(body) - - // TODO: add support for more types! we want replies, likes, boosts, etc! - // switch (body.type) { - // case "Follow": await follow(body); - // case "Undo": await undo(body); - // case "Accept": await accept(body); - // case "Reject": await reject(body); - // } - - // return new Response("", { status: 204 }) } -const getOutbox = async (req:Request, account:string):Promise => { - console.log("GetOutbox", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getOutbox = async (req:Request):Promise => { + console.log("GetOutbox") // TODO: Paging? const posts = await db.listOutboxActivities() return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", - id: `${ACTOR}/outbox`, + id: ACTOR.outbox, type: "OrderedCollection", totalItems: posts.length, orderedItems: posts.map((post) => ({ ...post, - actor: ACTOR + actor: ACTOR.id })).sort( (a,b) => new Date(b.published).getTime() - new Date(a.published).getTime()) }, { headers: { "Content-Type": "application/activity+json"} }) } -const getFollowers = async (req:Request, account:String):Promise => { - console.log("GetFollowers", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getFollowers = async (req:Request):Promise => { + console.log("GetFollowers") const url = new URL(req.url) const page = url.searchParams.get("page") @@ -118,24 +94,23 @@ const getFollowers = async (req:Request, account:String):Promise => { if(!page) return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", - id: `${ACTOR}/followers`, + id: ACTOR.followers, type: "OrderedCollection", totalItems: followers.length, - first: `${ACTOR}/followers?page=1`, + first: `${ACTOR.followers}?page=1`, }) else return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", - id: `${ACTOR}/followers?page=${page}`, + id: `${ACTOR.followers}?page=${page}`, type: "OrderedCollectionPage", - partOf: `${ACTOR}/followers`, + partOf: ACTOR.followers, totalItems: followers.length, orderedItems: followers.map(follower => follower.actor) }) } -const getFollowing = async (req:Request, account:String):Promise => { - console.log("GetFollowing", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getFollowing = async (req:Request):Promise => { + console.log("GetFollowing") const url = new URL(req.url) const page = url.searchParams.get("page") @@ -143,40 +118,37 @@ const getFollowing = async (req:Request, account:String):Promise => { if(!page) return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", - id: `${ACTOR}/following`, + id: ACTOR.following, type: "OrderedCollection", totalItems: following.length, - first: `${ACTOR}/following?page=1`, + first: `${ACTOR.following}?page=1`, }) else return Response.json({ "@context": "https://www.w3.org/ns/activitystreams", - id: `${ACTOR}/following?page=${page}`, + id: `${ACTOR.following}?page=${page}`, type: "OrderedCollectionPage", - partOf: `${ACTOR}/following`, + partOf: ACTOR.following, totalItems: following.length, orderedItems: following.map(follow => follow.actor) }) } -const getActor = async (req:Request, account:string):Promise => { - console.log("GetActor", account) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getActor = async (req:Request):Promise => { + console.log("GetActor") - if(reqIsActivityPub(req)) return Response.json(ACTOR_OBJ, { headers: { "Content-Type": "application/activity+json"}}) + if(reqIsActivityPub(req)) return Response.json(ACTOR, { headers: { "Content-Type": "application/activity+json"}}) else return Response.json(await db.listPosts()) } -const getPost = async (req:Request, account:string, id:string):Promise => { - console.log("GetPost", account, id) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getPost = async (req:Request, id:string):Promise => { + console.log("GetPost", id) if(reqIsActivityPub(req)) return Response.json((await db.getOutboxActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}}) else return Response.json(await db.getPost(id)) } -const getOutboxActivity = async (req:Request, account:string, id:string):Promise => { - console.log("GetOutboxActivity", account, id) - if (ACCOUNT !== account) return new Response("", { status: 404 }) +const getOutboxActivity = async (req:Request, id:string):Promise => { + console.log("GetOutboxActivity", id) return Response.json((await db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}}) } \ No newline at end of file diff --git a/src/admin.ts b/src/admin.ts index 3df6a4a..d113eba 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -1,8 +1,9 @@ import { idsFromValue } from "./activitypub" import * as db from "./db" -import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL, CONTENT_PATH, STATIC_PATH } from "./env" +import { ADMIN_PASSWORD, ADMIN_USERNAME } from "./env" import outbox from "./outbox" import { activityMimeTypes, fetchObject } from "./request" +import ACTOR from "../actor" export default (req: Request): Response | Promise | undefined => { const url = new URL(req.url) @@ -54,15 +55,15 @@ const create = async (req:Request, inReplyTo:string|null = null):Promise => { } catch { // this is not a valid url. Probably a someone@domain.tld format - const [user, host] = handle.split('@') + const [_, host] = handle.split('@') const res = await fetch(`https://${host}/.well-known/webfinger/?resource=acct:${handle}`) const webfinger = await res.json() if(!webfinger.links) return new Response("", { status: 404 }) @@ -107,7 +108,7 @@ const follow = async (req:Request, handle:string):Promise => { return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Follow", - actor: ACTOR, + actor: ACTOR.id, object: url, to: [url, "https://www.w3.org/ns/activitystreams#Public"] }) @@ -122,22 +123,22 @@ const unfollow = async (req:Request, handle:string):Promise => { return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Undo", - actor: ACTOR, + actor: ACTOR.id, object: activity, to: activity.to }) } const like = async (req:Request, object_url:string):Promise => { - const object = await (await fetchObject(ACTOR, object_url)).json() + const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Like", - actor: ACTOR, + actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], - cc: [ACTOR + 'followers'] + cc: [ACTOR.followers] }) } @@ -146,7 +147,7 @@ const unlike = async (req:Request, object_id:string):Promise => { const liked = await db.listLiked() let existing = liked.find(o => o.object_id === object_id) if (!existing){ - const object = await (await fetchObject(ACTOR, object_id)).json() + const object = await (await fetchObject(object_id)).json() idsFromValue(object).forEach(id => { const e = liked.find(o => o.object_id === id) if(e) existing = e @@ -158,15 +159,15 @@ const unlike = async (req:Request, object_id:string):Promise => { } const dislike = async (req:Request, object_url:string):Promise => { - const object = await (await fetchObject(ACTOR, object_url)).json() + const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Dislike", - actor: ACTOR, + actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], - cc: [ACTOR + 'followers'] + cc: [ACTOR.followers] }) } @@ -175,7 +176,7 @@ const undislike = async (req:Request, object_id:string):Promise => { const disliked = await db.listDisliked() let existing = disliked.find(o => o.object_id === object_id) if (!existing){ - const object = await (await fetchObject(ACTOR, object_id)).json() + const object = await (await fetchObject(object_id)).json() idsFromValue(object).forEach(id => { const e = disliked.find(o => o.object_id === id) if(e) existing = e @@ -187,15 +188,15 @@ const undislike = async (req:Request, object_id:string):Promise => { } const share = async (req:Request, object_url:string):Promise => { - const object = await (await fetchObject(ACTOR, object_url)).json() + const object = await (await fetchObject(object_url)).json() return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Announce", - actor: ACTOR, + actor: ACTOR.id, object: object, to: [...idsFromValue(object.attributedTo), "https://www.w3.org/ns/activitystreams#Public"], - cc: [ACTOR + 'followers'] + cc: [ACTOR.followers] }) } @@ -204,7 +205,7 @@ const unshare = async (req:Request, object_id:string):Promise => { const shared = await db.listShared() let existing = shared.find(o => o.object_id === object_id) if (!existing){ - const object = await (await fetchObject(ACTOR, object_id)).json() + const object = await (await fetchObject(object_id)).json() idsFromValue(object).forEach(id => { const e = shared.find(o => o.object_id === id) if(e) existing = e @@ -220,7 +221,7 @@ const undo = async(activity:any):Promise => { return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Undo", - actor: ACTOR, + actor: ACTOR.id, object: activity, to: activity.to, cc: activity.cc @@ -234,7 +235,7 @@ const deletePost = async (req:Request, id:string):Promise => { return await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Delete", - actor: ACTOR, + actor: ACTOR.id, to: activity.to, cc: activity.cc, audience: activity.audience, diff --git a/src/db.ts b/src/db.ts index 5e00a45..b786cf2 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,9 +1,10 @@ -import { ACCOUNT, ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, ACTOR, CONTENT_PATH, DATA_PATH, POSTS_PATH, PUBLIC_KEY, STATIC_PATH } from "./env"; +import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, CONTENT_PATH, DATA_PATH, POSTS_PATH, STATIC_PATH } from "./env" import path from "path" import { readdir } from "fs/promises" import { unlinkSync } from "node:fs" -import { fetchObject } from "./request"; -import { idsFromValue } from "./activitypub"; +import { fetchObject } from "./request" +import { idsFromValue } from "./activitypub" +import ACTOR from "../actor" const matter = require('gray-matter') const Eleventy = require("@11ty/eleventy") @@ -50,7 +51,7 @@ export async function listOutboxActivities() { export async function createPost(post_object:any, object_id:string) { const file = Bun.file(path.join(POSTS_PATH, `${object_id}.md`)) let {type, object, inReplyTo} = post_object - if(inReplyTo && typeof inReplyTo === 'string') inReplyTo = await fetchObject(ACTOR, inReplyTo) + if(inReplyTo && typeof inReplyTo === 'string') inReplyTo = await fetchObject(inReplyTo) if(object){ let { content, published, id, attributedTo } = object @@ -91,7 +92,7 @@ export async function getPost(id:string) { } export async function getPostByURL(url_id:string) { - if(!url_id || !url_id.startsWith(ACTOR + '/posts/')) return null + if(!url_id || !url_id.startsWith(ACTOR.url + '/posts/')) return null const match = url_id.match(/\/([0-9a-f]+)\/?$/) const local_id = match ? match[1] : url_id return await getPost(local_id) diff --git a/src/env.ts b/src/env.ts index 1e6d976..1eed62c 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,9 +1,6 @@ import forge from "node-forge" // import crypto from "node:crypto" import path from "path" -// change "activitypub" to whatever you want your account name to be -export const ACCOUNT = process.env.ACCOUNT || "activitypub" - // set up username and password for admin actions export const ADMIN_USERNAME = process.env.ADMIN_USERNAME || ""; export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ""; @@ -15,8 +12,6 @@ export const PORT = process.env.PORT || "3000" export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME -export const ACTOR = BASE_URL //+ '/' + ACCOUNT - // in development, generate a key pair to make it easier to get started const keypair = NODE_ENV === "development" @@ -50,30 +45,57 @@ export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [ 'default.htm' ] -export const ACTOR_OBJ = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - id: ACTOR, - type: "Person", - preferredUsername: ACCOUNT, - url: ACTOR, - manuallyApprovesFollowers: false, - discoverable: true, - published: "2023-09-14T00:00:00Z", - inbox: `${ACTOR}/inbox`, - outbox: `${ACTOR}/outbox`, - followers: `${ACTOR}/followers`, - following: `${ACTOR}/following`, - publicKey: { - id: `${ACTOR}#main-key`, - owner: ACTOR, - publicKeyPem: PUBLIC_KEY, - }, - icon: { - type: "Image", - mediaType: "image/png", - url: BASE_URL + "/assets/img/avatar-tt@800.png" - }, -} \ No newline at end of file +// export const ACTOR_OBJ = { +// "@context": [ +// "https://www.w3.org/ns/activitystreams", +// "https://w3id.org/security/v1", +// ], +// id: ACTOR, +// type: "Person", +// following: `${ACTOR}/following`, +// followers: `${ACTOR}/followers`, +// inbox: `${ACTOR}/inbox`, +// outbox: `${ACTOR}/outbox`, +// //featured: `${ACTOR}/featured`, +// //featuredTags: `${ACTOR}/tags`, +// preferredUsername: ACCOUNT, +// name: REAL_NAME, +// summary: SUMMARY, +// url: ACTOR, +// manuallyApprovesFollowers: false, +// discoverable: true, +// published: "2023-09-14T00:00:00Z", +// //devices: `${ACTOR}/devices` +// alsoKnownAs: ALSO_KNOWN_AS, +// publicKey: { +// id: `${ACTOR}#main-key`, +// owner: ACTOR, +// publicKeyPem: PUBLIC_KEY, +// }, +// // attachment: [ +// // { +// // "type": "PropertyValue", +// // "name": "Pronouns", +// // "value": "he/him" +// // }, +// // { +// // "type": "PropertyValue", +// // "name": "Website", +// // "value": "https://death.id.au" +// // } +// // ], +// // tag: [], +// // endpoints: { +// // sharedInbox: N/A +// // }, +// icon: { +// type: "Image", +// mediaType: "image/svg+xml", +// url: BASE_URL + "/assets/img/avatar-tt.svg" +// }, +// image: { +// type: "Image", +// mediaType: "image/jpeg", +// url: BASE_URL + "/assets/img/banner-1500x500.jpg" +// } +// } \ No newline at end of file diff --git a/src/inbox.ts b/src/inbox.ts index 57f8e6c..fd15ec8 100644 --- a/src/inbox.ts +++ b/src/inbox.ts @@ -1,8 +1,8 @@ -import { idsFromValue } from "./activitypub"; -import * as db from "./db"; -import { ACTOR, BASE_URL } from "./env"; -import outbox from "./outbox"; -import { send } from "./request"; +import { idsFromValue } from "./activitypub" +import * as db from "./db" +import outbox from "./outbox" +import { send } from "./request" +import ACTOR from "../actor" export default async function inbox(activity:any) { const date = new Date() @@ -10,8 +10,8 @@ export default async function inbox(activity:any) { const recipientList = [...new Set([...idsFromValue(activity.to), ...idsFromValue(activity.cc), ...idsFromValue(activity.audience)])] // if my list of followers in the list of recipients, then forward to them as well - if(recipientList.includes(ACTOR + "/followers")) { - (await db.listFollowers()).forEach(f => send(activity.attributedTo, f, activity)) + if(recipientList.includes(ACTOR.url + "/followers")) { + (await db.listFollowers()).forEach(f => send(f, activity, activity.attributedTo)) } // save this activity to my inbox @@ -37,7 +37,7 @@ const follow = async (activity:any, id:string) => { await outbox({ "@context": "https://www.w3.org/ns/activitystreams", type: "Accept", - actor: ACTOR, + actor: ACTOR.id, to: [activity.actor], object: activity, }); diff --git a/src/index.ts b/src/index.ts index 5005561..e3f5deb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ -import { ACCOUNT, ACTOR, DEFAULT_DOCUMENTS, HOSTNAME, STATIC_PATH } from "./env"; +import { DEFAULT_DOCUMENTS, HOSTNAME, STATIC_PATH } from "./env" import admin from './admin' -import activitypub from "./activitypub"; -import { fetchObject } from "./request"; +import activitypub from "./activitypub" +import { fetchObject } from "./request" import path from "path" -import { BunFile } from "bun"; -import { rebuild } from "./db"; +import { BunFile } from "bun" +import { rebuild } from "./db" +import ACTOR from "../actor" rebuild() @@ -22,7 +23,7 @@ const server = Bun.serve({ const object_url = url.searchParams.get('url') if(!object_url) return new Response("No url supplied", { status: 400}) - return fetchObject(ACTOR, object_url) + return fetchObject(object_url) } return admin(req) || activitypub(req) || staticFile(req) @@ -31,21 +32,21 @@ const server = Bun.serve({ const webfinger = async (req: Request, resource: string | null) => { - if(resource !== `acct:${ACCOUNT}@${HOSTNAME}`) return new Response("", { status: 404 }) + if(resource !== `acct:${ACTOR.preferredUsername}@${HOSTNAME}`) return new Response("", { status: 404 }) return Response.json({ - subject: `acct:${ACCOUNT}@${HOSTNAME}`, - aliases: [ACTOR], + subject: `acct:${ACTOR.preferredUsername}@${HOSTNAME}`, + aliases: [ACTOR.id], links: [ { "rel": "http://webfinger.net/rel/profile-page", "type": "text/html", - "href": ACTOR + "href": ACTOR.url }, { rel: "self", type: "application/activity+json", - href: ACTOR, + href: ACTOR.id, }, ], }, { headers: { "content-type": "application/activity+json" }}) diff --git a/src/outbox.ts b/src/outbox.ts index 5217614..314ca5c 100644 --- a/src/outbox.ts +++ b/src/outbox.ts @@ -1,7 +1,7 @@ import { idsFromValue } from "./activitypub" -import * as db from "./db"; -import { ACTOR } from "./env" -import { fetchObject, send } from "./request"; +import * as db from "./db" +import { fetchObject, send } from "./request" +import ACTOR from "../actor" export default async function outbox(activity:any):Promise { const date = new Date() @@ -14,7 +14,7 @@ export default async function outbox(activity:any):Promise { activity = { "@context": "https://www.w3.org/ns/activitystreams", type: "Create", - actor: ACTOR, + actor: ACTOR.id, object } const { to, bto, cc, bcc, audience } = object @@ -25,7 +25,7 @@ export default async function outbox(activity:any):Promise { if(audience) activity.audience = audience } - activity.id = `${ACTOR}/outbox/${id}` + activity.id = `${ACTOR.url}/outbox/${id}` if(!activity.published) activity.published = date.toISOString() if(activity.type === 'Create' && activity.object && Object(activity.object) === activity.object) { @@ -59,16 +59,16 @@ export default async function outbox(activity:any):Promise { // send to the appropriate recipients finalRecipientList.forEach((to) => { - if (to.startsWith(ACTOR + "/followers")) db.listFollowers().then(followers => followers.forEach(f => send(ACTOR, f.actor, activity))) + if (to.startsWith(ACTOR.url + "/followers")) db.listFollowers().then(followers => followers.forEach(f => send(f.actor, activity))) else if (to === "https://www.w3.org/ns/activitystreams#Public") return // there's nothing to "send" to here - else if (to) send(ACTOR, to, activity) + else if (to) send(to, activity) }) return new Response("", { status: 201, headers: { location: activity.id } }) } async function create(activity:any, id:string) { - activity.object.id = activity.object.url = `${ACTOR}/posts/${id}` + activity.object.id = activity.object.url = `${ACTOR.url}/posts/${id}` await db.createPost(activity.object, id) return true } @@ -85,7 +85,7 @@ async function follow(activity:any, id:string) { async function like(activity:any, id:string) { if(typeof activity.object === 'string'){ await db.createLiked(activity.object, id) - activity.object = await fetchObject(ACTOR, activity.object) + activity.object = await fetchObject(activity.object) } else { const liked = await idsFromValue(activity.object) @@ -98,7 +98,7 @@ async function like(activity:any, id:string) { async function dislike(activity:any, id:string) { if(typeof activity.object === 'string'){ await db.createDisliked(activity.object, id) - activity.object = await fetchObject(ACTOR, activity.object) + activity.object = await fetchObject(activity.object) } else { const disliked = await idsFromValue(activity.object) @@ -111,7 +111,7 @@ async function dislike(activity:any, id:string) { async function announce(activity:any, id:string) { if(typeof activity.object === 'string'){ await db.createShared(activity.object, id) - activity.object = await fetchObject(ACTOR, activity.object) + activity.object = await fetchObject(activity.object) } else { const shared = await idsFromValue(activity.object) diff --git a/src/request.ts b/src/request.ts index 55e32b1..555c30f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,5 +1,6 @@ import forge from "node-forge" // import crypto from "node:crypto" -import { ACTOR, PRIVATE_KEY } from "./env"; +import { PRIVATE_KEY } from "./env"; +import ACTOR from "../actor" export const activityMimeTypes:string[] = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'] @@ -39,7 +40,7 @@ export function signedFetch(url: string | URL | Request, init?: FetchRequestInit const data = Object.entries(dataObj).map(([key, value]) => `${key}: ${value}`).join('\n') const signature = forge.util.encode64(key.sign(forge.md.sha256.create().update(data))) const signatureObj = { - keyId: `${ACTOR}#main-key`, + keyId: `${ACTOR.id}#main-key`, headers: Object.keys(dataObj).join(' '), signature: signature } @@ -56,11 +57,11 @@ export function signedFetch(url: string | URL | Request, init?: FetchRequestInit /** Fetches and returns an actor at a URL. */ async function fetchActor(url:string) { - return await (await fetchObject(ACTOR, url)).json() + return await (await fetchObject(url)).json() } /** Fetches and returns an object at a URL. */ -export async function fetchObject(sender:string, object_url:string) { +export async function fetchObject(object_url:string) { console.log(`fetch ${object_url}`) const res = await fetch(object_url); @@ -77,7 +78,7 @@ export async function fetchObject(sender:string, object_url:string) { * @param recipient The recipient's actor URL. * @param message the body of the request to send. */ -export async function send(sender:string, recipient:string, message:any) { +export async function send(recipient:string, message:any, from:string=ACTOR.id) { console.log(`Sending to ${recipient}`, message) // TODO: revisit fetch actor to use webfinger to get the inbox maybe? const actor = await fetchActor(recipient)