]> gitweb.ps.run Git - chirp/blob - src/main.zig
<input></input> -> <input />
[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 => {
513                 try res.write("<input ", .{});
514                 try res.write(input[0], input[1]);
515                 try res.write(" />", .{});
516             },
517             else => {
518                 try res.write("<input ", .{});
519                 try res.write(input, .{});
520                 try res.write(" />", .{});
521             },
522         }
523     }
524
525     try res.write("</form>", .{});
526 }
527 // }}}
528
529 // write {{{
530 const TimeStr = std.BoundedArray(u8, 256);
531
532 // http://howardhinnant.github.io/date_algorithms.html
533 fn time_str(_t: i64) TimeStr {
534     const t: u64 = @intCast(_t);
535     var result = TimeStr.init(0) catch unreachable;
536
537     const nD = @divFloor(t, std.time.s_per_day);
538     const z: u64 = nD + 719468;
539     const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
540     const doe: u64 = z - era * 146097; // [0, 146096]
541     const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
542     const Y: u64 = yoe + era * 400;
543     const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
544     const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
545     const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
546     const M: u64 = if (mp < 10) mp + 3 else mp - 9;
547
548     const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
549     const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
550     const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;
551
552     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;
553
554     return result;
555 }
556 fn write_header(res: *http.Response, logged_in: ?Login) !void {
557     if (logged_in) |login| {
558         try res.write(
559             \\<a href="/">Home</a><br />
560         , .{});
561         try res.write(
562             \\<a href="/user/{s}">Profile</a><br />
563         , .{login.user.name.constSlice()});
564         try res.write(
565             \\<a href="/post">Post</a><br />
566         , .{});
567         try html_form(res, "/logout", .{
568             \\type="submit" value="Logout"
569         });
570         try res.write("<br /><br />", .{});
571     } else {
572         try res.write(
573             \\<a href="/">Home</a><br />
574             \\<form action="/register" method="post">
575             \\<input type="text" name="username" />
576             \\<input type="password" name="password" />
577             \\<input type="submit" value="Register" />
578             \\</form><br />
579             \\<form action="/login" method="post">
580             \\<input type="text" name="username" />
581             \\<input type="password" name="password" />
582             \\<input type="submit" value="Login" />
583             \\</form><br /><br />
584         , .{});
585     }
586 }
587 fn write_start(res: *http.Response) !void {
588     try res.write(
589         \\<!doctype html>
590         \\<html>
591         \\<head>
592         \\<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>">
593         \\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
594         \\<style>
595         \\  form {
596         \\    display: inline-block;
597         \\  }
598         \\</style>
599         \\</head>
600         \\<body>
601     , .{});
602 }
603 fn write_end(res: *http.Response) !void {
604     try res.write("</body></html>", .{});
605 }
606 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 {
607     const posts = try Db.posts(txn);
608     const post = posts.get(post_id) catch {
609         res.redirect("/") catch {};
610         return;
611     };
612     const users = try Db.users(txn);
613     const user = try users.get(post.user_id);
614
615     try res.write(
616         \\<div id="{x}">
617         \\<span><a href="/user/{s}">{s}</a>
618     , .{ @intFromEnum(post_id), user.name.constSlice(), user.display_name.constSlice() });
619     if (post.parent_id) |id| {
620         try res.write(" <a href=\"/post/{x}\">..</a>", .{@intFromEnum(id)});
621     }
622     try res.write(
623         \\ {s}</span><br />
624         \\<span>{s}</span><br />
625     , .{ time_str(post.time).constSlice(), post.text.constSlice() });
626
627     if (logged_in != null and post.user_id == logged_in.?.user.id) {
628         // Votes
629         try res.write(
630             \\<small>
631             \\<a href="/upvotes/{0x}">{1} Upvotes</a>
632             \\<a href="/downvotes/{0x}">{2} Downvotes</a>
633             \\</small>
634             \\<br />
635         , .{ @intFromEnum(post_id), post.upvotes, post.downvotes });
636     }
637
638     if (post.quote_id) |quote_id| {
639         try res.write("<div style=\"border: 1px solid black;\">", .{});
640         if (options.recurse > 0) {
641             try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 });
642         } else {
643             try res.write("<a href=\"/post/{x}\">...</a>", .{@intFromEnum(post_id)});
644         }
645         try res.write("</div>", .{});
646     }
647
648     const comments_view = try post.comments.open(txn);
649     const quotes_view = try post.quotes.open(txn);
650     const votes_view = try post.votes.open(txn);
651
652     // Votes
653     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;
654
655     if (vote != null and vote.?.kind == .Up) {
656         try html_form(res, "/upvote", .{
657             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
658             .{ "type=\"submit\" value=\"&#x2B06; {}\"", .{post.upvotes} },
659         });
660     } else {
661         try html_form(res, "/upvote", .{
662             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
663             .{ "type=\"submit\" value=\"&#x21E7; {}\"", .{post.upvotes} },
664         });
665     }
666     if (vote != null and vote.?.kind == .Down) {
667         try html_form(res, "/downvote", .{
668             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
669             .{ "type=\"submit\" value=\"&#x2B07; {}\"", .{post.downvotes} },
670         });
671     } else {
672         try html_form(res, "/downvote", .{
673             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
674             .{ "type=\"submit\" value=\"&#x21E9; {}\"", .{post.downvotes} },
675         });
676     }
677
678     // Comment Count
679     try res.write(
680         \\<a href="/post/{x}">&#x1F4AD; {}</a> 
681     , .{ @intFromEnum(post.id), comments_view.len() });
682
683     // Quote
684     try res.write(
685         \\<a href="/quoted/{x}">&#x1F501; {}</a> 
686     , .{ @intFromEnum(post.id), quotes_view.len() });
687
688     // Save to List
689     if (logged_in) |login| {
690         const lists_view = try login.user.post_lists.open(txn);
691         try res.write("<form action=\"/list_add\" method=\"post\">", .{});
692         try res.write("<select name=\"list_id\">", .{});
693         var it = lists_view.iterator();
694         // TODO: mark lists that already contain post
695         while (it.next()) |kv| {
696             const name = kv.val.name;
697             const id = kv.val.list.base.idx.?;
698             const list_view = try kv.val.list.open(txn);
699             try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(post_id) catch false) " *" else "" });
700         }
701         try res.write("</select>", .{});
702         try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\" />", .{@intFromEnum(post_id)});
703         try res.write("<input type=\"submit\" value=\"Save\" />", .{});
704         try res.write("</form>", .{});
705     }
706
707     // Comment field
708     // TODO: maybe always show comment field and prompt for login
709     if (options.show_comment_field and logged_in != null) {
710         try res.write("<br /><br />", .{});
711         try html_form(res, "/comment", .{
712             .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
713             "type=\"text\" name=\"text\" placeholder=\"Text\"",
714             "type=\"submit\" value=\"Comment\"",
715         });
716         try res.write("<br />", .{});
717     }
718
719     // Comments
720     if (options.recurse > 0 and comments_view.len() > 0) {
721         try res.write(
722             \\<details{s}>
723             \\<summary>Comments</summary>
724         , .{if (options.recurse > 1) " open" else ""});
725         try res.write("<div style=\"margin: 10px;\">", .{});
726         var it = comments_view.iterator();
727         var count: u8 = 0;
728         while (it.next()) |comment_id| {
729             try write_post(res, txn, logged_in, comment_id.key, .{ .recurse = options.recurse - 1 });
730             try res.write("<br />", .{});
731             if (options.recurse == 1) {
732                 count += 1;
733                 if (count >= 3) break;
734             }
735         }
736         try res.write(
737             \\</div>
738             \\</details>
739         , .{});
740     }
741
742     try res.write(
743         \\</div>
744     , .{});
745 }
746 fn write_profile(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
747     const following = try user.following.open(txn);
748     const followers = try user.followers.open(txn);
749
750     try res.write(
751         \\<h2 style="display: inline;"><a href="/user/{s}">{s}</a></h2>
752     , .{
753         user.name.constSlice(), user.display_name.constSlice(),
754     });
755     if (logged_in != null and user.id != logged_in.?.user.id) {
756         const login = logged_in.?;
757
758         // follow/unfollow
759         if (try followers.has(login.user.id)) {
760             try html_form(res, "/follow", .{
761                 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
762                 \\type="submit" value="Unfollow"
763             });
764         } else {
765             try html_form(res, "/follow", .{
766                 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
767                 \\type="submit" value="Follow"
768             });
769         }
770
771         // add to feed
772         const feeds_view = try login.user.feeds.open(txn);
773         try res.write("<form action=\"/feed_add\" method=\"post\">", .{});
774         try res.write("<select name=\"feed_id\">", .{});
775         var it = feeds_view.iterator();
776         while (it.next()) |kv| {
777             const name = kv.val.name;
778             const id = kv.val.list.base.idx.?;
779             const list_view = try kv.val.list.open(txn);
780             try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(user.id) catch false) " *" else "" });
781         }
782         try res.write("</select>", .{});
783         try res.write("<input type=\"hidden\" name=\"user_id\" value=\"{x}\" />", .{@intFromEnum(user.id)});
784         try res.write("<input type=\"submit\" value=\"Add to feed\" />", .{});
785         try res.write("</form>", .{});
786     }
787     try res.write(
788         \\ <a href="/following/{s}">{} following</a>
789         \\ <a href="/followers/{s}">{} followers</a>
790         \\<br />
791     , .{
792         user.name.constSlice(), following.len(),
793         user.name.constSlice(), followers.len(),
794     });
795
796     try res.write(
797         \\<a href="/all/{0s}">All Posts</a>
798         \\ <a href="/comments/{0s}">Comments</a>
799         \\ <a href="/quotes/{0s}">Quotes</a><br />
800     , .{
801         user.name.constSlice(),
802     });
803
804     if (logged_in != null and user.id == logged_in.?.user.id) {
805         try res.write(
806             \\<a href="/lists">Lists</a>
807             \\<a href="/feeds">Feeds</a>
808             \\<a href="/edit">Edit</a><br />
809             \\<br />
810         , .{});
811     }
812
813     if (user.description.len > 0) {
814         try res.write(
815             \\<div style="padding-left: 5px; border-left: 1px solid grey;">
816             // \\&#x00AB; {s} &#x00BB;
817             \\<i>{s}</i>
818             \\</div>
819         , .{user.description.constSlice()});
820     }
821
822     try res.write("<br />", .{});
823 }
824 fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList, options: struct {
825     show_posts: bool,
826     show_quotes: bool,
827     show_comments: bool,
828 }) !void {
829     const posts_view = try post_list.open(txn);
830
831     var paginate = try Paginate(PostList).init(res, posts_view, Chirp.PostsPerPage);
832
833     while (paginate.next()) |post_id| {
834         const posts = try Db.posts(txn);
835         const post = try posts.get(post_id.key);
836         if ((options.show_posts and (post.parent_id == null and post.quote_id == null)) or
837             (options.show_quotes and (post.quote_id != null)) or
838             (options.show_comments and (post.parent_id != null)))
839         {
840             try write_post(res, txn, logged_in, post_id.key, .{ .recurse = 1 });
841             try res.write("<br />", .{});
842         }
843     }
844
845     try paginate.write_navigation();
846 }
847 fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
848     const users = try Db.users(txn);
849     const posts = try Db.posts(txn);
850
851     var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
852     var prev_newest_post: ?Post = null;
853
854     const following = try user_list.open(txn);
855
856     while (true) {
857         var newest_post: ?Post = null;
858
859         var following_it = following.iterator();
860         while (following_it.next()) |following_id| {
861             const followed_user = try users.get(following_id.key);
862             const followed_posts = try followed_user.posts.open(txn);
863
864             if (followed_posts.len() == 0) {
865                 continue;
866             }
867
868             var followed_posts_it = followed_posts.reverse_iterator();
869             while (followed_posts_it.next()) |followed_post_id| {
870                 const p = try posts.get(followed_post_id.key);
871
872                 if ((prev_newest_post == null or p.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < p.time)) {
873                     newest_post = p;
874                     break;
875                 }
876             }
877         }
878         if (newest_post) |post| {
879             newest_post_ids.append(post.id) catch break;
880             prev_newest_post = post;
881         } else {
882             break;
883         }
884     }
885
886     for (newest_post_ids.constSlice()) |post_id| {
887         try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
888         try res.write("<br />", .{});
889     }
890 }
891 fn write_user(res: *http.Response, txn: lmdb.Txn, user_id: UserId) !void {
892     const users = try Db.users(txn);
893     const user = try users.get(user_id);
894     try res.write(
895         \\<a href="/user/{s}">{s}</a>
896     , .{ user.name.constSlice(), user.display_name.constSlice() });
897 }
898 fn write_votes(res: *http.Response, txn: lmdb.Txn, votes: VoteList, options: struct {
899     show_upvotes: bool = true,
900     show_downvotes: bool = true,
901 }) !void {
902     const votes_view = try votes.open(txn);
903
904     var paginate = try Paginate(VoteList).init(res, votes_view, Chirp.UsersPerPage);
905
906     while (paginate.next()) |kv| {
907         const user_id = kv.key;
908         const vote = kv.val;
909
910         if ((options.show_upvotes and vote.kind == .Up) or
911             (options.show_downvotes and vote.kind == .Down))
912         {
913             try write_user(res, txn, user_id);
914             try res.write(" <small>{s}</small><br />", .{time_str(vote.time).constSlice()});
915         }
916     }
917
918     try paginate.write_navigation();
919 }
920 fn check_login(env: lmdb.Env, req: http.Request, res: *http.Response) !?Login {
921     var result: ?Login = null;
922
923     if (req.get_cookie("session_token")) |session_token_str| {
924         var remove_session_token = true;
925
926         if (std.fmt.parseUnsigned(SessionToken, session_token_str, 16) catch null) |session_token| {
927             // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
928             // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
929             if (Chirp.get_session_user_id(env, session_token) catch null) |user_id| {
930                 const txn = try env.txn();
931                 defer txn.abort();
932                 const users = try Db.users(txn);
933
934                 result = .{
935                     .user = try users.get(user_id),
936                     .session_token = session_token,
937                 };
938
939                 remove_session_token = false;
940             }
941         }
942
943         if (remove_session_token) {
944             try res.add_header(
945                 "Set-Cookie",
946                 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
947             );
948         }
949     }
950
951     return result;
952 }
953 // }}}
954
955 // GET {{{
956 const GET = struct {
957     const Self = @This();
958
959     txn: lmdb.Txn,
960     req: http.Request,
961     res: *http.Response,
962     logged_in: ?Login,
963
964     fn handle(self: Self) !bool {
965         const ti = @typeInfo(Self);
966         inline for (ti.Struct.decls) |f_decl| {
967             const has_arg = f_decl.name.len > 1 and f_decl.name[f_decl.name.len - 1] == '/';
968             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);
969
970             if (match) {
971                 const f = @field(Self, f_decl.name);
972                 const fi = @typeInfo(@TypeOf(f));
973                 if (fi.Fn.params.len == 1) {
974                     try @call(.auto, f, .{self});
975                 } else {
976                     const arg_type = fi.Fn.params[1].type.?;
977                     const arg_info = @typeInfo(arg_type);
978                     var arg: arg_type = undefined;
979                     const field = arg_info.Struct.fields[0];
980                     if (self.req.target.len <= f_decl.name.len) {
981                         return error.NoArgProvided;
982                     }
983                     const str = self.req.target[f_decl.name.len..self.req.target.len];
984                     const field_ti = @typeInfo(field.type);
985                     switch (field_ti) {
986                         // TODO: maybe handle BoundedArray?
987                         .Int => {
988                             @field(arg, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
989                         },
990                         .Enum => {
991                             @field(arg, field.name) = try parse_enum(field.type, str, 16);
992                         },
993                         else => {
994                             @field(arg, field.name) = str;
995                         },
996                     }
997
998                     try @call(.auto, f, .{ self, arg });
999                 }
1000                 return true;
1001             }
1002         }
1003         return false;
1004     }
1005
1006     pub fn @"/user/"(self: Self, args: struct { username: []const u8 }) !void {
1007         const user_ids = try Db.user_ids(self.txn);
1008         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1009             const users = try Db.users(self.txn);
1010             const user = try users.get(user_id);
1011
1012             try write_profile(self.res, self.txn, self.logged_in, user);
1013
1014             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1015                 .show_posts = true,
1016                 .show_quotes = false,
1017                 .show_comments = false,
1018             });
1019         } else |err| {
1020             try self.res.write(
1021                 \\<p>User not found [{}]</p>
1022             , .{err});
1023         }
1024     }
1025     pub fn @"/comments/"(self: Self, args: struct { username: []const u8 }) !void {
1026         const user_ids = try Db.user_ids(self.txn);
1027         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1028             const users = try Db.users(self.txn);
1029             const user = try users.get(user_id);
1030
1031             try write_profile(self.res, self.txn, self.logged_in, user);
1032
1033             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1034                 .show_posts = false,
1035                 .show_quotes = false,
1036                 .show_comments = true,
1037             });
1038         } else |err| {
1039             try self.res.write(
1040                 \\<p>User not found [{}]</p>
1041             , .{err});
1042         }
1043     }
1044     pub fn @"/quotes/"(self: Self, args: struct { username: []const u8 }) !void {
1045         const user_ids = try Db.user_ids(self.txn);
1046         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1047             const users = try Db.users(self.txn);
1048             const user = try users.get(user_id);
1049
1050             try write_profile(self.res, self.txn, self.logged_in, user);
1051
1052             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1053                 .show_posts = false,
1054                 .show_quotes = true,
1055                 .show_comments = false,
1056             });
1057         } else |err| {
1058             try self.res.write(
1059                 \\<p>User not found [{}]</p>
1060             , .{err});
1061         }
1062     }
1063     pub fn @"/all/"(self: Self, args: struct { username: []const u8 }) !void {
1064         const user_ids = try Db.user_ids(self.txn);
1065         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1066             const users = try Db.users(self.txn);
1067             const user = try users.get(user_id);
1068
1069             try write_profile(self.res, self.txn, self.logged_in, user);
1070
1071             try write_posts(self.res, self.txn, self.logged_in, user.posts, .{
1072                 .show_posts = true,
1073                 .show_quotes = true,
1074                 .show_comments = true,
1075             });
1076         } else |err| {
1077             try self.res.write(
1078                 \\<p>User not found [{}]</p>
1079             , .{err});
1080         }
1081     }
1082     pub fn @"/following/"(self: Self, args: struct { username: []const u8 }) !void {
1083         const user_ids = try Db.user_ids(self.txn);
1084         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1085             const users = try Db.users(self.txn);
1086             const user = try users.get(user_id);
1087
1088             const following_view = try user.following.open(self.txn);
1089
1090             var paginate = try Paginate(UserList).init(self.res, following_view, Chirp.UsersPerPage);
1091
1092             try self.res.write(
1093                 \\<h2><a href="/user/{s}">{s}</a> follows:</h2>
1094             , .{ user.name.constSlice(), user.display_name.constSlice() });
1095
1096             while (paginate.next()) |following_id| {
1097                 const following_user = try users.get(following_id.key);
1098
1099                 try self.res.write(
1100                     \\<a href="/user/{s}">{s}</a><br />
1101                 , .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
1102             }
1103
1104             try paginate.write_navigation();
1105         } else |err| {
1106             try self.res.write(
1107                 \\<p>User not found [{}]</p>
1108             , .{err});
1109         }
1110     }
1111     pub fn @"/followers/"(self: Self, args: struct { username: []const u8 }) !void {
1112         const user_ids = try Db.user_ids(self.txn);
1113         if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
1114             const users = try Db.users(self.txn);
1115             const user = try users.get(user_id);
1116
1117             const followers_view = try user.followers.open(self.txn);
1118             var paginate = try Paginate(UserList).init(self.res, followers_view, Chirp.UsersPerPage);
1119
1120             try self.res.write(
1121                 \\<h2><a href="/user/{s}">{s}</a> followers:</h2>
1122             , .{ user.name.constSlice(), user.display_name.constSlice() });
1123
1124             while (paginate.next()) |follower_id| {
1125                 const follower_user = try users.get(follower_id.key);
1126
1127                 try self.res.write(
1128                     \\<a href="/user/{s}">{s}</a><br />
1129                 , .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
1130             }
1131
1132             try paginate.write_navigation();
1133         } else |err| {
1134             try self.res.write(
1135                 \\<p>User not found [{}]</p>
1136             , .{err});
1137         }
1138     }
1139     pub fn @"/post/"(self: Self, args: struct { post_id: PostId }) !void {
1140         try write_post(self.res, self.txn, self.logged_in, args.post_id, .{
1141             .recurse = 3, // TODO: factor out
1142             .show_comment_field = true,
1143         });
1144     }
1145     pub fn @"/upvotes/"(self: Self, args: struct { post_id: PostId }) !void {
1146         const posts = try Db.posts(self.txn);
1147         const post = try posts.get(args.post_id);
1148         try self.res.write("{} upvotes:<br />", .{post.upvotes});
1149         try write_votes(self.res, self.txn, post.votes, .{});
1150     }
1151     pub fn @"/quoted/"(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
1155         const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;
1156
1157         if (self.logged_in != null) {
1158             try html_form(self.res, "/quote", .{
1159                 .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
1160                 .{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
1161                 "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
1162                 "type=\"submit\" value=\"Quote\"",
1163             });
1164             try self.res.write("<br />", .{});
1165         }
1166
1167         // TODO: show all bc this only contains quotes?
1168         try write_posts(self.res, self.txn, self.logged_in, post.quotes, .{
1169             .show_posts = false,
1170             .show_quotes = true,
1171             .show_comments = false,
1172         });
1173     }
1174     pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
1175         try write_posts(self.res, self.txn, self.logged_in, PostList{ .base = .{ .idx = args.list_id } }, .{
1176             .show_posts = true,
1177             .show_quotes = true,
1178             .show_comments = true,
1179         });
1180     }
1181     pub fn @"/lists"(self: Self) !void {
1182         if (self.logged_in) |login| {
1183             const post_lists_view = try login.user.post_lists.open(self.txn);
1184
1185             try html_form(self.res, "/create_list", .{
1186                 "type=\"text\" name=\"name\"",
1187                 "type=\"submit\" value=\"Add\"",
1188             });
1189
1190             try self.res.write("<br /><br />", .{});
1191
1192             var it = post_lists_view.iterator();
1193             while (it.next()) |kv| {
1194                 const name = kv.val.name;
1195                 const post_list = kv.val.list;
1196                 try self.res.write(
1197                     \\<a href="/list/{x}">{s}</a> 
1198                 , .{ post_list.base.idx.?, name.constSlice() });
1199                 try html_form(self.res, "/delete_list", .{
1200                     .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
1201                     "type=\"submit\" value=\"Delete\"",
1202                 });
1203                 try self.res.write("<br />", .{});
1204             }
1205         } else {
1206             try self.res.write("not logged in", .{});
1207         }
1208     }
1209     pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Base.Index }) !void {
1210         try write_timeline(self.res, self.txn, self.logged_in, UserList{ .base = .{ .idx = args.feed_id } });
1211     }
1212     pub fn @"/feeds"(self: Self) !void {
1213         if (self.logged_in) |login| {
1214             const feeds_view = try login.user.feeds.open(self.txn);
1215
1216             try html_form(self.res, "/create_feed", .{
1217                 "type=\"text\" name=\"name\"",
1218                 "type=\"submit\" value=\"Add\"",
1219             });
1220
1221             try self.res.write("<br /><br />", .{});
1222
1223             var it = feeds_view.iterator();
1224             while (it.next()) |kv| {
1225                 const name = kv.val.name;
1226                 const user_list = kv.val.list;
1227                 try self.res.write(
1228                     \\<a href="/feed/{x}">{s}</a> 
1229                 , .{ user_list.base.idx.?, name.constSlice() });
1230                 try html_form(self.res, "/delete_feed", .{
1231                     .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
1232                     "type=\"submit\" value=\"Delete\"",
1233                 });
1234                 try self.res.write("<br />", .{});
1235             }
1236         } else {
1237             try self.res.write("not logged in", .{});
1238         }
1239     }
1240     pub fn @"/post"(self: Self) !void {
1241         if (self.logged_in) |login| {
1242             _ = login;
1243             const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;
1244
1245             try html_form(self.res, "/post", .{
1246                 .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
1247                 "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
1248                 "type=\"submit\" value=\"Post\"",
1249             });
1250         } else {
1251             try self.res.write("not logged in", .{});
1252         }
1253     }
1254     pub fn @"/edit"(self: Self) !void {
1255         if (self.logged_in) |login| {
1256             try self.res.write("<br />Username: ", .{});
1257             try html_form(self.res, "/set_username", .{
1258                 .{ "type=\"text\" name=\"username\" placeholder=\"{s}\"", .{login.user.name.constSlice()} },
1259                 "type=\"submit\" value=\"Change\"",
1260             });
1261             try self.res.write("<br />Display Name: ", .{});
1262             try html_form(self.res, "/set_display_name", .{
1263                 .{ "type=\"text\" name=\"display_name\" placeholder=\"{s}\"", .{login.user.display_name.constSlice()} },
1264                 "type=\"submit\" value=\"Change\"",
1265             });
1266             try self.res.write("<br />Description: ", .{});
1267             try html_form(self.res, "/set_description", .{
1268                 .{ "type=\"text\" name=\"description\" placeholder=\"{s}\"", .{login.user.description.constSlice()} },
1269                 "type=\"submit\" value=\"Change\"",
1270             });
1271             try self.res.write("<br />Password: ", .{});
1272             try html_form(self.res, "/set_password", .{
1273                 "type=\"text\" name=\"password\"",
1274                 "type=\"submit\" value=\"Change\"",
1275             });
1276         } else {
1277             try self.res.write("not logged in", .{});
1278         }
1279     }
1280     pub fn @"/"(self: Self) !void {
1281         if (self.logged_in) |login| {
1282             try write_timeline(self.res, self.txn, self.logged_in, login.user.following);
1283         } else {
1284             // TODO: generic home
1285             try self.res.write("Homepage", .{});
1286         }
1287     }
1288 };
1289 // }}}
1290
1291 // POST {{{
1292 const POST = struct {
1293     const Self = @This();
1294
1295     env: lmdb.Env,
1296     req: http.Request,
1297     res: *http.Response,
1298     logged_in: ?Login,
1299
1300     pub fn handle(self: Self) !bool {
1301         const ti = @typeInfo(Self);
1302         inline for (ti.Struct.decls) |f_decl| {
1303             if (std.mem.eql(u8, f_decl.name, self.req.target)) {
1304                 const f = @field(Self, f_decl.name);
1305                 const fi = @typeInfo(@TypeOf(f));
1306                 if (fi.Fn.params.len == 1) {
1307                     _ = try @call(.auto, f, .{self});
1308                 } else {
1309                     const args_type = fi.Fn.params[fi.Fn.params.len - 1].type.?;
1310                     const argsi = @typeInfo(args_type);
1311                     var args: args_type = undefined;
1312                     inline for (argsi.Struct.fields) |field| {
1313                         const str = self.req.get_value(field.name) orelse return error.ArgNotFound;
1314                         const field_ti = @typeInfo(field.type);
1315                         switch (field_ti) {
1316                             .Int => {
1317                                 @field(args, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
1318                             },
1319                             .Enum => {
1320                                 @field(args, field.name) = try parse_enum(field.type, str, 16);
1321                             },
1322                             else => {
1323                                 @field(args, field.name) = str;
1324                             },
1325                         }
1326                     }
1327                     try @call(.auto, f, .{ self, args });
1328                 }
1329                 return true;
1330             }
1331         }
1332         return false;
1333     }
1334
1335     pub fn @"/register"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
1336         // TODO: handle args not supplied
1337         std.debug.print("New user: {s} {s}\n", .{ args.username, args.password });
1338         _ = try Chirp.register_user(self.env, args.username, args.password);
1339     }
1340     pub fn @"/login"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void {
1341         // TODO: handle args not supplied
1342         std.debug.print("New login: {s} {s}\n", .{ args.username, args.password });
1343         if (Chirp.login_user(self.env, args.username, args.password)) |session_token| {
1344             self.res.status = .see_other;
1345             try self.res.add_header(
1346                 "Set-Cookie",
1347                 .{ "session_token={x}; HttpOnly", .{session_token} },
1348             );
1349         } else |err| {
1350             std.debug.print("login_user err: {}\n", .{err});
1351         }
1352     }
1353     pub fn @"/logout"(self: Self) !void {
1354         if (self.logged_in) |login| {
1355             try Chirp.logout_user(self.env, login.session_token);
1356
1357             try self.res.add_header(
1358                 "Set-Cookie",
1359                 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
1360             );
1361         }
1362     }
1363     pub fn @"/set_username"(self: Self, args: struct { username: []const u8 }) !void {
1364         const login = self.logged_in orelse return error.NotLoggedIn;
1365         const username = try Username.fromSlice(args.username);
1366
1367         const txn = try self.env.txn();
1368         defer txn.commit() catch {};
1369
1370         const user_ids = try Db.user_ids(txn);
1371
1372         if (!try user_ids.has(username)) {
1373             try user_ids.del(login.user.name);
1374             try user_ids.put(username, login.user.id);
1375
1376             const users = try Db.users(txn);
1377             var user = login.user;
1378             user.name = username;
1379             try users.put(login.user.id, user);
1380         }
1381     }
1382     pub fn @"/set_display_name"(self: Self, args: struct { display_name: []const u8 }) !void {
1383         const login = self.logged_in orelse return error.NotLoggedIn;
1384         const display_name = try DisplayName.fromSlice(args.display_name);
1385
1386         const txn = try self.env.txn();
1387         defer txn.commit() catch {};
1388
1389         const users = try Db.users(txn);
1390         var user = login.user;
1391         user.display_name = display_name;
1392         try users.put(login.user.id, user);
1393     }
1394     pub fn @"/set_description"(self: Self, args: struct { description: []const u8 }) !void {
1395         const login = self.logged_in orelse return error.NotLoggedIn;
1396         const description = try reencode(UserDescription, args.description);
1397
1398         const txn = try self.env.txn();
1399         defer txn.commit() catch {};
1400
1401         const users = try Db.users(txn);
1402         var user = login.user;
1403         user.description = description;
1404         try users.put(login.user.id, user);
1405     }
1406     pub fn @"/set_password"(self: Self, args: struct { password: []const u8 }) !void {
1407         const login = self.logged_in orelse return error.NotLoggedIn;
1408
1409         const txn = try self.env.txn();
1410         defer txn.commit() catch {};
1411
1412         const users = try Db.users(txn);
1413         var user = login.user;
1414         user.password_hash = try Chirp.hash_password(args.password);
1415         try users.put(login.user.id, user);
1416     }
1417     pub fn @"/post"(self: Self) !void {
1418         if (self.logged_in) |login| {
1419             const text = self.req.get_value("text").?;
1420             const has_referer = self.req.get_value("referer");
1421
1422             try Chirp.post(self.env, login.user.id, text);
1423
1424             if (has_referer) |r| {
1425                 const decoded = try decode(r);
1426                 try self.res.redirect(decoded.constSlice());
1427             }
1428         }
1429     }
1430     pub fn @"/comment"(self: Self) !void {
1431         if (self.logged_in) |login| {
1432             const text = self.req.get_value("text") orelse return error.NoText;
1433             const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1434             const post_id = try parse_enum(PostId, post_id_str, 16);
1435
1436             try Chirp.comment(self.env, login.user.id, post_id, text);
1437         }
1438     }
1439     pub fn @"/quote"(self: Self) !void {
1440         if (self.logged_in) |login| {
1441             const text = self.req.get_value("text") orelse return error.NoText;
1442             const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1443             const has_referer = self.req.get_value("referer");
1444
1445             const post_id = try parse_enum(PostId, post_id_str, 16);
1446
1447             try Chirp.quote(self.env, login.user.id, post_id, text);
1448
1449             if (has_referer) |r| {
1450                 const decoded = try decode(r);
1451                 try self.res.redirect(decoded.constSlice());
1452             }
1453         }
1454     }
1455     // TODO: add arguments instead of parsing manually
1456     pub fn @"/create_list"(self: Self) !void {
1457         if (self.logged_in) |login| {
1458             const name_str = self.req.get_value("name") orelse return error.NoName;
1459             const name = try Name.fromSlice(name_str);
1460             // TODO: decode name
1461
1462             var txn = try self.env.txn();
1463             const postlist = try PostList.init(txn);
1464             try txn.commit();
1465
1466             txn = try self.env.txn();
1467             var post_lists_view = try login.user.post_lists.open(txn);
1468             _ = try post_lists_view.append(.{ .name = name, .list = postlist });
1469             try txn.commit();
1470         }
1471     }
1472     pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
1473         if (self.logged_in) |login| {
1474             var post_list: ?PostList = null;
1475             {
1476                 const txn = try self.env.txn();
1477                 defer txn.commit() catch {};
1478                 var post_lists_view = try login.user.post_lists.open(txn);
1479                 post_list = (try post_lists_view.get(args.list_id)).list;
1480                 try post_lists_view.del(args.list_id);
1481             }
1482             if (post_list != null) {
1483                 const txn = try self.env.txn();
1484                 defer txn.commit() catch {};
1485                 var list_view = try post_list.?.open(txn);
1486                 try list_view.clear();
1487             }
1488         }
1489     }
1490     pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.Base.Index, post_id: PostId }) !void {
1491         if (self.logged_in) |login| {
1492             _ = login;
1493
1494             const txn = try self.env.txn();
1495             defer txn.commit() catch {};
1496
1497             const post_list = PostList{ .base = .{ .idx = args.list_id } };
1498             var post_list_view = try post_list.open(txn);
1499             if (try post_list_view.has(args.post_id)) {
1500                 try post_list_view.del(args.post_id);
1501             } else {
1502                 try post_list_view.append(args.post_id);
1503             }
1504         }
1505     }
1506     pub fn @"/create_feed"(self: Self) !void {
1507         if (self.logged_in) |login| {
1508             const name_str = self.req.get_value("name") orelse return error.NoName;
1509             const name = try Name.fromSlice(name_str);
1510
1511             var txn = try self.env.txn();
1512             const userlist = try UserList.init(txn);
1513             try txn.commit();
1514
1515             txn = try self.env.txn();
1516             var feeds_view = try login.user.feeds.open(txn);
1517             _ = try feeds_view.append(.{ .name = name, .list = userlist });
1518             try txn.commit();
1519         }
1520     }
1521     pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Base.Index }) !void {
1522         if (self.logged_in) |login| {
1523             var user_list: ?UserList = null;
1524
1525             {
1526                 const txn = try self.env.txn();
1527                 defer txn.commit() catch {};
1528                 var feeds_view = try login.user.feeds.open(txn);
1529                 user_list = (try feeds_view.get(args.list_id)).list;
1530                 try feeds_view.del(args.list_id);
1531             }
1532             if (user_list != null) {
1533                 const txn = try self.env.txn();
1534                 defer txn.commit() catch {};
1535                 var list_view = try user_list.?.open(txn);
1536                 try list_view.clear();
1537             }
1538         }
1539     }
1540     pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.Base.Index, user_id: UserId }) !void {
1541         if (self.logged_in) |login| {
1542             _ = login;
1543
1544             const txn = try self.env.txn();
1545             defer txn.commit() catch {};
1546
1547             const user_list = UserList{ .base = .{ .idx = args.feed_id } };
1548             var user_list_view = try user_list.open(txn);
1549             if (try user_list_view.has(args.user_id)) {
1550                 try user_list_view.del(args.user_id);
1551             } else {
1552                 try user_list_view.append(args.user_id);
1553             }
1554         }
1555     }
1556     pub fn @"/upvote"(self: Self) !void {
1557         const login = self.logged_in orelse return error.NotLoggedIn;
1558
1559         const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1560         const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1561
1562         try Chirp.vote(self.env, post_id, login.user.id, .Up);
1563     }
1564     pub fn @"/downvote"(self: Self) !void {
1565         const login = self.logged_in orelse return error.NotLoggedIn;
1566
1567         const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId;
1568         const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1569
1570         try Chirp.vote(self.env, post_id, login.user.id, .Down);
1571     }
1572     pub fn @"/follow"(self: Self) !void {
1573         const login = self.logged_in orelse return error.NotLoggedIn;
1574
1575         const user_id_str = self.req.get_value("user_id") orelse return error.NoUserId;
1576         const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16));
1577
1578         try Chirp.follow(self.env, login.user.id, user_id);
1579     }
1580     pub fn @"/quit"(self: Self) !void {
1581         if (self.req.get_header("Referer")) |ref| {
1582             try self.res.redirect(ref);
1583         } else {
1584             try self.res.redirect("/");
1585         }
1586         try self.res.send();
1587         // break :accept;
1588     }
1589 };
1590 // }}}
1591
1592 fn list_users(env: lmdb.Env) !void {
1593     const txn = try env.txn();
1594     defer txn.abort();
1595
1596     const users = try Db.users(txn);
1597     var it = try users.iterator();
1598
1599     while (it.next()) |kv| {
1600         const key = kv.key;
1601         const user = kv.val;
1602         std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() });
1603     }
1604 }
1605 fn list_user_ids(env: lmdb.Env) !void {
1606     const txn = try env.txn();
1607     defer txn.abort();
1608
1609     const user_ids = try Db.user_ids(txn);
1610     var it = try user_ids.iterator();
1611
1612     while (it.next()) |kv| {
1613         const key = kv.key;
1614         const user_id = kv.val;
1615         std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
1616     }
1617 }
1618
1619 fn list_sessions(env: lmdb.Env) !void {
1620     const txn = try env.txn();
1621     defer txn.abort();
1622
1623     const sessions = try Db.sessions(txn);
1624     var it = try sessions.iterator();
1625
1626     while (it.next()) |kv| {
1627         const key = kv.key;
1628         const user_id = kv.val;
1629         std.debug.print("[{x}] {}\n", .{ key, user_id });
1630     }
1631 }
1632
1633 fn list_posts(env: lmdb.Env) !void {
1634     const txn = try env.txn();
1635     defer txn.abort();
1636
1637     const posts = try Db.posts(txn);
1638     var it = try posts.iterator();
1639
1640     while (it.next()) |kv| {
1641         const key = kv.key;
1642         const post = kv.val;
1643         std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() });
1644     }
1645 }
1646
1647 const ReqBufferSize = 4096;
1648 const ResHeadBufferSize = 1024 * 64;
1649 const ResBodyBufferSize = 1024 * 64;
1650
1651 // TODO: static?
1652 var req_buffer: [ReqBufferSize]u8 = undefined;
1653 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
1654 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
1655
1656 pub fn main() !void {
1657     // server
1658     var server = try http.Server.init("::", 8080);
1659     defer server.deinit();
1660
1661     // lmdb
1662     var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
1663     defer env.close();
1664
1665     std.debug.print("Users:\n", .{});
1666     try list_users(env);
1667     std.debug.print("User IDs:\n", .{});
1668     try list_user_ids(env);
1669     std.debug.print("Sessions:\n", .{});
1670     try list_sessions(env);
1671     std.debug.print("Posts:\n", .{});
1672     try list_posts(env);
1673
1674     while (true) {
1675         server.wait();
1676         while (true) {
1677             const req = (server.next_request(&req_buffer) catch break) orelse break;
1678             // handle_request(env, req) catch {
1679             //     try handle_error(env, req);
1680             // };
1681             try handle_request(env, req);
1682         }
1683     }
1684     // const ThreadCount = 1;
1685     // var ts: [ThreadCount]std.Thread = undefined;
1686
1687     // for (0..ThreadCount) |i| {
1688     //     ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
1689     // }
1690     // for (0..ThreadCount) |i| {
1691     //     ts[i].join();
1692     // }
1693
1694     std.debug.print("done\n", .{});
1695 }
1696
1697 fn handle_error(env: lmdb.Env, req: http.Request) !void {
1698     _ = env;
1699     var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);
1700     try write_start(&res);
1701     try res.write("Oops, something went terribly wrong there D:", .{});
1702     try write_end(&res);
1703     try res.send();
1704 }
1705 fn handle_request(env: lmdb.Env, req: http.Request) !void {
1706     // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
1707     // std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? });
1708
1709     // reponse
1710     var res = http.Response.init(req, &res_head_buffer, &res_body_buffer);
1711
1712     // check session token
1713     const logged_in: ?Login = try check_login(env, req, &res);
1714
1715     // html
1716     if (req.method == .GET) {
1717         try write_start(&res);
1718         try write_header(&res, logged_in);
1719
1720         const txn = try env.txn();
1721         defer txn.abort();
1722
1723         const get = GET{
1724             .txn = txn,
1725             .req = req,
1726             .res = &res,
1727             .logged_in = logged_in,
1728         };
1729         if (try get.handle()) {} else {
1730             try res.redirect("/");
1731         }
1732
1733         try write_end(&res);
1734         try res.send();
1735     }
1736     // api
1737     else {
1738         const post = POST{
1739             .env = env,
1740             .req = req,
1741             .res = &res,
1742             .logged_in = logged_in,
1743         };
1744         if (try post.handle()) {} else {
1745             try res.write("<p>[POST] {s}</p>", .{req.target});
1746         }
1747
1748         if (!res.has_header("Location")) {
1749             if (req.get_header("Referer")) |ref| {
1750                 try res.redirect(ref);
1751             } else {
1752                 try res.redirect("/");
1753             }
1754         }
1755         try res.send();
1756     }
1757 }