import type { CollectionEntry } from "astro:content"; import { Blog, Entity, type Entry, type MicroEntry, type OriginalEntry, type TranslationEntry, } from "./schemas.ts"; import { getEntries, type z } from "astro:content"; import { defined, get, identity } from "../../utils/anonymous.ts"; import { createKeyFromArmor } from "../pgp/create.ts"; import { getUserIDsFromKey } from "../pgp/user.ts"; import type { UserIDPacket } from "openpgp"; import { getCollection } from "astro:content"; import { GetStaticPaths, GetStaticPathsItem, GetStaticPathsResult, } from "astro"; import { getEntry } from "astro:content"; export function getLastUpdate({ data }: CollectionEntry<"blog">): Date { return data.dateUpdated ?? data.dateCreated; } export const sortLastCreated = ( { data: a }: CollectionEntry<"blog">, { data: b }: CollectionEntry<"blog">, ): number => b.dateCreated - a.dateCreated; export const sortFirstCreated = ( a: CollectionEntry<"blog">, b: CollectionEntry<"blog">, ): number => sortLastCreated(b, a); export const sortLastUpdated = ( { data: a }: CollectionEntry<"blog">, { data: b }: CollectionEntry<"blog">, ): number => (b.dateUpdated ?? b.dateCreated) - (a.dateUpdated ?? a.dateCreated); export const sortFirstUpdated = ( a: CollectionEntry<"blog">, b: CollectionEntry<"blog">, ): number => sortLastUpdated(b, a); export async function getSigners( { data }: CollectionEntry<"blog">, ): Promise<{ id: string; entity: CollectionEntry<"entity">; role: z.infer["signers"][number]["role"] | undefined; }[]> { const post = Blog.parse(data); return await getEntries(post.signers.map(get("entity"))).then((x) => x.map((x) => ({ id: x.id, entity: x, role: post.signers?.find((y) => y.entity.id === x.id)?.role, })).filter(({ role }) => defined(role)) ); } export async function getFirstAuthorEmail( blog: CollectionEntry<"blog">, ): Promise { const signers = await getSigners(blog); const emails = await Promise.all( signers.filter(({ role }) => role === "author").map(async ({ entity }) => { const { publickey } = Entity.parse(entity.data); const key = await createKeyFromArmor(publickey.armor); const users = getUserIDsFromKey(undefined, key); return users.map(get("email")).filter(Boolean)?.[0]; }), ); return emails.filter(defined)?.[0]; } export async function getFirstUserID( blog: CollectionEntry<"blog">, ): Promise< (Partial & { entity: string; website: string | undefined }) > { const signers = await getSigners(blog); const userIDs = await Promise.all( signers.filter(({ role }) => role === "author").map( async ({ id, entity }) => { const { publickey, websites } = Entity.parse(entity.data); const website = websites?.[0]; const key = await createKeyFromArmor(publickey.armor); const users = getUserIDsFromKey(undefined, key); return users.map((user) => { return { ...user, entity: id, website }; })?.[0]; }, ), ); return userIDs.filter(defined)?.[0]; } export async function fromPosts( filter: (entry: CollectionEntry<"blog">) => entry is T, predicate: (entries: T[]) => U = identity as (entries: T[]) => U, ): Promise { const entries = await getCollection<"blog", T>("blog", filter); return predicate(entries); } export const isEntry = ( _entry: CollectionEntry<"blog">, ): _entry is Entry => true; export const isOriginal = ( entry: CollectionEntry<"blog">, ): entry is OriginalEntry => entry.data.kind === "original"; export const isTranslation = ( entry: CollectionEntry<"blog">, ): entry is TranslationEntry => entry.data.kind === "translation"; export const isMicro = ( entry: CollectionEntry<"blog">, ): entry is MicroEntry => entry.data.kind === "micro"; export async function getTranslationOriginal( translation: TranslationEntry, ): Promise { if (!isTranslation(translation)) { throw new Error(); } return await getEntry(translation.data.translationOf); } export const datePaths = (async (): Promise< { params: { date?: string }; props: { posts: OriginalEntry[]; next?: string; previous?: string; years: number[]; months: number[]; days?: number[]; }; }[] > => { const posts = await fromPosts(isEntry, identity); const archive = { years: new Set(), monthsByYear: new Map>(), daysByMonth: new Map>(), postsByDate: new Map(), sortedDates: [] as string[], }; const getYMD = (date: Date) => { const y = date.getFullYear(); const m = date.getMonth() + 1; const d = date.getDate(); return { y, m, d }; }; for (const post of posts) { const { y, m, d } = getYMD(post.data.dateCreated); archive.years.add(y); const months = archive.monthsByYear.get(y.toString()); if (months === undefined) { archive.monthsByYear.set(y.toString(), new Set([m])); } else { months.add(m); } const ym = `${y}/${String(m).padStart(2, "0")}`; const days = archive.daysByMonth.get(ym); if (days === undefined) { archive.daysByMonth.set(ym, new Set([d])); } else { days.add(d); } const ymd = `${ym}/${String(d).padStart(2, "0")}`; const posts = archive.postsByDate.get(ymd); if (posts === undefined) { archive.postsByDate.set(ymd, [post]); } else { posts.push(post); } } archive.sortedDates = Array.from(archive.postsByDate.keys()).sort(); const paths: { params: { date?: string }; props: { posts: OriginalEntry[]; next?: string; previous?: string; years: number[]; months: number[]; days?: number[]; }; }[] = [] satisfies GetStaticPathsItem[]; const sortedYears = Array.from(archive.years).sort(); const lastYear = Math.max(...sortedYears.map(Number)); paths.push({ params: { date: undefined }, props: { posts: posts.filter((p) => p.data.dateCreated.getFullYear() === lastYear), next: undefined, previous: sortedYears?.[sortedYears.length - 2]?.toString(), years: sortedYears, months: Array.from(archive.monthsByYear.get(lastYear.toString()) ?? []), }, }); for (const y of sortedYears) { const yearPosts = posts.filter((p) => p.data.dateCreated.getFullYear() === Number(y) ); const idx = sortedYears.indexOf(y); paths.push({ params: { date: y.toString() }, props: { posts: yearPosts, next: sortedYears?.[idx + 1]?.toString(), previous: sortedYears?.[idx - 1]?.toString(), years: sortedYears, months: Array.from(archive.monthsByYear.get(y.toString()) ?? []), }, }); } const allMonths = Array.from(archive.monthsByYear.entries()) .flatMap(([year, mset]) => Array.from(mset).map((m) => `${year}/${String(m).padStart(2, "0")}`) ) .sort(); for (const [y, months] of archive.monthsByYear) { const sortedMonths = Array.from(months).sort(); for (const m of sortedMonths) { const monthPosts = posts.filter((p) => { const d = p.data.dateCreated; return ( d.getFullYear() === Number(y) && d.getMonth() + 1 === m ); }); const ym = `${y}/${String(m).padStart(2, "0")}`; const idx = allMonths.indexOf(ym); paths.push({ params: { date: ym }, props: { posts: monthPosts, next: allMonths?.[idx + 1], previous: allMonths?.[idx - 1], years: sortedYears, months: Array.from(months).sort(), days: Array.from(archive.daysByMonth.get(ym) ?? []).sort(), }, }); } } for (let i = 0; i < archive.sortedDates.length; i++) { const ymd = archive.sortedDates[i]; const [y, m] = ymd.split("/"); paths.push({ params: { date: ymd }, props: { posts: archive.postsByDate.get(ymd) ?? [], next: archive.sortedDates?.[i + 1], previous: archive.sortedDates?.[i - 1], years: sortedYears, months: Array.from(archive.monthsByYear.get(y) ?? []).sort(), days: Array.from(archive.daysByMonth.get(`${y}/${m}`) ?? []).sort(), }, }); } return paths; }) satisfies GetStaticPaths;