atproto pds in zig pds.zat.dev
pds atproto
26

Configure Feed

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

Align sync HEAD and collection display

zzstoatzz (May 24, 2026, 9:56 PM -0500) 7fd4b093 c0570b09

+36 -22
+6 -2
src/atproto/sync.zig
··· 27 27 const blob = store.getBlob(allocator, did, cid) orelse { 28 28 return http_api.xrpcError(request, .not_found, "BlobNotFound", "Blob not found"); 29 29 }; 30 + const content_length = try std.fmt.allocPrint(allocator, "{d}", .{blob.data.len}); 30 31 const headers = [_]http.Header{ 31 32 .{ .name = "content-type", .value = blob.mime_type }, 33 + .{ .name = "content-length", .value = content_length }, 32 34 .{ .name = "cache-control", .value = "public, max-age=31536000, immutable" }, 33 35 .{ .name = "x-content-type-options", .value = "nosniff" }, 34 36 .{ .name = "access-control-allow-origin", .value = "*" }, 35 37 .{ .name = "access-control-allow-private-network", .value = "true" }, 36 38 .{ .name = "connection", .value = "close" }, 37 39 }; 38 - try request.respond(blob.data, .{ 40 + try request.respond(if (request.head.method == .HEAD) "" else blob.data, .{ 39 41 .status = .ok, 40 42 .extra_headers = &headers, 41 43 }); ··· 53 55 const body = store.writeRepoCar(allocator, did) catch { 54 56 return http_api.xrpcError(request, .not_found, "RepoNotFound", "Repo not found"); 55 57 }; 58 + const content_length = try std.fmt.allocPrint(allocator, "{d}", .{body.len}); 56 59 const headers = [_]http.Header{ 57 60 .{ .name = "content-type", .value = "application/vnd.ipld.car" }, 61 + .{ .name = "content-length", .value = content_length }, 58 62 .{ .name = "access-control-allow-origin", .value = "*" }, 59 63 .{ .name = "access-control-allow-private-network", .value = "true" }, 60 64 .{ .name = "connection", .value = "close" }, 61 65 }; 62 - try request.respond(body, .{ 66 + try request.respond(if (request.head.method == .HEAD) "" else body, .{ 63 67 .status = .ok, 64 68 .extra_headers = &headers, 65 69 });
+1 -1
src/http/api.zig
··· 234 234 235 235 const cors_headers = [_]http.Header{ 236 236 .{ .name = "access-control-allow-origin", .value = "*" }, 237 - .{ .name = "access-control-allow-methods", .value = "GET, POST, OPTIONS" }, 237 + .{ .name = "access-control-allow-methods", .value = "GET, HEAD, POST, OPTIONS" }, 238 238 .{ .name = "vary", .value = "Access-Control-Request-Headers" }, 239 239 .{ .name = "access-control-allow-headers", .value = "atproto-accept-labelers, atproto-proxy, authorization, content-type, dpop, x-bsky-topics" }, 240 240 .{ .name = "access-control-allow-private-network", .value = "true" },
+3 -1
src/http/landing/assets.zig
··· 92 92 \\.collections { display: grid; gap: 9px; list-style: none; margin: 0; padding: 0; } 93 93 \\.collection { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 10px 0 12px; border-bottom: 1px solid var(--line); overflow: hidden; } 94 94 \\.collection strong { display: block; color: var(--text); font-size: 13px; font-weight: 650; } 95 - \\.collection code { display: block; margin-top: 1px; color: var(--dim); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 95 + \\.collection code, .collection small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 96 + \\.collection code { margin-top: 1px; color: var(--muted); font-size: 11px; } 97 + \\.collection small { margin-top: 1px; color: var(--dim); font-size: 10px; } 96 98 \\.collection span { color: var(--muted); font-size: 12px; } 97 99 \\.collection i { position: absolute; left: 0; bottom: 0; height: 2px; background: linear-gradient(90deg, var(--green), var(--accent), var(--pink)); } 98 100 \\.empty { color: var(--muted); margin: 0; }
+12 -13
src/http/landing/mod.zig
··· 68 68 const max_count = collections[0].count; 69 69 for (collections) |summary| { 70 70 const collection = try escapeHtml(allocator, summary.collection); 71 - const label = collectionLabel(summary.collection); 71 + const label = try escapeHtml(allocator, nsidLeaf(summary.collection)); 72 + const namespace = try escapeHtml(allocator, nsidNamespace(summary.collection)); 72 73 const width = if (max_count == 0) 0 else @max(@as(u64, 8), summary.count * 100 / max_count); 73 74 try collection_items.writer.print( 74 75 \\<li class="collection"> 75 - \\ <div><strong>{s}</strong><code>{s}</code></div> 76 + \\ <div><strong>{s}</strong><code>{s}</code><small>{s}</small></div> 76 77 \\ <span>{d}</span> 77 78 \\ <i style="width:{d}%"></i> 78 79 \\</li> 79 - , .{ label, collection, summary.count, width }); 80 + , .{ label, namespace, collection, summary.count, width }); 80 81 } 81 82 } 82 83 ··· 181 182 ); 182 183 } 183 184 184 - fn collectionLabel(collection: []const u8) []const u8 { 185 - if (std.mem.eql(u8, collection, "app.bsky.actor.profile")) return "profile"; 186 - if (std.mem.eql(u8, collection, "app.bsky.feed.like")) return "likes"; 187 - if (std.mem.eql(u8, collection, "app.bsky.feed.post")) return "posts"; 188 - if (std.mem.eql(u8, collection, "app.bsky.feed.repost")) return "reposts"; 189 - if (std.mem.eql(u8, collection, "app.bsky.graph.follow")) return "follows"; 190 - if (std.mem.eql(u8, collection, "chat.bsky.actor.declaration")) return "chat declaration"; 191 - if (std.mem.eql(u8, collection, "sh.tangled.string")) return "tangled strings"; 192 - if (std.mem.eql(u8, collection, "io.zzstoatzz.status.record")) return "status"; 193 - return collection; 185 + fn nsidLeaf(nsid: []const u8) []const u8 { 186 + if (std.mem.lastIndexOfScalar(u8, nsid, '.')) |idx| return nsid[idx + 1 ..]; 187 + return nsid; 188 + } 189 + 190 + fn nsidNamespace(nsid: []const u8) []const u8 { 191 + if (std.mem.lastIndexOfScalar(u8, nsid, '.')) |idx| return nsid[0..idx]; 192 + return ""; 194 193 } 195 194 196 195 fn initialFor(handle: []const u8) u8 {
+8 -5
src/http/router.zig
··· 108 108 if (method == .POST and std.mem.eql(u8, path, "/xrpc/com.atproto.repo.importRepo")) return .repo_import_repo; 109 109 if (method == .POST and std.mem.eql(u8, path, "/xrpc/com.atproto.repo.uploadBlob")) return .repo_upload_blob; 110 110 if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.repo.listMissingBlobs")) return .repo_list_missing_blobs; 111 - if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getBlob")) return .sync_get_blob; 112 - if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getRepo")) return .sync_get_repo; 113 - if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getLatestCommit")) return .sync_get_latest_commit; 114 - if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.listRepos")) return .sync_list_repos; 115 - if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.listBlobs")) return .sync_list_blobs; 111 + if ((method == .GET or method == .HEAD) and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getBlob")) return .sync_get_blob; 112 + if ((method == .GET or method == .HEAD) and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getRepo")) return .sync_get_repo; 113 + if ((method == .GET or method == .HEAD) and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getLatestCommit")) return .sync_get_latest_commit; 114 + if ((method == .GET or method == .HEAD) and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.listRepos")) return .sync_list_repos; 115 + if ((method == .GET or method == .HEAD) and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.listBlobs")) return .sync_list_blobs; 116 116 if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.subscribeRepos")) return .sync_subscribe_repos; 117 117 if (method == .GET and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.getRepoStatus")) return .sync_get_repo_status; 118 118 if (method == .POST and std.mem.eql(u8, path, "/xrpc/com.atproto.sync.notifyOfUpdate")) return .sync_notify_of_update; ··· 163 163 try std.testing.expectEqual(Route.repo_create_record, route(.POST, "/xrpc/com.atproto.repo.createRecord")); 164 164 try std.testing.expectEqual(Route.repo_upload_blob, route(.POST, "/xrpc/com.atproto.repo.uploadBlob")); 165 165 try std.testing.expectEqual(Route.sync_list_repos, route(.GET, "/xrpc/com.atproto.sync.listRepos?limit=10")); 166 + try std.testing.expectEqual(Route.sync_list_repos, route(.HEAD, "/xrpc/com.atproto.sync.listRepos?limit=10")); 167 + try std.testing.expectEqual(Route.sync_get_blob, route(.HEAD, "/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aabc&cid=bafk")); 168 + try std.testing.expectEqual(Route.sync_get_repo, route(.HEAD, "/xrpc/com.atproto.sync.getRepo?did=did%3Aplc%3Aabc")); 166 169 try std.testing.expectEqual(Route.sync_get_latest_commit, route(.GET, "/xrpc/com.atproto.sync.getLatestCommit?did=did%3Aplc%3Aabc")); 167 170 try std.testing.expectEqual(Route.sync_subscribe_repos, route(.GET, "/xrpc/com.atproto.sync.subscribeRepos?cursor=0")); 168 171 try std.testing.expectEqual(Route.sync_request_crawl, route(.POST, "/xrpc/com.atproto.sync.requestCrawl"));
+6
tools/smoke.sh
··· 132 132 code=$(curl -sS -o "$repo_car" -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest") 133 133 test "$code" = "200" 134 134 test "$(wc -c < "$repo_car")" -gt 100 135 + repo_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getRepo?did=did:plc:smoketest") 136 + test "$repo_head" = "200" 135 137 136 138 blob_payload="${TMPDIR:-/tmp}/zds-smoke-blob.jpg" 137 139 printf '\377\330\377\340zds-smoke' > "$blob_payload" ··· 141 143 --data-binary "@$blob_payload") 142 144 printf '%s' "$blob" | grep -q '"blob":' 143 145 printf '%s' "$blob" | grep -q '"mimeType":"image/jpeg"' 146 + blob_cid=$(printf '%s' "$blob" | sed -n 's/.*"\$link":"\([^"]*\)".*/\1/p') 147 + test -n "$blob_cid" 148 + blob_head=$(curl -sSI -o /dev/null -w '%{http_code}' "$base/xrpc/com.atproto.sync.getBlob?did=did:plc:smoketest&cid=$blob_cid") 149 + test "$blob_head" = "200" 144 150 145 151 large_blob_payload="${TMPDIR:-/tmp}/zds-smoke-large-blob.jpg" 146 152 dd if=/dev/zero bs=1024 count=600 of="$large_blob_payload" 2>/dev/null