+const Db = struct {
+ fn users(txn: lmdb.Txn) !UserList {
+ return try db.Db(UserId, User).init(txn, "users");
+ }
+ fn user_ids(txn: lmdb.Txn) !UsernameList {
+ return try db.Db(Username, UserId).init(txn, "user_ids");
+ }
+ fn sessions(txn: lmdb.Txn) !SessionList {
+ return try db.Db(SessionToken, UserId).init(txn, "sessions");
+ }
+ fn posts(txn: lmdb.Txn) !PostList {
+ return try db.Db(PostId, Post).init(txn, "posts");
+ }
+ 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,
+ 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 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});
+ };
+
+ 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,
+ .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.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});