diff options
Diffstat (limited to 'zig-comments/src/main.zig')
-rw-r--r-- | zig-comments/src/main.zig | 294 |
1 files changed, 0 insertions, 294 deletions
diff --git a/zig-comments/src/main.zig b/zig-comments/src/main.zig deleted file mode 100644 index 898b689..0000000 --- a/zig-comments/src/main.zig +++ /dev/null @@ -1,294 +0,0 @@ -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(); -} |