+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);