activitypress/src/outbox.ts

103 lines
3.9 KiB
TypeScript

import { readFileSync } from "node:fs";
import db from "./db.js";
import { z } from "zod";
import { getId as getUserId, getActor } from "./user.js";
import { routes } from "./index.js";
import { getById as getArticleById } from "./article.js";
export const zRawOutboxRecord = z.object({
id: z.number().min(0),
users_id: z.number().min(0),
verb: z.string().regex(/^[a-zA-Z]+$/).min(1),
articles_id: z.number().min(0),
created_at: z.date(),
updated_at: z.union([z.date(), z.null()]),
slug: z.optional(z.string()),
file: z.optional(z.string()),
title: z.optional(z.string())
});
export type RawOutboxRecord = z.infer<typeof zRawOutboxRecord>;
export const get = async (nickname: string): Promise<Record<string, any> | null> => {
const userId = await getUserId(nickname);
if (userId) {
const actor = getActor(nickname);
const ret = db<RawOutboxRecord>("outboxes")
.select("outboxes.*", "slug", "file", "title")
.join("articles", "outboxes.articles_id", "=", "articles.id")
.where("outboxes.users_id", userId)
.orderBy("created_at", "desc")
.then((recs: RawOutboxRecord[]) => {
const activities: Record<string, any>[] = recs.map((rec) => {
const published = typeof rec.created_at === "number" ? new Date(rec.created_at) : rec.created_at;
const canonicalUrl = `https://${process.env.blog_host}/${rec.slug}.html`;
const context = `https://${process.env.blog_host}/c/${rec.articles_id}`;
const followers = `https://${process.env.blog_host}${routes.followers.replace(":actor", nickname)}`;
const activity: Record<string, any> = {
id: `https://${process.env.blog_host}${routes.activity.replace(":id", rec.id.toString())}`,
"@context": "https://www.w3.org/ns/activitystreams",
type: rec.verb,
actor,
context,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [followers],
published
};
const objectId = `https://${process.env.blog_host}${routes.object.replace(":id", rec.articles_id.toString())}`;
const content = readFileSync(rec.file as string, "utf-8");
activity.object = {
id: objectId,
actor,
attributedTo: actor,
type: "Article",
context,
content,
to: ["https://www.w3.org/ns/activitystreams#Public"],
cc: [followers],
url: canonicalUrl,
mediaType: "text/markdown",
published
};
return activity;
});
const collection = {
type: "OrderedCollectionPage",
context: "https://www.w3.org/ns/activitystreams",
id: `https://${process.env.blog_host}${routes.outbox.replace(":actor", nickname)}`,
orderedItems: activities
};
return collection;
});
return ret;
}
else {
return null;
}
};
export const add = async (articleId: number): Promise<number> => {
const article = await getArticleById(articleId);
if (article) {
return await db("outboxes").insert({
users_id: article.id,
verb: "Create",
articles_id: articleId,
created_at: article.created_at
})
.returning("id")
.then(([{ id: id }]: { id: number }[]) => id)
}
else throw "failed to add to outbox";
};