import { countFollowers, countFollowing, countStatuses, getAdminUser, getStatus, getUserById, getUserByIdOrUsername, listMediaForStatus, listProfileFields } from "./db"; import { html } from "./http"; import type { Media, Status, User } from "./types"; import { actorUrl, baseUrl, escapeHtml, hostFromBaseUrl, mediaUrl, objectUrl, profileUrl, statusUrl } from "./util"; type FeedEntry = { status: Status; user: User; media: Media[]; }; type ProfileStats = { followersCount: number; followingCount: number; statusesCount: number; fields: { name: string; value: string }[]; }; export async function homePage(env: Env): Promise { const admin = await getAdminUser(env); const [stats, feed] = await Promise.all([ loadProfileStats(env, admin.id), loadRecentFeed(env, 20) ]); return html(renderPage({ title: env.INSTANCE_NAME, description: `Recent public posts on ${hostFromBaseUrl(env)}`, canonical: baseUrl(env), alternate: `${baseUrl(env)}/nodeinfo/2.0`, alternateType: "application/json", ogType: "website", body: renderHomeBody(env, admin, stats, feed) })); } export async function profilePage(env: Env, key: string): Promise { const user = await getUserByIdOrUsername(env, key); if (!user) return html(renderNotFoundPage(env, key), 404); const [stats, feed] = await Promise.all([ loadProfileStats(env, user.id), loadFeedForUser(env, user.id, 20) ]); return html(renderPage({ title: `${user.display_name || user.username} (@${user.username})`, description: plainText(user.note) || `Account for @${user.username}@${hostFromBaseUrl(env)}`, canonical: profileUrl(env, user), alternate: actorUrl(env, user), ogType: "profile", body: renderProfileBody(env, user, stats, feed) })); } export async function statusPage(env: Env, username: string, statusId: string): Promise { const user = await getUserByIdOrUsername(env, username); if (!user) return html(renderNotFoundPage(env, `${username}/${statusId}`), 404); const status = await getStatus(env, statusId); if (!status || status.user_id !== user.id || !isVisibleStatus(status)) { const tomb = await env.DB.prepare("SELECT id FROM deleted_statuses WHERE id = ? AND user_id = ?") .bind(statusId, user.id).first<{ id: string }>(); if (tomb) return html(renderDeletedStatusPage(env, statusId), 410); return html(renderNotFoundPage(env, `${username}/${statusId}`), 404); } const media = await listMediaForStatus(env, status.id); return html(renderPage({ title: statusTitle(user, status), description: plainText(status.summary || status.content).slice(0, 180), canonical: statusUrl(env, user, status.id), alternate: objectUrl(env, status.id), body: renderStatusBody(env, user, status, media, { includeProfile: true }) })); } export async function objectPage(env: Env, objectId: string): Promise { const status = await getStatus(env, objectId); if (!status || !isVisibleStatus(status)) { const tomb = await env.DB.prepare("SELECT deleted_at FROM deleted_statuses WHERE id = ?").bind(objectId).first<{ deleted_at: string }>(); if (tomb) return html(renderDeletedStatusPage(env, objectId), 410); return html(renderNotFoundPage(env, objectId), 404); } const user = await getUserById(env, status.user_id); if (!user) return html(renderNotFoundPage(env, objectId), 404); const media = await listMediaForStatus(env, status.id); return html(renderPage({ title: statusTitle(user, status), description: plainText(status.summary || status.content).slice(0, 180), canonical: statusUrl(env, user, status.id), alternate: objectUrl(env, status.id), body: renderStatusBody(env, user, status, media, { includeProfile: true }) })); } async function loadProfileStats(env: Env, userId: string): Promise { const [followersCount, followingCount, statusesCount, fields] = await Promise.all([ countFollowers(env, userId), countFollowing(env, userId), countStatuses(env, userId), listProfileFields(env, userId) ]); return { followersCount, followingCount, statusesCount, fields }; } async function loadRecentFeed(env: Env, limit: number): Promise { const rows = await env.DB.prepare( "SELECT * FROM statuses WHERE visibility IN ('public', 'unlisted') ORDER BY created_at DESC LIMIT ?" ).bind(limit).all(); return loadFeedEntries(env, rows.results); } async function loadFeedForUser(env: Env, userId: string, limit: number): Promise { const rows = await env.DB.prepare( "SELECT * FROM statuses WHERE user_id = ? AND visibility IN ('public', 'unlisted') ORDER BY created_at DESC LIMIT ?" ).bind(userId, limit).all(); return loadFeedEntries(env, rows.results); } async function loadFeedEntries(env: Env, statuses: Status[]): Promise { const entries = await Promise.all(statuses.map(async (status) => { const user = await getUserById(env, status.user_id); if (!user) return null; const media = await listMediaForStatus(env, status.id); return { status, user, media }; })); return entries.filter((entry): entry is FeedEntry => Boolean(entry)); } function renderPage(options: { title: string; description: string; canonical: string; alternate: string; alternateType?: string; ogType?: string; body: string; }): string { return ` ${escapeHtml(options.title)} ${options.body} `; } function renderHomeBody(env: Env, user: User, stats: ProfileStats, feed: FeedEntry[]): string { const avatar = user.avatar_r2_key ? mediaUrl(env, user.avatar_r2_key) : `${baseUrl(env)}/avatar.png`; const header = user.header_r2_key ? mediaUrl(env, user.header_r2_key) : `${baseUrl(env)}/header.png`; return `

Instance

${escapeHtml(env.INSTANCE_NAME)}

${escapeHtml(hostFromBaseUrl(env))}

${escapeHtml(user.display_name || user.username)}

@${escapeHtml(user.username)}@${escapeHtml(hostFromBaseUrl(env))}

${renderNote(user.note)} ${renderFields(stats.fields)}
${renderStat(stats.statusesCount, "Posts")} ${renderStat(stats.followingCount, "Following")} ${renderStat(stats.followersCount, "Followers")}
${renderFeedSection(env, feed, "Recent public posts")}
`; } function renderProfileBody(env: Env, user: User, stats: ProfileStats, feed: FeedEntry[]): string { const avatar = user.avatar_r2_key ? mediaUrl(env, user.avatar_r2_key) : `${baseUrl(env)}/avatar.png`; const header = user.header_r2_key ? mediaUrl(env, user.header_r2_key) : `${baseUrl(env)}/header.png`; return `
Home / ActivityPub

${escapeHtml(user.display_name || user.username)}

@${escapeHtml(user.username)}@${escapeHtml(hostFromBaseUrl(env))}

${renderNote(user.note)} ${renderFields(stats.fields)}
${renderStat(stats.statusesCount, "Posts")} ${renderStat(stats.followingCount, "Following")} ${renderStat(stats.followersCount, "Followers")}
${renderFeedSection(env, feed, "Posts")}
`; } function renderStatusBody(env: Env, user: User, status: Status, media: Media[], options: { includeProfile: boolean }): string { const avatar = user.avatar_r2_key ? mediaUrl(env, user.avatar_r2_key) : `${baseUrl(env)}/avatar.png`; const header = user.header_r2_key ? mediaUrl(env, user.header_r2_key) : `${baseUrl(env)}/header.png`; const permalink = statusUrl(env, user, status.id); return `
Home / @${escapeHtml(user.username)} / ActivityPub

${escapeHtml(user.display_name || user.username)}

@${escapeHtml(user.username)}@${escapeHtml(hostFromBaseUrl(env))}

${status.summary ? `

${escapeHtml(status.summary)}

` : ""} ${options.includeProfile ? renderStatusContent(status) : ""} ${renderMediaGrid(env, media)}
${escapeHtml(status.visibility)} Permalink
`; } function renderFeedSection(env: Env, feed: FeedEntry[], title: string): string { return `

${escapeHtml(title)}

${feed.length ? `${formatCount(feed.length)} items` : "No items"}

${feed.length ? feed.map((entry) => renderFeedEntry(env, entry)).join("") : `

No public posts yet.

`}
`; } function renderFeedEntry(env: Env, entry: FeedEntry): string { const { status, user, media } = entry; const permalink = statusUrl(env, user, status.id); const avatar = user.avatar_r2_key ? mediaUrl(env, user.avatar_r2_key) : `${baseUrl(env)}/avatar.png`; return ` `; } function renderStatusContent(status: Status): string { return `
${sanitizeStatusHtml(status.content)}
`; } function renderNote(note: string): string { const text = plainText(note); if (!text) return ""; return `
${escapeHtml(text).replace(/\n/g, "
")}
`; } function renderFields(fields: { name: string; value: string }[]): string { const items = fields.filter((field) => field.name || field.value); if (items.length === 0) return ""; return `
${items.map((field) => `
${escapeHtml(field.name)}
${escapeHtml(plainText(field.value))}
`).join("")}
`; } function renderStat(value: number, label: string): string { return `
${formatCount(value)}
${escapeHtml(label)}
`; } function renderMediaGrid(env: Env, media: Media[]): string { if (media.length === 0) return ""; return `
${media.map((item) => { const url = mediaUrl(env, item.r2_key); const alt = item.description?.trim() || "Image attachment"; if (item.mime_type.startsWith("image/")) { return ` ${escapeHtml(alt)} `; } return `${escapeHtml(item.description || item.mime_type)}`; }).join("")}
`; } function renderNotFoundPage(env: Env, key: string): string { return renderPage({ title: "Not found", description: `${key} was not found on ${hostFromBaseUrl(env)}`, canonical: baseUrl(env), alternate: `${baseUrl(env)}/.well-known/webfinger`, body: `

404

Not found

${escapeHtml(key)} on ${escapeHtml(hostFromBaseUrl(env))}

` }); } function renderDeletedStatusPage(env: Env, objectId: string): string { return renderPage({ title: "Post removed", description: `This post has been removed.`, canonical: baseUrl(env), alternate: baseUrl(env), body: `

410

Post removed

${escapeHtml(objectId)}

` }); } function statusTitle(user: User, status: Status): string { const content = plainText(status.summary || status.content).slice(0, 70); return content ? `${user.display_name || user.username} - ${content}` : `${user.display_name || user.username}`; } function formatDate(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("en", { year: "numeric", month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(date); } function formatCount(value: number): string { return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value); } function plainText(value: string): string { return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); } function isVisibleStatus(status: Status): boolean { return status.visibility === "public" || status.visibility === "unlisted"; } function sanitizeStatusHtml(html: string): string { return html .replace(/<\/?(script|style|iframe|object|embed|link|meta)[^>]*>/gi, "") .replace(/\sstyle\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "") .replace(/\son[a-z-]+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "") .replace(/\s(href|src)\s*=\s*("|\')\s*javascript:[^"\']*\2/gi, " $1=\"#\""); } function siteCss(): string { return ` :root { color-scheme: light; --bg: #f5f7f9; --surface: #ffffff; --surface-alt: #eef2f5; --ink: #1f2429; --muted: #68727d; --line: #d7dee5; --accent: #0f766e; --accent-strong: #0b5d56; --link: #1d4ed8; --warn: #9a3412; --shadow: 0 14px 36px rgba(31, 36, 41, .08); } * { box-sizing: border-box; } html { scroll-behavior: smooth; } body { margin: 0; min-height: 100vh; background: var(--bg); color: var(--ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; } a { color: var(--link); text-decoration-thickness: .08em; text-underline-offset: .18em; } a:hover { text-decoration-thickness: .12em; } a:focus-visible, button:focus-visible { outline: 3px solid rgba(15, 118, 110, .35); outline-offset: 3px; } img { max-width: 100%; } .shell { width: min(100%, 920px); margin: 0 auto; padding: 20px 16px 56px; } .shell-home, .shell-profile, .shell-status { display: grid; gap: 16px; } .instance-bar, .crumbs, .section-head { display: flex; align-items: end; justify-content: space-between; gap: 12px; } .instance-bar, .crumbs { padding: 2px 2px 0; } .instance-bar h1, .message-panel h1 { margin: 0; font-size: 2.1rem; line-height: 1.05; letter-spacing: 0; } .eyebrow { margin: 0 0 4px; color: var(--muted); font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; } .subtle { margin: 6px 0 0; color: var(--muted); overflow-wrap: anywhere; } .text-link { display: inline-flex; align-items: center; gap: 6px; padding: 8px 0; font-weight: 650; } .panel, .status-card, .message-panel { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; box-shadow: var(--shadow); overflow: hidden; } .cover { aspect-ratio: 3 / 1; background: #dce3e8; border-bottom: 1px solid var(--line); } .cover img { display: block; width: 100%; height: 100%; object-fit: cover; } .identity { display: flex; align-items: end; gap: 16px; padding: 0 24px; transform: translateY(-34px); margin-bottom: -16px; } .avatar { width: 104px; height: 104px; border-radius: 8px; border: 5px solid var(--surface); background: var(--surface); object-fit: cover; box-shadow: 0 8px 22px rgba(31, 36, 41, .16); } .avatar-link { display: block; flex: none; border-radius: 8px; } .name-block { min-width: 0; padding-bottom: 10px; } .name-block h1, .name-block h2 { margin: 0; font-size: 1.95rem; line-height: 1.05; } .handle { margin: 8px 0 0; color: var(--muted); font-size: 1rem; overflow-wrap: anywhere; } .note { padding: 0 24px 18px; overflow-wrap: anywhere; } .fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; margin: 0 24px 22px; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--line); } .fields div { min-width: 0; padding: 12px 14px; background: var(--surface); } .fields dt { color: var(--muted); font-size: .75rem; font-weight: 700; text-transform: uppercase; } .fields dd { margin: 4px 0 0; overflow-wrap: anywhere; } .stats { display: grid; grid-template-columns: repeat(3, 1fr); margin: 0; border-top: 1px solid var(--line); } .stats div { padding: 16px 12px; text-align: center; } .stats div + div { border-left: 1px solid var(--line); } .stats dt { font-size: 1.2rem; font-weight: 800; color: var(--accent-strong); } .stats dd { margin: 2px 0 0; color: var(--muted); font-size: .85rem; } .feed { display: grid; gap: 14px; } .section-head { padding: 0 2px; } .section-head h2 { margin: 0; font-size: 1.2rem; } .section-head p { margin: 0; color: var(--muted); } .empty { margin: 0; padding: 24px; text-align: center; color: var(--muted); border: 1px dashed var(--line); border-radius: 8px; background: var(--surface); } .status-card { padding: 18px 20px; cursor: pointer; transition: border-color .16s ease, box-shadow .16s ease, transform .16s ease; } .status-card:hover { border-color: #b8c5cf; box-shadow: 0 18px 42px rgba(31, 36, 41, .11); } .status-card:active { transform: translateY(1px); } .status-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .author { display: inline-flex; align-items: center; gap: 12px; min-width: 0; color: inherit; text-decoration: none; } .author img { width: 44px; height: 44px; border-radius: 8px; object-fit: cover; flex: none; } .author strong, .author small { display: block; min-width: 0; } .author strong { font-size: .98rem; line-height: 1.1; } .author small { color: var(--muted); font-size: .8rem; overflow-wrap: anywhere; } .time-link { flex: none; color: var(--muted); font-size: .85rem; } .summary { margin: 14px 0 0; padding: 10px 12px; border-left: 3px solid var(--accent); background: #eef7f5; border-radius: 0 6px 6px 0; font-weight: 700; } .content { margin-top: 12px; overflow-wrap: anywhere; } .content p { margin: 0 0 12px; } .content p:last-child { margin-bottom: 0; } .content .mention, .content .hashtag { font-weight: 650; } .status-footer { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } .status-footer { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--line); color: var(--muted); font-size: .85rem; } .profile-compact > .summary, .profile-compact > .content, .profile-compact > .media-grid { margin-left: 24px; margin-right: 24px; } .profile-compact > .content { margin-top: 0; } .profile-compact > .status-footer { margin: 16px 24px 0; padding: 14px 0 18px; } .media-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 16px; } .media-tile, .attachment { display: block; min-width: 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--surface-alt); } .media-tile img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; } .attachment { padding: 14px; overflow-wrap: anywhere; color: var(--ink); text-decoration: none; } .lightbox[hidden] { display: none; } .lightbox { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; } .lightbox-backdrop { position: absolute; inset: 0; background: rgba(15, 20, 24, .82); } .lightbox-panel { position: relative; z-index: 1; width: min(92vw, 1180px); margin: 0; display: grid; gap: 10px; padding: 14px; border-radius: 8px; background: rgba(12, 15, 18, .92); box-shadow: 0 24px 70px rgba(0, 0, 0, .4); } .lightbox-panel img { display: block; max-width: 100%; max-height: 82vh; margin: 0 auto; object-fit: contain; } .lightbox-panel figcaption { color: rgba(255, 255, 255, .8); font-size: .9rem; text-align: center; overflow-wrap: anywhere; } .lightbox-close { position: absolute; top: 10px; right: 10px; width: 34px; height: 34px; border: 0; border-radius: 8px; background: rgba(255, 255, 255, .12); color: #fff; font-size: 1rem; line-height: 1; } .lightbox-close:hover { background: rgba(255, 255, 255, .18); } .shell-message { width: min(100%, 620px); padding-top: 24px; } .message-panel { padding: 28px; } .profile-compact { display: grid; } @media (max-width: 640px) { .shell { padding: 14px 10px 42px; } .instance-bar, .crumbs, .section-head, .status-head { align-items: start; } .instance-bar, .crumbs { flex-direction: column; } .identity { gap: 12px; padding: 0 16px; transform: translateY(-26px); margin-bottom: -10px; } .avatar { width: 82px; height: 82px; border-width: 4px; } .name-block h1, .name-block h2, .instance-bar h1, .message-panel h1 { font-size: 1.7rem; } .note { padding: 0 16px 16px; } .fields { grid-template-columns: 1fr; margin: 0 16px 18px; } .stats dt { font-size: 1.05rem; } .stats dd { font-size: .78rem; } .status-card { padding: 16px 14px; } .profile-compact > .summary, .profile-compact > .content, .profile-compact > .media-grid { margin-left: 16px; margin-right: 16px; } .profile-compact > .status-footer { margin: 16px 16px 0; } .media-grid { grid-template-columns: 1fr; } .lightbox-panel { width: min(94vw, 1000px); padding: 10px; } .lightbox-close { top: 8px; right: 8px; } } `; } function lightboxScript(): string { return `(() => { const overlay = document.querySelector('[data-lightbox-overlay]'); if (!overlay) return; const image = overlay.querySelector('[data-lightbox-image]'); const caption = overlay.querySelector('[data-lightbox-caption]'); const body = document.body; const open = (src, alt) => { if (!(image instanceof HTMLImageElement)) return; image.src = src; image.alt = alt || ''; if (caption) caption.textContent = alt || ''; overlay.hidden = false; body.style.overflow = 'hidden'; const close = overlay.querySelector('[data-lightbox-close]'); if (close instanceof HTMLElement) close.focus(); }; const close = () => { if (!(image instanceof HTMLImageElement)) return; overlay.hidden = true; image.removeAttribute('src'); image.alt = ''; if (caption) caption.textContent = ''; body.style.overflow = ''; }; document.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof Element)) return; const trigger = target.closest('[data-lightbox-trigger]'); if (trigger instanceof HTMLElement) { event.preventDefault(); open(trigger.dataset.lightboxSrc || '', trigger.dataset.lightboxAlt || ''); return; } if (target.closest('[data-lightbox-close]') || target === overlay) { event.preventDefault(); close(); return; } if (target.closest('a, button, input, textarea, select')) return; const card = target.closest('[data-post-href]'); if (card instanceof HTMLElement && card.dataset.postHref) { window.location.assign(card.dataset.postHref); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && !overlay.hidden) close(); if (event.key !== 'Enter' && event.key !== ' ') return; const target = event.target; if (!(target instanceof HTMLElement)) return; if (target.closest('a, button, input, textarea, select')) return; const card = target.closest('[data-post-href]'); if (card instanceof HTMLElement && card.dataset.postHref) { event.preventDefault(); window.location.assign(card.dataset.postHref); } }); })();`; }