1 const std = @import("std");
2 const lmdb = @import("lmdb");
3 const db = @import("db");
4 const http = @import("http");
9 fn users(txn: lmdb.Txn) !db.Db(UserId, User) {
10 return try db.Db(UserId, User).init(txn, "users");
12 fn user_ids(txn: lmdb.Txn) !db.Db(Username, UserId) {
13 return try db.Db(Username, UserId).init(txn, "user_ids");
15 fn sessions(txn: lmdb.Txn) !db.Db(SessionToken, UserId) {
16 return try db.Db(SessionToken, UserId).init(txn, "sessions");
18 fn posts(txn: lmdb.Txn) !db.Db(PostId, Post) {
19 return try db.Db(PostId, Post).init(txn, "posts");
31 display_name: DisplayName,
32 password_hash: PasswordHash,
56 const Kind = enum { Up, Down };
63 const Login = struct {
65 session_token: SessionToken,
67 const UserId = enum(u64) { _ };
68 const PostId = enum(u64) { _ };
69 const Timestamp = i64;
70 const Username = std.BoundedArray(u8, 16);
71 const DisplayName = std.BoundedArray(u8, 64);
72 const PasswordHash = std.BoundedArray(u8, 128);
73 const SessionToken = u64;
74 const CookieValue = std.BoundedArray(u8, 128);
75 const PostText = std.BoundedArray(u8, 1024);
76 const PostList = db.Set(PostId);
77 const UserList = db.Set(UserId);
78 const VoteList = db.SetList(UserId, Vote);
80 fn parse_enum(comptime E: type, buf: []const u8, base: u8) !E {
81 return @enumFromInt(try std.fmt.parseUnsigned(@typeInfo(E).Enum.tag_type, buf, base));
84 // https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding
85 fn reencode(text: []const u8) !PostText {
86 var result = try PostText.init(0);
88 const len = @min(text.len, 1024); // TODO: PostText length
91 while (idx < len) : (idx += 1) {
94 try result.append(' ');
95 } else if (c == '%') {
96 // special case of &#...
97 // assume only &#, no &#x
98 if (idx + 6 < text.len and std.mem.eql(u8, text[idx .. idx + 6], "%26%23")) {
99 const num_start = idx + 6;
100 var num_end = num_start;
101 while (num_end < text.len and std.ascii.isDigit(text[num_end])) {
105 if (num_end + 2 < text.len and
106 text[num_end] == '%' and
107 text[num_end + 1] == '3' and
108 std.ascii.toLower(text[num_end + 2]) == 'b')
110 try std.fmt.format(result.writer(), "&#{s};", .{text[num_start..num_end]});
116 try std.fmt.format(result.writer(), "&#x{s};", .{text[idx + 1 .. idx + 3]});
119 try result.append(c);
126 fn decode(text: []const u8) !std.BoundedArray(u8, 32) {
127 var result = try std.BoundedArray(u8, 32).init(0);
129 const max_len = @min(text.len, 1024); // TODO: PostText length
133 while (len < max_len and idx < text.len) : ({
139 try result.append(' ');
140 } else if (c == '%') {
141 if (idx + 2 < text.len) {
142 try std.fmt.format(result.writer(), "{c}", .{try std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16)});
146 try result.append(c);
153 const Chirp = struct {
154 pub fn hash_password(password: []const u8) !PasswordHash {
155 var hash_buffer = try PasswordHash.init(128);
157 // TODO: choose buffer size
158 // TODO: dont allocate on stack, maybe zero memory?
159 var buffer: [1024 * 10]u8 = undefined;
160 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
162 // TODO: choose limits
163 const result = try std.crypto.pwhash.argon2.strHash(password, .{
164 .allocator = alloc.allocator(),
165 .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024),
166 }, hash_buffer.slice());
168 try hash_buffer.resize(result.len);
173 pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
174 var buffer: [1024 * 10]u8 = undefined;
175 var alloc = std.heap.FixedBufferAllocator.init(&buffer);
177 if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
178 .allocator = alloc.allocator(),
182 std.debug.print("verify error: {}\n", .{err});
187 pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
188 const username_array = try Username.fromSlice(username);
189 const display_name = try DisplayName.fromSlice(username);
191 const txn = try env.txn();
192 defer txn.commit() catch {};
194 const users = try Db.users(txn);
195 const user_ids = try Db.user_ids(txn);
197 if (try user_ids.has(username_array)) {
200 const user_id = try db.Prng.gen(users.dbi, UserId);
202 try users.put(user_id, User{
204 .name = username_array,
205 .display_name = display_name,
206 .password_hash = try hash_password(password),
207 .posts = try PostList.init(txn),
208 .following = try UserList.init(txn),
209 .followers = try UserList.init(txn),
212 try user_ids.put(username_array, user_id);
220 username: []const u8,
221 password: []const u8,
223 const username_array = try Username.fromSlice(username);
225 const txn = try env.txn();
226 defer txn.commit() catch {};
228 const user_ids = try Db.user_ids(txn);
229 const user_id = try user_ids.get(username_array);
230 std.debug.print("user logging in, id: {}\n", .{user_id});
232 const users = try Db.users(txn);
233 const user = try users.get(user_id);
235 if (verify_password(password, user.password_hash)) {
236 const sessions = try Db.sessions(txn);
237 const session_token = try db.Prng.gen(sessions.dbi, SessionToken);
238 try sessions.put(session_token, user_id);
239 return session_token;
241 return error.IncorrectPassword;
245 fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
246 const txn = try env.txn();
247 defer txn.commit() catch {};
249 const sessions = try Db.sessions(txn);
250 try sessions.del(session_token);
253 fn append_post(env: *lmdb.Env, user_id: UserId, post_list: PostList, parent_id: ?PostId, quote_id: ?PostId, text: []const u8) !void {
254 var post_id: PostId = undefined;
256 // TODO: do this in one commit
258 var txn: lmdb.Txn = undefined;
262 defer txn.commit() catch {};
264 const posts = try Db.posts(txn);
265 post_id = try db.Prng.gen(posts.dbi, PostId);
267 const decoded_text = try reencode(text);
268 try posts.put(post_id, Post{
270 .parent_id = parent_id,
271 .quote_id = quote_id,
273 .time = std.time.timestamp(),
274 .votes = try VoteList.init(txn),
275 .comments = try PostList.init(txn),
276 .quotes = try PostList.init(txn),
277 .text = decoded_text,
282 // append to user's posts
284 defer txn.commit() catch {};
286 var posts_view = try post_list.open(txn);
287 try posts_view.append(post_id, {});
290 if (quote_id != null) {
292 defer txn.commit() catch {};
294 const posts = try Db.posts(txn);
295 const quote_post = try posts.get(quote_id.?);
296 var quotes = try quote_post.quotes.open(txn);
297 try quotes.append(post_id, {});
301 fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void {
302 const txn = try env.txn();
304 const users = try Db.users(txn);
305 const user = try users.get(user_id);
309 try append_post(env, user_id, user.posts, null, null, text);
312 fn comment(env: *lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void {
313 const txn = try env.txn();
315 const posts = try Db.posts(txn);
316 const parent_post = try posts.get(parent_post_id);
320 try append_post(env, user_id, parent_post.comments, parent_post_id, null, text);
323 fn quote(env: *lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void {
324 const txn = try env.txn();
326 const users = try Db.users(txn);
327 const user = try users.get(user_id);
331 try append_post(env, user_id, user.posts, null, quote_post_id, text);
334 fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
335 const txn = try env.txn();
336 defer txn.commit() catch {};
338 const posts = try Db.posts(txn);
340 var p = try posts.get(post_id);
341 var votes_view = try p.votes.open(txn);
345 if (try votes_view.has(user_id)) {
346 const old_vote = try votes_view.get(user_id);
348 add_vote = old_vote.kind != kind;
350 try votes_view.del(user_id);
352 switch (old_vote.kind) {
353 .Up => p.upvotes -= 1,
354 .Down => p.downvotes -= 1,
356 try posts.put(post_id, p);
360 try votes_view.append(user_id, Vote{
362 .time = std.time.timestamp(),
370 try posts.put(post_id, p);
374 fn follow(env: *lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
375 const txn = try env.txn();
376 defer txn.commit() catch {};
378 const users = try Db.users(txn);
380 const user = try users.get(user_id);
381 const user_to_follow = try users.get(user_id_to_follow);
383 var user_following = try user.following.open(txn);
384 var user_to_follow_followers = try user_to_follow.followers.open(txn);
386 if ((user_following.has(user_id_to_follow) catch false) and (user_to_follow_followers.has(user_id) catch false)) {
387 try user_following.del(user_id_to_follow);
388 try user_to_follow_followers.del(user_id);
389 } else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
390 try user_following.append(user_id_to_follow, {});
391 try user_to_follow_followers.append(user_id, {});
393 std.debug.print("Something went wrong when trying to unfollow\n", .{});
397 fn get_session_user_id(env: *lmdb.Env, session_token: SessionToken) !UserId {
398 const txn = try env.txn();
401 const sessions = try Db.sessions(txn);
403 return try sessions.get(session_token);
406 fn get_user(env: *lmdb.Env, user_id: UserId) !User {
407 const txn = try env.txn();
410 const users = try Db.users(txn);
411 return try users.get(user_id);
418 fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
419 try res.write("<form action=\"", .{});
420 try res.write(fmt_action, args_action);
421 try res.write("\" method=\"post\">", .{});
423 inline for (inputs) |input| {
424 switch (@typeInfo(@TypeOf(input))) {
426 try res.write("<input ", .{});
427 try res.write(input[0], input[1]);
428 try res.write(" />", .{});
431 try res.write("<input ", .{});
432 try res.write(input, .{});
433 try res.write(" />", .{});
438 try res.write("</form>", .{});
443 const TimeStr = std.BoundedArray(u8, 256);
444 // fn time_str(_t: i64) TimeStr {
445 // var result = TimeStr.init(0) catch unreachable;
447 // const nthSecond: u64 = @intCast(_t);
448 // const nthDay = nthSecond / std.time.s_per_day;
449 // const secondInDay = nthSecond - nthDay * std.time.s_per_day;
450 // const hourInDay = secondInDay / std.time.s_per_hour;
451 // const secondInHour = secondInDay % std.time.s_per_hour;
452 // const minuteInHour = secondInHour / 60;
453 // const secondInMinute = secondInDay % std.time.s_per_min;
455 // const nthYear = nthDay / 365; // maybe check
456 // const year = 1970 + nthYear;
457 // const leapdays = year / 4 - year / 100 + year / 400;
458 // const secondInYear = nthSecond - (nthYear * 365 + leapdays) * std.time.s_per_day;
459 // const dayInYear = secondInYear / std.time.s_per_day;
461 // std.fmt.format(result.writer(), "<time>{:0>2}:{:0>2}:{:0>2} {:0>2}.{:0>2}.{:0>4} UTC</time>", .{
468 // }) catch unreachable;
472 // http://howardhinnant.github.io/date_algorithms.html
473 fn time_str(_t: i64) TimeStr {
474 const t: u64 = @intCast(_t);
475 var result = TimeStr.init(0) catch unreachable;
477 const nD = @divFloor(t, std.time.s_per_day);
478 const z: u64 = nD + 719468;
479 const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
480 const doe: u64 = z - era * 146097; // [0, 146096]
481 const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
482 const Y: u64 = yoe + era * 400;
483 const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
484 const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
485 const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
486 const M: u64 = if (mp < 10) mp + 3 else mp - 9;
488 const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
489 const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
490 const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;
492 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;
496 fn write_header(res: *http.Response, logged_in: ?Login) !void {
497 if (logged_in) |login| {
499 \\<a href="/">Home</a><br />
502 \\<a href="/user/{s}">Profile</a><br />
503 , .{login.user.name.constSlice()});
505 \\<a href="/post">Post</a><br />
508 \\<a href="/edit">Edit</a><br />
510 try html_form(res, "/logout", .{}, .{
511 \\type="submit" value="Logout"
513 try html_form(res, "/quit", .{}, .{
514 \\type="submit" value="Quit"
516 try res.write("<br /><br />", .{});
519 \\<a href="/">Home</a><br />
520 \\<form action="/register" method="post">
521 \\<input type="text" name="username" />
522 \\<input type="password" name="password" />
523 \\<input type="submit" value="Register" />
525 \\<form action="/login" method="post">
526 \\<input type="text" name="username" />
527 \\<input type="password" name="password" />
528 \\<input type="submit" value="Login" />
531 try html_form(res, "/quit", .{}, .{
532 \\type="submit" value="Quit"
534 try res.write("<br /><br />", .{});
537 fn write_start(res: *http.Response) !void {
542 \\<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>🐣</text></svg>">
545 \\ display: inline-block;
547 \\ .toggle > form > input:placeholder-shown {
550 \\ .toggle:hover > form > input {
551 \\ display: inline-block;
553 \\ .toggle > form > input:placeholder-shown + input {
556 \\ .toggle:hover > form > input + input {
557 \\ display: inline-block;
564 fn write_end(res: *http.Response) !void {
565 try res.write("</body></html>", .{});
567 fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, recurse: enum { No, Once, Yes }) !void {
568 const posts = try Db.posts(txn);
569 const post = posts.get(post_id) catch {
570 res.redirect("/") catch {};
573 const users = try Db.users(txn);
574 const user = try users.get(post.user_id);
578 \\<span><a href="/user/{s}">{s}</a> {s}</span><br />
579 \\<span>{s}</span><br />
580 , .{ user.name.constSlice(), user.display_name.constSlice(), time_str(post.time).constSlice(), post.text.constSlice() });
582 if (recurse != .No and post.quote_id != null) {
583 try res.write("<div style=\"border: 1px solid black;\">", .{});
584 try write_post(res, txn, logged_in, post.quote_id.?, .No);
585 try res.write("</div>", .{});
588 const comments_view = try post.comments.open(txn);
589 const quotes_view = try post.quotes.open(txn);
590 const votes_view = try post.votes.open(txn);
593 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;
595 if (vote != null and vote.?.kind == .Up) {
596 try html_form(res, "/upvote", .{}, .{
597 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
598 .{ "type=\"submit\" value=\"⬆ {}\"", .{post.upvotes} },
601 try html_form(res, "/upvote", .{}, .{
602 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
603 .{ "type=\"submit\" value=\"⇧ {}\"", .{post.upvotes} },
606 if (vote != null and vote.?.kind == .Down) {
607 try html_form(res, "/downvote", .{}, .{
608 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
609 .{ "type=\"submit\" value=\"⬇ {}\"", .{post.downvotes} },
612 try html_form(res, "/downvote", .{}, .{
613 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
614 .{ "type=\"submit\" value=\"⇩ {}\"", .{post.downvotes} },
620 \\<span class="toggle">
621 \\<a href="/post/{x}">💭 {}</a>
622 , .{ @intFromEnum(post.id), comments_view.len() });
623 if (logged_in != null) {
624 try html_form(res, "/comment", .{}, .{
625 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
626 "type=\"text\" name=\"text\" placeholder=\"Text\"",
627 "type=\"submit\" value=\"Comment\"",
636 \\<span class="toggle">
637 \\<a href="/quotes/{x}">🔁 {}</a>
638 , .{ @intFromEnum(post.id), quotes_view.len() });
639 try html_form(res, "/quote", .{}, .{
640 .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
641 "type=\"text\" name=\"text\" placeholder=\"Text\"",
642 "type=\"submit\" value=\"Quote\"",
650 if (recurse != .No and comments_view.len() > 0) {
653 \\<summary>Comments</summary>
655 try res.write("<div style=\"margin: 10px;\">", .{});
656 var it = comments_view.iterator();
658 while (it.next()) |kv| {
659 try write_post(res, txn, logged_in, kv.key, switch (recurse) {
664 try res.write("<br />", .{});
665 if (recurse == .Once) {
667 if (count >= 3) break;
680 fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
681 const posts_view = try user.posts.open(txn);
683 var it = posts_view.reverse_iterator();
684 while (it.next()) |kv| {
685 const post_id = kv.key;
687 try write_post(res, txn, logged_in, post_id, .Once);
688 try res.write("<br />", .{});
691 fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void {
692 const users = try Db.users(txn);
693 const posts = try Db.posts(txn);
695 var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
696 var prev_newest_post: ?Post = null;
698 const following = try user.following.open(txn);
701 var newest_post: ?Post = null;
703 var it = following.iterator();
704 while (it.next()) |kv| {
705 const followed_user = try users.get(kv.key);
706 const followed_posts = try followed_user.posts.open(txn);
708 if (followed_posts.len() == 0) {
712 var followed_posts_it = followed_posts.reverse_iterator();
713 while (followed_posts_it.next()) |followed_post_kv| {
714 const last_post = try posts.get(followed_post_kv.key);
716 if ((prev_newest_post == null or last_post.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < last_post.time)) {
717 newest_post = last_post;
721 if (newest_post) |post| {
722 newest_post_ids.append(post.id) catch break;
723 prev_newest_post = post;
729 for (newest_post_ids.constSlice()) |post_id| {
730 try write_post(res, txn, logged_in, post_id, .Once);
731 try res.write("<br />", .{});
736 fn list_users(env: lmdb.Env) !void {
737 const txn = try env.txn();
740 const users = try Db.users(txn);
741 var it = try users.iterator();
743 while (it.next()) |kv| {
746 std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() });
749 fn list_user_ids(env: lmdb.Env) !void {
750 const txn = try env.txn();
753 const user_ids = try Db.user_ids(txn);
754 var it = try user_ids.iterator();
756 while (it.next()) |kv| {
758 const user_id = kv.val;
759 std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id });
763 fn list_sessions(env: lmdb.Env) !void {
764 const txn = try env.txn();
767 const sessions = try Db.sessions(txn);
768 var it = try sessions.iterator();
770 while (it.next()) |kv| {
772 const user_id = kv.val;
773 std.debug.print("[{x}] {}\n", .{ key, user_id });
777 fn list_posts(env: lmdb.Env) !void {
778 const txn = try env.txn();
781 const posts = try Db.posts(txn);
782 var it = try posts.iterator();
784 while (it.next()) |kv| {
787 std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() });
791 const ReqBufferSize = 4096;
792 const ResHeadBufferSize = 1024 * 64;
793 const ResBodyBufferSize = 1024 * 64;
795 pub fn main() !void {
797 var server = try http.Server.init("::", 8080);
798 defer server.deinit();
801 var env = try lmdb.Env.open("db", 1024 * 1024 * 10);
804 std.debug.print("Users:\n", .{});
806 std.debug.print("User IDs:\n", .{});
807 try list_user_ids(env);
808 std.debug.print("Sessions:\n", .{});
809 try list_sessions(env);
810 std.debug.print("Posts:\n", .{});
813 try handle_connection(&server, &env);
814 // const ThreadCount = 1;
815 // var ts: [ThreadCount]std.Thread = undefined;
817 // for (0..ThreadCount) |i| {
818 // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env });
820 // for (0..ThreadCount) |i| {
824 std.debug.print("done\n", .{});
827 fn handle_connection(server: *http.Server, env: *lmdb.Env) !void {
829 var req_buffer: [ReqBufferSize]u8 = undefined;
830 var res_head_buffer: [ResHeadBufferSize]u8 = undefined;
831 var res_body_buffer: [ResBodyBufferSize]u8 = undefined;
833 accept: while (true) {
836 while (try server.next_request(&req_buffer)) |*_req| {
837 var req: *http.Request = @constCast(_req);
838 // std.debug.print("[{}]: {s}\n", .{ req.method, req.target });
841 var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
843 // check session token
844 var logged_in: ?Login = null;
846 if (req.get_cookie("session_token")) |session_token_str| {
847 var remove_session_token = true;
849 if (std.fmt.parseUnsigned(SessionToken, session_token_str, 16) catch null) |session_token| {
850 // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10);
851 // const session_token = std.mem.bytesToValue(SessionToken, session_token_str);
852 if (Chirp.get_session_user_id(env, session_token) catch null) |user_id| {
853 const txn = try env.txn();
855 const users = try Db.users(txn);
858 .user = try users.get(user_id),
859 .session_token = session_token,
862 remove_session_token = false;
866 if (remove_session_token) {
869 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
874 // TODO: refactor into functions
875 // TODO: make sure we send a reply
878 if (req.method == .GET) {
879 try write_start(&res);
880 try write_header(&res, logged_in);
882 const txn = try env.txn();
885 if (std.mem.startsWith(u8, req.target, "/user/")) {
886 const username = req.target[6..req.target.len];
888 const user_ids = try Db.user_ids(txn);
889 if (user_ids.get(try Username.fromSlice(username))) |user_id| {
890 const users = try Db.users(txn);
891 const user = try users.get(user_id);
893 const following = try user.following.open(txn);
894 const followers = try user.followers.open(txn);
897 \\<a href="/user/{s}">{s}</a>
899 user.name.constSlice(), user.display_name.constSlice(),
901 if (logged_in != null and user_id != logged_in.?.user.id) {
902 if (try followers.has(logged_in.?.user.id)) {
903 try html_form(&res, "/follow", .{}, .{
904 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user_id)} },
905 \\type="submit" value="Unfollow"
908 try html_form(&res, "/follow", .{}, .{
909 .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user_id)} },
910 \\type="submit" value="Follow"
915 \\ <a href="/following/{s}">{} following</a>
916 \\ <a href="/followers/{s}">{} followers</a>
918 user.name.constSlice(), following.len(),
919 user.name.constSlice(), followers.len(),
922 try write_posts(&res, txn, logged_in, user);
925 \\<p>User not found [{}]</p>
928 } else if (std.mem.startsWith(u8, req.target, "/following/")) {
929 const username = req.target[11..req.target.len];
931 const user_ids = try Db.user_ids(txn);
932 if (user_ids.get(try Username.fromSlice(username))) |user_id| {
933 const users = try Db.users(txn);
934 const user = try users.get(user_id);
936 const following = try user.following.open(txn);
937 var it = following.iterator();
940 \\<h2><a href="/user/{s}">{s}</a> follows:</h2>
941 , .{ user.name.constSlice(), user.display_name.constSlice() });
943 while (it.next()) |kv| {
944 const following_user = try users.get(kv.key);
947 \\<a href="/user/{s}">{s}</a><br />
948 , .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
952 \\<p>User not found [{}]</p>
955 } else if (std.mem.startsWith(u8, req.target, "/followers/")) {
956 const username = req.target[11..req.target.len];
958 const user_ids = try Db.user_ids(txn);
959 if (user_ids.get(try Username.fromSlice(username))) |user_id| {
960 const users = try Db.users(txn);
961 const user = try users.get(user_id);
963 const followers = try user.followers.open(txn);
964 var it = followers.iterator();
967 \\<h2><a href="/user/{s}">{s}</a> followers:</h2>
968 , .{ user.name.constSlice(), user.display_name.constSlice() });
970 while (it.next()) |kv| {
971 const follower_user = try users.get(kv.key);
974 \\<a href="/user/{s}">{s}</a><br />
975 , .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
979 \\<p>User not found [{}]</p>
982 } else if (std.mem.startsWith(u8, req.target, "/post/")) {
983 const post_id_str = req.target[6..req.target.len];
984 const post_id = try parse_enum(PostId, post_id_str, 16);
986 try write_post(&res, txn, logged_in, post_id, .Yes);
987 } else if (std.mem.startsWith(u8, req.target, "/quotes/")) {
988 const post_id_str = req.target[8..req.target.len];
989 const post_id = try parse_enum(PostId, post_id_str, 16);
991 const posts = try Db.posts(txn);
992 const post = try posts.get(post_id);
994 const quotes_view = try post.quotes.open(txn);
995 var it = quotes_view.iterator();
996 while (it.next()) |kv| {
997 try write_post(&res, txn, logged_in, kv.key, .Once);
998 try res.write("<br />", .{});
1000 } else if (std.mem.eql(u8, req.target, "/post")) {
1001 if (logged_in) |login| {
1003 const referer = if (req.get_header("Referer")) |ref| ref else "/post";
1005 try html_form(&res, "/post", .{}, .{
1006 .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
1007 "type=\"text\" name=\"text\"",
1008 "type=\"submit\" value=\"Post\"",
1011 try res.write("not logged in", .{});
1013 } else if (std.mem.eql(u8, req.target, "/edit")) {
1014 if (logged_in) |login| {
1015 try res.write("<br />Username: ", .{});
1016 try html_form(&res, "/set_username", .{}, .{
1017 .{ "type=\"text\" name=\"username\" placeholder=\"{s}\"", .{login.user.name.constSlice()} },
1018 "type=\"submit\" value=\"Change\"",
1020 try res.write("<br />Display Name: ", .{});
1021 try html_form(&res, "/set_display_name", .{}, .{
1022 .{ "type=\"text\" name=\"display_name\" placeholder=\"{s}\"", .{login.user.display_name.constSlice()} },
1023 "type=\"submit\" value=\"Change\"",
1025 try res.write("<br />Password: ", .{});
1026 try html_form(&res, "/set_password", .{}, .{
1027 "type=\"text\" name=\"password\"",
1028 "type=\"submit\" value=\"Change\"",
1031 try res.write("not logged in", .{});
1033 } else if (std.mem.eql(u8, req.target, "/")) {
1034 if (logged_in) |login| {
1035 try write_timeline(&res, txn, logged_in, login.user);
1037 // TODO: generic home
1038 try res.write("Homepage", .{});
1041 try res.redirect("/");
1043 try write_end(&res);
1048 if (std.mem.eql(u8, req.target, "/register")) {
1049 // TODO: handle args not supplied
1050 const username = req.get_value("username").?;
1051 const password = req.get_value("password").?;
1053 std.debug.print("New user: {s} {s}\n", .{ username, password });
1054 _ = try Chirp.register_user(env, username, password);
1055 } else if (std.mem.eql(u8, req.target, "/login")) {
1056 // TODO: handle args not supplied
1057 const username = req.get_value("username").?;
1058 const password = req.get_value("password").?;
1060 std.debug.print("New login: {s} {s}\n", .{ username, password });
1061 if (Chirp.login_user(env, username, password)) |session_token| {
1062 res.status = .see_other;
1065 .{ "session_token={x}; HttpOnly", .{session_token} },
1068 std.debug.print("login_user err: {}\n", .{err});
1070 } else if (std.mem.eql(u8, req.target, "/logout")) {
1071 if (logged_in) |login| {
1072 try Chirp.logout_user(env, login.session_token);
1076 .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"},
1079 } else if (std.mem.eql(u8, req.target, "/set_username")) {
1080 const login = logged_in orelse return error.NotLoggedIn;
1081 const username_str = req.get_value("username") orelse return error.NoUsername;
1082 const username = try Username.fromSlice(username_str);
1084 const txn = try env.txn();
1085 defer txn.commit() catch {};
1087 const user_ids = try Db.user_ids(txn);
1089 if (!try user_ids.has(username)) {
1090 try user_ids.del(login.user.name);
1091 try user_ids.put(username, login.user.id);
1093 const users = try Db.users(txn);
1094 var user = login.user;
1095 user.name = username;
1096 try users.put(login.user.id, user);
1098 } else if (std.mem.eql(u8, req.target, "/set_display_name")) {
1099 const login = logged_in orelse return error.NotLoggedIn;
1100 const display_name_str = req.get_value("display_name") orelse return error.NoDisplayName;
1101 const display_name = try DisplayName.fromSlice(display_name_str);
1103 const txn = try env.txn();
1104 defer txn.commit() catch {};
1106 const users = try Db.users(txn);
1107 var user = login.user;
1108 user.display_name = display_name;
1109 try users.put(login.user.id, user);
1110 } else if (std.mem.eql(u8, req.target, "/set_password")) {
1111 const login = logged_in orelse return error.NotLoggedIn;
1112 const password_str = req.get_value("password") orelse return error.NoPassword;
1114 const txn = try env.txn();
1115 defer txn.commit() catch {};
1117 const users = try Db.users(txn);
1118 var user = login.user;
1119 user.password_hash = try Chirp.hash_password(password_str);
1120 try users.put(login.user.id, user);
1121 } else if (std.mem.eql(u8, req.target, "/post")) {
1122 if (logged_in) |login| {
1123 const text = req.get_value("text").?;
1124 const has_referer = req.get_value("referer");
1125 try Chirp.post(env, login.user.id, text);
1126 if (has_referer) |r| {
1127 const decoded = try decode(r);
1128 try res.redirect(decoded.constSlice());
1131 } else if (std.mem.eql(u8, req.target, "/comment")) {
1132 if (logged_in) |login| {
1133 const text = req.get_value("text") orelse return error.NoText;
1134 const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
1135 const post_id = try parse_enum(PostId, post_id_str, 16);
1137 try Chirp.comment(env, login.user.id, post_id, text);
1139 } else if (std.mem.eql(u8, req.target, "/quote")) {
1140 if (logged_in) |login| {
1141 const text = req.get_value("text") orelse return error.NoText;
1142 const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
1143 const post_id = try parse_enum(PostId, post_id_str, 16);
1145 try Chirp.quote(env, login.user.id, post_id, text);
1147 } else if (std.mem.eql(u8, req.target, "/upvote")) {
1148 const login = logged_in orelse return error.NotLoggedIn;
1150 const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
1151 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1153 try Chirp.vote(env, post_id, login.user.id, .Up);
1154 } else if (std.mem.eql(u8, req.target, "/downvote")) {
1155 const login = logged_in orelse return error.NotLoggedIn;
1157 const post_id_str = req.get_value("post_id") orelse return error.NoPostId;
1158 const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16));
1160 try Chirp.vote(env, post_id, login.user.id, .Down);
1161 } else if (std.mem.eql(u8, req.target, "/follow")) {
1162 const login = logged_in orelse return error.NotLoggedIn;
1164 const user_id_str = req.get_value("user_id") orelse return error.NoUserId;
1165 const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16));
1167 try Chirp.follow(env, login.user.id, user_id);
1168 } else if (std.mem.eql(u8, req.target, "/quit")) {
1169 if (req.get_header("Referer")) |ref| {
1170 try res.redirect(ref);
1172 try res.redirect("/");
1177 try res.write("<p>[POST] {s}</p>", .{req.target});
1180 if (!res.has_header("Location")) {
1181 if (req.get_header("Referer")) |ref| {
1182 try res.redirect(ref);
1184 try res.redirect("/");