import type { CollectionEntry } from "astro:content"; import { Blog, Entity, ENTITY_TYPES, type EntityTypesEnum, type Entry, type LICENSES, type MicroEntry, Original, type OriginalEntry, Translation, type TranslationEntry, } from "./schemas.ts"; import type { z } from "astro:content"; import { defined, get, identity, transform } from "../../utils/anonymous.ts"; import { createKeyFromArmor } from "../pgp/create.ts"; import { getUserIDsFromKey } from "../pgp/user.ts"; import type { UserIDPacket } from "openpgp"; import readingTime from "reading-time"; import { getCollection } from "astro:content"; import { getEntry } from "astro:content"; import { getEntries } from "astro:content"; import { listYearsWithRanges } from "../../utils/datetime.ts"; 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 function getSignersIDs( { data }: CollectionEntry<"blog">, ): Record, string[]> { const { signers } = Blog.parse(data); const id = ({ entity }: typeof signers[number]) => entity.id; return Object.fromEntries( ENTITY_TYPES.map((x) => [x, signers.filter((s) => s.role === x).map(id)]), ) as ReturnType; } export async function getSigners( { data }: CollectionEntry<"blog">, ): Promise<{ entity: CollectionEntry<"entity">; role?: z.infer["signers"][number]["role"]; }[]> { const post = Blog.parse(data); return await Promise.all( post.signers.map(async ({ entity, role }) => ({ entity: await getEntry(entity), 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 ({ 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: 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 function licenseNotice( license: typeof LICENSES[number], { title, holders, years }: { title: string; holders: { name: string; email?: string }[]; years: number[]; }, locale?: Intl.LocalesArgument, ): string | undefined { const list = new Intl.ListFormat(locale, { style: "narrow", type: "unit", }); switch (license) { case "CC0": return `${title} by ${ list.format(holders.map(get("name"))) } is marked CC0 1.0 Universal. To view a copy of this mark, visit https://creativecommons.org/publicdomain/zero/1.0/`; case "CC-BY": case "CC-BY-SA": case "CC-BY-ND": case "CC-BY-NC": case "CC-BY-NC-SA": case "CC-BY-NC-ND": return `${title} © ${ listYearsWithRanges(years, { locale, list: { type: "unit", style: "narrow" }, }) } by ${ list.format(holders.map(get("name"))) } is licensed under Creative Commons ${ license.slice(3).replace("BY", "Attribution").replace( "SA", "ShareAlike", ).replace("ND", "NoDerivatives").replace("NC", "NonCommercial") } 4.0 International. To view a copy of this license, visit https://creativecommons.org/licenses/${ license.slice(3).toLowerCase() }/4.0/`; case "WTFPL": return `Copyright (C) ${ listYearsWithRanges(years, { locale, list: { type: "unit", style: "narrow" }, }) } ${ list.format(holders.map(({ name, email }) => name + (email !== undefined ? ` ${email}` : "") )) }`; case "public domain": undefined; } } export function licenseURL(license: typeof LICENSES[number]): URL | undefined { switch (license) { case "CC0": case "CC-BY": case "CC-BY-SA": case "CC-BY-ND": case "CC-BY-NC": case "CC-BY-NC-SA": case "CC-BY-NC-ND": return new URL( `https://creativecommons.org/licenses/${ license.slice(3).toLowerCase() }/4.0/`, ); case "WTFPL": return new URL("http://www.wtfpl.net/"); case "public domain": return undefined; } }