]> gitweb.ps.run Git - chirp/blob - src/main.zig
commit a whole bunch of changes before removing list form chirp in favor of external...
[chirp] / src / main.zig
1 const std = @import("std");
2 const lmdb = @import("lmdb");
3 const http = @import("http");
4
5 // db {{{
6
7 const Prng = struct {
8     var prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0);
9
10     fn IdType(comptime T: type) type {
11         const ti = @typeInfo(@TypeOf(T.get));
12         return ti.Fn.params[1].type.?;
13     }
14
15     pub fn gen_id(dbi: anytype) IdType(@TypeOf(dbi)) {
16         var id: IdType(@TypeOf(dbi)) = @enumFromInt(Prng.prng.next());
17
18         while (dbi.has(id)) {
19             id = @enumFromInt(Prng.prng.next());
20         }
21
22         return id;
23     }
24
25     pub fn gen_str(dbi: anytype, comptime len: usize) [len]u8 {
26         var buf: [len / 2]u8 = undefined;
27         var res: [len]u8 = undefined;
28         Prng.prng.fill(&buf);
29         for (0..len / 2) |i| {
30             res[i * 2 + 0] = 'a' + (buf[i] % 16);
31             res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
32         }
33
34         while (dbi.has(res)) {
35             Prng.prng.fill(&buf);
36             for (0..len / 2) |i| {
37                 res[i * 2 + 0] = 'a' + (buf[i] % 16);
38                 res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16);
39             }
40         }
41
42         return res;
43     }
44 };
45
46 const Db = struct {
47     fn Key(types: anytype) type {
48         return std.meta.Tuple(&types);
49     }
50     fn users(txn: *const lmdb.Txn) !lmdb.Dbi(UserId, User) {
51         return try txn.dbi("users", UserId, User);
52     }
53     fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, UserId) {
54         return try txn.dbi("user_ids", Username, UserId);
55     }
56     fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, UserId) {
57         return try txn.dbi("sessions", SessionToken, UserId);
58     }
59     fn posts(txn: *const lmdb.Txn) !lmdb.Dbi(PostId, Post) {
60         return try txn.dbi("posts", PostId, Post);
61     }
62     fn lists(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(ListId, ListNode(T)) {
63         return try txn.dbi("lists", ListId, ListNode(T));
64     }
65     fn sets(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(Key(.{ SetId, T }), u0) {
66         return try txn.dbi("sets", Key(.{ SetId, T }), u0);
67     }
68
69     const ListId = enum(u64) { _ };
70
71     pub fn ListNode(comptime T: type) type {
72         return struct {
73             next: ?ListId,
74             prev: ?ListId,
75             data: T,
76         };
77     }
78
79     pub fn List(comptime T: type) type {
80         return struct {
81             const Self = @This();
82
83             first: ListId = undefined,
84             last: ListId = undefined,
85             len: usize = 0,
86
87             pub const Iterator = struct {
88                 dbi: lmdb.Dbi(ListId, ListNode(T)),
89                 id_maybe: ?ListId,
90
91                 pub fn next(self: *Iterator) ?T {
92                     const id = self.id_maybe orelse return null;
93
94                     // TODO: how to handle this?
95                     const ln = self.dbi.get(id) orelse return null;
96                     self.id_maybe = ln.next;
97                     return ln.data;
98                 }
99             };
100             pub fn it(self: *const Self, txn: *const lmdb.Txn) !Iterator {
101                 const list = try Db.lists(txn, T);
102                 return .{ .dbi = list, .id_maybe = if (self.len > 0) self.first else null };
103             }
104             pub fn append(self: *Self, txn: *const lmdb.Txn, t: T) !void {
105                 const list = try Db.lists(txn, T);
106                 const new_id = Prng.gen_id(list);
107
108                 if (self.len == 0) {
109                     self.first = new_id;
110                     self.last = new_id;
111                     self.len = 1;
112                     list.put(new_id, .{
113                         .next = null,
114                         .prev = null,
115                         .data = t,
116                     });
117                 } else {
118                     const prev_id = self.last;
119                     var last = list.get(prev_id).?;
120                     last.next = new_id;
121                     list.put(prev_id, last);
122
123                     list.put(new_id, .{
124                         .next = null,
125                         .prev = prev_id,
126                         .data = t,
127                     });
128
129                     self.last = new_id;
130                     self.len += 1;
131                 }
132             }
133         };
134     }
135
136     const SetId = enum(u64) { _ };
137
138     pub fn Set(comptime T: type) type {
139         return struct {
140             const Self = @This();
141             const SetKey = Key(.{ SetId, T });
142
143             len: usize = 0,
144             set_id: ?SetId = null,
145
146             pub fn init(self: *Self) void {
147                 self.set_id = @enumFromInt(Prng.prng.next());
148             }
149
150             pub fn has(self: Self, txn: *const lmdb.Txn, t: T) !bool {
151                 if (self.set_id == null) return false;
152
153                 const set = try Db.sets(txn, T);
154                 const key = SetKey{ self.set_id.?, t };
155
156                 return set.has(key);
157             }
158
159             pub fn add(self: *Self, txn: *const lmdb.Txn, t: T) !void {
160                 if (self.set_id == null) self.init();
161
162                 const set = try Db.sets(txn, T);
163                 const key = SetKey{ self.set_id.?, t };
164
165                 if (!set.has(key)) {
166                     set.put(key, 0);
167                     if (set.has(key)) {
168                         self.len += 1;
169                     }
170                 }
171             }
172
173             pub fn del(self: *Self, txn: *const lmdb.Txn, t: T) !void {
174                 if (self.set_id == null) self.init();
175
176                 const set = try Db.sets(txn, T);
177                 const key = SetKey{ self.set_id.?, t };
178
179                 if (set.has(key)) {
180                     set.del(key);
181                     if (!set.has(key)) {
182                         self.len -= 1;
183                     }
184                 }
185             }
186         };
187     }
188 };
189
190 // }}}
191
192 // content {{{
193
194 const User = struct {
195     // TODO: choose sizes
196     id: UserId,
197     username: Username,
198     password_hash: PasswordHash,
199     posts: PostList = PostList{},
200 };
201
202 const Post = struct {
203     id: PostId,
204
205     user_id: UserId,
206     time: Timestamp,
207
208     upvotes: UserList = UserList{},
209     downvotes: UserList = UserList{},
210     comments: PostList = PostList{},
211     // reposts
212
213     text: PostText,
214 };
215
216 const SessionTokenLen = 16;
217
218 const Id = u64;
219 const Login = struct {
220     user: User,
221     user_id: UserId,
222     session_token: SessionToken,
223 };
224 const UserId = enum(u64) { _ };
225 const PostId = enum(u64) { _ };
226 const Timestamp = i64;
227 const Username = std.BoundedArray(u8, 16);
228 const PasswordHash = std.BoundedArray(u8, 128);
229 const SessionToken = [SessionTokenLen]u8;
230 const CookieValue = std.BoundedArray(u8, 128);
231 const PostText = std.BoundedArray(u8, 1024);
232 const PostList = Db.List(PostId);
233 const UserList = Db.Set(UserId);
234
235 pub fn hash_password(password: []const u8) !PasswordHash {
236     var hash_buffer = try PasswordHash.init(128);
237
238     // TODO: choose buffer size
239     // TODO: dont allocate on stack, maybe zero memory?
240     var buffer: [1024 * 10]u8 = undefined;
241     var alloc = std.heap.FixedBufferAllocator.init(&buffer);
242
243     // TODO: choose limits
244     const result = try std.crypto.pwhash.argon2.strHash(password, .{
245         .allocator = alloc.allocator(),
246         .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
247     }, hash_buffer.slice());
248
249     try hash_buffer.resize(result.len);
250
251     return hash_buffer;
252 }
253
254 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
255     var buffer: [1024 * 10]u8 = undefined;
256     var alloc = std.heap.FixedBufferAllocator.init(&buffer);
257
258     if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
259         .allocator = alloc.allocator(),
260     })) {
261         return true;
262     } else |err| {
263         std.debug.print("verify error: {}\n", .{err});
264         return false;
265     }
266 }
267
268 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
269     const username_array = try Username.fromSlice(username);
270
271     const txn = try env.txn();
272     defer {
273         txn.commit();
274         env.sync();
275     }
276
277     const users = try Db.users(&txn);
278     const user_ids = try Db.user_ids(&txn);
279
280     if (user_ids.has(username_array)) {
281         return false;
282     } else {
283         const user_id = Prng.gen_id(users);
284         users.put(user_id, User{
285             .id = user_id,
286             .username = username_array,
287             .password_hash = try hash_password(password),
288         });
289
290         user_ids.put(username_array, user_id);
291
292         return true;
293     }
294 }
295
296 pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken {
297     const username_array = try Username.fromSlice(username);
298
299     const txn = try env.txn();
300     defer {
301         txn.commit();
302         env.sync();
303     }
304
305     const user_ids = try Db.user_ids(&txn);
306     const user_id = user_ids.get(username_array) orelse return error.UnknownUsername;
307     std.debug.print("id: {}\n", .{user_id});
308
309     const users = try Db.users(&txn);
310     if (users.get(user_id)) |user| {
311         if (verify_password(password, user.password_hash)) {
312             const sessions = try Db.sessions(&txn);
313             const session_token = Prng.gen_str(sessions, SessionTokenLen);
314             sessions.put(session_token, user_id);
315             return session_token;
316         } else {
317             return error.IncorrectPassword;
318         }
319     } else {
320         return error.UserNotFound;
321     }
322 }
323
324 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
325     const txn = try env.txn();
326     defer {
327         txn.commit();
328         env.sync();
329     }
330
331     const sessions = try Db.sessions(&txn);
332     sessions.del(session_token);
333 }
334
335 fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void {
336     var post_id: PostId = undefined;
337
338     var txn = try env.txn();
339     {
340         const posts = try Db.posts(&txn);
341         post_id = Prng.gen_id(posts);
342         posts.put(post_id, Post{
343             .id = post_id,
344             .user_id = user_id,
345             .time = std.time.timestamp(),
346             .text = try PostText.fromSlice(text),
347         });
348
349         txn.commit();
350         env.sync();
351     }
352
353     txn = try env.txn();
354     {
355         const users = try Db.users(&txn);
356         var user = users.get(user_id) orelse return error.UserNotFound;
357         try user.posts.append(&txn, post_id);
358         users.put(user_id, user);
359
360         txn.commit();
361         env.sync();
362     }
363 }
364
365 fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void {
366     const txn = try env.txn();
367     defer {
368         txn.commit();
369         env.sync();
370     }
371
372     const posts = try Db.posts(&txn);
373
374     var p = posts.get(post_id) orelse return error.PostNotFound;
375     if (dir == .Up) {
376         try p.upvotes.add(&txn, user_id);
377     } else {
378         try p.downvotes.add(&txn, user_id);
379     }
380     posts.put(post_id, p);
381 }
382
383 fn unvote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void {
384     const txn = try env.txn();
385     defer {
386         txn.commit();
387         env.sync();
388     }
389
390     const posts = try Db.posts(&txn);
391
392     var p = posts.get(post_id) orelse return error.PostNotFound;
393     if (dir == .Up) {
394         try p.upvotes.del(&txn, user_id);
395     } else {
396         try p.downvotes.del(&txn, user_id);
397     }
398     posts.put(post_id, p);
399 }
400
401 fn get_session_user_id(env: *lmdb.Env, session_token: SessionToken) !UserId {
402     const txn = try env.txn();
403     defer txn.abort();
404
405     const sessions = try Db.sessions(&txn);
406
407     if (sessions.get(session_token)) |user_id| {
408         return user_id;
409     } else {
410         return error.SessionNotFound;
411     }
412 }
413
414 fn get_user(env: *lmdb.Env, user_id: UserId) !?User {
415     const txn = try env.txn();
416     defer txn.abort();
417
418     const users = try Db.users(&txn);
419     return users.get(user_id);
420 }
421
422 // }}}
423
424 // html {{{
425 fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
426     try res.write("<form style=\"display: inline-block!important;\" action=\"", .{});
427     try res.write(fmt_action, args_action);
428     try res.write("\" method=\"post\">", .{});
429
430     inline for (inputs) |input| {
431         switch (@typeInfo(@TypeOf(input))) {
432             .Struct => {
433                 try res.write("<input ", .{});
434                 try res.write(input[0], input[1]);
435                 try res.write(" />", .{});
436             },
437             else => {
438                 try res.write("<input ", .{});
439                 try res.write(input, .{});
440                 try res.write(" />", .{});
441             },
442         }
443     }
444
445     try res.write("</form>", .{});
446 }
447 // }}}
448
449 // write {{{
450 fn write_header(res: *http.Response, logged_in: ?Login) !void {
451     if (logged_in) |login| {
452         try res.write(
453             \\<a href="/user/{s}">Home</a><br />
454         , .{login.user.username.constSlice()});
455         try html_form(res, "/logout", .{}, .{
456             \\type="submit" value="Logout"
457         });
458         try html_form(res, "/quit", .{}, .{
459             \\type="submit" value="Quit"
460         });
461         try res.write("<br />", .{});
462         try html_form(res, "/post", .{}, .{
463             \\type="text" name="text"
464             ,
465             \\type="submit" value="Post"
466         });
467     } else {
468         try res.write(
469             \\<a href="/">Home</a><br />
470             \\<a href="/register">Register</a>
471             \\<a href="/login">Login</a><br />
472         , .{});
473         try html_form(res, "/quit", .{}, .{
474             \\type="submit" value="Quit"
475         });
476     }
477 }
478 fn write_posts(res: *http.Response, txn: *const lmdb.Txn, user: User, login: ?Login) !void {
479     const posts = try Db.posts(txn);
480     var it = try user.posts.it(txn);
481     while (it.next()) |post_id| {
482         const p = posts.get(post_id) orelse break;
483         try res.write(
484             \\<div>
485             \\<p>{s}</p>
486         , .{p.text.constSlice()});
487         if (login != null and try p.upvotes.has(txn, login.?.user_id)) {
488             try html_form(res, "/unupvote/{}", .{@intFromEnum(post_id)}, .{
489                 .{ "type=\"submit\" value=\"&#x2B06; {}\"", .{p.upvotes.len} },
490             });
491         } else {
492             try html_form(res, "/upvote/{}", .{@intFromEnum(post_id)}, .{
493                 .{ "type=\"submit\" value=\"&#x2B06; {}\"", .{p.upvotes.len} },
494             });
495         }
496         if (login != null and try p.downvotes.has(txn, login.?.user_id)) {
497             try html_form(res, "/undownvote/{}", .{@intFromEnum(post_id)}, .{
498                 .{ "type=\"submit\" value=\"&#x2B07; {}\"", .{p.downvotes.len} },
499             });
500         } else {
501             try html_form(res, "/downvote/{}", .{@intFromEnum(post_id)}, .{
502                 .{ "type=\"submit\" value=\"&#x2B07; {}\"", .{p.downvotes.len} },
503             });
504         }
505         try res.write(
506             \\<span>&#x1F4AD; {}</span>
507             \\</div>
508         , .{p.comments.len});
509     }
510 }
511 // }}}
512
513 fn list_users(env: lmdb.Env) !void {
514     const txn = try env.txn();
515     defer txn.abort();
516
517     // const users = try Db.users(&txn);
518     const users = try txn.dbi("users", UserId, User);
519     var cursor = try users.cursor();
520
521     var key: UserId = undefined;
522     var user_maybe = cursor.get(&key, .First);
523
524     while (user_maybe) |*user| {
525         std.debug.print("[{}] {s}\n", .{ key, user.username.constSlice() });
526
527         user_maybe = cursor.get(&key, .Next);
528     }
529 }
530 fn list_user_ids(env: lmdb.Env) !void {
531     const txn = try env.txn();
532     defer txn.abort();
533
534     const user_ids = try Db.user_ids(&txn);
535     var cursor = try user_ids.cursor();
536
537     var key: Username = undefined;
538     var user_id_maybe = cursor.get(&key, .First);
539
540     while (user_id_maybe) |user_id| {
541         std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
542
543         user_id_maybe = cursor.get(&key, .Next);
544     }
545 }
546
547 fn list_sessions(env: lmdb.Env) !void {
548     const txn = try env.txn();
549     defer txn.abort();
550
551     const sessions = try Db.sessions(&txn);
552     var cursor = try sessions.cursor();
553
554     var key: SessionToken = undefined;
555     var user_id_maybe = cursor.get(&key, .First);
556
557     while (user_id_maybe) |user_id| {
558         std.debug.print("[{s}] {}\n", .{ key, user_id });
559
560         user_id_maybe = cursor.get(&key, .Next);
561     }
562 }
563
564 fn list_posts(env: lmdb.Env) !void {
565     const txn = try env.txn();
566     defer txn.abort();
567
568     const posts = try Db.posts(&txn);
569     var cursor = try posts.cursor();
570
571     var key: PostId = undefined;
572     var post_maybe = cursor.get(&key, .First);
573
574     while (post_maybe) |p| {
575         std.debug.print("[{}] {s}\n", .{ key, p.text.constSlice() });
576
577         post_maybe = cursor.get(&key, .Next);
578     }
579 }
580
581 const ReqBufferSize = 4096;
582 const ResHeadBufferSize = 1024 * 16;
583 const ResBodyBufferSize = 1024 * 16;
584
585 pub fn main() !void {
586     // server
587     var server = try http.Server.init("::", 8080);
588     defer server.deinit();
589
590     // lmdb
591     var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
592     defer env.close();
593
594     std.debug.print("Users:\n", .{});
595     try list_users(env);
596     std.debug.print("User IDs:\n", .{});
597     try list_user_ids(env);
598     std.debug.print("Sessions:\n", .{});
599     try list_sessions(env);
600     std.debug.print("Posts:\n", .{});
601     try list_posts(env);
602
603     try handle_connection(&server, &env);
604     // const ThreadCount = 1;
605     // var ts: [ThreadCount]std.Thread = undefined;
606
607     // for (0..ThreadCount) |i| {
608     //     ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
609     // }
610     // for (0..ThreadCount) |i| {
611     //     ts[i].join();
612     // }
613
614     std.debug.print("done\n", .{});
615 }
616
617 fn handle_connection(server: *http.Server, env: *lmdb.Env) !void {
618     // TODO: static?
619     var req_buffer: [ReqBufferSize]u8 = undefined;
620     var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
621     var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
622
623     accept: while (true) {
624         server.wait();
625
626         while (try server.next_request(&req_buffer)) |req| {
627             // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
628
629             // reponse
630             var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
631
632             // check session token
633             var logged_in: ?Login = null;
634
635             if (req.get_cookie("session_token")) |session_token_str| {
636                 var session_token: SessionToken = undefined;
637                 std.mem.copyForwards(u8, &session_token, session_token_str);
638                 // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
639                 // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
640                 if (get_session_user_id(env, session_token)) |user_id| {
641                     const txn = try env.txn();
642                     defer txn.abort();
643                     const users = try Db.users(&txn);
644
645                     logged_in = .{
646                         .user = users.get(user_id) orelse return error.UserNotFound,
647                         .user_id = user_id,
648                         .session_token = session_token,
649                     };
650                 } else |err| {
651                     std.debug.print("get_session_user err: {}\n", .{err});
652
653                     try res.add_header(
654                         "Set-Cookie",
655                         .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
656                     );
657                 }
658             }
659
660             // html
661             if (req.method == .GET) {
662                 try write_header(&res, logged_in);
663
664                 if (std.mem.eql(u8, req.target, "/register")) {
665                     try res.write(
666                         \\<form action="/register" method="post">
667                         \\<input type="text" name="username" />
668                         \\<input type="password" name="password" />
669                         \\<input type="submit" value="Register" />
670                         \\</form>
671                     , .{});
672                     try res.send();
673                 } else if (std.mem.eql(u8, req.target, "/login")) {
674                     try res.write(
675                         \\<form action="/login" method="post">
676                         \\<input type="text" name="username" />
677                         \\<input type="password" name="password" />
678                         \\<input type="submit" value="Login" />
679                         \\</form>
680                     , .{});
681                     try res.send();
682                 } else if (std.mem.startsWith(u8, req.target, "/user/")) {
683                     const username = req.target[6..req.target.len];
684
685                     const txn = try env.txn();
686                     defer txn.abort();
687
688                     const user_ids = try Db.user_ids(&txn);
689                     if (user_ids.get(try Username.fromSlice(username))) |user_id| {
690                         const users = try Db.users(&txn);
691                         const user = users.get(user_id).?;
692                         try write_posts(&res, &txn, user, logged_in);
693                     } else {
694                         try res.write(
695                             \\<p>User not found</pvo>
696                         , .{});
697                     }
698                     try res.send();
699                 } else {
700                     if (logged_in) |login| {
701                         const user = (try get_user(env, login.user_id)).?;
702
703                         const txn = try env.txn();
704                         defer txn.abort();
705
706                         try write_posts(&res, &txn, user, logged_in);
707
708                         try res.send();
709                     } else {
710                         try res.write("[GET] {s}", .{req.target});
711                         try res.send();
712                     }
713                 }
714             }
715             // api
716             else {
717                 if (std.mem.eql(u8, req.target, "/register")) {
718                     // TODO: handle args not supplied
719                     const username = req.get_value("username").?;
720                     const password = req.get_value("password").?;
721
722                     std.debug.print("New user: {s} {s}\n", .{ username, password });
723                     if (try register_user(env, username, password)) {
724                         try res.redirect("/login");
725                     } else {
726                         try res.redirect("/register");
727                     }
728                     try res.send();
729                 } else if (std.mem.eql(u8, req.target, "/login")) {
730                     // TODO: handle args not supplied
731                     const username = req.get_value("username").?;
732                     const password = req.get_value("password").?;
733
734                     std.debug.print("New login: {s} {s}\n", .{ username, password });
735                     if (login_user(env, username, password)) |session_token| {
736                         res.status = .see_other;
737                         try res.add_header(
738                             "Location",
739                             .{ "/user/{s}", .{username} },
740                         );
741                         try res.add_header(
742                             "Set-Cookie",
743                             .{ "session_token={s}; Secure; HttpOnly", .{session_token} },
744                         );
745
746                         try res.send();
747                     } else |err| {
748                         std.debug.print("login_user err: {}\n", .{err});
749                         try res.redirect("/login");
750                         try res.send();
751                     }
752                 } else if (std.mem.eql(u8, req.target, "/logout")) {
753                     if (logged_in) |login| {
754                         try logout_user(env, login.session_token);
755
756                         try res.add_header(
757                             "Set-Cookie",
758                             .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
759                         );
760
761                         try res.redirect("/");
762                         try res.send();
763                     }
764                 } else if (std.mem.eql(u8, req.target, "/post")) {
765                     if (logged_in) |login| {
766                         const text = req.get_value("text").?;
767                         try post(env, login.user_id, text);
768
769                         try res.redirect("/");
770                         try res.send();
771                     }
772                 } else if (std.mem.eql(u8, req.target, "/quit")) {
773                     try res.redirect("/");
774                     try res.send();
775                     break :accept;
776                 } else if (std.mem.startsWith(u8, req.target, "/upvote/")) {
777                     const login = logged_in orelse return error.NotLoggedIn;
778
779                     const post_id_str = req.target[8..req.target.len];
780                     const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
781
782                     try vote(env, post_id, login.user_id, .Up);
783                     try unvote(env, post_id, login.user_id, .Down);
784
785                     if (req.get_header("Referer")) |ref| {
786                         try res.redirect(ref);
787                     }
788                     try res.send();
789                 } else if (std.mem.startsWith(u8, req.target, "/downvote/")) {
790                     const login = logged_in orelse return error.NotLoggedIn;
791
792                     const post_id_str = req.target[10..req.target.len];
793                     const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
794
795                     try vote(env, post_id, login.user_id, .Down);
796                     try unvote(env, post_id, login.user_id, .Up);
797
798                     if (req.get_header("Referer")) |ref| {
799                         try res.redirect(ref);
800                     }
801                     try res.send();
802                 } else if (std.mem.startsWith(u8, req.target, "/unupvote/")) {
803                     const login = logged_in orelse return error.NotLoggedIn;
804
805                     const post_id_str = req.target[10..req.target.len];
806                     const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
807
808                     try unvote(env, post_id, login.user_id, .Up);
809
810                     if (req.get_header("Referer")) |ref| {
811                         try res.redirect(ref);
812                     }
813                     try res.send();
814                 } else if (std.mem.startsWith(u8, req.target, "/undownvote/")) {
815                     const login = logged_in orelse return error.NotLoggedIn;
816
817                     const post_id_str = req.target[12..req.target.len];
818                     const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10));
819
820                     try unvote(env, post_id, login.user_id, .Down);
821
822                     if (req.get_header("Referer")) |ref| {
823                         try res.redirect(ref);
824                     }
825                     try res.send();
826                 } else {
827                     // try req.respond(
828                     //     \\<p>POST</p>
829                     // , .{});
830                     try res.write("<p>[POST] {s}</p>", .{req.target});
831                     try res.send();
832                 }
833             }
834         }
835     }
836 }