atproto pds in zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283const std = @import("std");const assets = @import("assets.zig");const config = @import("../../core/config.zig");const entropy = @import("entropy.zig");const http_api = @import("../api.zig");const store = @import("../../storage/store.zig");
const http = std.http;
pub fn serve(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator();
const body = try render(allocator); const headers = [_]http.Header{ .{ .name = "content-type", .value = "text/html; charset=utf-8" }, .{ .name = "cache-control", .value = "no-store" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "access-control-allow-private-network", .value = "true" }, .{ .name = "connection", .value = "close" }, }; try http_api.respond(request, .ok, body, &headers);}
pub fn serveFavicon(request: *http_api.Request) !void { const headers = [_]http.Header{ .{ .name = "content-type", .value = "image/svg+xml; charset=utf-8" }, .{ .name = "cache-control", .value = "public, max-age=86400" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; try http_api.respond(request, .ok, assets.favicon, &headers);}
pub fn serveOgImage(request: *http_api.Request) !void { const headers = [_]http.Header{ .{ .name = "content-type", .value = "image/png" }, .{ .name = "cache-control", .value = "public, max-age=86400" }, .{ .name = "access-control-allow-origin", .value = "*" }, .{ .name = "connection", .value = "close" }, }; try http_api.respond(request, .ok, assets.og_image, &headers);}
fn render(allocator: std.mem.Allocator) ![]const u8 { const residents = try store.listResidents(allocator, 24); var accounts: std.Io.Writer.Allocating = .init(allocator); defer accounts.deinit();
if (residents.len == 0) { try accounts.writer.writeAll("<li class=\"empty\">no residents yet.</li>"); } else { for (residents) |resident| { const handle = try escapeHtml(allocator, resident.handle); const did = try escapeHtml(allocator, resident.did); const avatar = try renderAvatar(allocator, resident); const status = if (resident.active) "active" else "paused"; try accounts.writer.print( \\<li class="resident"> \\ <a href="https://bsky.app/profile/{s}" data-profile-handle={f} data-profile-did={f} rel="me"> \\ {s} \\ <span class="who"> \\ <span class="handle">{s}</span> \\ <span class="did">{s}</span> \\ </span> \\ <span class="meta"><span>{s}</span><span>{d} records</span></span> \\ </a> \\</li> , .{ handle, std.json.fmt(resident.handle, .{}), std.json.fmt(resident.did, .{}), avatar, handle, did, status, resident.record_count }); } }
const collections = try store.listCollectionSummaries(allocator, 10); const activity = try store.listLandingActivity(allocator, 18); const signal = try entropy.fromState(allocator, residents, collections, activity); var collection_items: std.Io.Writer.Allocating = .init(allocator); defer collection_items.deinit(); if (collections.len == 0) { try collection_items.writer.writeAll("<li class=\"empty\">no records yet.</li>"); } else { const max_count = collections[0].count; for (collections) |summary| { const collection = try escapeHtml(allocator, summary.collection); const width = if (max_count == 0) 0 else @max(@as(u64, 8), summary.count * 100 / max_count); try collection_items.writer.print( \\<li class="collection"> \\ <a href="https://ufos.microcosm.blue/collection/?nsid={s}"><code>{s}</code></a> \\ <span>{d}</span> \\ <i style="width:{d}%"></i> \\</li> , .{ collection, collection, summary.count, width }); } }
const recent_records = try store.listLandingRecentRecords(allocator, 8); const pulse = try renderPulse(allocator, recent_records); const public_url = config.publicUrl(); const public_host = try escapeHtml(allocator, displayHost(public_url)); const public_url_html = try escapeHtml(allocator, public_url); const preview_image = try escapeHtml(allocator, try std.fmt.allocPrint(allocator, "{s}/og-image", .{public_url}));
return std.fmt.allocPrint(allocator, \\<!doctype html> \\<html lang="en"> \\<head> \\ <meta charset="utf-8"> \\ <meta name="viewport" content="width=device-width, initial-scale=1"> \\ <meta name="color-scheme" content="dark light"> \\ <title>zds</title> \\ <meta name="description" content="an at protocol personal data server"> \\ <link rel="canonical" href="{s}/"> \\ <meta property="og:type" content="website"> \\ <meta property="og:site_name" content="zds"> \\ <meta property="og:title" content="{s}"> \\ <meta property="og:description" content="an at protocol personal data server"> \\ <meta property="og:url" content="{s}/"> \\ <meta property="og:image" content="{s}"> \\ <meta property="og:image:width" content="1200"> \\ <meta property="og:image:height" content="630"> \\ <meta name="twitter:card" content="summary_large_image"> \\ <meta name="twitter:title" content="{s}"> \\ <meta name="twitter:description" content="an at protocol personal data server"> \\ <meta name="twitter:image" content="{s}"> \\ <link rel="icon" href="/favicon.svg" type="image/svg+xml"> \\ <style>{s}</style> \\</head> \\<body style="{s}"> \\ <main class="shell"> \\ <header> \\ <a class="brand" href="/" aria-label="zds home">zds</a> \\ <nav aria-label="project and server links"> \\ <a class="icon-link logo-link" href="https://tangled.org/zat.dev/zds" aria-label="source on Tangled" title="source"><img alt="" src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"></a> \\ <a class="icon-link logo-link" href="https://zat.dev" aria-label="zat.dev" title="zat.dev"><img alt="" src="https://zat.dev/favicon.svg"></a> \\ <a class="icon-link logo-link" href="https://atproto.com" aria-label="AT Protocol" title="atproto"><img alt="" src="https://atproto.com/favicon.ico"></a> \\ <button class="settings" type="button" id="system" aria-label="server links" title="server links"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h14"/><path d="M8 7v10M16 7v10"/></svg></button> \\ <button class="settings" type="button" id="settings" aria-label="profile client settings" title="profile client"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg></button> \\ </nav> \\ </header> \\ <section class="hero" aria-labelledby="title"> \\ <h1 id="title">{s}</h1> \\ <p class="lede">an <a href="https://atproto.com">atproto</a> personal data server.</p> \\ {s} \\ </section> \\ {s} \\ <div class="grid"> \\ <section aria-labelledby="residents-title"> \\ <h2 class="section-title" id="residents-title">residents</h2> \\ <ul class="residents">{s}</ul> \\ </section> \\ <section aria-labelledby="records-title"> \\ <h2 class="section-title" id="records-title">collections</h2> \\ <ul class="collections">{s}</ul> \\ </section> \\ </div> \\ </main> \\ <div class="sheet" id="client-sheet" role="dialog" aria-modal="true" aria-labelledby="client-title"> \\ <div class="sheet-card"> \\ <div class="sheet-head"><h2 id="client-title">settings</h2><button class="close" type="button" aria-label="close" id="close-client"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M7 7l10 10M17 7L7 17"/></svg></button></div> \\ <div class="client-body"> \\ <div class="pref-section"> \\ <div class="pref-label">appearance</div> \\ <div class="theme-grid" role="group" aria-label="theme"> \\ <button class="theme-option" type="button" data-theme="system">system</button> \\ <button class="theme-option" type="button" data-theme="dark">dark</button> \\ <button class="theme-option" type="button" data-theme="light">light</button> \\ </div> \\ </div> \\ <div class="pref-section"> \\ <div class="pref-row"><label class="pref-label" for="accent-hue">accent hue</label><output id="accent-value" for="accent-hue"></output></div> \\ <input class="hue-slider" id="accent-hue" type="range" min="0" max="359" step="1"> \\ </div> \\ <div class="pref-label">open profiles with</div> \\ <div class="client-grid" id="client-grid"></div> \\ </div> \\ </div> \\ </div> \\ <div class="sheet" id="system-sheet" role="dialog" aria-modal="true" aria-labelledby="system-title"> \\ <div class="sheet-card"> \\ <div class="sheet-head"><h2 id="system-title">server links</h2><button class="close" type="button" aria-label="close" id="close-system"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M7 7l10 10M17 7L7 17"/></svg></button></div> \\ <div class="client-body"> \\ <p class="hint">public endpoints exposed by this PDS.</p> \\ <div class="system-links"> \\ <a href="/api"><strong>api reference</strong><span>/api</span></a> \\ <a href="/api/openapi.json"><strong>openapi json</strong><span>/api/openapi.json</span></a> \\ <a href="/stats"><strong>stats</strong><span>/stats</span></a> \\ <a href="/xrpc/_health"><strong>health</strong><span>/xrpc/_health</span></a> \\ <a href="/xrpc/com.atproto.server.describeServer"><strong>server</strong><span>describeServer</span></a> \\ <a href="/.well-known/did.json"><strong>did document</strong><span>/.well-known/did.json</span></a> \\ </div> \\ </div> \\ </div> \\ </div> \\ <script>{s}</script> \\</body> \\</html> , .{ public_url_html, public_host, public_url_html, preview_image, public_host, preview_image, assets.css, signal.style, public_host, signal.html, pulse, accounts.written(), collection_items.written(), assets.script, });}
fn renderPulse(allocator: std.mem.Allocator, records: []const store.RecentRecord) ![]const u8 { if (records.len == 0) { return allocator.dupe(u8, \\<section class="pulse" aria-label="repo signal"> \\ <div><div class="pulse-kicker">repo signal</div><div class="pulse-row"><span class="pulse-path">waiting for first write.</span></div></div> \\</section> ); }
const newest = records[0]; const collection = try escapeHtml(allocator, newest.collection); const rkey = try escapeHtml(allocator, newest.rkey); const cid = try escapeHtml(allocator, newest.cid); const did = try escapeHtml(allocator, newest.did);
return std.fmt.allocPrint(allocator, \\<section class="pulse" aria-label="repo signal"> \\ <div> \\ <div class="pulse-kicker">latest indexed record</div> \\ <a class="pulse-link" href="https://pdsls.dev/at/{s}/{s}/{s}" title="open record in PDSls"> \\ <span><code>{s}</code>/{s}</span> \\ <em>seq {d}</em> \\ </a> \\ <div class="pulse-cid">{s}</div> \\ </div> \\</section> , .{ did, collection, rkey, collection, rkey, newest.seq, cid });}
fn renderAvatar(allocator: std.mem.Allocator, resident: store.Resident) ![]const u8 { const avatar_cid = try store.profileAvatarCid(allocator, resident.did) orelse { return std.fmt.allocPrint(allocator, "<span class=\"avatar\" aria-hidden=\"true\">{c}</span>", .{initialFor(resident.handle)}); }; const did = try escapeHtml(allocator, resident.did); const cid = try escapeHtml(allocator, avatar_cid); return std.fmt.allocPrint( allocator, "<span class=\"avatar\"><img src=\"/xrpc/com.atproto.sync.getBlob?did={s}&cid={s}\" alt=\"\"></span>", .{ did, cid }, );}
fn initialFor(handle: []const u8) u8 { for (handle) |c| if (std.ascii.isAlphanumeric(c)) return std.ascii.toLower(c); return 'z';}
fn escapeHtml(allocator: std.mem.Allocator, text: []const u8) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); for (text) |c| switch (c) { '&' => try out.writer.writeAll("&"), '<' => try out.writer.writeAll("<"), '>' => try out.writer.writeAll(">"), '"' => try out.writer.writeAll("""), '\'' => try out.writer.writeAll("'"), else => try out.writer.writeByte(c), }; return out.toOwnedSlice();}
fn displayHost(public_url: []const u8) []const u8 { var value = public_url; if (std.mem.startsWith(u8, value, "https://")) value = value["https://".len..]; if (std.mem.startsWith(u8, value, "http://")) value = value["http://".len..]; if (std.mem.indexOfScalar(u8, value, '/')) |idx| value = value[0..idx]; return value;}