const std = @import("std"); const zws = @import("zws"); const pq = @import("pq.zig"); const mustache = @import("mustache"); const Params = zws.Params; const Err = error{ Overflow, InvalidCharacter, StreamTooLong, ColumnNotFound } || pq.PqError || std.http.Server.Response.WaitError || std.http.Server.Response.DoError || std.http.Server.Response.ReadError || std.http.Server.Response.FinishError || zws.Path.ParseError; 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 Rtr = zws.Router(Ctx, Err); const router = Rtr{ .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, }; pub fn main() !void { 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 server = zws.Server(Ctx, Rtr){ .allocator = gpa.allocator(), .address = std.net.Address{ .in = std.net.Ip4Address.init(.{ 127, 0, 0, 1 }, 5678) }, .context = Ctx{ .db = db }, .handler = router, }; try server.serve(); } fn notfound(res: *std.http.Server.Response, _: Ctx) Err!void { const rr = @embedFile("templates/notfound.html"); try constresponse(res, rr, .not_found); } fn badrequest(res: *std.http.Server.Response, _: Ctx) Err!void { const rr = @embedFile("templates/badrequest.html"); try constresponse(res, rr, .bad_request); } fn constresponse(res: *std.http.Server.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.writeAll(rr); try res.finish(); } fn get_comment(res: *std.http.Server.Response, ctx: Ctx, params: Params) Err!void { _ = params; 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); } std.log.debug("found {} comments for url {s}", .{ comments.items.len, url }); 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: *std.http.Server.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: *std.http.Server.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(); }