X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/8ce4f0b76cab1963cd0a8ad55bf5b30b9eae917f..637a79f940f620181f94fff37bd7bfce2a564a1b:/src/main.zig?ds=inline diff --git a/src/main.zig b/src/main.zig index b016e1b..f5e66ea 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,65 +1,191 @@ const std = @import("std"); const lmdb = @import("lmdb"); +const http = @import("http"); // db {{{ 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; } + + pub fn gen_str(dbi: anytype, comptime len: usize) [len]u8 { + var buf: [len / 2]u8 = undefined; + var res: [len]u8 = undefined; + Prng.prng.fill(&buf); + for (0..len / 2) |i| { + res[i * 2 + 0] = 'a' + (buf[i] % 16); + res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16); + } + + while (dbi.has(res)) { + Prng.prng.fill(&buf); + for (0..len / 2) |i| { + res[i * 2 + 0] = 'a' + (buf[i] % 16); + res[i * 2 + 1] = 'a' + (buf[i] >> 4 % 16); + } + } + + return res; + } }; -// }}} +const Db = struct { + 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); + } -// http stuff {{{ + const ListId = enum(u64) { _ }; -pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void { - try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} }); -} + pub fn ListNode(comptime T: type) type { + return struct { + next: ?ListId, + prev: ?ListId, + data: T, + }; + } -pub fn get_body(req: *std.http.Server.Request) []const u8 { - return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)]; -} + pub fn List(comptime T: type) type { + return struct { + const Self = @This(); -pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 { - const body = get_body(req); - if (std.mem.indexOf(u8, body, name)) |name_index| { - if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| { - if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| { - return body[eql_index + 1 .. amp_index]; - } + first: ListId = undefined, + last: ListId = undefined, + len: usize = 0, - return body[eql_index + 1 .. body.len]; - } + 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; + } + } + }; } - return null; -} -pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue { - var header_it = req.iterateHeaders(); - while (header_it.next()) |header| { - if (std.mem.eql(u8, header.name, "Cookie")) { - if (std.mem.indexOf(u8, header.value, name)) |name_index| { - if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| { - if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| { - return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null; + 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 }; - return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null; + if (set.has(key)) { + set.del(key); + if (!set.has(key)) { + self.len -= 1; + } } } - } + }; } - return null; -} +}; // }}} @@ -67,15 +193,44 @@ pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue 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 = u64; +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); @@ -110,7 +265,7 @@ pub fn verify_password(password: []const u8, hash: PasswordHash) bool { } } -pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !void { +pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool { const username_array = try Username.fromSlice(username); const txn = try env.txn(); @@ -119,15 +274,23 @@ pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) env.sync(); } - const users = try txn.dbi("users", Id, User); - const user_id = Prng.gen_id(users); - users.put(user_id, User{ - .username = username_array, - .password_hash = try hash_password(password), - }); + const users = try Db.users(&txn); + const user_ids = try Db.user_ids(&txn); + + if (user_ids.has(username_array)) { + return false; + } 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), + }); + + user_ids.put(username_array, user_id); - const user_ids = try txn.dbi("user_ids", Username, Id); - user_ids.put(username_array, user_id); + return true; + } } pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !SessionToken { @@ -139,15 +302,15 @@ pub fn login_user(env: *lmdb.Env, username: []const u8, password: []const u8) !S env.sync(); } - const user_ids = try txn.dbi("user_ids", Username, Id); + const user_ids = try Db.user_ids(&txn); const user_id = user_ids.get(username_array) orelse return error.UnknownUsername; std.debug.print("id: {}\n", .{user_id}); - const users = try txn.dbi("users", Id, User); + const users = try Db.users(&txn); if (users.get(user_id)) |user| { if (verify_password(password, user.password_hash)) { - const sessions = try txn.dbi("sessions", Id, Id); - const session_token = Prng.gen_id(sessions); + const sessions = try Db.sessions(&txn); + const session_token = Prng.gen_str(sessions, SessionTokenLen); sessions.put(session_token, user_id); return session_token; } else { @@ -165,47 +328,210 @@ fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void { env.sync(); } - const sessions = try txn.dbi("sessions", Id, Id); + const sessions = try Db.sessions(&txn); 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 txn.dbi("sessions", Id, Id); - const users = try txn.dbi("users", Id, User); + const sessions = try Db.sessions(&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| { - var response_buffer = try std.BoundedArray(u8, 1024).init(0); - try std.fmt.format(response_buffer.writer(), - \\Home - \\
- \\ - , .{login.user.username.constSlice()}); - try req.respond(response_buffer.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 req.respond( - \\Register - \\Login - \\ - , .{}); + try res.write("[GET] {s}", .{req.target}); + try res.send(); } } } // api else { - if (std.mem.eql(u8, req.head.target, "/register")) { + if (std.mem.eql(u8, req.target, "/register")) { // TODO: handle args not supplied - const username = get_value(&req, "username").?; - const password = get_value(&req, "password").?; + const username = req.get_value("username").?; + const password = req.get_value("password").?; std.debug.print("New user: {s} {s}\n", .{ username, password }); - try register_user(&env, username, password); - - try redirect(&req, "/login"); - } else if (std.mem.eql(u8, req.head.target, "/login")) { + if (try register_user(env, username, password)) { + try res.redirect("/login"); + } else { + try res.redirect("/register"); + } + try res.send(); + } else if (std.mem.eql(u8, req.target, "/login")) { // TODO: handle args not supplied - const username = get_value(&req, "username").?; - const password = get_value(&req, "password").?; + const username = req.get_value("username").?; + const password = req.get_value("password").?; std.debug.print("New login: {s} {s}\n", .{ username, password }); - if (login_user(&env, username, password)) |session_token| { - var redirect_buffer = try std.BoundedArray(u8, 128).init(0); - try std.fmt.format(redirect_buffer.writer(), "/user/{s}", .{username}); - - var cookie_buffer = try std.BoundedArray(u8, 128).init(0); - try std.fmt.format(cookie_buffer.writer(), "session_token={}; Secure; HttpOnly", .{session_token}); - - try req.respond("", .{ - .status = .see_other, - .extra_headers = &.{ - .{ .name = "Location", .value = redirect_buffer.constSlice() }, - .{ .name = "Set-Cookie", .value = cookie_buffer.constSlice() }, - }, - }); + if (login_user(env, username, password)) |session_token| { + res.status = .see_other; + try res.add_header( + "Location", + .{ "/user/{s}", .{username} }, + ); + try res.add_header( + "Set-Cookie", + .{ "session_token={s}; Secure; HttpOnly", .{session_token} }, + ); + + try res.send(); } else |err| { std.debug.print("login_user err: {}\n", .{err}); - try redirect(&req, "/login"); + try res.redirect("/login"); + try res.send(); } - } else if (std.mem.eql(u8, req.head.target, "/logout")) { + } else if (std.mem.eql(u8, req.target, "/logout")) { if (logged_in) |login| { - try logout_user(&env, login.session_token); - try req.respond("", .{ - .status = .see_other, - .extra_headers = &.{ - .{ .name = "Location", .value = "/" }, - .{ .name = "Set-Cookie", .value = "session_token=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT" }, - }, - }); + try logout_user(env, login.session_token); + + try res.add_header( + "Set-Cookie", + .{"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.head.target, "/quit")) { - try redirect(&req, "/"); + } 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(); + } + } else if (std.mem.eql(u8, req.target, "/quit")) { + 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 req.respond( + // \\POST
+ // , .{}); + try res.write("[POST] {s}
", .{req.target}); + try res.send(); } } }