aboutsummaryrefslogtreecommitdiff
path: root/comments/src/main.zig
diff options
context:
space:
mode:
authorMartin Ashby <martin@ashbysoft.com>2023-08-22 10:49:17 +0100
committerMartin Ashby <martin@ashbysoft.com>2023-08-22 10:49:17 +0100
commit7ed589a06539fcc91ab779a1e6de0a995f8238b5 (patch)
treef23bd017d43af4714280aea2d152379370393214 /comments/src/main.zig
parent8f7f272d68b4197f17d1e76c97546017b8ebea90 (diff)
downloadmfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.gz
mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.bz2
mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.tar.xz
mfashby.net-7ed589a06539fcc91ab779a1e6de0a995f8238b5.zip
Replace comments with zig version
Diffstat (limited to 'comments/src/main.zig')
-rw-r--r--comments/src/main.zig294
1 files changed, 294 insertions, 0 deletions
diff --git a/comments/src/main.zig b/comments/src/main.zig
new file mode 100644
index 0000000..898b689
--- /dev/null
+++ b/comments/src/main.zig
@@ -0,0 +1,294 @@
+const std = @import("std");
+const zws = @import("zws");
+const pq = @import("pq.zig");
+const mustache = @import("mustache");
+
+const Params = zws.Params;
+
+const Err = error{
+ Unexpected,
+ AccessDenied,
+ OutOfMemory,
+ InputOutput,
+ SystemResources,
+ IsDir,
+ OperationAborted,
+ BrokenPipe,
+ ConnectionResetByPeer,
+ ConnectionTimedOut,
+ NotOpenForReading,
+ NetNameDeleted,
+ WouldBlock,
+ StreamTooLong,
+ Malformatted,
+ InvalidLength,
+ InvalidCharacter,
+ NoSpaceLeft,
+ PqError,
+ ColumnNotFound,
+ DiskQuota,
+ FileTooBig,
+ DeviceBusy,
+ InvalidArgument,
+ NotOpenForWriting,
+ LockViolation,
+ InvalidRequestMethod,
+};
+const Ctx = struct {
+ db: pq.Db,
+ pub fn clone(self: @This()) Ctx {
+ return Ctx{
+ .db = self.db,
+ };
+ }
+ pub fn deinit(self: @This()) void {
+ _ = self;
+ }
+};
+
+var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+const Request = struct {
+ method: std.http.Method,
+ target: []const u8,
+};
+const ResponseTransfer = union(enum) {
+ content_length: u64,
+ chunked: void,
+ none: void,
+};
+const Headers = struct {
+ const Entry = struct { key: []const u8, val: []const u8 };
+ _internal: std.ArrayList(Entry),
+
+ fn init(allocator: std.mem.Allocator) Headers {
+ return .{ ._internal = std.ArrayList(Entry).init(allocator) };
+ }
+ fn append(self: *@This(), key: []const u8, val: []const u8) !void {
+ try self._internal.append(.{ .key = key, .val = val });
+ }
+};
+const Response = struct {
+ allocator: std.mem.Allocator,
+ request: Request,
+ // TODO other fields and writer function to write headers and body to stdout
+ status: std.http.Status,
+ transfer_encoding: ResponseTransfer,
+ headers: Headers,
+ fn reader(_: @This()) std.fs.File.Reader {
+ return std.io.getStdIn().reader();
+ }
+ fn do(self: @This()) !void {
+ for (self.headers._internal.items) |tup| {
+ try std.io.getStdOut().writeAll(tup.key);
+ try std.io.getStdOut().writeAll(": ");
+ try std.io.getStdOut().writeAll(tup.val);
+ try std.io.getStdOut().writeAll("\r\n");
+ }
+ try std.io.getStdOut().writeAll("\r\n");
+ }
+ fn writer(_: @This()) std.fs.File.Writer {
+ return std.io.getStdOut().writer();
+ }
+ fn finish(_: @This()) !void {
+ // TODO Write empty lines? or just do nothing
+ }
+};
+
+const Rtr = zws.Router(*Response, Ctx, Err);
+const router = Rtr{
+ .allocator = gpa.allocator(),
+ .handlers = &[_]Rtr.Handler{
+ .{
+ .method = .GET,
+ .pattern = "/api/comment",
+ .handle_fn = get_comment,
+ },
+ .{
+ .method = .POST,
+ .pattern = "/api/comment",
+ .handle_fn = post_comment,
+ },
+ .{
+ .method = .GET,
+ .pattern = "/api/form",
+ .handle_fn = get_form,
+ },
+ },
+ .notfound = notfound,
+};
+
+/// Run as a CGI program!
+pub fn main() !void {
+ const allocator = gpa.allocator();
+ const db_url = std.os.getenv("DATABASE_URL") orelse "postgresql://comments@localhost/comments";
+ var db = try pq.Db.init(db_url);
+ // try db.exec(@embedFile("migrations/0_init.sql"));
+ // try db.exec(@embedFile("migrations/1_capcha.sql"));
+ defer db.deinit();
+ const req = Request{
+ .method = std.meta.stringToEnum(std.http.Method, std.os.getenv("REQUEST_METHOD") orelse "GET") orelse {
+ return error.InvalidRequestMethod;
+ },
+ .target = std.os.getenv("REQUEST_URI") orelse "/",
+ };
+ var res = Response{
+ .allocator = allocator,
+ .request = req,
+ .status = .bad_request,
+ .transfer_encoding = .none,
+ .headers = Headers.init(allocator),
+ };
+ const ctx = Ctx{ .db = db };
+ try router.handle(&res, ctx);
+}
+
+fn notfound(res: *Response, _: Ctx) Err!void {
+ const rr = @embedFile("templates/notfound.html");
+ try constresponse(res, rr, .not_found);
+}
+
+fn badrequest(res: *Response, _: Ctx) Err!void {
+ const rr = @embedFile("templates/badrequest.html");
+ try constresponse(res, rr, .bad_request);
+}
+
+fn constresponse(res: *Response, rr: []const u8, status: std.http.Status) Err!void {
+ res.status = status;
+ res.transfer_encoding = .{ .content_length = rr.len };
+ try res.headers.append("content-type", "text/html");
+ try res.do();
+ try res.writer().writeAll(rr);
+ try res.finish();
+}
+
+fn get_comment(res: *Response, ctx: Ctx, _: Params) Err!void {
+ var p = try zws.Path.parse(res.allocator, res.request.target);
+ defer p.deinit();
+ const url: []const u8 = try p.get_query_param("url") orelse {
+ try badrequest(res, ctx);
+ return;
+ };
+
+ const Comment = struct {
+ author: []const u8,
+ comment: []const u8,
+ ts: []const u8,
+ };
+ var comments = std.ArrayList(Comment).init(res.allocator);
+ var stmt = try ctx.db.prepare_statement(res.allocator,
+ \\ select author,comment,ts from comments where url = $1 order by ts
+ );
+ defer stmt.deinit();
+ try stmt.bind(0, url);
+ while (try stmt.step()) {
+ const cmt = try stmt.read_struct(Comment);
+ try comments.append(cmt);
+ }
+
+ const rr = @embedFile("templates/comments.html");
+ const tt = comptime mustache.parseComptime(rr, .{}, .{});
+ res.transfer_encoding = .chunked;
+ try res.headers.append("content-type", "text/html");
+ try res.do();
+
+ const data = struct {
+ comments: []const Comment,
+ };
+ try mustache.render(tt, data{
+ .comments = comments.items,
+ }, res.writer());
+ try res.finish();
+}
+
+fn post_comment(res: *Response, ctx: Ctx, _: Params) Err!void {
+ var body_aa = std.ArrayList(u8).init(res.allocator);
+ try res.reader().readAllArrayList(&body_aa, 1_000_000);
+ var body = try body_aa.toOwnedSlice();
+ var form = try zws.Form.parse(res.allocator, body);
+ const Form = struct {
+ url: []const u8,
+ capcha_id: []const u8,
+ author: []const u8,
+ comment: []const u8,
+ capcha_answer: []const u8,
+ };
+ const form_val = form.form_to_struct(Form) catch {
+ try badrequest(res, ctx);
+ return;
+ };
+
+ // Validate the capcha
+ {
+ var stmt = try ctx.db.prepare_statement(res.allocator, "select answer from capchas where id = $1");
+ defer stmt.deinit();
+ try stmt.bind(0, form_val.capcha_id);
+ if (!try stmt.step()) {
+ std.log.err("missing capcha_id {s}", .{form_val.capcha_id});
+ try badrequest(res, ctx);
+ return;
+ }
+ const ans = try stmt.read_column(0, []const u8);
+ if (!std.mem.eql(u8, ans, form_val.capcha_answer)) {
+ try constresponse(res, @embedFile("templates/capchainvalid.html"), std.http.Status.unauthorized);
+ return;
+ }
+ }
+
+ // Add the comment...
+ {
+ var stmt = try ctx.db.prepare_statement(res.allocator, "insert into comments(url,author,comment) values($1, $2, $3)");
+ defer stmt.deinit();
+ try stmt.bind(0, form_val.url);
+ try stmt.bind(1, form_val.author);
+ try stmt.bind(2, form_val.comment);
+ _ = try stmt.step();
+ }
+
+ // And redirect!
+ res.transfer_encoding = .none;
+ res.status = .found;
+ try res.headers.append("location", form_val.url);
+ try res.do();
+ try res.finish();
+}
+
+fn get_form(res: *Response, ctx: Ctx, _: Params) Err!void {
+ var p = try zws.Path.parse(res.allocator, res.request.target);
+ defer p.deinit();
+ const url: []const u8 = try p.get_query_param("url") orelse {
+ try badrequest(res, ctx);
+ return;
+ };
+
+ var stmt = try ctx.db.prepare_statement(res.allocator, "select id, question from capchas order by random() limit 1");
+ defer stmt.deinit();
+ if (!try stmt.step()) {
+ std.log.err("no capcha!", .{});
+ try badrequest(res, ctx);
+ return;
+ }
+ const Capcha = struct {
+ id: []const u8,
+ question: []const u8,
+ };
+ const capcha = try stmt.read_struct(Capcha);
+
+ const rr = @embedFile("templates/form.html");
+ const tt = comptime mustache.parseComptime(rr, .{}, .{});
+
+ res.transfer_encoding = .chunked;
+ try res.headers.append("content-type", "text/html");
+ try res.do();
+ // For some reason, mustache.render won't work with anonymous struct
+ const data = struct {
+ capcha_id: []const u8,
+ capcha_question: []const u8,
+ url: []const u8,
+ };
+ try mustache.render(tt, data{
+ .capcha_id = capcha.id,
+ .capcha_question = capcha.question,
+ .url = url,
+ }, res.writer());
+ try res.finish();
+}