const 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 @import("../../internal/web/icons.zig").decorate(allocator, 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("
no residents yet.");
} 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(
\\
\\
\\ {s}
\\
\\ {s}
\\ {s}
\\
\\ {s}{d} records
\\
\\
, .{ 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("no records yet.");
} 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(
\\
\\ {s}
\\ {d}
\\
\\
, .{ 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, @embedFile("../../internal/web/home.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,
\\
\\ repo signal
waiting for first write.
\\
);
}
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,
\\
, .{ 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, "{c}", .{initialFor(resident.handle)});
};
const did = try escapeHtml(allocator, resident.did);
const cid = try escapeHtml(allocator, avatar_cid);
return std.fmt.allocPrint(
allocator,
"
",
.{ 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;
}