atproto pds in zig pds.zat.dev
pds atproto
Something went wrong. Try again.
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150const std = @import("std");const auth = @import("../auth/tokens.zig");const http_api = @import("../http/api.zig");const store = @import("../storage/store.zig");
const http = std.http;
const app_bsky_namespace = "app.bsky";const max_preference_size = 10_000;const personal_details_pref = "app.bsky.actor.defs#personalDetailsPref";const declared_age_pref = "app.bsky.actor.defs#declaredAgePref";
pub fn getPreferences(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator();
const account = requireAccount(request, allocator) catch return; const rows = try store.getAppPreferences(allocator, account.did, app_bsky_namespace);
var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); try out.writer.writeAll("{\"preferences\":["); var first = true; var personal_details: ?std.json.Value = null; for (rows) |row| { const parsed = std.json.parseFromSlice(std.json.Value, allocator, row.value_json, .{}) catch continue; if (std.mem.eql(u8, row.name, declared_age_pref)) continue; if (std.mem.eql(u8, row.name, personal_details_pref)) personal_details = parsed.value; if (!first) try out.writer.writeByte(','); first = false; try out.writer.writeAll(row.value_json); } if (declaredAgeJson(allocator, personal_details)) |age_json| { if (!first) try out.writer.writeByte(','); try out.writer.writeAll(age_json); } try out.writer.writeAll("]}"); const body = try out.toOwnedSlice(); return http_api.json(request, .ok, body);}
pub fn putPreferences(request: *http_api.Request) !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator();
const account = requireAccount(request, allocator) catch return; const body = http_api.readBodyAlloc(request, allocator, 1024 * 1024) catch |err| switch (err) { error.BodyTooLarge => return http_api.xrpcError(request, .payload_too_large, "PayloadTooLarge", "Request body too large"), else => return err, }; const parsed = try http_api.parseJsonBody(request, allocator, body); const preferences = switch (parsed.value) { .object => |object| object.get("preferences") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Missing preferences"); }, else => return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Expected object"), }; if (preferences != .array) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "preferences must be an array"); } var preference_rows: std.ArrayList(store.AppPreference) = .empty; for (preferences.array.items) |preference| { if (preference != .object) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Preference must be an object"); } const type_name = http_api.valueString(preference, "$type") orelse { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Preference is missing a $type"); }; if (type_name.len == 0) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Preference is missing a $type"); } if (!std.mem.startsWith(u8, type_name, app_bsky_namespace)) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Some preferences are not in the app.bsky namespace"); } const preference_json = try stringifyJson(allocator, preference); if (preference_json.len > max_preference_size) { return http_api.xrpcError(request, .bad_request, "InvalidRequest", "Preference is too large"); } if (std.mem.eql(u8, type_name, declared_age_pref)) continue; try preference_rows.append(allocator, .{ .name = type_name, .value_json = preference_json, }); }
try store.replaceAppPreferences(allocator, account.did, app_bsky_namespace, preference_rows.items); return http_api.json(request, .ok, "{}");}
fn stringifyJson(allocator: std.mem.Allocator, value: std.json.Value) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); defer out.deinit(); try std.json.Stringify.value(value, .{}, &out.writer); return out.toOwnedSlice();}
fn declaredAgeJson(allocator: std.mem.Allocator, personal_details: ?std.json.Value) ?[]const u8 { const personal = personal_details orelse return null; const birth_date = http_api.valueString(personal, "birthDate") orelse return null; const age = ageFromDateString(birth_date) orelse return null; return std.fmt.allocPrint( allocator, "{{\"$type\":\"{s}\",\"isOverAge13\":{},\"isOverAge16\":{},\"isOverAge18\":{}}}", .{ declared_age_pref, age >= 13, age >= 16, age >= 18 }, ) catch null;}
fn ageFromDateString(date: []const u8) ?i32 { if (date.len < 10) return null; const year = std.fmt.parseInt(i32, date[0..4], 10) catch return null; const month = std.fmt.parseInt(u8, date[5..7], 10) catch return null; const day = std.fmt.parseInt(u8, date[8..10], 10) catch return null; if (date[4] != '-' or date[7] != '-' or month == 0 or month > 12 or day == 0 or day > 31) return null;
const today = civilFromDays(@divTrunc(@divTrunc(store.nowMs(), 1000), 86400)); var age = today.year - year; if (today.month < month or (today.month == month and today.day < day)) age -= 1; return age;}
const CivilDate = struct { year: i32, month: u8, day: u8,};
fn civilFromDays(days_since_unix_epoch: i64) CivilDate { const z = days_since_unix_epoch + 719468; const era = if (z >= 0) @divTrunc(z, 146097) else @divTrunc(z - 146096, 146097); const doe: u32 = @intCast(z - era * 146097); const yoe: u32 = @intCast(@divTrunc(doe - @divTrunc(doe, 1460) + @divTrunc(doe, 36524) - @divTrunc(doe, 146096), 365)); var year: i32 = @intCast(yoe); year += @intCast(era * 400); const doy: u32 = doe - (365 * yoe + @divTrunc(yoe, 4) - @divTrunc(yoe, 100)); const mp: u32 = @divTrunc(5 * doy + 2, 153); const day: u8 = @intCast(doy - @divTrunc(153 * mp + 2, 5) + 1); const month: u8 = @intCast(if (mp < 10) mp + 3 else mp - 9); if (month <= 2) year += 1; return .{ .year = year, .month = month, .day = day };}
fn requireAccount(request: *http_api.Request, allocator: std.mem.Allocator) !auth.Account { return http_api.requireBearerAccount(request, allocator) catch |err| { try http_api.bearerErrorResponse(request, err); return error.HandledResponse; };}