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