X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/a68112f4ed195cb5dbe21e8401f04e2f6cc1a7e8..e8c4ebe56c1382517e02f9a770397799b0e0b066:/src/main.zig diff --git a/src/main.zig b/src/main.zig index b54841d..2fa757c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -31,6 +31,7 @@ const User = struct { display_name: DisplayName, password_hash: PasswordHash, posts: PostList, + replies: PostList, following: UserList, followers: UserList, @@ -222,6 +223,7 @@ const Chirp = struct { .display_name = display_name, .password_hash = try hash_password(password), .posts = try PostList.init(txn), + .replies = try PostList.init(txn), .following = try UserList.init(txn), .followers = try UserList.init(txn), .post_lists = try PostListList.init(txn), @@ -269,7 +271,7 @@ const Chirp = struct { 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 { + 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 @@ -315,39 +317,53 @@ const Chirp = struct { 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 { - const txn = try env.txn(); - + var 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); + const post_id = try append_post(env, user_id, user.posts, null, null, text); + + txn = try env.txn(); + var replies_view = try user.replies.open(txn); + try replies_view.append(post_id); + try txn.commit(); } fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void { - const txn = try env.txn(); + 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(); - try append_post(env, user_id, parent_post.comments, parent_post_id, null, text); + 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.replies.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 { - const txn = try env.txn(); - + var 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); + const post_id = try append_post(env, user_id, user.posts, null, quote_post_id, text); + + txn = try env.txn(); + var replies_view = try user.replies.open(txn); + try replies_view.append(post_id); + try txn.commit(); } fn vote(env: lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void { @@ -434,10 +450,8 @@ const Chirp = struct { // }}} // html {{{ -fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void { - try res.write("
", .{}); +fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void { + try res.write("", .{action}); inline for (inputs) |input| { switch (@typeInfo(@TypeOf(input))) { @@ -496,7 +510,7 @@ fn write_header(res: *http.Response, logged_in: ?Login) !void { try res.write( \\Post
, .{}); - try html_form(res, "/logout", .{}, .{ + try html_form(res, "/logout", .{ \\type="submit" value="Logout" }); try res.write("

", .{}); @@ -507,17 +521,13 @@ fn write_header(res: *http.Response, logged_in: ?Login) !void { \\ \\ \\ - \\
+ \\
\\
\\ \\ \\ - \\
+ \\

, .{}); - try html_form(res, "/quit", .{}, .{ - \\type="submit" value="Quit" - }); - try res.write("

", .{}); } } fn write_start(res: *http.Response) !void { @@ -549,16 +559,22 @@ fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: Po try res.write( \\
- \\{s} {s}
+ \\{s} + , .{ user.name.constSlice(), user.display_name.constSlice() }); + if (post.parent_id) |id| { + try res.write(" ..", .{@intFromEnum(id)}); + } + try res.write( + \\ {s}
\\{s}
- , .{ user.name.constSlice(), user.display_name.constSlice(), time_str(post.time).constSlice(), post.text.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("...", .{@intFromEnum(post_id)}); } try res.write("
", .{}); } @@ -571,23 +587,23 @@ fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: Po 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", .{}, .{ + 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", .{}, .{ + 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", .{}, .{ + 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", .{}, .{ + try html_form(res, "/downvote", .{ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} }, .{ "type=\"submit\" value=\"⇩ {}\"", .{post.downvotes} }, }); @@ -609,6 +625,7 @@ fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: Po try res.write("
", .{}); try res.write("", .{}); + var it = feeds_view.iterator(); + while (it.next()) |kv| { + const name = kv.val.name; + const id = kv.val.list.idx.?; + try res.write("", .{ id, name.constSlice() }); + } + 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("Replies
", .{ + user.name.constSlice(), + }); + + if (logged_in != null and user.id == logged_in.?.user.id) { + try res.write( + \\Lists + \\Feeds + \\Edit
+ \\
+ , .{}); + } + + 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); @@ -712,6 +793,39 @@ fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_li try res.write("
", .{}); } } +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 {{{ @@ -719,7 +833,7 @@ const GET = struct { const Self = @This(); txn: lmdb.Txn, - req: *http.Request, + req: http.Request, res: *http.Response, logged_in: ?Login, @@ -771,48 +885,24 @@ const GET = struct { 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 write_profile(self.res, self.txn, self.logged_in, user); + try write_posts(self.res, self.txn, self.logged_in, user.posts); + } else |err| { 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
- \\
- , .{}); - } + \\

User not found [{}]

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

User not found [{}]

@@ -884,7 +974,7 @@ const GET = struct { 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", .{}, .{ + 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\"", @@ -900,14 +990,14 @@ const GET = struct { try self.res.write("
", .{}); } } - pub fn @"/list/"(self: Self, args: struct { list_id: PostListList.Index }) !void { + 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", .{}, .{ + try html_form(self.res, "/create_list", .{ "type=\"text\" name=\"name\"", "type=\"submit\" value=\"Add\"", }); @@ -919,8 +1009,44 @@ const GET = struct { const name = kv.val.name; const post_list = kv.val.list; try self.res.write( - \\{s}
+ \\{s} , .{ post_list.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.Index }) !void { + try write_timeline(self.res, self.txn, self.logged_in, UserList{ .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.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", .{}); @@ -931,7 +1057,7 @@ const GET = struct { _ = login; const referer = if (self.req.get_header("Referer")) |ref| ref else self.req.target; - try html_form(self.res, "/post", .{}, .{ + try html_form(self.res, "/post", .{ .{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} }, "type=\"text\" name=\"text\"", "type=\"submit\" value=\"Post\"", @@ -943,17 +1069,17 @@ const GET = struct { pub fn @"/edit"(self: Self) !void { if (self.logged_in) |login| { try self.res.write("
Username: ", .{}); - try html_form(self.res, "/set_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", .{}, .{ + 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", .{}, .{ + try html_form(self.res, "/set_password", .{ "type=\"text\" name=\"password\"", "type=\"submit\" value=\"Change\"", }); @@ -977,7 +1103,7 @@ const POST = struct { const Self = @This(); env: lmdb.Env, - req: *http.Request, + req: http.Request, res: *http.Response, logged_in: ?Login, @@ -1124,13 +1250,14 @@ const POST = struct { } } } - pub fn @"/new_list"(self: Self) !void { + // 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(); @@ -1140,24 +1267,87 @@ const POST = struct { try txn.commit(); } } - pub fn @"/list_add"(self: Self) !void { + pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.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.Index, post_id: PostId }) !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 }; + const post_list = PostList{ .idx = args.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); + 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.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.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{ .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 post_list_view.append(post_id); + try user_list_view.append(args.user_id); } } } @@ -1256,6 +1446,11 @@ 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); @@ -1274,7 +1469,15 @@ pub fn main() !void { std.debug.print("Posts:\n", .{}); try list_posts(env); - try handle_connection(&server, 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); + }; + } + } // const ThreadCount = 1; // var ts: [ThreadCount]std.Thread = undefined; @@ -1288,99 +1491,64 @@ pub fn main() !void { 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; - } - } +fn handle_error(env: lmdb.Env, req: http.Request) !void { + _ = env; + var res = http.Response.init(req.fd, &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.? }); - if (remove_session_token) { - try res.add_header( - "Set-Cookie", - .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, - ); - } - } + // reponse + var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer); - // TODO: refactor into functions - // TODO: make sure we send a reply + // 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); + // html + if (req.method == .GET) { + try write_start(&res); + try write_header(&res, logged_in); - const txn = try env.txn(); - defer txn.abort(); + 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("/"); - } + 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}); - } + 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(); + if (!res.has_header("Location")) { + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } else { + try res.redirect("/"); } } + try res.send(); } }