+const Db = struct {
+ pub fn init(env: lmdb.Env) !void {
+ const txn = try env.txn();
+ const dbi = try txn.dbi(null);
+ if (try dbi.has(1001)) {
+ users = try dbi.get(1001, @TypeOf(users));
+ } else {
+ users = try @TypeOf(users).init(txn);
+ }
+ if (try dbi.has(1002)) {
+ user_ids = try dbi.get(1002, @TypeOf(user_ids));
+ } else {
+ user_ids = try @TypeOf(user_ids).init(txn);
+ }
+ if (try dbi.has(1003)) {
+ sessions = try dbi.get(1003, @TypeOf(sessions));
+ } else {
+ sessions = try @TypeOf(sessions).init(txn);
+ }
+ if (try dbi.has(1004)) {
+ posts = try dbi.get(1004, @TypeOf(posts));
+ } else {
+ posts = try @TypeOf(posts).init(txn);
+ }
+ }
+ var users: UserList = undefined;
+ var user_ids: UsernameList = undefined;
+ var sessions: SessionList = undefined;
+ var posts: PostList = undefined;
+};
+
+// }}}
+
+// content {{{
+
+const User = struct {
+ // TODO: choose sizes
+ id: UserId,
+ name: Username,
+ display_name: DisplayName,
+ description: UserDescription,
+ password_hash: PasswordHash,
+
+ posts: PostSet,
+
+ following: UserSet,
+ followers: UserSet,
+
+ 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: PostSet,
+ quotes: PostSet,
+
+ text: PostText,
+};
+
+const SavedPostList = struct {
+ name: Name,
+ list: PostSet,
+};
+const SavedUserList = struct {
+ name: Name,
+ list: UserSet,
+};
+
+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 PostSet = db.Set(PostId);
+const UserSet = db.Set(UserId);
+const PostList = db.SetList(PostId, Post);
+const UserList = db.SetList(UserId, User);
+const UsernameList = db.SetList(Username, UserId);
+const SessionList = db.SetList(SessionToken, 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(), "<br />", .{});
+ } 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;
+ var HashBuffer = std.mem.zeroes([1024 * 1024 * 50]u8);
+
+ pub fn hash_password(password: []const u8) !PasswordHash {
+ var hash_buffer = try PasswordHash.init(128);
+
+ // TODO: choose buffer size
+ var alloc = std.heap.FixedBufferAllocator.init(&HashBuffer);
+
+ // TODO: choose limits
+ const result = try std.crypto.pwhash.argon2.strHash(password, .{
+ .allocator = alloc.allocator(),
+ .params = std.crypto.pwhash.argon2.Params.owasp_2id,
+ }, hash_buffer.slice());
+
+ try hash_buffer.resize(result.len);
+
+ return hash_buffer;
+ }
+
+ pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
+ var alloc = std.heap.FixedBufferAllocator.init(&HashBuffer);
+
+ 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});
+ };
+
+ var users = try Db.users.open(txn);
+ var user_ids = try Db.user_ids.open(txn);
+
+ if (try user_ids.has(username_array)) {
+ return false;
+ } else {
+ const user_id = try db.Prng.gen(users.base.dbi, UserId);
+
+ try users.append(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 PostSet.init(txn),
+ .following = try UserSet.init(txn),
+ .followers = try UserSet.init(txn),
+ .post_lists = try PostListList.init(txn),
+ .feeds = try UserListList.init(txn),
+ });
+
+ try user_ids.append(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.open(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.open(txn);
+ const user = try users.get(user_id);
+
+ if (verify_password(password, user.password_hash)) {
+ var sessions = try Db.sessions.open(txn);
+ const session_token = try db.Prng.gen(sessions.base.dbi, SessionToken);
+ try sessions.append(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 {};
+
+ var sessions = try Db.sessions.open(txn);
+ try sessions.del(session_token);
+ }
+
+ fn append_post(env: lmdb.Env, user_id: UserId, post_list: PostSet, 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.open(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 PostSet.init(txn),
+ .quotes = try PostSet.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.open(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.open(txn);
+ const user = try users.get(user_id);
+ txn.abort();