X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/8c4068986ff26fe12e46d6ba3208e279678ce524..b30a60adbaae5f6af46d32b3a45195fd8664662c:/src/main.zig
diff --git a/src/main.zig b/src/main.zig
index 65dab07..9411261 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -28,12 +28,23 @@ const User = struct {
// TODO: choose sizes
id: UserId,
name: Username,
+ display_name: DisplayName,
+ description: UserDescription,
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,
@@ -42,11 +53,20 @@ const Post = struct {
downvotes: u64 = 0,
votes: VoteList,
comments: PostList,
- // quote posts
+ 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 };
@@ -57,22 +77,93 @@ const Vote = struct {
const Id = u64;
const Login = struct {
user: User,
- user_id: UserId,
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, 16);
+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.SetList(PostId, void);
-const UserList = db.SetList(UserId, User);
+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(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(), "
", .{});
+ } 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});
+ }
+
+ 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;
+
pub fn hash_password(password: []const u8) !PasswordHash {
var hash_buffer = try PasswordHash.init(128);
@@ -106,11 +197,14 @@ const Chirp = struct {
}
}
- pub fn register_user(env: *lmdb.Env, username: []const u8, password: []const u8) !bool {
+ 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 {};
+ 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);
@@ -119,13 +213,18 @@ const Chirp = struct {
return false;
} else {
const user_id = try db.Prng.gen(users.dbi, UserId);
- const posts = try Db.posts(txn);
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 PostList.init(posts.dbi),
+ .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);
@@ -135,7 +234,7 @@ const Chirp = struct {
}
pub fn login_user(
- env: *lmdb.Env,
+ env: lmdb.Env,
username: []const u8,
password: []const u8,
) !SessionToken {
@@ -161,7 +260,7 @@ const Chirp = struct {
}
}
- fn logout_user(env: *lmdb.Env, session_token: SessionToken) !void {
+ fn logout_user(env: lmdb.Env, session_token: SessionToken) !void {
const txn = try env.txn();
defer txn.commit() catch {};
@@ -169,7 +268,7 @@ const Chirp = struct {
try sessions.del(session_token);
}
- fn post(env: *lmdb.Env, user_id: UserId, text: []const u8) !void {
+ fn append_post(env: lmdb.Env, user_id: UserId, post_list: PostList, parent_id: ?PostId, quote_id: ?PostId, text: []const u8) !PostId {
var post_id: PostId = undefined;
// TODO: do this in one commit
@@ -182,14 +281,18 @@ const Chirp = struct {
const posts = try Db.posts(txn);
post_id = try db.Prng.gen(posts.dbi, PostId);
- const votes = try txn.dbi("votes");
+
+ 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(votes),
- .comments = try PostList.init(posts.dbi),
- .text = try PostText.fromSlice(text),
+ .votes = try VoteList.init(txn),
+ .comments = try PostList.init(txn),
+ .quotes = try PostList.init(txn),
+ .text = decoded_text,
});
}
@@ -198,79 +301,124 @@ const Chirp = struct {
txn = try env.txn();
defer txn.commit() catch {};
- const users = try Db.users(txn);
- var user = try users.get(user_id);
+ 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(txn);
- var posts_view = try user.posts.open(posts.dbi);
- try posts_view.append(post_id, {});
+ 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(txn);
+ const user = try users.get(user_id);
+ txn.abort();
+
+ const post_id = try append_post(env, user_id, user.posts, null, null, text);
+ _ = post_id;
+ }
+
+ 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 user = try users.get(user_id);
+
+ const posts = try Db.posts(txn);
+ const parent_post = try posts.get(parent_post_id);
+ txn.abort();
+
+ const post_id = try append_post(env, user_id, parent_post.comments, parent_post_id, null, text);
+
+ txn = try env.txn();
+ var replies_view = try user.posts.open(txn);
+ try replies_view.append(post_id);
+ try txn.commit();
+ }
+
+ 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 user = try users.get(user_id);
+ txn.abort();
+
+ const post_id = try append_post(env, user_id, user.posts, null, quote_post_id, text);
+ _ = post_id;
}
- fn vote(env: *lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
+ fn vote(env: lmdb.Env, post_id: PostId, user_id: UserId, kind: Vote.Kind) !void {
const txn = try env.txn();
defer txn.commit() catch {};
const posts = try Db.posts(txn);
- const votes = try txn.dbi("votes");
var p = try posts.get(post_id);
- var votes_view = try p.votes.open(votes);
+ var votes_view = try p.votes.open(txn);
+
+ var add_vote = true;
if (try votes_view.has(user_id)) {
const old_vote = try votes_view.get(user_id);
- if (old_vote.kind == kind) {
- return;
- } else {
- try votes_view.del(user_id);
+ add_vote = old_vote.kind != kind;
- if (old_vote.kind == .Up) {
- p.upvotes -= 1;
- } else {
- p.downvotes -= 1;
- }
- try posts.put(post_id, p);
+ try votes_view.del(user_id);
+
+ switch (old_vote.kind) {
+ .Up => p.upvotes -= 1,
+ .Down => p.downvotes -= 1,
}
+ try posts.put(post_id, p);
}
- try votes_view.append(user_id, Vote{
- .kind = kind,
- .time = std.time.timestamp(),
- });
- if (kind == .Up) {
- p.upvotes += 1;
- } else {
- p.downvotes += 1;
+ if (add_vote) {
+ try votes_view.append(user_id, Vote{
+ .kind = kind,
+ .time = std.time.timestamp(),
+ });
+
+ if (kind == .Up) {
+ p.upvotes += 1;
+ } else {
+ p.downvotes += 1;
+ }
+ try posts.put(post_id, p);
}
- try posts.put(post_id, p);
}
- fn unvote(env: *lmdb.Env, post_id: PostId, user_id: UserId) !void {
+ fn follow(env: lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
const txn = try env.txn();
defer txn.commit() catch {};
- const posts = try Db.posts(txn);
- const votes = try txn.dbi("votes");
-
- var p = try posts.get(post_id);
- var votes_view = try p.votes.open(votes);
+ const users = try Db.users(txn);
- if (try votes_view.has(user_id)) {
- const v = try votes_view.get(user_id);
+ const user = try users.get(user_id);
+ const user_to_follow = try users.get(user_id_to_follow);
- if (v.kind == .Up) {
- p.upvotes -= 1;
- } else {
- p.downvotes -= 1;
- }
- try posts.put(post_id, p);
+ var user_following = try user.following.open(txn);
+ var user_to_follow_followers = try user_to_follow.followers.open(txn);
- try votes_view.del(user_id);
+ if ((user_following.has(user_id_to_follow) catch false) and (user_to_follow_followers.has(user_id) catch false)) {
+ try user_following.del(user_id_to_follow);
+ try user_to_follow_followers.del(user_id);
+ } else if (!(user_following.has(user_id_to_follow) catch true) and !(user_to_follow_followers.has(user_id) catch true)) {
+ try user_following.append(user_id_to_follow);
+ try user_to_follow_followers.append(user_id);
+ } else {
+ std.debug.print("Something went wrong when trying to unfollow\n", .{});
}
}
- fn get_session_user_id(env: *lmdb.Env, session_token: SessionToken) !UserId {
+ fn get_session_user_id(env: lmdb.Env, session_token: SessionToken) !UserId {
const txn = try env.txn();
defer txn.abort();
@@ -279,7 +427,7 @@ const Chirp = struct {
return try sessions.get(session_token);
}
- fn get_user(env: *lmdb.Env, user_id: UserId) !User {
+ fn get_user(env: lmdb.Env, user_id: UserId) !User {
const txn = try env.txn();
defer txn.abort();
@@ -291,10 +439,73 @@ const Chirp = struct {
// }}}
// html {{{
-fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
- try res.write("
{s}
- , .{post.text.constSlice()}); + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/comments/"(self: Self, args: struct { username: []const u8 }) !void { + const user_ids = try Db.user_ids(self.txn); + if (user_ids.get(try Username.fromSlice(args.username))) |user_id| { + const users = try Db.users(self.txn); + const user = try users.get(user_id); + + try write_profile(self.res, self.txn, self.logged_in, user); + + try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ + .show_posts = false, + .show_quotes = false, + .show_comments = true, + }); + } else |err| { + try self.res.write( + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/quotes/"(self: Self, args: struct { username: []const u8 }) !void { + const user_ids = try Db.user_ids(self.txn); + if (user_ids.get(try Username.fromSlice(args.username))) |user_id| { + const users = try Db.users(self.txn); + const user = try users.get(user_id); + + try write_profile(self.res, self.txn, self.logged_in, user); + + try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ + .show_posts = false, + .show_quotes = true, + .show_comments = false, + }); + } else |err| { + try self.res.write( + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/all/"(self: Self, args: struct { username: []const u8 }) !void { + const user_ids = try Db.user_ids(self.txn); + if (user_ids.get(try Username.fromSlice(args.username))) |user_id| { + const users = try Db.users(self.txn); + const user = try users.get(user_id); + + try write_profile(self.res, self.txn, self.logged_in, user); + + try write_posts(self.res, self.txn, self.logged_in, user.posts, .{ + .show_posts = true, + .show_quotes = true, + .show_comments = true, + }); + } else |err| { + try self.res.write( + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/following/"(self: Self, args: struct { username: []const u8 }) !void { + const user_ids = try Db.user_ids(self.txn); + if (user_ids.get(try Username.fromSlice(args.username))) |user_id| { + const users = try Db.users(self.txn); + const user = try users.get(user_id); + + const following_view = try user.following.open(self.txn); + + var paginate = try Paginate(UserList).init(self.res, following_view, Chirp.UsersPerPage); + + try self.res.write( + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/followers/"(self: Self, args: struct { username: []const u8 }) !void { + const user_ids = try Db.user_ids(self.txn); + if (user_ids.get(try Username.fromSlice(args.username))) |user_id| { + const users = try Db.users(self.txn); + const user = try users.get(user_id); + + const followers_view = try user.followers.open(self.txn); + var paginate = try Paginate(UserList).init(self.res, followers_view, Chirp.UsersPerPage); + + try self.res.write( + \\User not found [{}]
+ , .{err}); + } + } + pub fn @"/post/"(self: Self, args: struct { post_id: PostId }) !void { + try write_post(self.res, self.txn, self.logged_in, args.post_id, .{ + .recurse = 3, // TODO: factor out + .show_comment_field = true, + }); + } + pub fn @"/upvotes/"(self: Self, args: struct { post_id: PostId }) !void { + const posts = try Db.posts(self.txn); + const post = try posts.get(args.post_id); + try self.res.write("{} upvotes:User not found [{}]
- , .{err}); - } - try res.send(); - } else { - if (logged_in) |login| { - const user = try Chirp.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 res.write("[GET] {s}", .{req.target}); - try res.send(); - } - } - } - // api - else { - if (std.mem.eql(u8, req.target, "/register")) { - // TODO: handle args not supplied - const username = req.get_value("username").?; - const password = req.get_value("password").?; - - std.debug.print("New user: {s} {s}\n", .{ username, password }); - if (try Chirp.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 = req.get_value("username").?; - const password = req.get_value("password").?; - - std.debug.print("New login: {s} {s}\n", .{ username, password }); - if (Chirp.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={x}; Secure; HttpOnly", .{session_token} }, - ); - - try res.send(); - } else |err| { - std.debug.print("login_user err: {}\n", .{err}); - try res.redirect("/login"); - try res.send(); - } - } else if (std.mem.eql(u8, req.target, "/logout")) { - if (logged_in) |login| { - try Chirp.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.target, "/post")) { - if (logged_in) |login| { - const text = req.get_value("text").?; - try Chirp.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 Chirp.vote(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, "/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 Chirp.vote(env, post_id, login.user_id, .Down); +fn handle_error(env: lmdb.Env, req: http.Request) !void { + _ = env; + var res = http.Response.init(req, &res_head_buffer, &res_body_buffer); + try write_start(&res); + try res.write("Oops, something went terribly wrong there D:", .{}); + try write_end(&res); + try res.send(); +} +fn handle_request(env: lmdb.Env, req: http.Request) !void { + // std.debug.print("[{}]: {s}\n", .{ req.method, req.target }); + // std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? }); - if (req.get_header("Referer")) |ref| { - try res.redirect(ref); - } - try res.send(); - } else if (std.mem.startsWith(u8, req.target, "/unupvote/")) { - // TODO: maybe move to one /unvote? - const login = logged_in orelse return error.NotLoggedIn; + // reponse + var res = http.Response.init(req, &res_head_buffer, &res_body_buffer); - const post_id_str = req.target[10..req.target.len]; - const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + // check session token + const logged_in: ?Login = try check_login(env, req, &res); - try Chirp.unvote(env, post_id, login.user_id); + // html + if (req.method == .GET) { + try write_start(&res); + try write_header(&res, logged_in); - 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 txn = try env.txn(); + defer txn.abort(); - const post_id_str = req.target[12..req.target.len]; - const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 10)); + const get = GET{ + .txn = txn, + .req = req, + .res = &res, + .logged_in = logged_in, + }; + if (try get.handle()) {} else { + try res.redirect("/"); + } - try Chirp.unvote(env, post_id, login.user_id); + try write_end(&res); + try res.send(); + } + // api + else { + const post = POST{ + .env = env, + .req = req, + .res = &res, + .logged_in = logged_in, + }; + if (try post.handle()) {} else { + try res.write("[POST] {s}
", .{req.target}); + } - if (req.get_header("Referer")) |ref| { - try res.redirect(ref); - } - try res.send(); - } else { - // try req.respond( - // \\POST
- // , .{}); - try res.write("[POST] {s}
", .{req.target}); - try res.send(); - } + if (!res.has_header("Location")) { + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } else { + try res.redirect("/"); } } + try res.send(); } }