X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/d4496703a509fb5ad5352787ae277ebe204195c8..refs/heads/refactor:/src/main.zig
diff --git a/src/main.zig b/src/main.zig
index 0b22dbb..460ea4c 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -6,18 +6,34 @@ const http = @import("http");
// db {{{
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");
+ 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;
};
// }}}
@@ -29,10 +45,13 @@ const User = struct {
id: UserId,
name: Username,
display_name: DisplayName,
+ description: UserDescription,
password_hash: PasswordHash,
- posts: PostList,
- following: UserList,
- followers: UserList,
+
+ posts: PostSet,
+
+ following: UserSet,
+ followers: UserSet,
post_lists: PostListList,
feeds: UserListList,
@@ -49,19 +68,19 @@ const Post = struct {
upvotes: u64 = 0,
downvotes: u64 = 0,
votes: VoteList,
- comments: PostList,
- quotes: PostList,
+ comments: PostSet,
+ quotes: PostSet,
text: PostText,
};
const SavedPostList = struct {
name: Name,
- list: PostList,
+ list: PostSet,
};
const SavedUserList = struct {
name: Name,
- list: UserList,
+ list: UserSet,
};
const Vote = struct {
@@ -82,23 +101,28 @@ 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 PostList = db.Set(PostId);
-const UserList = db.Set(UserId);
+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));
+ 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);
+fn reencode(comptime T: type, text: []const u8) !T {
+ var result = try T.init(0);
const len = @min(text.len, 1024); // TODO: PostText length
@@ -107,28 +131,19 @@ fn reencode(text: []const u8) !PostText {
const c = text[idx];
if (c == '+') {
try result.append(' ');
- } else if (c == '%') {
- // special case of ...
- // assume only , no
- 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;
- }
+ } else if (c == '%' and idx + 2 < text.len) {
+ const allow = &[_]u8{ 0x26, 0x23, 0x3b, 0x0a };
- 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;
- }
+ 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(), "
", .{});
+ } 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};", .{escaped_value});
}
- try std.fmt.format(result.writer(), "{s};", .{text[idx + 1 .. idx + 3]});
idx += 2;
} else {
try result.append(c);
@@ -166,18 +181,20 @@ fn decode(text: []const u8) !std.BoundedArray(u8, 1024) {
}
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
- // TODO: dont allocate on stack, maybe zero memory?
- var buffer: [1024 * 10]u8 = undefined;
- var alloc = std.heap.FixedBufferAllocator.init(&buffer);
+ 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.fromLimits(1000, 1024),
+ .params = std.crypto.pwhash.argon2.Params.owasp_2id,
}, hash_buffer.slice());
try hash_buffer.resize(result.len);
@@ -186,8 +203,7 @@ const Chirp = struct {
}
pub fn verify_password(password: []const u8, hash: PasswordHash) bool {
- var buffer: [1024 * 10]u8 = undefined;
- var alloc = std.heap.FixedBufferAllocator.init(&buffer);
+ var alloc = std.heap.FixedBufferAllocator.init(&HashBuffer);
if (std.crypto.pwhash.argon2.strVerify(hash.constSlice(), password, .{
.allocator = alloc.allocator(),
@@ -208,27 +224,28 @@ const Chirp = struct {
std.debug.print("error registering user: {}\n", .{err});
};
- const users = try Db.users(txn);
- const user_ids = try Db.user_ids(txn);
+ 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.dbi, UserId);
+ const user_id = try db.Prng.gen(users.base.dbi, UserId);
- try users.put(user_id, User{
+ 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 PostList.init(txn),
- .following = try UserList.init(txn),
- .followers = try UserList.init(txn),
+ .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);
+ try user_ids.append(username_array, user_id);
return true;
}
@@ -244,17 +261,17 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.commit() catch {};
- const user_ids = try Db.user_ids(txn);
+ 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(txn);
+ const users = try Db.users.open(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);
+ 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;
@@ -265,11 +282,11 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.commit() catch {};
- const sessions = try Db.sessions(txn);
+ var sessions = try Db.sessions.open(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) !PostId {
+ 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
@@ -280,10 +297,10 @@ const Chirp = struct {
txn = try env.txn();
defer txn.commit() catch {};
- const posts = try Db.posts(txn);
+ const posts = try Db.posts.open(txn);
post_id = try db.Prng.gen(posts.dbi, PostId);
- const decoded_text = try reencode(text);
+ const decoded_text = try reencode(PostText, text);
try posts.put(post_id, Post{
.id = post_id,
.parent_id = parent_id,
@@ -291,8 +308,8 @@ const Chirp = struct {
.user_id = user_id,
.time = std.time.timestamp(),
.votes = try VoteList.init(txn),
- .comments = try PostList.init(txn),
- .quotes = try PostList.init(txn),
+ .comments = try PostSet.init(txn),
+ .quotes = try PostSet.init(txn),
.text = decoded_text,
});
}
@@ -310,7 +327,7 @@ const Chirp = struct {
txn = try env.txn();
defer txn.commit() catch {};
- const posts = try Db.posts(txn);
+ 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);
@@ -321,7 +338,7 @@ const Chirp = struct {
fn post(env: lmdb.Env, user_id: UserId, text: []const u8) !void {
var txn = try env.txn();
- const users = try Db.users(txn);
+ const users = try Db.users.open(txn);
const user = try users.get(user_id);
txn.abort();
@@ -331,10 +348,10 @@ const Chirp = struct {
fn comment(env: lmdb.Env, user_id: UserId, parent_post_id: PostId, text: []const u8) !void {
var txn = try env.txn();
- const users = try Db.users(txn);
+ const users = try Db.users.open(txn);
const user = try users.get(user_id);
- const posts = try Db.posts(txn);
+ const posts = try Db.posts.open(txn);
const parent_post = try posts.get(parent_post_id);
txn.abort();
@@ -348,7 +365,7 @@ const Chirp = struct {
fn quote(env: lmdb.Env, user_id: UserId, quote_post_id: PostId, text: []const u8) !void {
var txn = try env.txn();
- const users = try Db.users(txn);
+ const users = try Db.users.open(txn);
const user = try users.get(user_id);
txn.abort();
@@ -360,7 +377,7 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.commit() catch {};
- const posts = try Db.posts(txn);
+ const posts = try Db.posts.open(txn);
var p = try posts.get(post_id);
var votes_view = try p.votes.open(txn);
@@ -400,7 +417,7 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.commit() catch {};
- const users = try Db.users(txn);
+ const users = try Db.users.open(txn);
const user = try users.get(user_id);
const user_to_follow = try users.get(user_id_to_follow);
@@ -423,7 +440,7 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.abort();
- const sessions = try Db.sessions(txn);
+ const sessions = try Db.sessions.open(txn);
return try sessions.get(session_token);
}
@@ -432,7 +449,7 @@ const Chirp = struct {
const txn = try env.txn();
defer txn.abort();
- const users = try Db.users(txn);
+ const users = try Db.users.open(txn);
return try users.get(user_id);
}
};
@@ -440,15 +457,86 @@ const Chirp = struct {
// }}}
// html {{{
+pub fn Paginate(comptime T: type) type {
+ return struct {
+ const Self = @This();
+
+ const IterateResult = T.Base.View.Iterator.Result;
+
+ res: *http.Response,
+ view: T.View,
+ per_page: u64,
+
+ it: T.Base.View.Iterator,
+ starting_idx: ?T.Base.Key,
+ count: u64 = 0,
+
+ pub fn init(res: *http.Response, view: T.View, per_page: u64) !Self {
+ var it = view.reverse_iterator();
+ if (res.req.get_param("starting_at")) |starting_at_str| {
+ it.idx = try parse_enum(T.Base.Key, starting_at_str, 16);
+ }
+
+ return .{
+ .res = res,
+ .view = view,
+ .per_page = per_page,
+ .it = it,
+ .starting_idx = it.idx,
+ };
+ }
+ pub fn next(self: *Self) ?IterateResult {
+ if (self.it.next()) |kv| {
+ if (self.count < self.per_page) {
+ self.count += 1;
+ return kv;
+ }
+ }
+ return null;
+ }
+ pub fn write_navigation(self: *Self) !void {
+ const next_idx = self.it.next();
+
+ if (self.view.base.head.last != self.starting_idx) {
+ var prev_it = self.view.iterator();
+ prev_it.idx = self.starting_idx.?;
+ var oldest_idx = self.starting_idx.?;
+
+ var count: u64 = 0;
+ while (prev_it.next()) |kv| {
+ oldest_idx = kv.key;
+
+ if (count > self.per_page) {
+ break;
+ } else {
+ count += 1;
+ }
+ }
+
+ try self.res.write("Prev ", .{ self.res.req.target, @intFromEnum(oldest_idx) });
+ }
+
+ if (next_idx) |kv| {
+ try self.res.write("Next", .{ self.res.req.target, @intFromEnum(kv.key) });
+ }
+ }
+ };
+}
fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void {
try res.write("