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)