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, 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 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(text: []const u8) !PostText { var result = try PostText.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 == '%') { // special case of &#... // assume only &#, no &#x if (idx + 6 < text.len and std.mem.eql(u8, text[idx .. idx + 6], "%26%23")) { const num_start = idx + 6; var num_end = num_start; while (num_end < text.len and std.ascii.isDigit(text[num_end])) { num_end += 1; } if (num_end + 2 < text.len and text[num_end] == '%' and text[num_end + 1] == '3' and std.ascii.toLower(text[num_end + 2]) == 'b') { try std.fmt.format(result.writer(), "&#{s};", .{text[num_start..num_end]}); idx = num_end + 2; continue; } } try std.fmt.format(result.writer(), "&#x{s};", .{text[idx + 1 .. idx + 3]}); 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 { 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, .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) !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("
", .{}); inline for (inputs) |input| { switch (@typeInfo(@TypeOf(input))) { .Struct => { 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
\\
\\ \\ \\ \\
\\
\\ \\ \\ \\
, .{}); try html_form(res, "/quit", .{}, .{ \\type="submit" value="Quit" }); try res.write("

", .{}); } } 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} {s}
\\{s}
, .{ user.name.constSlice(), user.display_name.constSlice(), time_str(post.time).constSlice(), post.text.constSlice() }); 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(quote_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, .{ .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_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("
", .{}); } } 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("
", .{}); } } // }}} // 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( \\

{s}

, .{ user.name.constSlice(), user.display_name.constSlice(), }); if (self.logged_in != null and user_id != self.logged_in.?.user.id) { if (try followers.has(self.logged_in.?.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" }); } } try self.res.write( \\ {} following \\ {} followers \\
, .{ 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( \\Lists \\Feeds \\Edit
\\
, .{}); } try self.res.write("
", .{}); try write_posts(self.res, self.txn, self.logged_in, user.posts); } 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 = try user.following.open(self.txn); var it = following.iterator(); try self.res.write( \\

{s} follows:

, .{ user.name.constSlice(), user.display_name.constSlice() }); while (it.next()) |following_id| { const following_user = try users.get(following_id); try self.res.write( \\{s}
, .{ following_user.name.constSlice(), following_user.display_name.constSlice() }); } } 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 = try user.followers.open(self.txn); var it = followers.iterator(); try self.res.write( \\

{s} followers:

, .{ user.name.constSlice(), user.display_name.constSlice() }); while (it.next()) |follower_id| { const follower_user = try users.get(follower_id); try self.res.write( \\{s}
, .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() }); } } 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 @"/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; 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("
", .{}); 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("
", .{}); } } pub fn @"/list/"(self: Self, args: struct { list_id: PostListList.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\"", }); 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.idx.?, name.constSlice() }); } } 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\"", "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("
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_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()); } } } pub fn @"/new_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); 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 @"/list_add"(self: Self) !void { if (self.logged_in) |login| { _ = login; const list_id_str = self.req.get_value("list_id") orelse return error.NoListId; const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; const list_id = try std.fmt.parseUnsigned(PostList.Index, list_id_str, 16); const post_id = try parse_enum(PostId, post_id_str, 16); const txn = try self.env.txn(); defer txn.commit() catch {}; const post_list = PostList{ .idx = list_id }; var post_list_view = try post_list.open(txn); if (try post_list_view.has(post_id)) { try post_list_view.del(post_id); } else { try post_list_view.append(post_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; 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); try handle_connection(&server, env); // 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_connection(server: *http.Server, env: lmdb.Env) !void { // TODO: static? var req_buffer: [ReqBufferSize]u8 = undefined; var res_head_buffer: [ResHeadBufferSize]u8 = undefined; var res_body_buffer: [ResBodyBufferSize]u8 = undefined; while (true) { server.wait(); while (try server.next_request(&req_buffer)) |*_req| { var req: *http.Request = @constCast(_req); // 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.fd, &res_head_buffer, &res_body_buffer); // check session token var logged_in: ?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); logged_in = .{ .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"}, ); } } // TODO: refactor into functions // TODO: make sure we send a reply // 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(); } } } }