Refactored to store actor info in a separate file
This makes it more easy to modify for future users
This commit is contained in:
parent
6647dabdc8
commit
6f2e122dd6
9 changed files with 247 additions and 161 deletions
88
actor.ts
Normal file
88
actor.ts
Normal file
|
@ -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: `<a href="${BASE_URL}" target="_blank" rel="nofollow noopener noreferrer me">${BASE_URL}</a>`
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
}
|
|
@ -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<Response> | 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<Response> => {
|
||||
console.log("PostOutbox", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const postOutbox = async (req:Request):Promise<Response> => {
|
||||
console.log("PostOutbox")
|
||||
|
||||
const bodyText = await req.text()
|
||||
|
||||
|
@ -51,14 +40,13 @@ const postOutbox = async (req:Request, account:string):Promise<Response> => {
|
|||
|
||||
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<Response> => {
|
||||
console.log("PostInbox", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const postInbox = async (req:Request):Promise<Response> => {
|
||||
console.log("PostInbox")
|
||||
|
||||
const bodyText = await req.text()
|
||||
|
||||
|
@ -77,40 +65,28 @@ const postInbox = async (req:Request, account:string):Promise<Response> => {
|
|||
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<Response> => {
|
||||
console.log("GetOutbox", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getOutbox = async (req:Request):Promise<Response> => {
|
||||
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<Response> => {
|
||||
console.log("GetFollowers", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getFollowers = async (req:Request):Promise<Response> => {
|
||||
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<Response> => {
|
|||
|
||||
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<Response> => {
|
||||
console.log("GetFollowing", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getFollowing = async (req:Request):Promise<Response> => {
|
||||
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<Response> => {
|
|||
|
||||
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<Response> => {
|
||||
console.log("GetActor", account)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getActor = async (req:Request):Promise<Response> => {
|
||||
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<Response> => {
|
||||
console.log("GetPost", account, id)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getPost = async (req:Request, id:string):Promise<Response> => {
|
||||
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<Response> => {
|
||||
console.log("GetOutboxActivity", account, id)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
const getOutboxActivity = async (req:Request, id:string):Promise<Response> => {
|
||||
console.log("GetOutboxActivity", id)
|
||||
|
||||
return Response.json((await db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
|
||||
}
|
45
src/admin.ts
45
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<Response> | undefined => {
|
||||
const url = new URL(req.url)
|
||||
|
@ -54,15 +55,15 @@ const create = async (req:Request, inReplyTo:string|null = null):Promise<Respons
|
|||
|
||||
if(!inReplyTo && body.object.inReplyTo) inReplyTo = body.object.inReplyTo
|
||||
|
||||
const original = inReplyTo ? await (await fetchObject(ACTOR, inReplyTo)).json() : null
|
||||
const original = inReplyTo ? await (await fetchObject(inReplyTo)).json() : null
|
||||
|
||||
// create the object, merging in supplied data
|
||||
const date = new Date()
|
||||
const object = {
|
||||
attributedTo: ACTOR,
|
||||
attributedTo: ACTOR.id,
|
||||
published: date.toISOString(),
|
||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
cc: [`${ACTOR}/followers`],
|
||||
cc: [ACTOR.followers],
|
||||
...body.object
|
||||
}
|
||||
if(inReplyTo){
|
||||
|
@ -74,7 +75,7 @@ const create = async (req:Request, inReplyTo:string|null = null):Promise<Respons
|
|||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
type: "Create",
|
||||
published: date.toISOString(),
|
||||
actor: ACTOR,
|
||||
actor: ACTOR.id,
|
||||
to: object.to,
|
||||
cc: object.cc,
|
||||
...body,
|
||||
|
@ -92,7 +93,7 @@ const follow = async (req:Request, handle:string):Promise<Response> => {
|
|||
}
|
||||
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<Response> => {
|
|||
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<Response> => {
|
|||
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<Response> => {
|
||||
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<Response> => {
|
|||
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<Response> => {
|
|||
}
|
||||
|
||||
const dislike = async (req:Request, object_url:string):Promise<Response> => {
|
||||
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<Response> => {
|
|||
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<Response> => {
|
|||
}
|
||||
|
||||
const share = async (req:Request, object_url:string):Promise<Response> => {
|
||||
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<Response> => {
|
|||
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<Response> => {
|
|||
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<Response> => {
|
|||
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,
|
||||
|
|
11
src/db.ts
11
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)
|
||||
|
|
86
src/env.ts
86
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"
|
||||
},
|
||||
}
|
||||
// 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": "<a href=\"https://death.id.au\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://</span>death.id.au</a>"
|
||||
// // }
|
||||
// // ],
|
||||
// // 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"
|
||||
// }
|
||||
// }
|
16
src/inbox.ts
16
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,
|
||||
});
|
||||
|
|
23
src/index.ts
23
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" }})
|
||||
|
|
|
@ -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<Response> {
|
||||
const date = new Date()
|
||||
|
@ -14,7 +14,7 @@ export default async function outbox(activity:any):Promise<Response> {
|
|||
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<Response> {
|
|||
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<Response> {
|
|||
|
||||
// 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue