Utilize 11ty to build static pages and modify server to serve them
This commit is contained in:
parent
283220111f
commit
b76eb76e1a
10 changed files with 97 additions and 34 deletions
10
.eleventy.js
Normal file
10
.eleventy.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = function(eleventyConfig) {
|
||||
eleventyConfig.setUseGitIgnore(false)
|
||||
// Return your Object options:
|
||||
return {
|
||||
dir: {
|
||||
input: "_content",
|
||||
output: "_site"
|
||||
}
|
||||
}
|
||||
};
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -170,3 +170,4 @@ dist
|
|||
|
||||
# My custom ignores
|
||||
_content
|
||||
_site
|
||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -14,6 +14,7 @@
|
|||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@11ty/eleventy": "^2.0.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
"node-forge": "^1.3.1"
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ export default (req: Request): Response | Promise<Response> | undefined => {
|
|||
const url = new URL(req.url)
|
||||
let match
|
||||
|
||||
if(req.method === "GET" && url.pathname === "/test") return new Response("", { status: 204 })
|
||||
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])
|
||||
|
@ -26,7 +26,8 @@ export default (req: Request): Response | Promise<Response> | undefined => {
|
|||
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 getActivity(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])
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
@ -190,13 +191,13 @@ const getPost = async (req:Request, account:string, id:string):Promise<Response>
|
|||
console.log("GetPost", account, id)
|
||||
if (ACCOUNT !== account) return new Response("", { status: 404 })
|
||||
|
||||
if(reqIsActivityPub(req)) return Response.json((await db.getActivity(id)).object, { headers: { "Content-Type": "application/activity+json"}})
|
||||
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 getActivity = async (req:Request, account:string, id:string):Promise<Response> => {
|
||||
console.log("GetActivity", account, 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 })
|
||||
|
||||
return Response.json((await db.getActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
|
||||
return Response.json((await db.getOutboxActivity(id)), { headers: { "Content-Type": "application/activity+json"}})
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { idsFromValue } from "./activitypub"
|
||||
import * as db from "./db"
|
||||
import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL } from "./env"
|
||||
import { ACTOR, ADMIN_PASSWORD, ADMIN_USERNAME, BASE_URL, CONTENT_PATH, STATIC_PATH } from "./env"
|
||||
import outbox from "./outbox"
|
||||
import { activityMimeTypes, fetchObject } from "./request"
|
||||
|
||||
|
@ -14,6 +14,7 @@ export default (req: Request): Response | Promise<Response> | undefined => {
|
|||
|
||||
let match
|
||||
if(req.method === "GET" && (match = url.pathname.match(/^\/test\/?$/i))) return new Response("", { status: 204 })
|
||||
else if(req.method == "POST" && (match = url.pathname.match(/^\/rebuild\/?$/i))) return rebuild(req)
|
||||
else if(req.method == "POST" && (match = url.pathname.match(/^\/create\/?$/i))) return create(req)
|
||||
else if(req.method == "POST" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return follow(req, match[1])
|
||||
else if(req.method == "DELETE" && (match = url.pathname.match(/^\/follow\/([^\/]+)\/?$/i))) return unfollow(req, match[1])
|
||||
|
@ -41,6 +42,12 @@ const checkAuth = (headers: Headers): Boolean => {
|
|||
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD
|
||||
}
|
||||
|
||||
// rebuild the 11ty static pages
|
||||
export const rebuild = async(req:Request):Promise<Response> => {
|
||||
await db.rebuild()
|
||||
return new Response("", { status: 201 })
|
||||
}
|
||||
|
||||
// create an activity
|
||||
const create = async (req:Request, inReplyTo:string|null = null):Promise<Response> => {
|
||||
const body = await req.json()
|
||||
|
|
37
src/db.ts
37
src/db.ts
|
@ -1,21 +1,16 @@
|
|||
import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, ACTIVITY_PATH, ACTOR, BASE_URL, DATA_PATH, POSTS_PATH } from "./env";
|
||||
import { ACTIVITY_INBOX_PATH, ACTIVITY_OUTBOX_PATH, ACTOR, 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";
|
||||
const matter = require('gray-matter')
|
||||
const Eleventy = require("@11ty/eleventy")
|
||||
|
||||
export async function doActivity(activity:any, object_id:string|null|undefined) {
|
||||
if(activity.type === "Create" && activity.object) {
|
||||
if(!object_id) object_id = new Date(activity.object.published).getTime().toString(16)
|
||||
const file = Bun.file(path.join(POSTS_PATH, `${object_id}.md`))
|
||||
const { content, published, id, attributedTo } = activity.object
|
||||
//TODO: add appropriate content for different types (e.g. like-of, etc)
|
||||
await Bun.write(file, matter.stringify(content || "", { id, published, attributedTo }))
|
||||
const activityFile = Bun.file(path.join(ACTIVITY_PATH, `${object_id}.activity.json`))
|
||||
await Bun.write(activityFile, JSON.stringify(activity))
|
||||
}
|
||||
// rebuild the 11ty static pages
|
||||
export async function rebuild() {
|
||||
console.info(`Building 11ty from ${CONTENT_PATH}, to ${STATIC_PATH}`)
|
||||
await new Eleventy(CONTENT_PATH, STATIC_PATH, { configPath: '.eleventy.js' }).write()
|
||||
}
|
||||
|
||||
export async function createInboxActivity(activity:any, object_id:any) {
|
||||
|
@ -82,6 +77,7 @@ export async function createPost(post_object:any, object_id:string) {
|
|||
if(inReplyTo) data.inReplyTo = idsFromValue(inReplyTo).at(0)
|
||||
await Bun.write(file, matter.stringify((reply_content || "") + (content || ""), data))
|
||||
}
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function getPost(id:string) {
|
||||
|
@ -95,7 +91,7 @@ export async function getPost(id:string) {
|
|||
}
|
||||
|
||||
export async function getPostByURL(url_id:string) {
|
||||
if(!url_id || !url_id.startsWith(ACTOR + '/post/')) return null
|
||||
if(!url_id || !url_id.startsWith(ACTOR + '/posts/')) return null
|
||||
const match = url_id.match(/\/([0-9a-f]+)\/?$/)
|
||||
const local_id = match ? match[1] : url_id
|
||||
return await getPost(local_id)
|
||||
|
@ -103,28 +99,26 @@ export async function getPostByURL(url_id:string) {
|
|||
|
||||
export async function deletePost(id:string) {
|
||||
unlinkSync(path.join(POSTS_PATH, id + '.md'))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function listPosts() {
|
||||
return await Promise.all((await readdir(POSTS_PATH)).filter(v => v.endsWith('.md')).map(async filename => await getPost(filename.slice(0, -3))))
|
||||
}
|
||||
|
||||
export async function getActivity(id:string) {
|
||||
const file = Bun.file(path.join(ACTIVITY_PATH, `${id}.activity.json`))
|
||||
return await file.json()
|
||||
}
|
||||
|
||||
export async function createFollowing(handle:string, id:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `following.json`))
|
||||
const following_list = await file.json() as Array<any>
|
||||
if(!following_list.find(v => v.id === id || v.handle === handle)) following_list.push({id, handle, createdAt: new Date().toISOString()})
|
||||
await Bun.write(file, JSON.stringify(following_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function deleteFollowing(handle:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `following.json`))
|
||||
const following_list = await file.json() as Array<any>
|
||||
await Bun.write(file, JSON.stringify(following_list.filter(v => v.handle !== handle)))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function getFollowing(handle:string) {
|
||||
|
@ -144,6 +138,7 @@ export async function acceptFollowing(handle:string) {
|
|||
const following = following_list.find(v => v.handle === handle)
|
||||
if(following) following.accepted = new Date().toISOString()
|
||||
await Bun.write(file, JSON.stringify(following_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function createFollower(actor:string, id:string) {
|
||||
|
@ -151,12 +146,14 @@ export async function createFollower(actor:string, id:string) {
|
|||
const followers_list = await file.json() as Array<any>
|
||||
if(!followers_list.find(v => v.id === id || v.actor === actor)) followers_list.push({id, actor, createdAt: new Date().toISOString()})
|
||||
await Bun.write(file, JSON.stringify(followers_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function deleteFollower(actor:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `followers.json`))
|
||||
const followers_list = await file.json() as Array<any>
|
||||
await Bun.write(file, JSON.stringify(followers_list.filter(v => v.actor !== actor)))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function getFollower(actor:string) {
|
||||
|
@ -175,12 +172,14 @@ export async function createLiked(object_id:string, id:string) {
|
|||
const liked_list = await file.json() as Array<any>
|
||||
if(!liked_list.find(v => v.object_id === object_id)) liked_list.push({id, object_id, createdAt: new Date().toISOString()})
|
||||
await Bun.write(file, JSON.stringify(liked_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function deleteLiked(object_id:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `liked.json`))
|
||||
const liked_list = await file.json() as Array<any>
|
||||
await Bun.write(file, JSON.stringify(liked_list.filter(v => v.object_id !== object_id)))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function listLiked() {
|
||||
|
@ -193,12 +192,14 @@ export async function createDisliked(object_id:string, id:string) {
|
|||
const disliked_list = await file.json() as Array<any>
|
||||
if(!disliked_list.find(v => v.object_id === object_id)) disliked_list.push({id, object_id, createdAt: new Date().toISOString()})
|
||||
await Bun.write(file, JSON.stringify(disliked_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function deleteDisliked(object_id:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `disliked.json`))
|
||||
const disliked_list = await file.json() as Array<any>
|
||||
await Bun.write(file, JSON.stringify(disliked_list.filter(v => v.object_id !== object_id)))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function listDisliked() {
|
||||
|
@ -211,12 +212,14 @@ export async function createShared(object_id:string, id:string) {
|
|||
const shared_list = await file.json() as Array<any>
|
||||
if(!shared_list.find(v => v.object_id === object_id)) shared_list.push({id, object_id, createdAt: new Date().toISOString()})
|
||||
await Bun.write(file, JSON.stringify(shared_list))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function deleteShared(object_id:string) {
|
||||
const file = Bun.file(path.join(DATA_PATH, `shared.json`))
|
||||
const shared_list = await file.json() as Array<any>
|
||||
await Bun.write(file, JSON.stringify(shared_list.filter(v => v.object_id !== object_id)))
|
||||
rebuild()
|
||||
}
|
||||
|
||||
export async function listShared() {
|
||||
|
|
21
src/env.ts
21
src/env.ts
|
@ -2,16 +2,16 @@ 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 = Bun.env.ACCOUNT || "activitypub"
|
||||
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 || "";
|
||||
|
||||
// get the hostname (`PROJECT_DOMAIN` is set via glitch, but since we're using Bun now, this won't matter)
|
||||
export const HOSTNAME = /*(Bun.env.PROJECT_DOMAIN && `${Bun.env.PROJECT_DOMAIN}.glitch.me`) ||*/ Bun.env.HOSTNAME || "localhost"
|
||||
export const NODE_ENV = Bun.env.NODE_ENV || "development"
|
||||
export const PORT = Bun.env.PORT || "3000"
|
||||
export const HOSTNAME = /*(process.env.PROJECT_DOMAIN && `${process.env.PROJECT_DOMAIN}.glitch.me`) ||*/ process.env.HOSTNAME || "localhost"
|
||||
export const NODE_ENV = process.env.NODE_ENV || "development"
|
||||
export const PORT = process.env.PORT || "3000"
|
||||
|
||||
export const BASE_URL = (HOSTNAME === "localhost" ? "http://" : "https://") + HOSTNAME
|
||||
|
||||
|
@ -32,9 +32,20 @@ export const PRIVATE_KEY =
|
|||
(keypair && forge.pki.privateKeyToPem(keypair.privateKey)) || //keypair?.privateKey.export({ type: "pkcs8", format: "pem" }) ||
|
||||
""
|
||||
|
||||
export const STATIC_PATH = path.join('.', '_site')
|
||||
export const CONTENT_PATH = path.join('.', '_content')
|
||||
export const POSTS_PATH = path.join(CONTENT_PATH, "posts")
|
||||
export const ACTIVITY_PATH = path.join(CONTENT_PATH, "posts")
|
||||
export const DATA_PATH = path.join(CONTENT_PATH, "_data")
|
||||
export const ACTIVITY_INBOX_PATH = path.join(DATA_PATH, "_inbox")
|
||||
export const ACTIVITY_OUTBOX_PATH = path.join(DATA_PATH, "_outbox")
|
||||
|
||||
export const DEFAULT_DOCUMENTS = process.env.DEFAULT_DOCUMENTS || [
|
||||
'index.html',
|
||||
'index.shtml',
|
||||
'index.htm',
|
||||
'Index.html',
|
||||
'Index.shtml',
|
||||
'Index.htm',
|
||||
'default.html',
|
||||
'default.htm'
|
||||
]
|
33
src/index.ts
33
src/index.ts
|
@ -1,7 +1,10 @@
|
|||
import { ACCOUNT, ACTOR, HOSTNAME, PORT } from "./env";
|
||||
import { ACCOUNT, ACTOR, DEFAULT_DOCUMENTS, HOSTNAME, PORT, STATIC_PATH } from "./env";
|
||||
import admin from './admin'
|
||||
import activitypub from "./activitypub";
|
||||
import { fetchObject } from "./request";
|
||||
import path from "path"
|
||||
import { BunFile } from "bun";
|
||||
const { stat } = require("fs").promises
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
|
@ -20,7 +23,7 @@ const server = Bun.serve({
|
|||
return fetchObject(ACTOR, object_url)
|
||||
}
|
||||
|
||||
return admin(req) || activitypub(req) || new Response("How did we get here?", { status: 404 })
|
||||
return admin(req) || activitypub(req) || staticFile(req)
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -46,4 +49,30 @@ const webfinger = async (req: Request, resource: string | null) => {
|
|||
}, { headers: { "content-type": "application/activity+json" }})
|
||||
}
|
||||
|
||||
const getDefaultDocument = async(base_path: string) => {
|
||||
for(const d of DEFAULT_DOCUMENTS){
|
||||
const filePath = path.join(base_path, d)
|
||||
const file = Bun.file(filePath)
|
||||
if(await file.exists()) return file
|
||||
}
|
||||
}
|
||||
|
||||
const staticFile = async (req:Request): Promise<Response> => {
|
||||
try{
|
||||
const url = new URL(req.url)
|
||||
const filePath = path.join(STATIC_PATH, url.pathname)
|
||||
let file:BunFile|undefined = Bun.file(filePath)
|
||||
// if the file doesn't exist, attempt to get the default document for the path
|
||||
if(!(await file.exists())) file = await getDefaultDocument(filePath)
|
||||
|
||||
if(file && await file.exists()) return new Response(file)
|
||||
// if the file still doesn't exist, just return a 404
|
||||
else return new Response("", { status: 404 })
|
||||
}
|
||||
catch(err) {
|
||||
console.error(err)
|
||||
return new Response("", { status: 404 })
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Listening on http://localhost:${server.port} ...`);
|
|
@ -68,7 +68,7 @@ export default async function outbox(activity:any):Promise<Response> {
|
|||
}
|
||||
|
||||
async function create(activity:any, id:string) {
|
||||
activity.object.id = activity.object.url = `${ACTOR}/post/${id}`
|
||||
activity.object.id = activity.object.url = `${ACTOR}/posts/${id}`
|
||||
await db.createPost(activity.object, id)
|
||||
return true
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue