+ }
+
+ fn logout_user(env: lmdb.Env, session_token: SessionToken) !void {
+ const txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const sessions = try Db.sessions(txn);
+ try sessions.del(session_token);
+ }
+
+ fn append_post(env: lmdb.Env, user_id: UserId, post_list: PostList, parent_id: ?PostId, quote_id: ?PostId, text: []const u8) !void {
+ var post_id: PostId = undefined;
+
+ // TODO: do this in one commit
+
+ var txn: lmdb.Txn = undefined;
+ {
+ // create post
+ txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const posts = try Db.posts(txn);
+ post_id = try db.Prng.gen(posts.dbi, PostId);
+
+ const decoded_text = try reencode(text);
+ try posts.put(post_id, Post{
+ .id = post_id,
+ .parent_id = parent_id,
+ .quote_id = quote_id,
+ .user_id = user_id,
+ .time = std.time.timestamp(),
+ .votes = try VoteList.init(txn),
+ .comments = try PostList.init(txn),
+ .quotes = try PostList.init(txn),
+ .text = decoded_text,
+ });
+ }
+
+ {
+ // append to user's posts
+ txn = try env.txn();
+ defer txn.commit() catch {};
+
+ var posts_view = try post_list.open(txn);
+ try posts_view.append(post_id);
+ }
+
+ if (quote_id != null) {
+ txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const posts = try Db.posts(txn);
+ const quote_post = try posts.get(quote_id.?);
+ var quotes = try quote_post.quotes.open(txn);
+ try quotes.append(post_id);
+ }
+ }
+
+ fn post(env: lmdb.Env, user_id: UserId, text: []const u8) !void {
+ const txn = try env.txn();
+
+ const users = try Db.users(txn);
+ const user = try users.get(user_id);
+
+ txn.abort();
+
+ try append_post(env, user_id, user.posts, null, null, text);
+ }
+
+ fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void {
+ const txn = try env.txn();
+
+ const posts = try Db.posts(txn);
+ const parent_post = try posts.get(parent_post_id);
+
+ txn.abort();
+
+ try append_post(env, user_id, parent_post.comments, parent_post_id, null, text);
+ }
+
+ fn quote(env: lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void {
+ const txn = try env.txn();
+
+ const users = try Db.users(txn);
+ const user = try users.get(user_id);
+
+ txn.abort();
+
+ try append_post(env, user_id, user.posts, null, quote_post_id, text);
+ }
+
+ fn vote(env: lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
+ const txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const posts = try Db.posts(txn);
+
+ var p = try posts.get(post_id);
+ var votes_view = try p.votes.open(txn);
+
+ var add_vote = true;
+
+ if (try votes_view.has(user_id)) {
+ const old_vote = try votes_view.get(user_id);
+
+ add_vote = old_vote.kind != kind;
+
+ try votes_view.del(user_id);
+
+ switch (old_vote.kind) {
+ .Up => p.upvotes -= 1,
+ .Down => p.downvotes -= 1,
+ }
+ try posts.put(post_id, p);
+ }
+
+ if (add_vote) {
+ try votes_view.append(user_id, Vote{
+ .kind = kind,
+ .time = std.time.timestamp(),
+ });
+
+ if (kind == .Up) {
+ p.upvotes += 1;
+ } else {
+ p.downvotes += 1;
+ }
+ try posts.put(post_id, p);
+ }
+ }
+
+ fn follow(env: lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
+ const txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const users = try Db.users(txn);
+
+ const user = try users.get(user_id);
+ const user_to_follow = try users.get(user_id_to_follow);
+
+ var user_following = try user.following.open(txn);
+ var user_to_follow_followers = try user_to_follow.followers.open(txn);
+
+ if ((user_following.has(user_id_to_follow) catch false) and (user_to_follow_followers.has(user_id) catch false)) {
+ try user_following.del(user_id_to_follow);
+ try user_to_follow_followers.del(user_id);
+ } else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
+ try user_following.append(user_id_to_follow);
+ try user_to_follow_followers.append(user_id);
+ } else {
+ std.debug.print("Something went wrong when trying to unfollow\n", .{});
+ }
+ }
+
+ fn get_session_user_id(env: lmdb.Env, session_token: SessionToken) !UserId {
+ const txn = try env.txn();
+ defer txn.abort();
+
+ const sessions = try Db.sessions(txn);
+
+ return try sessions.get(session_token);
+ }
+
+ fn get_user(env: lmdb.Env, user_id: UserId) !User {
+ const txn = try env.txn();
+ defer txn.abort();
+
+ const users = try Db.users(txn);
+ return try users.get(user_id);
+ }
+};
+
+// }}}
+
+// html {{{
+fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
+ try res.write("<form action=\"", .{});
+ try res.write(fmt_action, args_action);
+ try res.write("\" method=\"post\">", .{});
+
+ inline for (inputs) |input| {
+ switch (@typeInfo(@TypeOf(input))) {
+ .Struct => {
+ try res.write("<input ", .{});
+ try res.write(input[0], input[1]);
+ try res.write(" />", .{});
+ },
+ else => {
+ try res.write("<input ", .{});
+ try res.write(input, .{});
+ try res.write(" />", .{});
+ },
+ }
+ }
+
+ try res.write("</form>", .{});
+}
+// }}}
+
+// write {{{
+const TimeStr = std.BoundedArray(u8, 256);
+
+// http://howardhinnant.github.io/date_algorithms.html
+fn time_str(_t: i64) TimeStr {
+ const t: u64 = @intCast(_t);
+ var result = TimeStr.init(0) catch unreachable;
+
+ const nD = @divFloor(t, std.time.s_per_day);
+ const z: u64 = nD + 719468;
+ const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
+ const doe: u64 = z - era * 146097; // [0, 146096]
+ const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
+ const Y: u64 = yoe + era * 400;
+ const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
+ const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
+ const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
+ const M: u64 = if (mp < 10) mp + 3 else mp - 9;
+
+ const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
+ const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
+ const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;
+
+ 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;
+
+ return result;
+}
+fn write_header(res: *http.Response, logged_in: ?Login) !void {
+ if (logged_in) |login| {
+ try res.write(
+ \\<a href="/">Home</a><br />
+ , .{});
+ try res.write(
+ \\<a href="/user/{s}">Profile</a><br />
+ , .{login.user.name.constSlice()});
+ try res.write(
+ \\<a href="/post">Post</a><br />
+ , .{});
+ try html_form(res, "/logout", .{}, .{
+ \\type="submit" value="Logout"
+ });
+ try res.write("<br /><br />", .{});