]> gitweb.ps.run Git - chirp/blob - src/main.zig
add ability to specify tag type to html_form
[chirp] / src / main.zig
1 const std = @import("std");
2 const lmdb = @import("lmdb");
3 const db = @import("db");
4 const http = @import("http");
5
6 // db {{{
7
8 const Db = struct {
9     fn users(txn: lmdb.Txn) !db.Db(UserId, User) {
10         return try db.Db(UserId, User).init(txn, "users");
11     }
12     fn user_ids(txn: lmdb.Txn) !db.Db(Username, UserId) {
13         return try db.Db(Username, UserId).init(txn, "user_ids");
14     }
15     fn sessions(txn: lmdb.Txn) !db.Db(SessionToken, UserId) {
16         return try db.Db(SessionToken, UserId).init(txn, "sessions");
17     }
18     fn posts(txn: lmdb.Txn) !db.Db(PostId, Post) {
19         return try db.Db(PostId, Post).init(txn, "posts");
20     }
21 };
22
23 // }}}
24
25 // content {{{
26
27 const User = struct {
28     // TODO: choose sizes
29     id: UserId,
30     name: Username,
31     display_name: DisplayName,
32     description: UserDescription,
33     password_hash: PasswordHash,
34
35     posts: PostList,
36
37     following: UserList,
38     followers: UserList,
39
40     post_lists: PostListList,
41     feeds: UserListList,
42 };
43
44 const Post = struct {
45     id: PostId,
46     parent_id: ?PostId,
47     quote_id: ?PostId,
48
49     user_id: UserId,
50     time: Timestamp,
51
52     upvotes: u64 = 0,
53     downvotes: u64 = 0,
54     votes: VoteList,
55     comments: PostList,
56     quotes: PostList,
57
58     text: PostText,
59 };
60
61 const SavedPostList = struct {
62     name: Name,
63     list: PostList,
64 };
65 const SavedUserList = struct {
66     name: Name,
67     list: UserList,
68 };
69
70 const Vote = struct {
71     const Kind = enum { Up, Down };
72
73     kind: Kind,
74     time: Timestamp,
75 };
76
77 const Id = u64;
78 const Login = struct {
79     user: User,
80     session_token: SessionToken,
81 };
82 const Name = std.BoundedArray(u8, 32);
83 const UserId = enum(u64) { _ };
84 const PostId = enum(u64) { _ };
85 const Timestamp = i64;
86 const Username = std.BoundedArray(u8, 32);
87 const DisplayName = std.BoundedArray(u8, 64);
88 const UserDescription = std.BoundedArray(u8, 1024);
89 const PasswordHash = std.BoundedArray(u8, 128);
90 const SessionToken = u64;
91 const CookieValue = std.BoundedArray(u8, 128);
92 const PostText = std.BoundedArray(u8, 1024);
93 const PostList = db.Set(PostId);
94 const UserList = db.Set(UserId);
95 const VoteList = db.SetList(UserId, Vote);
96 const PostListList = db.List(SavedPostList);
97 const UserListList = db.List(SavedUserList);
98
99 fn parse_enum(comptime E: type, buf: []const u8, base: u8) !E {
100     return @enumFromInt(try std.fmt.parseUnsigned(@typeInfo(E).Enum.tag_type, buf, base));
101 }
102
103 // https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding
104 fn reencode(comptime T: type, text: []const u8) !T {
105     var result = try T.init(0);
106
107     const len = @min(text.len, 1024); // TODO: PostText length
108
109     var idx: usize = 0;
110     while (idx < len) : (idx += 1) {
111         const c = text[idx];
112         if (c == '+') {
113             try result.append(' ');
114         } else if (c == '%' and idx + 2 < text.len) {
115             const allow = &[_]u8{ 0x26, 0x23, 0x3b, 0x0a };
116
117             const escaped_value = std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16) catch continue;
118
119             if (escaped_value == 0x0d) {
120                 try std.fmt.format(result.writer(), "<br />", .{});
121             } else if (std.mem.indexOfScalar(u8, allow, escaped_value) != null) {
122                 try std.fmt.format(result.writer(), "{c}", .{escaped_value});
123             } else {
124                 try std.fmt.format(result.writer(), "&#x{x};", .{escaped_value});
125             }
126
127             idx += 2;
128         } else {
129             try result.append(c);
130         }
131     }
132
133     return result;
134 }
135
136 fn decode(text: []const u8) !std.BoundedArray(u8, 1024) {
137     var result = try std.BoundedArray(u8, 1024).init(0);
138
139     const max_len = @min(text.len, 1024); // TODO: PostText length
140
141     var idx: usize = 0;
142     var len: usize = 0;
143     while (len < max_len and idx < text.len) : ({
144         idx += 1;
145         len += 1;
146     }) {
147         const c = text[idx];
148         if (c == '+') {
149             try result.append(' ');
150         } else if (c == '%') {
151             if (idx + 2 < text.len) {
152                 try std.fmt.format(result.writer(), "{c}", .{try std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16)});
153             }
154             idx += 2;
155         } else {
156             try result.append(c);
157         }
158     }
159
160     return result;
161 }
162
163 const Chirp = struct {
164     const PostsPerPage = 10;
165     const UsersPerPage = 10;
166
167     pub fn hash_password(password: []const u8) !PasswordHash {
168         var hash_buffer = try PasswordHash.init(128);
169
170         // TODO: choose buffer size
171         // TODO: dont allocate on stack, maybe zero memory?
172         var buffer: [1024 * 10]u8 = undefined;
173         var alloc = std.heap.FixedBufferAllocator.init(&buffer);
174
175         // TODO: choose limits
176         const result = try std.crypto.pwhash.argon2.strHash(password, .{
177             .allocator = alloc.allocator(),
178             .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
179         }, hash_buffer.slice());
180
181         try hash_buffer.resize(result.len);
182
183         return hash_buffer;
184     }
185
186     pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
187         var buffer: [1024 * 10]u8 = undefined;
188         var alloc = std.heap.FixedBufferAllocator.init(&buffer);
189
190         if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
191             .allocator = alloc.allocator(),
192         })) {
193             return true;
194         } else |err| {
195             std.debug.print("verify error: {}\n", .{err});
196             return false;
197         }
198     }
199
200     pub fn register_user(env: lmdb.Env, username: []const u8, password: []const u8) !bool {
201         const username_array = try Username.fromSlice(username);
202         const display_name = try DisplayName.fromSlice(username);
203
204         const txn = try env.txn();
205         defer txn.commit() catch |err| {
206             std.debug.print("error registering user: {}\n", .{err});
207         };
208
209         const users = try Db.users(txn);
210         const user_ids = try Db.user_ids(txn);
211
212         if (try user_ids.has(username_array)) {
213             return false;
214         } else {
215             const user_id = try db.Prng.gen(users.dbi, UserId);
216
217             try users.put(user_id, User{
218                 .id = user_id,
219                 .name = username_array,
220                 .display_name = display_name,
221                 .description = try UserDescription.init(0),
222                 .password_hash = try hash_password(password),
223                 .posts = try PostList.init(txn),
224                 .following = try UserList.init(txn),
225                 .followers = try UserList.init(txn),
226                 .post_lists = try PostListList.init(txn),
227                 .feeds = try UserListList.init(txn),
228             });
229
230             try user_ids.put(username_array, user_id);
231
232             return true;
233         }
234     }
235
236     pub fn login_user(
237         env: lmdb.Env,
238         username: []const u8,
239         password: []const u8,
240     ) !SessionToken {
241         const username_array = try Username.fromSlice(username);
242
243         const txn = try env.txn();
244         defer txn.commit() catch {};
245
246         const user_ids = try Db.user_ids(txn);
247         const user_id = try user_ids.get(username_array);
248         std.debug.print("user logging in, id: {}\n", .{user_id});
249
250         const users = try Db.users(txn);
251         const user = try users.get(user_id);
252
253         if (verify_password(password, user.password_hash)) {
254             const sessions = try Db.sessions(txn);
255             const session_token = try db.Prng.gen(sessions.dbi, SessionToken);
256             try sessions.put(session_token, user_id);
257             return session_token;
258         } else {
259             return error.IncorrectPassword;
260         }
261     }
262
263     fn logout_user(env: lmdb.Env, session_token: SessionToken) !void {
264         const txn = try env.txn();
265         defer txn.commit() catch {};
266
267         const sessions = try Db.sessions(txn);
268         try sessions.del(session_token);
269     }
270
271     fn append_post(env: lmdb.Env, user_id: UserId, post_list: PostList, parent_id: ?PostId, quote_id: ?PostId, text: []const u8) !PostId {
272         var post_id: PostId = undefined;
273
274         // TODO: do this in one commit
275
276         var txn: lmdb.Txn = undefined;
277         {
278             // create post
279             txn = try env.txn();
280             defer txn.commit() catch {};
281
282             const posts = try Db.posts(txn);
283             post_id = try db.Prng.gen(posts.dbi, PostId);
284
285             const decoded_text = try reencode(PostText, text);
286             try posts.put(post_id, Post{
287                 .id = post_id,
288                 .parent_id = parent_id,
289                 .quote_id = quote_id,
290                 .user_id = user_id,
291                 .time = std.time.timestamp(),
292                 .votes = try VoteList.init(txn),
293                 .comments = try PostList.init(txn),
294                 .quotes = try PostList.init(txn),
295                 .text = decoded_text,
296             });
297         }
298
299         {
300             // append to user's posts
301             txn = try env.txn();
302             defer txn.commit() catch {};
303
304             var posts_view = try post_list.open(txn);
305             try posts_view.append(post_id);
306         }
307
308         if (quote_id != null) {
309             txn = try env.txn();
310             defer txn.commit() catch {};
311
312             const posts = try Db.posts(txn);
313             const quote_post = try posts.get(quote_id.?);
314             var quotes = try quote_post.quotes.open(txn);
315             try quotes.append(post_id);
316         }
317
318         return post_id;
319     }
320
321     fn post(env: lmdb.Env, user_id: UserId, text: []const u8) !void {
322         var txn = try env.txn();
323         const users = try Db.users(txn);
324         const user = try users.get(user_id);
325         txn.abort();
326
327         const post_id = try append_post(env, user_id, user.posts, null, null, text);
328         _ = post_id;
329     }
330
331     fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void {
332         var txn = try env.txn();
333         const users = try Db.users(txn);
334         const user = try users.get(user_id);
335
336         const posts = try Db.posts(txn);
337         const parent_post = try posts.get(parent_post_id);
338         txn.abort();
339
340         const post_id = try append_post(env, user_id, parent_post.comments, parent_post_id, null, text);
341
342         txn = try env.txn();
343         var replies_view = try user.posts.open(txn);
344         try replies_view.append(post_id);
345         try txn.commit();
346     }
347
348     fn quote(env: lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void {
349         var txn = try env.txn();
350         const users = try Db.users(txn);
351         const user = try users.get(user_id);
352         txn.abort();
353
354         const post_id = try append_post(env, user_id, user.posts, null, quote_post_id, text);
355         _ = post_id;
356     }
357
358     fn vote(env: lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
359         const txn = try env.txn();
360         defer txn.commit() catch {};
361
362         const posts = try Db.posts(txn);
363
364         var p = try posts.get(post_id);
365         var votes_view = try p.votes.open(txn);
366
367         var add_vote = true;
368
369         if (try votes_view.has(user_id)) {
370             const old_vote = try votes_view.get(user_id);
371
372             add_vote = old_vote.kind != kind;
373
374             try votes_view.del(user_id);
375
376             switch (old_vote.kind) {
377                 .Up => p.upvotes -= 1,
378                 .Down => p.downvotes -= 1,
379             }
380             try posts.put(post_id, p);
381         }
382
383         if (add_vote) {
384             try votes_view.append(user_id, Vote{
385                 .kind = kind,
386                 .time = std.time.timestamp(),
387             });
388
389             if (kind == .Up) {
390                 p.upvotes += 1;
391             } else {
392                 p.downvotes += 1;
393             }
394             try posts.put(post_id, p);
395         }
396     }
397
398     fn follow(env: lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
399         const txn = try env.txn();
400         defer txn.commit() catch {};
401
402         const users = try Db.users(txn);
403
404         const user = try users.get(user_id);
405         const user_to_follow = try users.get(user_id_to_follow);
406
407         var user_following = try user.following.open(txn);
408         var user_to_follow_followers = try user_to_follow.followers.open(txn);
409
410         if ((user_following.has(user_id_to_follow) catch false) and (user_to_follow_followers.has(user_id) catch false)) {
411             try user_following.del(user_id_to_follow);
412             try user_to_follow_followers.del(user_id);
413         } else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
414             try user_following.append(user_id_to_follow);
415             try user_to_follow_followers.append(user_id);
416         } else {
417             std.debug.print("Something went wrong when trying to unfollow\n", .{});
418         }
419     }
420
421     fn get_session_user_id(env: lmdb.Env, session_token: SessionToken) !UserId {
422         const txn = try env.txn();
423         defer txn.abort();
424
425         const sessions = try Db.sessions(txn);
426
427         return try sessions.get(session_token);
428     }
429
430     fn get_user(env: lmdb.Env, user_id: UserId) !User {
431         const txn = try env.txn();
432         defer txn.abort();
433
434         const users = try Db.users(txn);
435         return try users.get(user_id);
436     }
437 };
438
439 // }}}
440
441 // html {{{
442 pub fn Paginate(comptime T: type) type {
443     return struct {
444         const Self = @This();
445
446         const IterateResult = T.Base.View.Iterator.Result;
447
448         res: *http.Response,
449         view: T.View,
450         per_page: u64,
451
452         it: T.Base.View.Iterator,
453         starting_idx: ?T.Base.Key,
454         count: u64 = 0,
455
456         pub fn init(res: *http.Response, view: T.View, per_page: u64) !Self {
457             var it = view.reverse_iterator();
458             if (res.req.get_param("starting_at")) |starting_at_str| {
459                 it.idx = try parse_enum(T.Base.Key, starting_at_str, 16);
460             }
461
462             return .{
463                 .res = res,
464                 .view = view,
465                 .per_page = per_page,
466                 .it = it,
467                 .starting_idx = it.idx,
468             };
469         }
470         pub fn next(self: *Self) IterateResult {
471             if (self.it.next()) |kv| {
472                 if (self.count < self.per_page) {
473                     self.count += 1;
474                     return kv;
475                 }
476             }
477             return null;
478         }
479         pub fn write_navigation(self: *Self) !void {
480             const next_idx = self.it.next();
481
482             if (self.view.base.head.last != self.starting_idx) {
483                 var prev_it = self.view.iterator();
484                 prev_it.idx = self.starting_idx.?;
485                 var oldest_idx = self.starting_idx.?;
486
487                 var count: u64 = 0;
488                 while (prev_it.next()) |kv| {
489                     oldest_idx = kv.key;
490
491                     if (count > self.per_page) {
492                         break;
493                     } else {
494                         count += 1;
495                     }
496                 }
497
498                 try self.res.write("<a href=\"{s}?starting_at={x}\">Prev</a> ", .{ self.res.req.target, @intFromEnum(oldest_idx) });
499             }
500
501             if (next_idx) |kv| {
502                 try self.res.write("<a href=\"{s}?starting_at={x}\">Next</a>", .{ self.res.req.target, @intFromEnum(kv.key) });
503             }
504         }
505     };
506 }
507 fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void {
508     try res.write("<form action=\"{s}\" method=\"post\">", .{action});
509
510     inline for (inputs) |input| {
511         switch (@typeInfo(@TypeOf(input))) {
512             .Struct => |s| {
513                 if (s.fields.len == 3) {
514                     try res.write("<{s} ", .{input[0]});
515                     try res.write(input[1], input[2]);
516                     try res.write("></{s}>", .{input[0]});
517                 } else {
518                     try res.write("<input ", .{});
519                     try res.write(input[0], input[1]);
520                     try res.write(" />", .{});
521                 }
522             },
523             else => {
524                 try res.write("<input ", .{});
525                 try res.write(input, .{});
526                 try res.write(" />", .{});
527             },
528         }
529     }
530
531     try res.write("</form>", .{});
532 }
533 // }}}
534
535 // write {{{
536 const TimeStr = std.BoundedArray(u8, 256);
537
538 // http://howardhinnant.github.io/date_algorithms.html
539 fn time_str(_t: i64) TimeStr {
540     const t: u64 = @intCast(_t);
541     var result = TimeStr.init(0) catch unreachable;
542
543     const nD = @divFloor(t, std.time.s_per_day);
544     const z: u64 = nD + 719468;
545     const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
546     const doe: u64 = z - era * 146097; // [0, 146096]
547     const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
548     const Y: u64 = yoe + era * 400;
549     const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
550     const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
551     const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
552     const M: u64 = if (mp < 10) mp + 3 else mp - 9;
553
554     const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
555     const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
556     const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;
557
558     std.fmt.format(result.writer(), "<time><span>{:0>4}-{:0>2}-{:0>2} {:0>2}:{:0>2}:{:0>2} UTC</span><script>document.currentScript.parentElement.innerHTML=new Date({}).toLocaleString()</script></time>", .{ Y, M, D, h, m, s, t * 1000 }) catch unreachable;
559
560     return result;
561 }
562 fn write_header(res: *http.Response, logged_in: ?Login) !void {
563     if (logged_in) |login| {
564         try res.write(
565             \\<a href="/">Home</a><br />
566         , .{});
567         try res.write(
568             \\<a href="/user/{s}">Profile</a><br />
569         , .{login.user.name.constSlice()});
570         try res.write(
571             \\<a href="/post">Post</a><br />
572         , .{});
573         try html_form(res, "/logout", .{
574             \\type="submit" value="Logout"
575         });
576         try res.write("<br /><br />", .{});
577     } else {
578         try res.write(
579             \\<a href="/">Home</a><br />
580             \\<form action="/register" method="post">
581             \\<input type="text" name="username" />
582             \\<input type="password" name="password" />
583             \\<input type="submit" value="Register" />
584             \\</form><br />
585             \\<form action="/login" method="post">
586             \\<input type="text" name="username" />
587             \\<input type="password" name="password" />
588             \\<input type="submit" value="Login" />
589             \\</form><br /><br />
590         , .{});
591     }
592 }
593 fn write_start(res: *http.Response) !void {
594     try res.write(
595         \\<!doctype html>
596         \\<html>
597         \\<head>
598         \\<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>&#128035;</text></svg>">
599         \\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
600         \\<style>
601         \\  form {
602         \\    display: inline-block;
603         \\  }
604         \\</style>
605         \\</head>
606         \\<body>
607     , .{});
608 }
609 fn write_end(res: *http.Response) !void {
610     try res.write("</body></html>", .{});
611 }
612 fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, options: struct { recurse: u8 = 0, show_comment_field: bool = false }) !void {
613     const posts = try Db.posts(txn);
614     const post = posts.get(post_id) catch {
615         res.redirect("/") catch {};
616         return;
617     };
618     const users = try Db.users(txn);
619     const user = try users.get(post.user_id);
620
621     try res.write(
622         \\<div id="{x}">
623         \\<span><a href="/user/{s}">{s}</a>
624     , .{ @intFromEnum(post_id), user.name.constSlice(), user.display_name.constSlice() });
625     if (post.parent_id) |id| {
626         try res.write(" <a href=\"/post/{x}\">..</a>", .{@intFromEnum(id)});
627     }
628     try res.write(
629         \\ {s}</span><br />
630         \\<span>{s}</span><br />
631     , .{ time_str(post.time).constSlice(), post.text.constSlice() });
632
633     if (logged_in != null and post.user_id == logged_in.?.user.id) {
634         // Votes
635         try res.write(
636             \\<small>
637             \\<a href="/upvotes/{0x}">{1} Upvotes</a>
638             \\<a href="/downvotes/{0x}">{2} Downvotes</a>
639             \\</small>
640             \\<br />
641         , .{ @intFromEnum(post_id), post.upvotes, post.downvotes });
642     }
643
644     if (post.quote_id) |quote_id| {
645         try res.write("<div style=\"border: 1px solid black;\">", .{});
646         if (options.recurse > 0) {
647             try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 });
648         } else {
649             try res.write("<a href=\"/post/{x}\">...</a>", .{@intFromEnum(post_id)});
650         }
651         try res.write("</div>", .{});
652     }
653
654     const comments_view = try post.comments.open(txn);
655     const quotes_view = try post.quotes.open(txn);
656     const votes_view = try post.votes.open(txn);
657
658     // Votes
659     const vote: ?Vote = if (logged_in != null and try votes_view.has(logged_in.?.user.id)) try votes_view.get(logged_in.?.user.id) else null;
660
661     if (vote != null and vote.?.kind == .Up) {
662         try html_form(res, "/upvote", .{
663             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
664             .{ "type=\"submit\" value=\"&#x2B06; {}\"", .{post.upvotes} },
665         });
666     } else {
667         try html_form(res, "/upvote", .{
668             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
669             .{ "type=\"submit\" value=\"&#x21E7; {}\"", .{post.upvotes} },
670         });
671     }
672     if (vote != null and vote.?.kind == .Down) {
673         try html_form(res, "/downvote", .{
674             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
675             .{ "type=\"submit\" value=\"&#x2B07; {}\"", .{post.downvotes} },
676         });
677     } else {
678         try html_form(res, "/downvote", .{
679             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
680             .{ "type=\"submit\" value=\"&#x21E9; {}\"", .{post.downvotes} },
681         });
682     }
683
684     // Comment Count
685     try res.write(
686         \\<a href="/post/{x}">&#x1F4AD; {}</a> 
687     , .{ @intFromEnum(post.id), comments_view.len() });
688
689     // Quote
690     try res.write(
691         \\<a href="/quoted/{x}">&#x1F501; {}</a> 
692     , .{ @intFromEnum(post.id), quotes_view.len() });
693
694     // Save to List
695     if (logged_in) |login| {
696         const lists_view = try login.user.post_lists.open(txn);
697         try res.write("<form action=\"/list_add\" method=\"post\">", .{});
698         try res.write("<select name=\"list_id\">", .{});
699         var it = lists_view.iterator();
700         // TODO: mark lists that already contain post
701         while (it.next()) |kv| {
702             const name = kv.val.name;
703             const id = kv.val.list.base.idx.?;
704             const list_view = try kv.val.list.open(txn);
705             try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(post_id) catch false) " *" else "" });
706         }
707         try res.write("</select>", .{});
708         try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\" />", .{@intFromEnum(post_id)});
709         try res.write("<input type=\"submit\" value=\"Save\" />", .{});
710         try res.write("</form>", .{});
711     }
712
713     // Comment field
714     // TODO: maybe always show comment field and prompt for login
715     if (options.show_comment_field and logged_in != null) {
716         try res.write("<br /><br />", .{});
717         try html_form(res, "/comment", .{
718             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
719             "type=\"text\" name=\"text\" placeholder=\"Text\"",
720             "type=\"submit\" value=\"Comment\"",
721         });
722         try res.write("<br />", .{});
723     }
724
725     // Comments
726     if (options.recurse > 0 and comments_view.len() > 0) {
727         try res.write(
728             \\<details{s}>
729             \\<summary>Comments</summary>
730         , .{if (options.recurse > 1) " open" else ""});
731         try res.write("<div style=\"margin: 10px;\">", .{});
732         var it = comments_view.iterator();
733         var count: u8 = 0;
734         while (it.next()) |comment_id| {
735             try write_post(res, txn, logged_in, comment_id.key, .{ .recurse = options.recurse - 1 });
736             try res.write("<br />", .{});
737             if (options.recurse == 1) {
738                 count += 1;
739                 if (count >= 3) break;
740             }
741         }
742         try res.write(
743             \\</div>
744             \\</details>
745         , .{});
746     }
747
748     try res.write(
749         \\</div>
750     , .{});
751 }
752 fn write_profile(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
753     const following = try user.following.open(txn);
754     const followers = try user.followers.open(txn);
755
756     try res.write(
757         \\<h2 style="display: inline;"><a href="/user/{s}">{s}</a></h2>
758     , .{
759         user.name.constSlice(), user.display_name.constSlice(),
760     });
761     if (logged_in != null and user.id != logged_in.?.user.id) {
762         const login = logged_in.?;
763
764         // follow/unfollow
765         if (try followers.has(login.user.id)) {
766             try html_form(res, "/follow", .{
767                 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
768                 \\type="submit" value="Unfollow"
769             });
770         } else {
771             try html_form(res, "/follow", .{
772                 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
773                 \\type="submit" value="Follow"
774             });
775         }
776
777         // add to feed
778         const feeds_view = try login.user.feeds.open(txn);
779         try res.write("<form action=\"/feed_add\" method=\"post\">", .{});
780         try res.write("<select name=\"feed_id\">", .{});
781         var it = feeds_view.iterator();
782         while (it.next()) |kv| {
783             const name = kv.val.name;
784             const id = kv.val.list.base.idx.?;
785             const list_view = try kv.val.list.open(txn);
786             try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(user.id) catch false) " *" else "" });
787         }
788         try res.write("</select>", .{});
789         try res.write("<input type=\"hidden\" name=\"user_id\" value=\"{x}\" />", .{@intFromEnum(user.id)});
790         try res.write("<input type=\"submit\" value=\"Add to feed\" />", .{});
791         try res.write("</form>", .{});
792     }
793     try res.write(
794         \\ <a href="/following/{s}">{} following</a>
795         \\ <a href="/followers/{s}">{} followers</a>
796         \\<br />
797     , .{
798         user.name.constSlice(), following.len(),
799         user.name.constSlice(), followers.len(),
800     });
801
802     try res.write(
803         \\<a href="/all/{0s}">All Posts</a>
804         \\ <a href="/comments/{0s}">Comments</a>
805         \\ <a href="/quotes/{0s}">Quotes</a><br />
806     , .{
807         user.name.constSlice(),
808     });
809
810     if (logged_in != null and user.id == logged_in.?.user.id) {
811         try res.write(
812             \\<a href="/lists">Lists</a>
813             \\<a href="/feeds">Feeds</a>
814             \\<a href="/edit">Edit</a><br />
815             \\<br />
816         , .{});
817     }
818
819     if (user.description.len > 0) {
820         try res.write(
821             \\<div style="padding-left: 5px; border-left: 1px solid grey;">
822             // \\&#x00AB; {s} &#x00BB;
823             \\<i>{s}</i>
824             \\</div>
825         , .{user.description.constSlice()});
826     }
827
828     try res.write("<br />", .{});
829 }
830 fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList, options: struct {
831     show_posts: bool,
832     show_quotes: bool,
833     show_comments: bool,
834 }) !void {
835     const posts_view = try post_list.open(txn);
836
837     var paginate = try Paginate(PostList).init(res, posts_view, Chirp.PostsPerPage);
838
839     while (paginate.next()) |post_id| {
840         const posts = try Db.posts(txn);
841         const post = try posts.get(post_id.key);
842         if ((options.show_posts and (post.parent_id == null and post.quote_id == null)) or
843             (options.show_quotes and (post.quote_id != null)) or
844             (options.show_comments and (post.parent_id != null)))
845         {
846             try write_post(res, txn, logged_in, post_id.key, .{ .recurse = 1 });
847             try res.write("<br />", .{});
848         }
849     }
850
851     try paginate.write_navigation();
852 }
853 fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
854     const users = try Db.users(txn);
855     const posts = try Db.posts(txn);
856
857     var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
858     var prev_newest_post: ?Post = null;
859
860     const following = try user_list.open(txn);
861
862     while (true) {
863         var newest_post: ?Post = null;
864
865         var following_it = following.iterator();
866         while (following_it.next()) |following_id| {
867             const followed_user = try users.get(following_id.key);
868             const followed_posts = try followed_user.posts.open(txn);
869
870             if (followed_posts.len() == 0) {
871                 continue;
872             }
873
874             var followed_posts_it = followed_posts.reverse_iterator();
875             while (followed_posts_it.next()) |followed_post_id| {
876                 const p = try posts.get(followed_post_id.key);
877
878                 if ((prev_newest_post == null or p.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < p.time)) {
879                     newest_post = p;
880                     break;
881                 }
882             }
883         }
884         if (newest_post) |post| {
885             newest_post_ids.append(post.id) catch break;
886             prev_newest_post = post;
887         } else {
888             break;
889         }
890     }
891
892     for (newest_post_ids.constSlice()) |post_id| {
893         try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
894         try res.write("<br />", .{});
895     }
896 }
897 fn write_user(res: *http.Response, txn: lmdb.Txn, user_id: UserId) !void {
898     const users = try Db.users(txn);
899     const user = try users.get(user_id);
900     try res.write(
901         \\<a href="/user/{s}">{s}</a>
902     , .{ user.name.constSlice(), user.display_name.constSlice() });
903 }
904 fn write_votes(res: *http.Response, txn: lmdb.Txn, votes: VoteList, options: struct {
905     show_upvotes: bool = true,
906     show_downvotes: bool = true,
907 }) !void {
908     const votes_view = try votes.open(txn);
909
910     var paginate = try Paginate(VoteList).init(res, votes_view, Chirp.UsersPerPage);
911
912     while (paginate.next()) |kv| {
913         const user_id = kv.key;
914         const vote = kv.val;
915
916         if ((options.show_upvotes and vote.kind == .Up) or
917             (options.show_downvotes and vote.kind == .Down))
918         {
919             try write_user(res, txn, user_id);
920             try res.write(" <small>{s}</small><br />", .{time_str(vote.time).constSlice()});
921         }
922     }
923
924     try paginate.write_navigation();
925 }
926 fn check_login(env: lmdb.Env, req: http.Request, res: *http.Response) !?Login {
927     var result: ?Login = null;
928
929     if (req.get_cookie("session_token")) |session_token_str| {
930         var remove_session_token = true;
931
932         if (std.fmt.parseUnsigned(SessionToken, session_token_str, 16) catch null) |session_token| {
933             // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
934             // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
935             if (Chirp.get_session_user_id(env, session_token) catch null) |user_id| {
936                 const txn = try env.txn();
937                 defer txn.abort();
938                 const users = try Db.users(txn);
939
940                 result = .{
941                     .user = try users.get(user_id),
942                     .session_token = session_token,
943                 };
944
945                 remove_session_token = false;
946             }
947         }
948
949         if (remove_session_token) {
950             try res.add_header(
951                 "Set-Cookie",
952                 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
953             );
954         }
955     }
956
957     return result;
958 }
959 // }}}
960
961 // GET {{{
962 const GET = struct {
963     const Self = @This();
964
965     txn: lmdb.Txn,
966     req: http.Request,
967     res: *http.Response,
968     logged_in: ?Login,
969
970     fn handle(self: Self) !bool {
971         const ti = @typeInfo(Self);
972         inline for (ti.Struct.decls) |f_decl| {
973             const has_arg = f_decl.name.len > 1 and f_decl.name[f_decl.name.len - 1] == '/';
974             const match = if (has_arg) std.mem.startsWith(u8, self.req.target, f_decl.name) else std.mem.eql(u8, self.req.target, f_decl.name);
975
976             if (match) {
977                 const f = @field(Self, f_decl.name);
978                 const fi = @typeInfo(@TypeOf(f));
979                 if (fi.Fn.params.len == 1) {
980                     try @call(.auto, f, .{self});
981                 } else {
982                     const arg_type = fi.Fn.params[1].type.?;
983                     const arg_info = @typeInfo(arg_type);
984                     var arg: arg_type = undefined;
985                     const field = arg_info.Struct.fields[0];
986                     if (self.req.target.len <= f_decl.name.len) {
987                         return error.NoArgProvided;
988                     }
989                     const str = self.req.target[f_decl.name.len..self.req.target.len];
990                     const field_ti = @typeInfo(field.type);
991                     switch (field_ti) {
992                         // TODO: maybe handle BoundedArray?
993                         .Int => {
994                             @field(arg, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
995                         },
996                         .Enum => {
997                             @field(arg, field.name) = try parse_enum(field.type, str, 16);
998                         },
999                         else => {
1000                             @field(arg, field.name) = str;
1001                         },
1002                     }
1003
1004                     try @call(.auto, f, .{ self, arg });
1005                 }
1006                 return true;
1007             }
1008         }
1009         return false;
1010     }
1011
1012     pub fn @"/user/"(self: Self, args: struct { username: []const u8 }) !void {
1013         const user_ids = try Db.user_ids(self.txn);
1014         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1015             const users = try Db.users(self.txn);
1016             const user = try users.get(user_id);
1017
1018             try write_profile(self.res, self.txn, self.logged_in, user);
1019
1020             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1021                 .show_posts = true,
1022                 .show_quotes = false,
1023                 .show_comments = false,
1024             });
1025         } else |err| {
1026             try self.res.write(
1027                 \\<p>User not found [{}]</p>
1028             , .{err});
1029         }
1030     }
1031     pub fn @"/comments/"(self: Self, args: struct { username: []const u8 }) !void {
1032         const user_ids = try Db.user_ids(self.txn);
1033         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1034             const users = try Db.users(self.txn);
1035             const user = try users.get(user_id);
1036
1037             try write_profile(self.res, self.txn, self.logged_in, user);
1038
1039             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1040                 .show_posts = false,
1041                 .show_quotes = false,
1042                 .show_comments = true,
1043             });
1044         } else |err| {
1045             try self.res.write(
1046                 \\<p>User not found [{}]</p>
1047             , .{err});
1048         }
1049     }
1050     pub fn @"/quotes/"(self: Self, args: struct { username: []const u8 }) !void {
1051         const user_ids = try Db.user_ids(self.txn);
1052         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1053             const users = try Db.users(self.txn);
1054             const user = try users.get(user_id);
1055
1056             try write_profile(self.res, self.txn, self.logged_in, user);
1057
1058             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1059                 .show_posts = false,
1060                 .show_quotes = true,
1061                 .show_comments = false,
1062             });
1063         } else |err| {
1064             try self.res.write(
1065                 \\<p>User not found [{}]</p>
1066             , .{err});
1067         }
1068     }
1069     pub fn @"/all/"(self: Self, args: struct { username: []const u8 }) !void {
1070         const user_ids = try Db.user_ids(self.txn);
1071         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1072             const users = try Db.users(self.txn);
1073             const user = try users.get(user_id);
1074
1075             try write_profile(self.res, self.txn, self.logged_in, user);
1076
1077             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1078                 .show_posts = true,
1079                 .show_quotes = true,
1080                 .show_comments = true,
1081             });
1082         } else |err| {
1083             try self.res.write(
1084                 \\<p>User not found [{}]</p>
1085             , .{err});
1086         }
1087     }
1088     pub fn @"/following/"(self: Self, args: struct { username: []const u8 }) !void {
1089         const user_ids = try Db.user_ids(self.txn);
1090         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1091             const users = try Db.users(self.txn);
1092             const user = try users.get(user_id);
1093
1094             const following_view = try user.following.open(self.txn);
1095
1096             var paginate = try Paginate(UserList).init(self.res, following_view, Chirp.UsersPerPage);
1097
1098             try self.res.write(
1099                 \\<h2><a href="/user/{s}">{s}</a> follows:</h2>
1100             , .{ user.name.constSlice(), user.display_name.constSlice() });
1101
1102             while (paginate.next()) |following_id| {
1103                 const following_user = try users.get(following_id.key);
1104
1105                 try self.res.write(
1106                     \\<a href="/user/{s}">{s}</a><br />
1107                 , .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
1108             }
1109
1110             try paginate.write_navigation();
1111         } else |err| {
1112             try self.res.write(
1113                 \\<p>User not found [{}]</p>
1114             , .{err});
1115         }
1116     }
1117     pub fn @"/followers/"(self: Self, args: struct { username: []const u8 }) !void {
1118         const user_ids = try Db.user_ids(self.txn);
1119         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1120             const users = try Db.users(self.txn);
1121             const user = try users.get(user_id);
1122
1123             const followers_view = try user.followers.open(self.txn);
1124             var paginate = try Paginate(UserList).init(self.res, followers_view, Chirp.UsersPerPage);
1125
1126             try self.res.write(
1127                 \\<h2><a href="/user/{s}">{s}</a> followers:</h2>
1128             , .{ user.name.constSlice(), user.display_name.constSlice() });
1129
1130             while (paginate.next()) |follower_id| {
1131                 const follower_user = try users.get(follower_id.key);
1132
1133                 try self.res.write(
1134                     \\<a href="/user/{s}">{s}</a><br />
1135                 , .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
1136             }
1137
1138             try paginate.write_navigation();
1139         } else |err| {
1140             try self.res.write(
1141                 \\<p>User not found [{}]</p>
1142             , .{err});
1143         }
1144     }
1145     pub fn @"/post/"(self: Self, args: struct { post_id: PostId }) !void {
1146         try write_post(self.res, self.txn, self.logged_in, args.post_id, .{
1147             .recurse = 3, // TODO: factor out
1148             .show_comment_field = true,
1149         });
1150     }
1151     pub fn @"/upvotes/"(self: Self, args: struct { post_id: PostId }) !void {
1152         const posts = try Db.posts(self.txn);
1153         const post = try posts.get(args.post_id);
1154         try self.res.write("{} upvotes:<br />", .{post.upvotes});
1155         try write_votes(self.res, self.txn, post.votes, .{});
1156     }
1157     pub fn @"/quoted/"(self: Self, args: struct { post_id: PostId }) !void {
1158         const posts = try Db.posts(self.txn);
1159         const post = try posts.get(args.post_id);
1160
1161         const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;
1162
1163         if (self.logged_in != null) {
1164             try html_form(self.res, "/quote", .{
1165                 .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
1166                 .{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
1167                 "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
1168                 "type=\"submit\" value=\"Quote\"",
1169             });
1170             try self.res.write("<br />", .{});
1171         }
1172
1173         // TODO: show all bc this only contains quotes?
1174         try write_posts(self.res, self.txn, self.logged_in, post.quotes, .{
1175             .show_posts = false,
1176             .show_quotes = true,
1177             .show_comments = false,
1178         });
1179     }
1180     pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
1181         try write_posts(self.res, self.txn, self.logged_in, PostList{ .base = .{ .idx = args.list_id } }, .{
1182             .show_posts = true,
1183             .show_quotes = true,
1184             .show_comments = true,
1185         });
1186     }
1187     pub fn @"/lists"(self: Self) !void {
1188         if (self.logged_in) |login| {
1189             const post_lists_view = try login.user.post_lists.open(self.txn);
1190
1191             try html_form(self.res, "/create_list", .{
1192                 "type=\"text\" name=\"name\"",
1193                 "type=\"submit\" value=\"Add\"",
1194             });
1195
1196             try self.res.write("<br /><br />", .{});
1197
1198             var it = post_lists_view.iterator();
1199             while (it.next()) |kv| {
1200                 const name = kv.val.name;
1201                 const post_list = kv.val.list;
1202                 try self.res.write(
1203                     \\<a href="/list/{x}">{s}</a> 
1204                 , .{ post_list.base.idx.?, name.constSlice() });
1205                 try html_form(self.res, "/delete_list", .{
1206                     .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
1207                     "type=\"submit\" value=\"Delete\"",
1208                 });
1209                 try self.res.write("<br />", .{});
1210             }
1211         } else {
1212             try self.res.write("not logged in", .{});
1213         }
1214     }
1215     pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Base.Index }) !void {
1216         try write_timeline(self.res, self.txn, self.logged_in, UserList{ .base = .{ .idx = args.feed_id } });
1217     }
1218     pub fn @"/feeds"(self: Self) !void {
1219         if (self.logged_in) |login| {
1220             const feeds_view = try login.user.feeds.open(self.txn);
1221
1222             try html_form(self.res, "/create_feed", .{
1223                 "type=\"text\" name=\"name\"",
1224                 "type=\"submit\" value=\"Add\"",
1225             });
1226
1227             try self.res.write("<br /><br />", .{});
1228
1229             var it = feeds_view.iterator();
1230             while (it.next()) |kv| {
1231                 const name = kv.val.name;
1232                 const user_list = kv.val.list;
1233                 try self.res.write(
1234                     \\<a href="/feed/{x}">{s}</a> 
1235                 , .{ user_list.base.idx.?, name.constSlice() });
1236                 try html_form(self.res, "/delete_feed", .{
1237                     .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
1238                     "type=\"submit\" value=\"Delete\"",
1239                 });
1240                 try self.res.write("<br />", .{});
1241             }
1242         } else {
1243             try self.res.write("not logged in", .{});
1244         }
1245     }
1246     pub fn @"/post"(self: Self) !void {
1247         if (self.logged_in) |login| {
1248             _ = login;
1249             const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;
1250
1251             try html_form(self.res, "/post", .{
1252                 .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
1253                 "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
1254                 "type=\"submit\" value=\"Post\"",
1255             });
1256         } else {
1257             try self.res.write("not logged in", .{});
1258         }
1259     }
1260     pub fn @"/edit"(self: Self) !void {
1261         if (self.logged_in) |login| {
1262             try self.res.write("<br />Username: ", .{});
1263             try html_form(self.res, "/set_username", .{
1264                 .{ "type=\"text\" name=\"username\" placeholder=\"{s}\"", .{login.user.name.constSlice()} },
1265                 "type=\"submit\" value=\"Change\"",
1266             });
1267             try self.res.write("<br />Display Name: ", .{});
1268             try html_form(self.res, "/set_display_name", .{
1269                 .{ "type=\"text\" name=\"display_name\" placeholder=\"{s}\"", .{login.user.display_name.constSlice()} },
1270                 "type=\"submit\" value=\"Change\"",
1271             });
1272             try self.res.write("<br />Description: ", .{});
1273             try html_form(self.res, "/set_description", .{
1274                 .{ "textarea", "type=\"text\" name=\"description\" placeholder=\"{s}\"", .{login.user.description.constSlice()} },
1275                 "type=\"submit\" value=\"Change\"",
1276             });
1277             try self.res.write("<br />Password: ", .{});
1278             try html_form(self.res, "/set_password", .{
1279                 "type=\"text\" name=\"password\"",
1280                 "type=\"submit\" value=\"Change\"",
1281             });
1282         } else {
1283             try self.res.write("not logged in", .{});
1284         }
1285     }
1286     pub fn @"/"(self: Self) !void {
1287         if (self.logged_in) |login| {
1288             try write_timeline(self.res, self.txn, self.logged_in, login.user.following);
1289         } else {
1290             // TODO: generic home
1291             try self.res.write("Homepage", .{});
1292         }
1293     }
1294 };
1295 // }}}
1296
1297 // POST {{{
1298 const POST = struct {
1299     const Self = @This();
1300
1301     env: lmdb.Env,
1302     req: http.Request,
1303     res: *http.Response,
1304     logged_in: ?Login,
1305
1306     pub fn handle(self: Self) !bool {
1307         const ti = @typeInfo(Self);
1308         inline for (ti.Struct.decls) |f_decl| {
1309             if (std.mem.eql(u8, f_decl.name, self.req.target)) {
1310                 const f = @field(Self, f_decl.name);
1311                 const fi = @typeInfo(@TypeOf(f));
1312                 if (fi.Fn.params.len == 1) {
1313                     _ = try @call(.auto, f, .{self});
1314                 } else {
1315                     const args_type = fi.Fn.params[fi.Fn.params.len - 1].type.?;
1316                     const argsi = @typeInfo(args_type);
1317                     var args: args_type = undefined;
1318                     inline for (argsi.Struct.fields) |field| {
1319                         const str = self.req.get_value(field.name) orelse return error.ArgNotFound;
1320                         const field_ti = @typeInfo(field.type);
1321                         switch (field_ti) {
1322                             .Int => {
1323                                 @field(args, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
1324                             },
1325                             .Enum => {
1326                                 @field(args, field.name) = try parse_enum(field.type, str, 16);
1327                             },
1328                             else => {
1329                                 @field(args, field.name) = str;
1330                             },
1331                         }
1332                     }
1333                     try @call(.auto, f, .{ self, args });
1334                 }
1335                 return true;
1336             }
1337         }
1338         return false;
1339     }
1340
1341     pub fn @"/register"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
1342         // TODO: handle args not supplied
1343         std.debug.print("New user: {s} {s}\n", .{ args.username, args.password });
1344         _ = try Chirp.register_user(self.env, args.username, args.password);
1345     }
1346     pub fn @"/login"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
1347         // TODO: handle args not supplied
1348         std.debug.print("New login: {s} {s}\n", .{ args.username, args.password });
1349         if (Chirp.login_user(self.env, args.username, args.password)) |session_token| {
1350             self.res.status = .see_other;
1351             try self.res.add_header(
1352                 "Set-Cookie",
1353                 .{ "session_token={x}; HttpOnly", .{session_token} },
1354             );
1355         } else |err| {
1356             std.debug.print("login_user err: {}\n", .{err});
1357         }
1358     }
1359     pub fn @"/logout"(self: Self) !void {
1360         if (self.logged_in) |login| {
1361             try Chirp.logout_user(self.env, login.session_token);
1362
1363             try self.res.add_header(
1364                 "Set-Cookie",
1365                 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
1366             );
1367         }
1368     }
1369     pub fn @"/set_username"(self: Self, args: struct { username: []const u8 }) !void {
1370         const login = self.logged_in orelse return error.NotLoggedIn;
1371         const username = try Username.fromSlice(args.username);
1372
1373         const txn = try self.env.txn();
1374         defer txn.commit() catch {};
1375
1376         const user_ids = try Db.user_ids(txn);
1377
1378         if (!try user_ids.has(username)) {
1379             try user_ids.del(login.user.name);
1380             try user_ids.put(username, login.user.id);
1381
1382             const users = try Db.users(txn);
1383             var user = login.user;
1384             user.name = username;
1385             try users.put(login.user.id, user);
1386         }
1387     }
1388     pub fn @"/set_display_name"(self: Self, args: struct { display_name: []const u8 }) !void {
1389         const login = self.logged_in orelse return error.NotLoggedIn;
1390         const display_name = try DisplayName.fromSlice(args.display_name);
1391
1392         const txn = try self.env.txn();
1393         defer txn.commit() catch {};
1394
1395         const users = try Db.users(txn);
1396         var user = login.user;
1397         user.display_name = display_name;
1398         try users.put(login.user.id, user);
1399     }
1400     pub fn @"/set_description"(self: Self, args: struct { description: []const u8 }) !void {
1401         const login = self.logged_in orelse return error.NotLoggedIn;
1402         const description = try reencode(UserDescription, args.description);
1403
1404         const txn = try self.env.txn();
1405         defer txn.commit() catch {};
1406
1407         const users = try Db.users(txn);
1408         var user = login.user;
1409         user.description = description;
1410         try users.put(login.user.id, user);
1411     }
1412     pub fn @"/set_password"(self: Self, args: struct { password: []const u8 }) !void {
1413         const login = self.logged_in orelse return error.NotLoggedIn;
1414
1415         const txn = try self.env.txn();
1416         defer txn.commit() catch {};
1417
1418         const users = try Db.users(txn);
1419         var user = login.user;
1420         user.password_hash = try Chirp.hash_password(args.password);
1421         try users.put(login.user.id, user);
1422     }
1423     pub fn @"/post"(self: Self) !void {
1424         if (self.logged_in) |login| {
1425             const text = self.req.get_value("text").?;
1426             const has_referer = self.req.get_value("referer");
1427
1428             try Chirp.post(self.env, login.user.id, text);
1429
1430             if (has_referer) |r| {
1431                 const decoded = try decode(r);
1432                 try self.res.redirect(decoded.constSlice());
1433             }
1434         }
1435     }
1436     pub fn @"/comment"(self: Self) !void {
1437         if (self.logged_in) |login| {
1438             const text = self.req.get_value("text") orelse return error.NoText;
1439             const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1440             const post_id = try parse_enum(PostId, post_id_str, 16);
1441
1442             try Chirp.comment(self.env, login.user.id, post_id, text);
1443         }
1444     }
1445     pub fn @"/quote"(self: Self) !void {
1446         if (self.logged_in) |login| {
1447             const text = self.req.get_value("text") orelse return error.NoText;
1448             const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1449             const has_referer = self.req.get_value("referer");
1450
1451             const post_id = try parse_enum(PostId, post_id_str, 16);
1452
1453             try Chirp.quote(self.env, login.user.id, post_id, text);
1454
1455             if (has_referer) |r| {
1456                 const decoded = try decode(r);
1457                 try self.res.redirect(decoded.constSlice());
1458             }
1459         }
1460     }
1461     // TODO: add arguments instead of parsing manually
1462     pub fn @"/create_list"(self: Self) !void {
1463         if (self.logged_in) |login| {
1464             const name_str = self.req.get_value("name") orelse return error.NoName;
1465             const name = try Name.fromSlice(name_str);
1466             // TODO: decode name
1467
1468             var txn = try self.env.txn();
1469             const postlist = try PostList.init(txn);
1470             try txn.commit();
1471
1472             txn = try self.env.txn();
1473             var post_lists_view = try login.user.post_lists.open(txn);
1474             _ = try post_lists_view.append(.{ .name = name, .list = postlist });
1475             try txn.commit();
1476         }
1477     }
1478     pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
1479         if (self.logged_in) |login| {
1480             var post_list: ?PostList = null;
1481             {
1482                 const txn = try self.env.txn();
1483                 defer txn.commit() catch {};
1484                 var post_lists_view = try login.user.post_lists.open(txn);
1485                 post_list = (try post_lists_view.get(args.list_id)).list;
1486                 try post_lists_view.del(args.list_id);
1487             }
1488             if (post_list != null) {
1489                 const txn = try self.env.txn();
1490                 defer txn.commit() catch {};
1491                 var list_view = try post_list.?.open(txn);
1492                 try list_view.clear();
1493             }
1494         }
1495     }
1496     pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.Base.Index, post_id: PostId }) !void {
1497         if (self.logged_in) |login| {
1498             _ = login;
1499
1500             const txn = try self.env.txn();
1501             defer txn.commit() catch {};
1502
1503             const post_list = PostList{ .base = .{ .idx = args.list_id } };
1504             var post_list_view = try post_list.open(txn);
1505             if (try post_list_view.has(args.post_id)) {
1506                 try post_list_view.del(args.post_id);
1507             } else {
1508                 try post_list_view.append(args.post_id);
1509             }
1510         }
1511     }
1512     pub fn @"/create_feed"(self: Self) !void {
1513         if (self.logged_in) |login| {
1514             const name_str = self.req.get_value("name") orelse return error.NoName;
1515             const name = try Name.fromSlice(name_str);
1516
1517             var txn = try self.env.txn();
1518             const userlist = try UserList.init(txn);
1519             try txn.commit();
1520
1521             txn = try self.env.txn();
1522             var feeds_view = try login.user.feeds.open(txn);
1523             _ = try feeds_view.append(.{ .name = name, .list = userlist });
1524             try txn.commit();
1525         }
1526     }
1527     pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Base.Index }) !void {
1528         if (self.logged_in) |login| {
1529             var user_list: ?UserList = null;
1530
1531             {
1532                 const txn = try self.env.txn();
1533                 defer txn.commit() catch {};
1534                 var feeds_view = try login.user.feeds.open(txn);
1535                 user_list = (try feeds_view.get(args.list_id)).list;
1536                 try feeds_view.del(args.list_id);
1537             }
1538             if (user_list != null) {
1539                 const txn = try self.env.txn();
1540                 defer txn.commit() catch {};
1541                 var list_view = try user_list.?.open(txn);
1542                 try list_view.clear();
1543             }
1544         }
1545     }
1546     pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.Base.Index, user_id: UserId }) !void {
1547         if (self.logged_in) |login| {
1548             _ = login;
1549
1550             const txn = try self.env.txn();
1551             defer txn.commit() catch {};
1552
1553             const user_list = UserList{ .base = .{ .idx = args.feed_id } };
1554             var user_list_view = try user_list.open(txn);
1555             if (try user_list_view.has(args.user_id)) {
1556                 try user_list_view.del(args.user_id);
1557             } else {
1558                 try user_list_view.append(args.user_id);
1559             }
1560         }
1561     }
1562     pub fn @"/upvote"(self: Self) !void {
1563         const login = self.logged_in orelse return error.NotLoggedIn;
1564
1565         const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1566         const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1567
1568         try Chirp.vote(self.env, post_id, login.user.id, .Up);
1569     }
1570     pub fn @"/downvote"(self: Self) !void {
1571         const login = self.logged_in orelse return error.NotLoggedIn;
1572
1573         const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1574         const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1575
1576         try Chirp.vote(self.env, post_id, login.user.id, .Down);
1577     }
1578     pub fn @"/follow"(self: Self) !void {
1579         const login = self.logged_in orelse return error.NotLoggedIn;
1580
1581         const user_id_str = self.req.get_value("user_id") orelse return error.NoUserId;
1582         const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16));
1583
1584         try Chirp.follow(self.env, login.user.id, user_id);
1585     }
1586     pub fn @"/quit"(self: Self) !void {
1587         if (self.req.get_header("Referer")) |ref| {
1588             try self.res.redirect(ref);
1589         } else {
1590             try self.res.redirect("/");
1591         }
1592         try self.res.send();
1593         // break :accept;
1594     }
1595 };
1596 // }}}
1597
1598 fn list_users(env: lmdb.Env) !void {
1599     const txn = try env.txn();
1600     defer txn.abort();
1601
1602     const users = try Db.users(txn);
1603     var it = try users.iterator();
1604
1605     while (it.next()) |kv| {
1606         const key = kv.key;
1607         const user = kv.val;
1608         std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() });
1609     }
1610 }
1611 fn list_user_ids(env: lmdb.Env) !void {
1612     const txn = try env.txn();
1613     defer txn.abort();
1614
1615     const user_ids = try Db.user_ids(txn);
1616     var it = try user_ids.iterator();
1617
1618     while (it.next()) |kv| {
1619         const key = kv.key;
1620         const user_id = kv.val;
1621         std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
1622     }
1623 }
1624
1625 fn list_sessions(env: lmdb.Env) !void {
1626     const txn = try env.txn();
1627     defer txn.abort();
1628
1629     const sessions = try Db.sessions(txn);
1630     var it = try sessions.iterator();
1631
1632     while (it.next()) |kv| {
1633         const key = kv.key;
1634         const user_id = kv.val;
1635         std.debug.print("[{x}] {}\n", .{ key, user_id });
1636     }
1637 }
1638
1639 fn list_posts(env: lmdb.Env) !void {
1640     const txn = try env.txn();
1641     defer txn.abort();
1642
1643     const posts = try Db.posts(txn);
1644     var it = try posts.iterator();
1645
1646     while (it.next()) |kv| {
1647         const key = kv.key;
1648         const post = kv.val;
1649         std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() });
1650     }
1651 }
1652
1653 const ReqBufferSize = 4096;
1654 const ResHeadBufferSize = 1024 * 64;
1655 const ResBodyBufferSize = 1024 * 64;
1656
1657 // TODO: static?
1658 var req_buffer: [ReqBufferSize]u8 = undefined;
1659 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
1660 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
1661
1662 pub fn main() !void {
1663     // server
1664     var server = try http.Server.init("::", 8080);
1665     defer server.deinit();
1666
1667     // lmdb
1668     var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
1669     defer env.close();
1670
1671     std.debug.print("Users:\n", .{});
1672     try list_users(env);
1673     std.debug.print("User IDs:\n", .{});
1674     try list_user_ids(env);
1675     std.debug.print("Sessions:\n", .{});
1676     try list_sessions(env);
1677     std.debug.print("Posts:\n", .{});
1678     try list_posts(env);
1679
1680     while (true) {
1681         server.wait();
1682         while (true) {
1683             const req = (server.next_request(&req_buffer) catch break) orelse break;
1684             // handle_request(env, req) catch {
1685             //     try handle_error(env, req);
1686             // };
1687             try handle_request(env, req);
1688         }
1689     }
1690     // const ThreadCount = 1;
1691     // var ts: [ThreadCount]std.Thread = undefined;
1692
1693     // for (0..ThreadCount) |i| {
1694     //     ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
1695     // }
1696     // for (0..ThreadCount) |i| {
1697     //     ts[i].join();
1698     // }
1699
1700     std.debug.print("done\n", .{});
1701 }
1702
1703 fn handle_error(env: lmdb.Env, req: http.Request) !void {
1704     _ = env;
1705     var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);
1706     try write_start(&res);
1707     try res.write("Oops, something went terribly wrong there D:", .{});
1708     try write_end(&res);
1709     try res.send();
1710 }
1711 fn handle_request(env: lmdb.Env, req: http.Request) !void {
1712     // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
1713     // std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? });
1714
1715     // reponse
1716     var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);
1717
1718     // check session token
1719     const logged_in: ?Login = try check_login(env, req, &res);
1720
1721     // html
1722     if (req.method == .GET) {
1723         try write_start(&res);
1724         try write_header(&res, logged_in);
1725
1726         const txn = try env.txn();
1727         defer txn.abort();
1728
1729         const get = GET{
1730             .txn = txn,
1731             .req = req,
1732             .res = &res,
1733             .logged_in = logged_in,
1734         };
1735         if (try get.handle()) {} else {
1736             try res.redirect("/");
1737         }
1738
1739         try write_end(&res);
1740         try res.send();
1741     }
1742     // api
1743     else {
1744         const post = POST{
1745             .env = env,
1746             .req = req,
1747             .res = &res,
1748             .logged_in = logged_in,
1749         };
1750         if (try post.handle()) {} else {
1751             try res.write("<p>[POST] {s}</p>", .{req.target});
1752         }
1753
1754         if (!res.has_header("Location")) {
1755             if (req.get_header("Referer")) |ref| {
1756                 try res.redirect(ref);
1757             } else {
1758                 try res.redirect("/");
1759             }
1760         }
1761         try res.send();
1762     }
1763 }