atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 245 lines 8.8 kB view raw
1const std = @import("std"); 2const zat = @import("zat"); 3 4const Allocator = std.mem.Allocator; 5 6const DocEntry = struct { path: []const u8, file: []const u8 }; 7 8/// docs to publish as site.standard.document records 9const docs = [_]DocEntry{ 10 .{ .path = "/", .file = "README.md" }, 11 .{ .path = "/roadmap", .file = "docs/roadmap.md" }, 12 .{ .path = "/changelog", .file = "CHANGELOG.md" }, 13}; 14 15/// devlog entries 16const devlog = [_]DocEntry{ 17 .{ .path = "/devlog/001", .file = "devlog/001-self-publishing-docs.md" }, 18 .{ .path = "/devlog/002", .file = "devlog/002-firehose-and-benchmarks.md" }, 19 .{ .path = "/devlog/003", .file = "devlog/003-trust-chain.md" }, 20 .{ .path = "/devlog/004", .file = "devlog/004-sig-verify.md" }, 21 .{ .path = "/devlog/005", .file = "devlog/005-three-way-verify.md" }, 22 .{ .path = "/devlog/006", .file = "devlog/006-building-a-relay.md" }, 23 .{ .path = "/devlog/007", .file = "devlog/007-up-and-to-the-right.md" }, 24 .{ .path = "/devlog/008", .file = "devlog/008-the-io-migration.md" }, 25}; 26 27pub fn main() !void { 28 // use page_allocator for CLI tool - OS reclaims on exit 29 const allocator = std.heap.page_allocator; 30 31 const handle = "zat.dev"; 32 33 const password = if (std.c.getenv("ATPROTO_PASSWORD")) |p| std.mem.span(p) else { 34 std.debug.print("error: ATPROTO_PASSWORD not set\n", .{}); 35 return error.MissingEnv; 36 }; 37 38 const pds = if (std.c.getenv("ATPROTO_PDS")) |p| std.mem.span(p) else "https://bsky.social"; 39 40 var client = zat.XrpcClient.init(std.Options.debug_io, allocator, pds); 41 defer client.deinit(); 42 43 const session = try createSession(&client, allocator, handle, password); 44 defer { 45 allocator.free(session.did); 46 allocator.free(session.access_token); 47 } 48 49 std.debug.print("authenticated as {s}\n", .{session.did}); 50 client.setAuth(session.access_token); 51 52 // generate TID for publication (fixed timestamp for deterministic rkey) 53 // using 2024-01-01 00:00:00 UTC as base timestamp (1704067200 seconds = 1704067200000000 microseconds) 54 const pub_tid = zat.Tid.fromTimestamp(1704067200000000, 0); 55 const pub_record = Publication{ 56 .url = "https://zat.dev", 57 .name = "zat", 58 .description = "AT Protocol building blocks for zig", 59 }; 60 61 try putRecord(&client, allocator, session.did, "site.standard.publication", pub_tid.str(), pub_record); 62 std.debug.print("created publication: at://{s}/site.standard.publication/{s}\n", .{ session.did, pub_tid.str() }); 63 64 var pub_uri_buf: std.ArrayList(u8) = .empty; 65 defer pub_uri_buf.deinit(allocator); 66 try pub_uri_buf.print(allocator, "at://{s}/site.standard.publication/{s}", .{ session.did, pub_tid.str() }); 67 const pub_uri = pub_uri_buf.items; 68 69 // publish each doc with deterministic TIDs (same base timestamp, incrementing clock_id) 70 const now = timestamp(); 71 72 try publishEntries(&client, allocator, session.did, &docs, pub_uri, 1, &now); 73 74 // devlog publication (clock_id 100 to separate from docs) 75 const devlog_tid = zat.Tid.fromTimestamp(1704067200000000, 100); 76 const devlog_pub = Publication{ 77 .url = "https://zat.dev", 78 .name = "zat devlog", 79 .description = "building zat in public", 80 }; 81 82 try putRecord(&client, allocator, session.did, "site.standard.publication", devlog_tid.str(), devlog_pub); 83 std.debug.print("created publication: at://{s}/site.standard.publication/{s}\n", .{ session.did, devlog_tid.str() }); 84 85 var devlog_uri_buf: std.ArrayList(u8) = .empty; 86 defer devlog_uri_buf.deinit(allocator); 87 try devlog_uri_buf.print(allocator, "at://{s}/site.standard.publication/{s}", .{ session.did, devlog_tid.str() }); 88 const devlog_uri = devlog_uri_buf.items; 89 90 try publishEntries(&client, allocator, session.did, &devlog, devlog_uri, 101, &now); 91 92 std.debug.print("done\n", .{}); 93} 94 95const Publication = struct { 96 @"$type": []const u8 = "site.standard.publication", 97 url: []const u8, 98 name: []const u8, 99 description: ?[]const u8 = null, 100}; 101 102const Document = struct { 103 @"$type": []const u8 = "site.standard.document", 104 site: []const u8, 105 title: []const u8, 106 path: ?[]const u8 = null, 107 textContent: ?[]const u8 = null, 108 publishedAt: []const u8, 109}; 110 111const Session = struct { 112 did: []const u8, 113 access_token: []const u8, 114}; 115 116fn createSession(client: *zat.XrpcClient, allocator: Allocator, handle: []const u8, password: []const u8) !Session { 117 const CreateSessionInput = struct { 118 identifier: []const u8, 119 password: []const u8, 120 }; 121 122 var buf: std.ArrayList(u8) = .empty; 123 defer buf.deinit(allocator); 124 try buf.print(allocator, "{f}", .{std.json.fmt(CreateSessionInput{ 125 .identifier = handle, 126 .password = password, 127 }, .{})}); 128 129 const nsid = zat.Nsid.parse("com.atproto.server.createSession").?; 130 var response = try client.procedure(nsid, buf.items); 131 defer response.deinit(); 132 133 if (!response.ok()) { 134 std.debug.print("createSession failed: {s}\n", .{response.body}); 135 return error.AuthFailed; 136 } 137 138 var parsed = try response.json(); 139 defer parsed.deinit(); 140 141 const did = zat.json.getString(parsed.value, "did") orelse return error.MissingDid; 142 const token = zat.json.getString(parsed.value, "accessJwt") orelse return error.MissingToken; 143 144 return .{ 145 .did = try allocator.dupe(u8, did), 146 .access_token = try allocator.dupe(u8, token), 147 }; 148} 149 150fn putRecord(client: *zat.XrpcClient, allocator: Allocator, repo: []const u8, collection: []const u8, rkey: []const u8, record: anytype) !void { 151 const PutRecordInput = struct { 152 repo: []const u8, 153 collection: []const u8, 154 rkey: []const u8, 155 record: @TypeOf(record), 156 }; 157 158 var buf: std.ArrayList(u8) = .empty; 159 defer buf.deinit(allocator); 160 try buf.print(allocator, "{f}", .{std.json.fmt(PutRecordInput{ 161 .repo = repo, 162 .collection = collection, 163 .rkey = rkey, 164 .record = record, 165 }, .{})}); 166 167 const nsid = zat.Nsid.parse("com.atproto.repo.putRecord").?; 168 var response = try client.procedure(nsid, buf.items); 169 defer response.deinit(); 170 171 if (!response.ok()) { 172 std.debug.print("putRecord failed: {s}\n", .{response.body}); 173 return error.PutFailed; 174 } 175} 176 177fn extractTitle(content: []const u8) ?[]const u8 { 178 var lines = std.mem.splitScalar(u8, content, '\n'); 179 while (lines.next()) |line| { 180 const trimmed = std.mem.trim(u8, line, " \t\r"); 181 if (trimmed.len > 2 and trimmed[0] == '#' and trimmed[1] == ' ') { 182 var title = trimmed[2..]; 183 // strip markdown link: [text](url) -> text 184 if (std.mem.indexOf(u8, title, "](")) |bracket| { 185 if (title[0] == '[') { 186 title = title[1..bracket]; 187 } 188 } 189 return title; 190 } 191 } 192 return null; 193} 194 195fn publishEntries( 196 client: *zat.XrpcClient, 197 allocator: Allocator, 198 did: []const u8, 199 entries: []const DocEntry, 200 site_uri: []const u8, 201 clock_id_base: usize, 202 now: *const [20]u8, 203) !void { 204 for (entries, 0..) |entry, i| { 205 const content = std.Io.Dir.readFileAlloc(.cwd(), std.Options.debug_io, entry.file, allocator, .limited(1024 * 1024)) catch |err| { 206 std.debug.print("warning: could not read {s}: {}\n", .{ entry.file, err }); 207 continue; 208 }; 209 defer allocator.free(content); 210 211 const title = extractTitle(content) orelse entry.file; 212 const tid = zat.Tid.fromTimestamp(1704067200000000, @intCast(clock_id_base + i)); 213 214 const record = Document{ 215 .site = site_uri, 216 .title = title, 217 .path = entry.path, 218 .textContent = content, 219 .publishedAt = now, 220 }; 221 222 try putRecord(client, allocator, did, "site.standard.document", tid.str(), record); 223 std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ entry.file, did, tid.str() }); 224 } 225} 226 227fn timestamp() [20]u8 { 228 const ns = std.Io.Timestamp.now(std.Options.debug_io, .real).nanoseconds; 229 const secs: u64 = @intCast(@divFloor(ns, std.time.ns_per_s)); 230 const es = std.time.epoch.EpochSeconds{ .secs = secs }; 231 const yd = es.getEpochDay().calculateYearDay(); 232 const md = yd.calculateMonthDay(); 233 const ds = es.getDaySeconds(); 234 235 var buf: [20]u8 = undefined; 236 _ = std.fmt.bufPrint(&buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{ 237 yd.year, 238 @as(u32, md.month.numeric()), 239 @as(u32, md.day_index) + 1, 240 @as(u32, ds.getHoursIntoDay()), 241 @as(u32, ds.getMinutesIntoHour()), 242 @as(u32, ds.getSecondsIntoMinute()), 243 }) catch unreachable; 244 return buf; 245}