+ \\<details{s}>
+ \\<summary>Comments</summary>
+ , .{if (options.recurse > 1) " open" else ""});
+ try res.write("<div style=\"margin: 10px;\">", .{});
+ var it = comments_view.iterator();
+ var count: u8 = 0;
+ while (it.next()) |comment_id| {
+ try write_post(res, txn, logged_in, comment_id, .{ .recurse = options.recurse - 1 });
+ try res.write("<br />", .{});
+ if (options.recurse == 1) {
+ count += 1;
+ if (count >= 3) break;
+ }
+ }
+ try res.write(
+ \\</div>
+ \\</details>
+ , .{});
+ }
+
+ try res.write(
+ \\</div>
+ , .{});
+}
+fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList) !void {
+ const posts_view = try post_list.open(txn);
+
+ var it = posts_view.reverse_iterator();
+ while (it.next()) |post_id| {
+ try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
+ try res.write("<br />", .{});
+ }
+}
+fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
+ const users = try Db.users(txn);
+ const posts = try Db.posts(txn);
+
+ var newest_post_ids = try std.BoundedArray(PostId, 10).init(0); // TODO: TimelinePostsCount
+ var prev_newest_post: ?Post = null;
+
+ const following = try user_list.open(txn);
+
+ while (true) {
+ var newest_post: ?Post = null;
+
+ var following_it = following.iterator();
+ while (following_it.next()) |following_id| {
+ const followed_user = try users.get(following_id);
+ const followed_posts = try followed_user.posts.open(txn);
+
+ if (followed_posts.len() == 0) {
+ continue;
+ }
+
+ var followed_posts_it = followed_posts.reverse_iterator();
+ while (followed_posts_it.next()) |followed_post_id| {
+ const p = try posts.get(followed_post_id);
+
+ if ((prev_newest_post == null or p.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < p.time)) {
+ newest_post = p;
+ break;
+ }
+ }
+ }
+ if (newest_post) |post| {
+ newest_post_ids.append(post.id) catch break;
+ prev_newest_post = post;
+ } else {
+ break;
+ }
+ }
+
+ for (newest_post_ids.constSlice()) |post_id| {
+ try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
+ try res.write("<br />", .{});
+ }
+}
+// }}}
+
+// GET {{{
+const GET = struct {
+ const Self = @This();
+
+ txn: lmdb.Txn,
+ req: *http.Request,
+ res: *http.Response,
+ logged_in: ?Login,
+
+ fn handle(self: Self) !bool {
+ const ti = @typeInfo(Self);
+ inline for (ti.Struct.decls) |f_decl| {
+ const has_arg = f_decl.name.len > 1 and f_decl.name[f_decl.name.len - 1] == '/';
+ 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);
+
+ if (match) {
+ const f = @field(Self, f_decl.name);
+ const fi = @typeInfo(@TypeOf(f));
+ if (fi.Fn.params.len == 1) {
+ try @call(.auto, f, .{self});
+ } else {
+ const arg_type = fi.Fn.params[1].type.?;
+ const arg_info = @typeInfo(arg_type);
+ var arg: arg_type = undefined;
+ const field = arg_info.Struct.fields[0];
+ if (self.req.target.len <= f_decl.name.len) {
+ return error.NoArgProvided;
+ }
+ const str = self.req.target[f_decl.name.len..self.req.target.len];
+ const field_ti = @typeInfo(field.type);
+ switch (field_ti) {
+ // TODO: maybe handle BoundedArray?
+ .Int => {
+ @field(arg, field.name) = try std.fmt.parseUnsigned(field.type, str, 16);
+ },
+ .Enum => {
+ @field(arg, field.name) = try parse_enum(field.type, str, 16);
+ },
+ else => {
+ @field(arg, field.name) = str;
+ },
+ }
+
+ try @call(.auto, f, .{ self, arg });
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ pub fn @"/user/"(self: Self, args: struct { username: []const u8 }) !void {
+ const user_ids = try Db.user_ids(self.txn);
+ if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
+ const users = try Db.users(self.txn);
+ const user = try users.get(user_id);
+
+ const following = try user.following.open(self.txn);
+ const followers = try user.followers.open(self.txn);
+
+ try self.res.write(
+ \\<h2 style="display: inline;"><a href="/user/{s}">{s}</a></h2>
+ , .{
+ user.name.constSlice(), user.display_name.constSlice(),
+ });
+ if (self.logged_in != null and user_id != self.logged_in.?.user.id) {
+ const login = self.logged_in.?;
+
+ // follow/unfollow
+ if (try followers.has(login.user.id)) {
+ try html_form(self.res, "/follow", .{}, .{
+ .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user_id)} },
+ \\type="submit" value="Unfollow"
+ });
+ } else {
+ try html_form(self.res, "/follow", .{}, .{
+ .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user_id)} },
+ \\type="submit" value="Follow"
+ });
+ }
+
+ // add to feed
+ const feeds_view = try login.user.feeds.open(self.txn);
+ try self.res.write("<form action=\"/feed_add\" method=\"post\">", .{});
+ try self.res.write("<select name=\"feed_id\">", .{});
+ var it = feeds_view.iterator();
+ while (it.next()) |kv| {
+ const name = kv.val.name;
+ const id = kv.val.list.idx.?;
+ try self.res.write("<option value=\"{x}\">{s}</option>", .{ id, name.constSlice() });
+ }
+ try self.res.write("</select>", .{});
+ try self.res.write("<input type=\"hidden\" name=\"user_id\" value=\"{x}\"></input>", .{@intFromEnum(user_id)});
+ try self.res.write("<input type=\"submit\" value=\"Add to feed\"></input>", .{});
+ try self.res.write("</form>", .{});
+ }
+ try self.res.write(
+ \\ <a href="/following/{s}">{} following</a>
+ \\ <a href="/followers/{s}">{} followers</a>
+ \\<br />
+ , .{
+ user.name.constSlice(), following.len(),
+ user.name.constSlice(), followers.len(),
+ });
+
+ if (self.logged_in != null and user_id == self.logged_in.?.user.id) {
+ try self.res.write(
+ \\<a href="/lists">Lists</a>
+ \\<a href="/feeds">Feeds</a>
+ \\<a href="/edit">Edit</a><br />
+ \\<br />
+ , .{});
+ }
+
+ try self.res.write("<br />", .{});
+
+ try write_posts(self.res, self.txn, self.logged_in, user.posts);
+ } else |err| {
+ try self.res.write(
+ \\<p>User not found [{}]</p>
+ , .{err});
+ }
+ }
+ pub fn @"/following/"(self: Self, args: struct { username: []const u8 }) !void {
+ const user_ids = try Db.user_ids(self.txn);
+ if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
+ const users = try Db.users(self.txn);
+ const user = try users.get(user_id);
+
+ const following = try user.following.open(self.txn);
+ var it = following.iterator();
+
+ try self.res.write(
+ \\<h2><a href="/user/{s}">{s}</a> follows:</h2>
+ , .{ user.name.constSlice(), user.display_name.constSlice() });
+
+ while (it.next()) |following_id| {
+ const following_user = try users.get(following_id);
+
+ try self.res.write(
+ \\<a href="/user/{s}">{s}</a><br />
+ , .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
+ }
+ } else |err| {
+ try self.res.write(
+ \\<p>User not found [{}]</p>
+ , .{err});
+ }
+ }
+ pub fn @"/followers/"(self: Self, args: struct { username: []const u8 }) !void {
+ const user_ids = try Db.user_ids(self.txn);
+ if (user_ids.get(try Username.fromSlice(args.username))) |user_id| {
+ const users = try Db.users(self.txn);
+ const user = try users.get(user_id);
+
+ const followers = try user.followers.open(self.txn);
+ var it = followers.iterator();
+
+ try self.res.write(
+ \\<h2><a href="/user/{s}">{s}</a> followers:</h2>
+ , .{ user.name.constSlice(), user.display_name.constSlice() });
+
+ while (it.next()) |follower_id| {
+ const follower_user = try users.get(follower_id);
+
+ try self.res.write(
+ \\<a href="/user/{s}">{s}</a><br />
+ , .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
+ }
+ } else |err| {
+ try self.res.write(
+ \\<p>User not found [{}]</p>
+ , .{err});
+ }
+ }
+ pub fn @"/post/"(self: Self, args: struct { post_id: PostId }) !void {
+ try write_post(self.res, self.txn, self.logged_in, args.post_id, .{
+ .recurse = 3, // TODO: factor out
+ .show_comment_field = true,
+ });
+ }
+ pub fn @"/quotes/"(self: Self, args: struct { post_id: PostId }) !void {
+ const posts = try Db.posts(self.txn);
+ const post = try posts.get(args.post_id);
+
+ const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target;
+
+ if (self.logged_in != null) {
+ try html_form(self.res, "/quote", .{}, .{
+ .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
+ .{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
+ "type=\"text\" name=\"text\" placeholder=\"Text\"",
+ "type=\"submit\" value=\"Quote\"",
+ });
+ try self.res.write("<br />", .{});
+ }
+
+ const quotes_view = try post.quotes.open(self.txn);
+ var it = quotes_view.iterator();
+ while (it.next()) |quote_id| {
+ try write_post(self.res, self.txn, self.logged_in, quote_id, .{ .recurse = 1 });
+ try self.res.write("<br />", .{});
+ }
+ }
+ pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Index }) !void {
+ try write_posts(self.res, self.txn, self.logged_in, PostList{ .idx = args.list_id });
+ }
+ pub fn @"/lists"(self: Self) !void {
+ if (self.logged_in) |login| {
+ const post_lists_view = try login.user.post_lists.open(self.txn);
+
+ try html_form(self.res, "/new_list", .{}, .{
+ "type=\"text\" name=\"name\"",
+ "type=\"submit\" value=\"Add\"",