X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/aaea9506e1fd3857501ce02326f223c5cdb8b867..5ef9c3c08b5509fb3c9d0d13dfeb32e9067f901a:/src/main.zig diff --git a/src/main.zig b/src/main.zig index 8682008..f5e66ea 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,11 +7,16 @@ const http = @import("http"); const Prng = struct { var prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0); - pub fn gen_id(dbi: anytype) Id { - var id = Prng.prng.next(); + fn IdType(comptime T: type) type { + const ti = @typeInfo(@TypeOf(T.get)); + return ti.Fn.params[1].type.?; + } + + pub fn gen_id(dbi: anytype) IdType(@TypeOf(dbi)) { + var id: IdType(@TypeOf(dbi)) = @enumFromInt(Prng.prng.next()); while (dbi.has(id)) { - id = Prng.prng.next(); + id = @enumFromInt(Prng.prng.next()); } return id; @@ -39,14 +44,146 @@ const Prng = struct { }; const Db = struct { - fn users(txn: *const lmdb.Txn) !lmdb.Dbi(Id, User) { - return try txn.dbi("users", Id, User); + fn Key(types: anytype) type { + return std.meta.Tuple(&types); + } + fn users(txn: *const lmdb.Txn) !lmdb.Dbi(UserId, User) { + return try txn.dbi("users", UserId, User); + } + fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, UserId) { + return try txn.dbi("user_ids", Username, UserId); + } + fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, UserId) { + return try txn.dbi("sessions", SessionToken, UserId); + } + fn posts(txn: *const lmdb.Txn) !lmdb.Dbi(PostId, Post) { + return try txn.dbi("posts", PostId, Post); + } + fn lists(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(ListId, ListNode(T)) { + return try txn.dbi("lists", ListId, ListNode(T)); + } + fn sets(txn: *const lmdb.Txn, comptime T: type) !lmdb.Dbi(Key(.{ SetId, T }), u0) { + return try txn.dbi("sets", Key(.{ SetId, T }), u0); + } + + const ListId = enum(u64) { _ }; + + pub fn ListNode(comptime T: type) type { + return struct { + next: ?ListId, + prev: ?ListId, + data: T, + }; } - fn user_ids(txn: *const lmdb.Txn) !lmdb.Dbi(Username, Id) { - return try txn.dbi("user_ids", Username, Id); + + pub fn List(comptime T: type) type { + return struct { + const Self = @This(); + + first: ListId = undefined, + last: ListId = undefined, + len: usize = 0, + + pub const Iterator = struct { + dbi: lmdb.Dbi(ListId, ListNode(T)), + id_maybe: ?ListId, + + pub fn next(self: *Iterator) ?T { + const id = self.id_maybe orelse return null; + + // TODO: how to handle this? + const ln = self.dbi.get(id) orelse return null; + self.id_maybe = ln.next; + return ln.data; + } + }; + pub fn it(self: *const Self, txn: *const lmdb.Txn) !Iterator { + const list = try Db.lists(txn, T); + return .{ .dbi = list, .id_maybe = if (self.len > 0) self.first else null }; + } + pub fn append(self: *Self, txn: *const lmdb.Txn, t: T) !void { + const list = try Db.lists(txn, T); + const new_id = Prng.gen_id(list); + + if (self.len == 0) { + self.first = new_id; + self.last = new_id; + self.len = 1; + list.put(new_id, .{ + .next = null, + .prev = null, + .data = t, + }); + } else { + const prev_id = self.last; + var last = list.get(prev_id).?; + last.next = new_id; + list.put(prev_id, last); + + list.put(new_id, .{ + .next = null, + .prev = prev_id, + .data = t, + }); + + self.last = new_id; + self.len += 1; + } + } + }; } - fn sessions(txn: *const lmdb.Txn) !lmdb.Dbi(SessionToken, Id) { - return try txn.dbi("sessions", SessionToken, Id); + + const SetId = enum(u64) { _ }; + + pub fn Set(comptime T: type) type { + return struct { + const Self = @This(); + const SetKey = Key(.{ SetId, T }); + + len: usize = 0, + set_id: ?SetId = null, + + pub fn init(self: *Self) void { + self.set_id = @enumFromInt(Prng.prng.next()); + } + + pub fn has(self: Self, txn: *const lmdb.Txn, t: T) !bool { + if (self.set_id == null) return false; + + const set = try Db.sets(txn, T); + const key = SetKey{ self.set_id.?, t }; + + return set.has(key); + } + + pub fn add(self: *Self, txn: *const lmdb.Txn, t: T) !void { + if (self.set_id == null) self.init(); + + const set = try Db.sets(txn, T); + const key = SetKey{ self.set_id.?, t }; + + if (!set.has(key)) { + set.put(key, 0); + if (set.has(key)) { + self.len += 1; + } + } + } + + pub fn del(self: *Self, txn: *const lmdb.Txn, t: T) !void { + if (self.set_id == null) self.init(); + + const set = try Db.sets(txn, T); + const key = SetKey{ self.set_id.?, t }; + + if (set.has(key)) { + set.del(key); + if (!set.has(key)) { + self.len -= 1; + } + } + } + }; } }; @@ -56,17 +193,44 @@ const Db = struct { const User = struct { // TODO: choose sizes + id: UserId, username: Username, password_hash: PasswordHash, + posts: PostList = PostList{}, +}; + +const Post = struct { + id: PostId, + + user_id: UserId, + time: Timestamp, + + upvotes: UserList = UserList{}, + downvotes: UserList = UserList{}, + comments: PostList = PostList{}, + // reposts + + text: PostText, }; const SessionTokenLen = 16; const Id = u64; +const Login = struct { + user: User, + user_id: UserId, + session_token: SessionToken, +}; +const UserId = enum(u64) { _ }; +const PostId = enum(u64) { _ }; +const Timestamp = i64; const Username = std.BoundedArray(u8, 16); const PasswordHash = std.BoundedArray(u8, 128); const SessionToken = [SessionTokenLen]u8; const CookieValue = std.BoundedArray(u8, 128); +const PostText = std.BoundedArray(u8, 1024); +const PostList = Db.List(PostId); +const UserList = Db.Set(UserId); pub fn hash_password(password: []const u8) !PasswordHash { var hash_buffer = try PasswordHash.init(128); @@ -118,6 +282,7 @@ pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) } else { const user_id = Prng.gen_id(users); users.put(user_id, User{ + .id = user_id, .username = username_array, .password_hash = try hash_password(password), }); @@ -167,31 +332,193 @@ fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void { sessions.del(session_token); } -fn get_session_user(env: *lmdb.Env, session_token: SessionToken) !User { +fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void { + var post_id: PostId = undefined; + + var txn = try env.txn(); + { + const posts = try Db.posts(&txn); + post_id = Prng.gen_id(posts); + posts.put(post_id, Post{ + .id = post_id, + .user_id = user_id, + .time = std.time.timestamp(), + .text = try PostText.fromSlice(text), + }); + + txn.commit(); + env.sync(); + } + + txn = try env.txn(); + { + const users = try Db.users(&txn); + var user = users.get(user_id) orelse return error.UserNotFound; + try user.posts.append(&txn, post_id); + users.put(user_id, user); + + txn.commit(); + env.sync(); + } +} + +fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void { + const txn = try env.txn(); + defer { + txn.commit(); + env.sync(); + } + + const posts = try Db.posts(&txn); + + var p = posts.get(post_id) orelse return error.PostNotFound; + if (dir == .Up) { + try p.upvotes.add(&txn, user_id); + } else { + try p.downvotes.add(&txn, user_id); + } + posts.put(post_id, p); +} + +fn unvote(env: *lmdb.Env, post_id: PostId, user_id: UserId, dir: enum { Up, Down }) !void { + const txn = try env.txn(); + defer { + txn.commit(); + env.sync(); + } + + const posts = try Db.posts(&txn); + + var p = posts.get(post_id) orelse return error.PostNotFound; + if (dir == .Up) { + try p.upvotes.del(&txn, user_id); + } else { + try p.downvotes.del(&txn, user_id); + } + posts.put(post_id, p); +} + +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); - const users = try Db.users(&txn); if (sessions.get(session_token)) |user_id| { - return users.get(user_id) orelse error.UnknownUser; + return user_id; } else { return error.SessionNotFound; } } +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 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("
", .{}); +} +// }}} + +// write {{{ +fn write_header(res: *http.Response, logged_in: ?Login) !void { + if (logged_in) |login| { + try res.write( + \\Home{s}
+ , .{p.text.constSlice()}); + if (login != null and try p.upvotes.has(txn, login.?.user_id)) { + try html_form(res, "/unupvote/{}", .{@intFromEnum(post_id)}, .{ + .{ "type=\"submit\" value=\"⬆ {}\"", .{p.upvotes.len} }, + }); + } else { + try html_form(res, "/upvote/{}", .{@intFromEnum(post_id)}, .{ + .{ "type=\"submit\" value=\"⬆ {}\"", .{p.upvotes.len} }, + }); + } + if (login != null and try p.downvotes.has(txn, login.?.user_id)) { + try html_form(res, "/undownvote/{}", .{@intFromEnum(post_id)}, .{ + .{ "type=\"submit\" value=\"⬇ {}\"", .{p.downvotes.len} }, + }); + } else { + try html_form(res, "/downvote/{}", .{@intFromEnum(post_id)}, .{ + .{ "type=\"submit\" value=\"⬇ {}\"", .{p.downvotes.len} }, + }); + } + try res.write( + \\💭 {} + \\User not found + , .{}); + } + try res.send(); } else { if (logged_in) |login| { - try res.write( - \\Home - \\
- \\ - , .{login.user.username.constSlice()}); + const user = (try get_user(env, login.user_id)).?; + + const txn = try env.txn(); + defer txn.abort(); + + try write_posts(&res, &txn, user, logged_in); + try res.send(); } else { - try res.write( - \\Register - \\Login - \\ - , .{}); + try res.write("[GET] {s}", .{req.target}); try res.send(); } } @@ -393,6 +758,14 @@ fn handle_connection(server: *http.Server, env: *lmdb.Env) !void { .{"session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT"}, ); + try res.redirect("/"); + try res.send(); + } + } else if (std.mem.eql(u8, req.target, "/post")) { + if (logged_in) |login| { + const text = req.get_value("text").?; + try post(env, login.user_id, text); + try res.redirect("/"); try res.send(); } @@ -400,11 +773,61 @@ fn handle_connection(server: *http.Server, env: *lmdb.Env) !void { try res.redirect("/"); try res.send(); break :accept; + } else if (std.mem.startsWith(u8, req.target, "/upvote/")) { + const login = logged_in orelse return error.NotLoggedIn; + + const post_id_str = req.target[8..req.target.len]; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + + try vote(env, post_id, login.user_id, .Up); + try unvote(env, post_id, login.user_id, .Down); + + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } + try res.send(); + } else if (std.mem.startsWith(u8, req.target, "/downvote/")) { + const login = logged_in orelse return error.NotLoggedIn; + + const post_id_str = req.target[10..req.target.len]; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + + try vote(env, post_id, login.user_id, .Down); + try unvote(env, post_id, login.user_id, .Up); + + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } + try res.send(); + } else if (std.mem.startsWith(u8, req.target, "/unupvote/")) { + const login = logged_in orelse return error.NotLoggedIn; + + const post_id_str = req.target[10..req.target.len]; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + + try unvote(env, post_id, login.user_id, .Up); + + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } + try res.send(); + } else if (std.mem.startsWith(u8, req.target, "/undownvote/")) { + const login = logged_in orelse return error.NotLoggedIn; + + const post_id_str = req.target[12..req.target.len]; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + + try unvote(env, post_id, login.user_id, .Down); + + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } + try res.send(); } else { // try req.respond( // \\POST
// , .{}); - try res.write("{s}
", .{req.target}); + try res.write("[POST] {s}
", .{req.target}); try res.send(); } }