const std = @import("std"); const lmdb = @import("lmdb"); const db = @import("db"); const http = @import("http"); // db {{{ const Db = struct { fn users(txn: lmdb.Txn) !db.Db(UserId, User) { return try db.Db(UserId, User).init(txn, "users"); } fn user_ids(txn: lmdb.Txn) !db.Db(Username, UserId) { return try db.Db(Username, UserId).init(txn, "user_ids"); } fn sessions(txn: lmdb.Txn) !db.Db(SessionToken, UserId) { return try db.Db(SessionToken, UserId).init(txn, "sessions"); } fn posts(txn: lmdb.Txn) !db.Db(PostId, Post) { return try db.Db(PostId, Post).init(txn, "posts"); } }; // }}} // content {{{ const User = struct { // TODO: choose sizes id: UserId, name: Username, display_name: DisplayName, description: UserDescription, password_hash: PasswordHash, posts: PostList, following: UserList, followers: UserList, post_lists: PostListList, feeds: UserListList, }; const Post = struct { id: PostId, parent_id: ?PostId, quote_id: ?PostId, user_id: UserId, time: Timestamp, upvotes: u64 = 0, downvotes: u64 = 0, votes: VoteList, comments: PostList, quotes: PostList, text: PostText, }; const SavedPostList = struct { name: Name, list: PostList, }; const SavedUserList = struct { name: Name, list: UserList, }; const Vote = struct { const Kind = enum { Up, Down }; kind: Kind, time: Timestamp, }; const Id = u64; const Login = struct { user: User, session_token: SessionToken, }; const Name = std.BoundedArray(u8, 32); const UserId = enum(u64) { _ }; const PostId = enum(u64) { _ }; const Timestamp = i64; const Username = std.BoundedArray(u8, 32); const DisplayName = std.BoundedArray(u8, 64); const UserDescription = std.BoundedArray(u8, 1024); const PasswordHash = std.BoundedArray(u8, 128); const SessionToken = u64; const CookieValue = std.BoundedArray(u8, 128); const PostText = std.BoundedArray(u8, 1024); const PostList = db.Set(PostId); const UserList = db.Set(UserId); const VoteList = db.SetList(UserId, Vote); const PostListList = db.List(SavedPostList); const UserListList = db.List(SavedUserList); fn parse_enum(comptime E: type, buf: []const u8, base: u8) !E { return @enumFromInt(try std.fmt.parseUnsigned(@typeInfo(E).Enum.tag_type, buf, base)); } // https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding fn reencode(comptime T: type, text: []const u8) !T { var result = try T.init(0); const len = @min(text.len, 1024); // TODO: PostText length var idx: usize = 0; while (idx < len) : (idx += 1) { const c = text[idx]; if (c == '+') { try result.append(' '); } else if (c == '%' and idx + 2 < text.len) { const allow = &[_]u8{ 0x26, 0x23, 0x3b, 0x0a }; const escaped_value = std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16) catch continue; if (escaped_value == 0x0d) { try std.fmt.format(result.writer(), "
", .{}); } else if (std.mem.indexOfScalar(u8, allow, escaped_value) != null) { try std.fmt.format(result.writer(), "{c}", .{escaped_value}); } else { try std.fmt.format(result.writer(), "&#x{x};", .{escaped_value}); } idx += 2; } else { try result.append(c); } } return result; } fn decode(text: []const u8) !std.BoundedArray(u8, 1024) { var result = try std.BoundedArray(u8, 1024).init(0); const max_len = @min(text.len, 1024); // TODO: PostText length var idx: usize = 0; var len: usize = 0; while (len < max_len and idx < text.len) : ({ idx += 1; len += 1; }) { const c = text[idx]; if (c == '+') { try result.append(' '); } else if (c == '%') { if (idx + 2 < text.len) { try std.fmt.format(result.writer(), "{c}", .{try std.fmt.parseUnsigned(u8, text[idx + 1 .. idx + 3], 16)}); } idx += 2; } else { try result.append(c); } } return result; } const Chirp = struct { const PostsPerPage = 10; const UsersPerPage = 10; pub fn hash_password(password: []const u8) !PasswordHash { var hash_buffer = try PasswordHash.init(128); // TODO: choose buffer size // TODO: dont allocate on stack, maybe zero memory? var buffer: [1024 * 10]u8 = undefined; var alloc = std.heap.FixedBufferAllocator.init(&buffer); // TODO: choose limits const result = try std.crypto.pwhash.argon2.strHash(password, .{ .allocator = alloc.allocator(), .params = std.crypto.pwhash.argon2.Params.fromLimits(1000, 1024), }, hash_buffer.slice()); try hash_buffer.resize(result.len); return hash_buffer; } pub fn verify_password(password: []const u8, hash: PasswordHash) bool { var buffer: [1024 * 10]u8 = undefined; var alloc = std.heap.FixedBufferAllocator.init(&buffer); if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{ .allocator = alloc.allocator(), })) { return true; } else |err| { std.debug.print("verify error: {}\n", .{err}); return false; } } pub fn register_user(env: lmdb.Env, username: []const u8, password: []const u8) !bool { const username_array = try Username.fromSlice(username); const display_name = try DisplayName.fromSlice(username); const txn = try env.txn(); defer txn.commit() catch |err| { std.debug.print("error registering user: {}\n", .{err}); }; const users = try Db.users(txn); const user_ids = try Db.user_ids(txn); if (try user_ids.has(username_array)) { return false; } else { const user_id = try db.Prng.gen(users.dbi, UserId); try users.put(user_id, User{ .id = user_id, .name = username_array, .display_name = display_name, .description = try UserDescription.init(0), .password_hash = try hash_password(password), .posts = try PostList.init(txn), .following = try UserList.init(txn), .followers = try UserList.init(txn), .post_lists = try PostListList.init(txn), .feeds = try UserListList.init(txn), }); try user_ids.put(username_array, user_id); return true; } } pub fn login_user( env: lmdb.Env, username: []const u8, password: []const u8, ) !SessionToken { const username_array = try Username.fromSlice(username); const txn = try env.txn(); defer txn.commit() catch {}; const user_ids = try Db.user_ids(txn); const user_id = try user_ids.get(username_array); std.debug.print("user logging in, id: {}\n", .{user_id}); const users = try Db.users(txn); const user = try users.get(user_id); if (verify_password(password, user.password_hash)) { const sessions = try Db.sessions(txn); const session_token = try db.Prng.gen(sessions.dbi, SessionToken); try sessions.put(session_token, user_id); return session_token; } else { return error.IncorrectPassword; } } 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) !PostId { 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(PostText, 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); } return post_id; } fn post(env: lmdb.Env, user_id: UserId, text: []const u8) !void { var txn = try env.txn(); const users = try Db.users(txn); const user = try users.get(user_id); txn.abort(); const post_id = try append_post(env, user_id, user.posts, null, null, text); _ = post_id; } fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void { var txn = try env.txn(); const users = try Db.users(txn); const user = try users.get(user_id); const posts = try Db.posts(txn); const parent_post = try posts.get(parent_post_id); txn.abort(); const post_id = try append_post(env, user_id, parent_post.comments, parent_post_id, null, text); txn = try env.txn(); var replies_view = try user.posts.open(txn); try replies_view.append(post_id); try txn.commit(); } fn quote(env: lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void { var txn = try env.txn(); const users = try Db.users(txn); const user = try users.get(user_id); txn.abort(); const post_id = try append_post(env, user_id, user.posts, null, quote_post_id, text); _ = post_id; } 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 {{{ pub fn Paginate(comptime T: type) type { return struct { const Self = @This(); const IterateResult = T.Base.View.Iterator.Result; res: *http.Response, view: T.View, per_page: u64, it: T.Base.View.Iterator, starting_idx: ?T.Base.Key, count: u64 = 0, pub fn init(res: *http.Response, view: T.View, per_page: u64) !Self { var it = view.reverse_iterator(); if (res.req.get_param("starting_at")) |starting_at_str| { it.idx = try parse_enum(T.Base.Key, starting_at_str, 16); } return .{ .res = res, .view = view, .per_page = per_page, .it = it, .starting_idx = it.idx, }; } pub fn next(self: *Self) IterateResult { if (self.it.next()) |kv| { if (self.count < self.per_page) { self.count += 1; return kv; } } return null; } pub fn write_navigation(self: *Self) !void { const next_idx = self.it.next(); if (self.view.base.head.last != self.starting_idx) { var prev_it = self.view.iterator(); prev_it.idx = self.starting_idx.?; var oldest_idx = self.starting_idx.?; var count: u64 = 0; while (prev_it.next()) |kv| { oldest_idx = kv.key; if (count > self.per_page) { break; } else { count += 1; } } try self.res.write("Prev ", .{ self.res.req.target, @intFromEnum(oldest_idx) }); } if (next_idx) |kv| { try self.res.write("Next", .{ self.res.req.target, @intFromEnum(kv.key) }); } } }; } fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void { try res.write("
", .{action}); inline for (inputs) |input| { switch (@typeInfo(@TypeOf(input))) { .Struct => |s| { if (s.fields.len == 3) { try res.write("<{s} ", .{input[0]}); try res.write(input[1], input[2]); try res.write(">", .{input[0]}); } else { try res.write("", .{}); } }, else => { try res.write("", .{}); }, } } try res.write("
", .{}); } // }}} // 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(), "", .{ 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( \\Home
, .{}); try res.write( \\Profile
, .{login.user.name.constSlice()}); try res.write( \\Post
, .{}); try html_form(res, "/logout", .{ \\type="submit" value="Logout" }); try res.write("

", .{}); } else { try res.write( \\Home
\\
\\ \\ \\ \\

\\
\\ \\ \\ \\


, .{}); } } fn write_start(res: *http.Response) !void { try res.write( \\ \\ \\ \\ \\ \\ \\ \\ , .{}); } fn write_end(res: *http.Response) !void { try res.write("", .{}); } 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 { const posts = try Db.posts(txn); const post = posts.get(post_id) catch { res.redirect("/") catch {}; return; }; const users = try Db.users(txn); const user = try users.get(post.user_id); try res.write( \\
\\{s} , .{ @intFromEnum(post_id), user.name.constSlice(), user.display_name.constSlice() }); if (post.parent_id) |id| { try res.write(" ..", .{@intFromEnum(id)}); } try res.write( \\ {s}
\\{s}
, .{ time_str(post.time).constSlice(), post.text.constSlice() }); if (logged_in != null and post.user_id == logged_in.?.user.id) { // Votes try res.write( \\ \\{1} Upvotes \\{2} Downvotes \\ \\
, .{ @intFromEnum(post_id), post.upvotes, post.downvotes }); } if (post.quote_id) |quote_id| { try res.write("
", .{}); if (options.recurse > 0) { try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 }); } else { try res.write("...", .{@intFromEnum(post_id)}); } try res.write("
", .{}); } const comments_view = try post.comments.open(txn); const quotes_view = try post.quotes.open(txn); const votes_view = try post.votes.open(txn); // Votes 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; if (vote != null and vote.?.kind == .Up) { try html_form(res, "/upvote", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, .{ "type=\"submit\" value=\"⬆ {}\"", .{post.upvotes} }, }); } else { try html_form(res, "/upvote", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, .{ "type=\"submit\" value=\"⇧ {}\"", .{post.upvotes} }, }); } if (vote != null and vote.?.kind == .Down) { try html_form(res, "/downvote", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, .{ "type=\"submit\" value=\"⬇ {}\"", .{post.downvotes} }, }); } else { try html_form(res, "/downvote", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, .{ "type=\"submit\" value=\"⇩ {}\"", .{post.downvotes} }, }); } // Comment Count try res.write( \\💭 {} , .{ @intFromEnum(post.id), comments_view.len() }); // Quote try res.write( \\🔁 {} , .{ @intFromEnum(post.id), quotes_view.len() }); // Save to List if (logged_in) |login| { const lists_view = try login.user.post_lists.open(txn); try res.write("
", .{}); try res.write("", .{}); try res.write("", .{@intFromEnum(post_id)}); try res.write("", .{}); try res.write("
", .{}); } // Comment field // TODO: maybe always show comment field and prompt for login if (options.show_comment_field and logged_in != null) { try res.write("

", .{}); try html_form(res, "/comment", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, "type=\"text\" name=\"text\" placeholder=\"Text\"", "type=\"submit\" value=\"Comment\"", }); try res.write("
", .{}); } // Comments if (options.recurse > 0 and comments_view.len() > 0) { try res.write( \\ \\Comments , .{if (options.recurse > 1) " open" else ""}); try res.write("
", .{}); var it = comments_view.iterator(); var count: u8 = 0; while (it.next()) |comment_id| { try write_post(res, txn, logged_in, comment_id.key, .{ .recurse = options.recurse - 1 }); try res.write("
", .{}); if (options.recurse == 1) { count += 1; if (count >= 3) break; } } try res.write( \\
\\ , .{}); } try res.write( \\
, .{}); } fn write_profile(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user: User) !void { const following = try user.following.open(txn); const followers = try user.followers.open(txn); try res.write( \\

{s}

, .{ user.name.constSlice(), user.display_name.constSlice(), }); if (logged_in != null and user.id != logged_in.?.user.id) { const login = logged_in.?; // follow/unfollow if (try followers.has(login.user.id)) { try html_form(res, "/follow", .{ .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} }, \\type="submit" value="Unfollow" }); } else { try html_form(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(txn); try res.write("
", .{}); try res.write("", .{}); try res.write("", .{@intFromEnum(user.id)}); try res.write("", .{}); try res.write("
", .{}); } try res.write( \\ {} following \\ {} followers \\
, .{ user.name.constSlice(), following.len(), user.name.constSlice(), followers.len(), }); try res.write( \\All Posts \\ Comments \\ Quotes
, .{ user.name.constSlice(), }); if (logged_in != null and user.id == logged_in.?.user.id) { try res.write( \\Lists \\Feeds \\Edit
\\
, .{}); } if (user.description.len > 0) { try res.write( \\
// \\« {s} » \\{s} \\
, .{user.description.constSlice()}); } try res.write("
", .{}); } fn write_posts(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_list: PostList, options: struct { show_posts: bool, show_quotes: bool, show_comments: bool, }) !void { const posts_view = try post_list.open(txn); var paginate = try Paginate(PostList).init(res, posts_view, Chirp.PostsPerPage); while (paginate.next()) |post_id| { const posts = try Db.posts(txn); const post = try posts.get(post_id.key); if ((options.show_posts and (post.parent_id == null and post.quote_id == null)) or (options.show_quotes and (post.quote_id != null)) or (options.show_comments and (post.parent_id != null))) { try write_post(res, txn, logged_in, post_id.key, .{ .recurse = 1 }); try res.write("
", .{}); } } try paginate.write_navigation(); } 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.key); 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.key); 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("
", .{}); } } fn write_user(res: *http.Response, txn: lmdb.Txn, user_id: UserId) !void { const users = try Db.users(txn); const user = try users.get(user_id); try res.write( \\{s} , .{ user.name.constSlice(), user.display_name.constSlice() }); } fn write_votes(res: *http.Response, txn: lmdb.Txn, votes: VoteList, options: struct { show_upvotes: bool = true, show_downvotes: bool = true, }) !void { const votes_view = try votes.open(txn); var paginate = try Paginate(VoteList).init(res, votes_view, Chirp.UsersPerPage); while (paginate.next()) |kv| { const user_id = kv.key; const vote = kv.val; if ((options.show_upvotes and vote.kind == .Up) or (options.show_downvotes and vote.kind == .Down)) { try write_user(res, txn, user_id); try res.write(" {s}
", .{time_str(vote.time).constSlice()}); } } try paginate.write_navigation(); } fn check_login(env: lmdb.Env, req: http.Request, res: *http.Response) !?Login { var result: ?Login = null; if (req.get_cookie("session_token")) |session_token_str| { var remove_session_token = true; if (std.fmt.parseUnsigned(SessionToken, session_token_str, 16) catch null) |session_token| { // const session_token = try std.fmt.parseUnsigned(SessionToken, session_token_str, 10); // const session_token = std.mem.bytesToValue(SessionToken, session_token_str); if (Chirp.get_session_user_id(env, session_token) catch null) |user_id| { const txn = try env.txn(); defer txn.abort(); const users = try Db.users(txn); result = .{ .user = try users.get(user_id), .session_token = session_token, }; remove_session_token = false; } } if (remove_session_token) { try res.add_header( "Set-Cookie", .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, ); } } return result; } // }}} // 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); try write_profile(self.res, self.txn, self.logged_in, user); try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ .show_posts = true, .show_quotes = false, .show_comments = false, }); } else |err| { try self.res.write( \\

User not found [{}]

, .{err}); } } pub fn @"/comments/"(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); try write_profile(self.res, self.txn, self.logged_in, user); try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ .show_posts = false, .show_quotes = false, .show_comments = true, }); } else |err| { try self.res.write( \\

User not found [{}]

, .{err}); } } pub fn @"/quotes/"(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); try write_profile(self.res, self.txn, self.logged_in, user); try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ .show_posts = false, .show_quotes = true, .show_comments = false, }); } else |err| { try self.res.write( \\

User not found [{}]

, .{err}); } } pub fn @"/all/"(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); try write_profile(self.res, self.txn, self.logged_in, user); try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ .show_posts = true, .show_quotes = true, .show_comments = true, }); } else |err| { try self.res.write( \\

User not found [{}]

, .{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_view = try user.following.open(self.txn); var paginate = try Paginate(UserList).init(self.res, following_view, Chirp.UsersPerPage); try self.res.write( \\

{s} follows:

, .{ user.name.constSlice(), user.display_name.constSlice() }); while (paginate.next()) |following_id| { const following_user = try users.get(following_id.key); try self.res.write( \\{s}
, .{ following_user.name.constSlice(), following_user.display_name.constSlice() }); } try paginate.write_navigation(); } else |err| { try self.res.write( \\

User not found [{}]

, .{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_view = try user.followers.open(self.txn); var paginate = try Paginate(UserList).init(self.res, followers_view, Chirp.UsersPerPage); try self.res.write( \\

{s} followers:

, .{ user.name.constSlice(), user.display_name.constSlice() }); while (paginate.next()) |follower_id| { const follower_user = try users.get(follower_id.key); try self.res.write( \\{s}
, .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() }); } try paginate.write_navigation(); } else |err| { try self.res.write( \\

User not found [{}]

, .{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 @"/upvotes/"(self: Self, args: struct { post_id: PostId }) !void { const posts = try Db.posts(self.txn); const post = try posts.get(args.post_id); try self.res.write("{} upvotes:
", .{post.upvotes}); try write_votes(self.res, self.txn, post.votes, .{}); } pub fn @"/quoted/"(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\" autofocus", "type=\"submit\" value=\"Quote\"", }); try self.res.write("
", .{}); } // TODO: show all bc this only contains quotes? try write_posts(self.res, self.txn, self.logged_in, post.quotes, .{ .show_posts = false, .show_quotes = true, .show_comments = false, }); } pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Base.Index }) !void { try write_posts(self.res, self.txn, self.logged_in, PostList{ .base = .{ .idx = args.list_id } }, .{ .show_posts = true, .show_quotes = true, .show_comments = true, }); } 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, "/create_list", .{ "type=\"text\" name=\"name\"", "type=\"submit\" value=\"Add\"", }); try self.res.write("

", .{}); var it = post_lists_view.iterator(); while (it.next()) |kv| { const name = kv.val.name; const post_list = kv.val.list; try self.res.write( \\{s} , .{ post_list.base.idx.?, name.constSlice() }); try html_form(self.res, "/delete_list", .{ .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} }, "type=\"submit\" value=\"Delete\"", }); try self.res.write("
", .{}); } } else { try self.res.write("not logged in", .{}); } } pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Base.Index }) !void { try write_timeline(self.res, self.txn, self.logged_in, UserList{ .base = .{ .idx = args.feed_id } }); } pub fn @"/feeds"(self: Self) !void { if (self.logged_in) |login| { const feeds_view = try login.user.feeds.open(self.txn); try html_form(self.res, "/create_feed", .{ "type=\"text\" name=\"name\"", "type=\"submit\" value=\"Add\"", }); try self.res.write("

", .{}); var it = feeds_view.iterator(); while (it.next()) |kv| { const name = kv.val.name; const user_list = kv.val.list; try self.res.write( \\{s} , .{ user_list.base.idx.?, name.constSlice() }); try html_form(self.res, "/delete_feed", .{ .{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} }, "type=\"submit\" value=\"Delete\"", }); try self.res.write("
", .{}); } } else { try self.res.write("not logged in", .{}); } } pub fn @"/post"(self: Self) !void { if (self.logged_in) |login| { _ = login; const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target; try html_form(self.res, "/post", .{ .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} }, "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus", "type=\"submit\" value=\"Post\"", }); } else { try self.res.write("not logged in", .{}); } } pub fn @"/edit"(self: Self) !void { if (self.logged_in) |login| { try self.res.write("
Username: ", .{}); try html_form(self.res, "/set_username", .{ .{ "type=\"text\" name=\"username\" placeholder=\"{s}\"", .{login.user.name.constSlice()} }, "type=\"submit\" value=\"Change\"", }); try self.res.write("
Display Name: ", .{}); try html_form(self.res, "/set_display_name", .{ .{ "type=\"text\" name=\"display_name\" placeholder=\"{s}\"", .{login.user.display_name.constSlice()} }, "type=\"submit\" value=\"Change\"", }); try self.res.write("
Description: ", .{}); try html_form(self.res, "/set_description", .{ .{ "textarea", "type=\"text\" name=\"description\" placeholder=\"{s}\"", .{login.user.description.constSlice()} }, "type=\"submit\" value=\"Change\"", }); try self.res.write("
Password: ", .{}); try html_form(self.res, "/set_password", .{ "type=\"text\" name=\"password\"", "type=\"submit\" value=\"Change\"", }); } else { try self.res.write("not logged in", .{}); } } pub fn @"/"(self: Self) !void { if (self.logged_in) |login| { try write_timeline(self.res, self.txn, self.logged_in, login.user.following); } else { // TODO: generic home try self.res.write("Homepage", .{}); } } }; // }}} // POST {{{ const POST = struct { const Self = @This(); env: lmdb.Env, req: http.Request, res: *http.Response, logged_in: ?Login, pub fn handle(self: Self) !bool { const ti = @typeInfo(Self); inline for (ti.Struct.decls) |f_decl| { if (std.mem.eql(u8, f_decl.name, self.req.target)) { 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 args_type = fi.Fn.params[fi.Fn.params.len - 1].type.?; const argsi = @typeInfo(args_type); var args: args_type = undefined; inline for (argsi.Struct.fields) |field| { const str = self.req.get_value(field.name) orelse return error.ArgNotFound; const field_ti = @typeInfo(field.type); switch (field_ti) { .Int => { @field(args, field.name) = try std.fmt.parseUnsigned(field.type, str, 16); }, .Enum => { @field(args, field.name) = try parse_enum(field.type, str, 16); }, else => { @field(args, field.name) = str; }, } } try @call(.auto, f, .{ self, args }); } return true; } } return false; } pub fn @"/register"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void { // TODO: handle args not supplied std.debug.print("New user: {s} {s}\n", .{ args.username, args.password }); _ = try Chirp.register_user(self.env, args.username, args.password); } pub fn @"/login"(self: Self, args: struct { username: []const u8, password: []const u8 }) !void { // TODO: handle args not supplied std.debug.print("New login: {s} {s}\n", .{ args.username, args.password }); if (Chirp.login_user(self.env, args.username, args.password)) |session_token| { self.res.status = .see_other; try self.res.add_header( "Set-Cookie", .{ "session_token={x}; HttpOnly", .{session_token} }, ); } else |err| { std.debug.print("login_user err: {}\n", .{err}); } } pub fn @"/logout"(self: Self) !void { if (self.logged_in) |login| { try Chirp.logout_user(self.env, login.session_token); try self.res.add_header( "Set-Cookie", .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, ); } } pub fn @"/set_username"(self: Self, args: struct { username: []const u8 }) !void { const login = self.logged_in orelse return error.NotLoggedIn; const username = try Username.fromSlice(args.username); const txn = try self.env.txn(); defer txn.commit() catch {}; const user_ids = try Db.user_ids(txn); if (!try user_ids.has(username)) { try user_ids.del(login.user.name); try user_ids.put(username, login.user.id); const users = try Db.users(txn); var user = login.user; user.name = username; try users.put(login.user.id, user); } } pub fn @"/set_display_name"(self: Self, args: struct { display_name: []const u8 }) !void { const login = self.logged_in orelse return error.NotLoggedIn; const display_name = try DisplayName.fromSlice(args.display_name); const txn = try self.env.txn(); defer txn.commit() catch {}; const users = try Db.users(txn); var user = login.user; user.display_name = display_name; try users.put(login.user.id, user); } pub fn @"/set_description"(self: Self, args: struct { description: []const u8 }) !void { const login = self.logged_in orelse return error.NotLoggedIn; const description = try reencode(UserDescription, args.description); const txn = try self.env.txn(); defer txn.commit() catch {}; const users = try Db.users(txn); var user = login.user; user.description = description; try users.put(login.user.id, user); } pub fn @"/set_password"(self: Self, args: struct { password: []const u8 }) !void { const login = self.logged_in orelse return error.NotLoggedIn; const txn = try self.env.txn(); defer txn.commit() catch {}; const users = try Db.users(txn); var user = login.user; user.password_hash = try Chirp.hash_password(args.password); try users.put(login.user.id, user); } pub fn @"/post"(self: Self) !void { if (self.logged_in) |login| { const text = self.req.get_value("text").?; const has_referer = self.req.get_value("referer"); try Chirp.post(self.env, login.user.id, text); if (has_referer) |r| { const decoded = try decode(r); try self.res.redirect(decoded.constSlice()); } } } pub fn @"/comment"(self: Self) !void { if (self.logged_in) |login| { const text = self.req.get_value("text") orelse return error.NoText; const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; const post_id = try parse_enum(PostId, post_id_str, 16); try Chirp.comment(self.env, login.user.id, post_id, text); } } pub fn @"/quote"(self: Self) !void { if (self.logged_in) |login| { const text = self.req.get_value("text") orelse return error.NoText; const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; const has_referer = self.req.get_value("referer"); const post_id = try parse_enum(PostId, post_id_str, 16); try Chirp.quote(self.env, login.user.id, post_id, text); if (has_referer) |r| { const decoded = try decode(r); try self.res.redirect(decoded.constSlice()); } } } // TODO: add arguments instead of parsing manually pub fn @"/create_list"(self: Self) !void { if (self.logged_in) |login| { const name_str = self.req.get_value("name") orelse return error.NoName; const name = try Name.fromSlice(name_str); // TODO: decode name var txn = try self.env.txn(); const postlist = try PostList.init(txn); try txn.commit(); txn = try self.env.txn(); var post_lists_view = try login.user.post_lists.open(txn); _ = try post_lists_view.append(.{ .name = name, .list = postlist }); try txn.commit(); } } pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Base.Index }) !void { if (self.logged_in) |login| { var post_list: ?PostList = null; { const txn = try self.env.txn(); defer txn.commit() catch {}; var post_lists_view = try login.user.post_lists.open(txn); post_list = (try post_lists_view.get(args.list_id)).list; try post_lists_view.del(args.list_id); } if (post_list != null) { const txn = try self.env.txn(); defer txn.commit() catch {}; var list_view = try post_list.?.open(txn); try list_view.clear(); } } } pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.Base.Index, post_id: PostId }) !void { if (self.logged_in) |login| { _ = login; const txn = try self.env.txn(); defer txn.commit() catch {}; const post_list = PostList{ .base = .{ .idx = args.list_id } }; var post_list_view = try post_list.open(txn); if (try post_list_view.has(args.post_id)) { try post_list_view.del(args.post_id); } else { try post_list_view.append(args.post_id); } } } pub fn @"/create_feed"(self: Self) !void { if (self.logged_in) |login| { const name_str = self.req.get_value("name") orelse return error.NoName; const name = try Name.fromSlice(name_str); var txn = try self.env.txn(); const userlist = try UserList.init(txn); try txn.commit(); txn = try self.env.txn(); var feeds_view = try login.user.feeds.open(txn); _ = try feeds_view.append(.{ .name = name, .list = userlist }); try txn.commit(); } } pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Base.Index }) !void { if (self.logged_in) |login| { var user_list: ?UserList = null; { const txn = try self.env.txn(); defer txn.commit() catch {}; var feeds_view = try login.user.feeds.open(txn); user_list = (try feeds_view.get(args.list_id)).list; try feeds_view.del(args.list_id); } if (user_list != null) { const txn = try self.env.txn(); defer txn.commit() catch {}; var list_view = try user_list.?.open(txn); try list_view.clear(); } } } pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.Base.Index, user_id: UserId }) !void { if (self.logged_in) |login| { _ = login; const txn = try self.env.txn(); defer txn.commit() catch {}; const user_list = UserList{ .base = .{ .idx = args.feed_id } }; var user_list_view = try user_list.open(txn); if (try user_list_view.has(args.user_id)) { try user_list_view.del(args.user_id); } else { try user_list_view.append(args.user_id); } } } pub fn @"/upvote"(self: Self) !void { const login = self.logged_in orelse return error.NotLoggedIn; const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16)); try Chirp.vote(self.env, post_id, login.user.id, .Up); } pub fn @"/downvote"(self: Self) !void { const login = self.logged_in orelse return error.NotLoggedIn; const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16)); try Chirp.vote(self.env, post_id, login.user.id, .Down); } pub fn @"/follow"(self: Self) !void { const login = self.logged_in orelse return error.NotLoggedIn; const user_id_str = self.req.get_value("user_id") orelse return error.NoUserId; const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16)); try Chirp.follow(self.env, login.user.id, user_id); } pub fn @"/quit"(self: Self) !void { if (self.req.get_header("Referer")) |ref| { try self.res.redirect(ref); } else { try self.res.redirect("/"); } try self.res.send(); // break :accept; } }; // }}} fn list_users(env: lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const users = try Db.users(txn); var it = try users.iterator(); while (it.next()) |kv| { const key = kv.key; const user = kv.val; std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() }); } } fn list_user_ids(env: lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const user_ids = try Db.user_ids(txn); var it = try user_ids.iterator(); while (it.next()) |kv| { const key = kv.key; const user_id = kv.val; std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id }); } } fn list_sessions(env: lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const sessions = try Db.sessions(txn); var it = try sessions.iterator(); while (it.next()) |kv| { const key = kv.key; const user_id = kv.val; std.debug.print("[{x}] {}\n", .{ key, user_id }); } } fn list_posts(env: lmdb.Env) !void { const txn = try env.txn(); defer txn.abort(); const posts = try Db.posts(txn); var it = try posts.iterator(); while (it.next()) |kv| { const key = kv.key; const post = kv.val; std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() }); } } const ReqBufferSize = 4096; const ResHeadBufferSize = 1024 * 64; const ResBodyBufferSize = 1024 * 64; // TODO: static? var req_buffer: [ReqBufferSize]u8 = undefined; var res_head_buffer: [ResHeadBufferSize]u8 = undefined; var res_body_buffer: [ResBodyBufferSize]u8 = undefined; pub fn main() !void { // server var server = try http.Server.init("::", 8080); defer server.deinit(); // lmdb var env = try lmdb.Env.open("db", 1024 * 1024 * 10); defer env.close(); std.debug.print("Users:\n", .{}); try list_users(env); std.debug.print("User IDs:\n", .{}); try list_user_ids(env); std.debug.print("Sessions:\n", .{}); try list_sessions(env); std.debug.print("Posts:\n", .{}); try list_posts(env); while (true) { server.wait(); while (true) { const req = (server.next_request(&req_buffer) catch break) orelse break; // handle_request(env, req) catch { // try handle_error(env, req); // }; try handle_request(env, req); } } // const ThreadCount = 1; // var ts: [ThreadCount]std.Thread = undefined; // for (0..ThreadCount) |i| { // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env }); // } // for (0..ThreadCount) |i| { // ts[i].join(); // } std.debug.print("done\n", .{}); } fn handle_error(env: lmdb.Env, req: http.Request) !void { _ = env; var res = http.Response.init(req, &res_head_buffer, &res_body_buffer); try write_start(&res); try res.write("Oops, something went terribly wrong there D:", .{}); try write_end(&res); try res.send(); } fn handle_request(env: lmdb.Env, req: http.Request) !void { // std.debug.print("[{}]: {s}\n", .{ req.method, req.target }); // std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? }); // reponse var res = http.Response.init(req, &res_head_buffer, &res_body_buffer); // check session token const logged_in: ?Login = try check_login(env, req, &res); // html if (req.method == .GET) { try write_start(&res); try write_header(&res, logged_in); const txn = try env.txn(); defer txn.abort(); const get = GET{ .txn = txn, .req = req, .res = &res, .logged_in = logged_in, }; if (try get.handle()) {} else { try res.redirect("/"); } try write_end(&res); try res.send(); } // api else { const post = POST{ .env = env, .req = req, .res = &res, .logged_in = logged_in, }; if (try post.handle()) {} else { try res.write("

[POST] {s}

", .{req.target}); } if (!res.has_header("Location")) { if (req.get_header("Referer")) |ref| { try res.redirect(ref); } else { try res.redirect("/"); } } try res.send(); } }