X-Git-Url: https://gitweb.ps.run/chirp/blobdiff_plain/0b2f4de9c1db5ec1059f6cd9b8c44e07f3af0b51..48e80ae12c563913af56f867268c02650292f7ee:/src/main.zig
diff --git a/src/main.zig b/src/main.zig
index 1871dcc..9fb872f 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,416 +1,1799 @@
const std = @import("std");
const lmdb = @import("lmdb");
+const db = @import("db");
+const http = @import("http");
// db {{{
const Db = struct {
- const Self = @This();
+ 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");
+ }
+};
- env: ?*lmdb.MDB_env = undefined,
- txn: ?*lmdb.MDB_txn = undefined,
- dbi: lmdb.MDB_dbi = undefined,
- prng: std.Random.DefaultPrng = std.Random.DefaultPrng.init(0),
+// }}}
- pub fn gen_id(self: *Self) Id {
- var id = self.prng.next();
+// content {{{
- while (self.has(id)) {
- id = self.prng.next();
- }
+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,
- return id;
+ 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(), "
", .{});
+ } 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);
+ }
}
- pub fn open(self: *Self, name: [*c]const u8) void {
- _ = lmdb.mdb_env_create(&self.env);
- _ = lmdb.mdb_env_set_maxdbs(self.env, 10);
- _ = lmdb.mdb_env_set_mapsize(self.env, 1024 * 1024 * 1);
- _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_WRITEMAP, 0o664);
- // _ = lmdb.mdb_env_open(self.env, name, lmdb.MDB_NOSYNC | lmdb.MDB_WRITEMAP, 0o664);
+ 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);
+ }
}
- pub fn close(self: *Self) void {
- lmdb.mdb_env_close(self.env);
+ 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 begin(self: *Self, name: [*c]const u8) void {
- switch (lmdb.mdb_txn_begin(self.env, null, 0, &self.txn)) {
- 0 => {},
- else => |err| {
- std.debug.print("txn err: {}\n", .{err});
- },
+ 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;
}
+ }
- // TODO: lmdb.MDB_INTEGERKEY?
- _ = lmdb.mdb_dbi_open(self.txn, name, lmdb.MDB_CREATE, &self.dbi);
+ 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 commit(self: *Self) void {
- switch (lmdb.mdb_txn_commit(self.txn)) {
- 0 => {},
- lmdb.MDB_MAP_FULL => {
- std.debug.print("resize\n", .{});
- _ = lmdb.mdb_env_set_mapsize(self.env, 0);
- },
- else => |err| {
- std.debug.print("commit err: {}\n", .{err});
- },
+ 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 {};
- // TODO: necessary?
- lmdb.mdb_dbi_close(self.env, self.dbi);
+ const sessions = try Db.sessions(txn);
+ try sessions.del(session_token);
}
- pub fn sync(self: *Self) void {
- switch (lmdb.mdb_env_sync(self.env, 1)) {
- 0 => {},
- else => |err| {
- std.debug.print("sync err: {}\n", .{err});
- },
+ 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(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(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;
}
- pub fn put(self: *Self, key: anytype, value: anytype) void {
- lmdb.put(self.txn, self.dbi, key, value);
+ 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;
}
- pub fn get(self: *Self, key: anytype, comptime T: type) ?T {
- return lmdb.get(self.txn, self.dbi, key, T);
+ 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();
}
- pub fn del(self: *Self, key: anytype) void {
- lmdb.del(self.txn, self.dbi, key);
+ 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;
}
- pub fn has(self: *Self, key: anytype) bool {
- return lmdb.has(self.txn, self.dbi, key);
+ 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);
+
+ var p = try posts.get(post_id);
+ 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);
+
+ add_vote = old_vote.kind != kind;
+
+ try votes_view.del(user_id);
+
+ switch (old_vote.kind) {
+ .Up => p.upvotes -= 1,
+ .Down => p.downvotes -= 1,
+ }
+ try posts.put(post_id, p);
+ }
+
+ 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);
+ }
+ }
+
+ fn follow(env: lmdb.Env, user_id: UserId, user_id_to_follow: UserId) !void {
+ const txn = try env.txn();
+ defer txn.commit() catch {};
+
+ const users = try Db.users(txn);
+
+ const user = try users.get(user_id);
+ const user_to_follow = try users.get(user_id_to_follow);
+
+ var user_following = try user.following.open(txn);
+ var user_to_follow_followers = try user_to_follow.followers.open(txn);
+
+ 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 {
+ const txn = try env.txn();
+ defer txn.abort();
+
+ const sessions = try Db.sessions(txn);
+
+ return try sessions.get(session_token);
+ }
+
+ fn get_user(env: lmdb.Env, user_id: UserId) !User {
+ const txn = try env.txn();
+ defer txn.abort();
+
+ const users = try Db.users(txn);
+ return try users.get(user_id);
}
};
// }}}
-// http stuff {{{
+// html {{{
+pub fn Paginate(comptime T: type) type {
+ return struct {
+ const Self = @This();
-pub fn redirect(req: *std.http.Server.Request, location: []const u8) !void {
- try req.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "Location", .value = location }} });
-}
+ const IterateResult = T.Base.View.Iterator.Result;
-pub fn get_body(req: *std.http.Server.Request) []const u8 {
- return req.server.read_buffer[req.head_end .. req.head_end + (req.head.content_length orelse 0)];
-}
+ res: *http.Response,
+ view: T.View,
+ per_page: u64,
-pub fn get_value(req: *std.http.Server.Request, name: []const u8) ?[]const u8 {
- const body = get_body(req);
- if (std.mem.indexOf(u8, body, name)) |name_index| {
- if (std.mem.indexOfScalarPos(u8, body, name_index, '=')) |eql_index| {
- if (std.mem.indexOfScalarPos(u8, body, name_index, '&')) |amp_index| {
- return body[eql_index + 1 .. amp_index];
+ 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 body[eql_index + 1 .. body.len];
+ return .{
+ .res = res,
+ .view = view,
+ .per_page = per_page,
+ .it = it,
+ .starting_idx = it.idx,
+ };
}
- }
- return null;
-}
+ 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();
-pub fn get_cookie(req: *std.http.Server.Request, name: []const u8) ?CookieValue {
- var header_it = req.iterateHeaders();
- while (header_it.next()) |header| {
- if (std.mem.eql(u8, header.name, "Cookie")) {
- if (std.mem.indexOf(u8, header.value, name)) |name_index| {
- if (std.mem.indexOfScalarPos(u8, header.value, name_index, '=')) |eql_index| {
- if (std.mem.indexOfPos(u8, header.value, name_index, "; ")) |semi_index| {
- return CookieValue.fromSlice(header.value[eql_index + 1 .. semi_index]) catch null;
- }
+ 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;
- return CookieValue.fromSlice(header.value[eql_index + 1 .. header.value.len]) catch null;
+ 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) });
}
}
- }
- return null;
+ };
}
+fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void {
+ try res.write("
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(UserSet).init(self.res, following_view, Chirp.UsersPerPage); - while (result != lmdb.MDB_NOTFOUND) { - const user_id = @as(*align(1) Id, @ptrCast(key.mv_data.?)).*; - const user = @as(*align(1) User, @ptrCast(val.mv_data.?)).*; + 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); - _ = lmdb.mdb_cursor_close(cursor); + const followers_view = try user.followers.open(self.txn); + var paginate = try Paginate(UserSet).init(self.res, followers_view, Chirp.UsersPerPage); - _ = lmdb.mdb_dbi_close(db.env, db.dbi); + 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:POST
- , .{}); - } + } + } + // TODO: add arguments instead of parsing manually + pub fn @"/create_list"(self: Self) !void { + if (self.logged_in) |login| { + const name_str = self.req.get_value("name") orelse return error.NoName; + const name = try Name.fromSlice(name_str); + // TODO: decode name + + var txn = try self.env.txn(); + const postlist = try PostSet.init(txn); + try txn.commit(); + + txn = try self.env.txn(); + var post_lists_view = try login.user.post_lists.open(txn); + _ = try post_lists_view.append(.{ .name = name, .list = postlist }); + try txn.commit(); + } + } + pub fn @"/delete_list"(self: Self, args: struct { list_id: PostSet.Base.Index }) !void { + if (self.logged_in) |login| { + var post_list: ?PostSet = null; + { + const txn = try self.env.txn(); + defer txn.commit() catch {}; + var post_lists_view = try login.user.post_lists.open(txn); + post_list = (try post_lists_view.get(args.list_id)).list; + try post_lists_view.del(args.list_id); + } + if (post_list != null) { + const txn = try self.env.txn(); + defer txn.commit() catch {}; + var list_view = try post_list.?.open(txn); + try list_view.clear(); + } + } + } + pub fn @"/list_add"(self: Self, args: struct { list_id: PostSet.Base.Index, post_id: PostId }) !void { + if (self.logged_in) |login| { + _ = login; + + const txn = try self.env.txn(); + defer txn.commit() catch {}; + + const post_list = PostSet{ .base = .{ .idx = args.list_id } }; + var post_list_view = try post_list.open(txn); + if (try post_list_view.has(args.post_id)) { + try post_list_view.del(args.post_id); + } else { + try post_list_view.append(args.post_id); + } + } + } + pub fn @"/create_feed"(self: Self) !void { + if (self.logged_in) |login| { + const name_str = self.req.get_value("name") orelse return error.NoName; + const name = try Name.fromSlice(name_str); + + var txn = try self.env.txn(); + const userlist = try UserSet.init(txn); + try txn.commit(); + + txn = try self.env.txn(); + var feeds_view = try login.user.feeds.open(txn); + _ = try feeds_view.append(.{ .name = name, .list = userlist }); + try txn.commit(); + } + } + pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserSet.Base.Index }) !void { + if (self.logged_in) |login| { + var user_list: ?UserSet = null; + + { + const txn = try self.env.txn(); + defer txn.commit() catch {}; + var feeds_view = try login.user.feeds.open(txn); + user_list = (try feeds_view.get(args.list_id)).list; + try feeds_view.del(args.list_id); + } + if (user_list != null) { + const txn = try self.env.txn(); + defer txn.commit() catch {}; + var list_view = try user_list.?.open(txn); + try list_view.clear(); + } + } + } + pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserSet.Base.Index, user_id: UserId }) !void { + if (self.logged_in) |login| { + _ = login; + + const txn = try self.env.txn(); + defer txn.commit() catch {}; + + const user_list = UserSet{ .base = .{ .idx = args.feed_id } }; + var user_list_view = try user_list.open(txn); + if (try user_list_view.has(args.user_id)) { + try user_list_view.del(args.user_id); + } else { + try user_list_view.append(args.user_id); + } + } + } + pub fn @"/upvote"(self: Self) !void { + const login = self.logged_in orelse return error.NotLoggedIn; + + const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16)); + + try Chirp.vote(self.env, post_id, login.user.id, .Up); + } + pub fn @"/downvote"(self: Self) !void { + const login = self.logged_in orelse return error.NotLoggedIn; + + const post_id_str = self.req.get_value("post_id") orelse return error.NoPostId; + const post_id: PostId = @enumFromInt(try std.fmt.parseUnsigned(u64, post_id_str, 16)); + + try Chirp.vote(self.env, post_id, login.user.id, .Down); + } + pub fn @"/follow"(self: Self) !void { + const login = self.logged_in orelse return error.NotLoggedIn; + + const user_id_str = self.req.get_value("user_id") orelse return error.NoUserId; + const user_id: UserId = @enumFromInt(try std.fmt.parseUnsigned(u64, user_id_str, 16)); + + try Chirp.follow(self.env, login.user.id, user_id); + } + pub fn @"/quit"(self: Self) !void { + if (self.req.get_header("Referer")) |ref| { + try self.res.redirect(ref); + } else { + try self.res.redirect("/"); + } + try self.res.send(); + // break :accept; + } +}; +// }}} + +fn list_users(env: lmdb.Env) !void { + const txn = try env.txn(); + defer txn.abort(); + + const users = try Db.users(txn); + var it = try users.iterator(); + + while (it.next()) |kv| { + const key = kv.key; + const user = kv.val; + std.debug.print("[{}] {s}\n", .{ key, user.name.constSlice() }); + } +} +fn list_user_ids(env: lmdb.Env) !void { + const txn = try env.txn(); + defer txn.abort(); + + const user_ids = try Db.user_ids(txn); + var it = try user_ids.iterator(); + + while (it.next()) |kv| { + const key = kv.key; + const user_id = kv.val; + std.debug.print("[{s}] {}\n", .{ key.constSlice(), user_id }); + } +} + +fn list_sessions(env: lmdb.Env) !void { + const txn = try env.txn(); + defer txn.abort(); + + const sessions = try Db.sessions(txn); + var it = try sessions.iterator(); + + while (it.next()) |kv| { + const key = kv.key; + const user_id = kv.val; + std.debug.print("[{x}] {}\n", .{ key, user_id }); + } +} + +fn list_posts(env: lmdb.Env) !void { + const txn = try env.txn(); + defer txn.abort(); + + const posts = try Db.posts(txn); + var it = try posts.iterator(); + + while (it.next()) |kv| { + const key = kv.key; + const post = kv.val; + std.debug.print("[{}] {s}\n", .{ key, post.text.constSlice() }); + } +} + +const ReqBufferSize = 4096; +const ResHeadBufferSize = 1024 * 64; +const ResBodyBufferSize = 1024 * 64; + +// TODO: static? +var req_buffer: [ReqBufferSize]u8 = undefined; +var res_head_buffer: [ResHeadBufferSize]u8 = undefined; +var res_body_buffer: [ResBodyBufferSize]u8 = undefined; + +pub fn main() !void { + // server + var server = try http.Server.init("::", 8080); + defer server.deinit(); + + // lmdb + var env = try lmdb.Env.open("db", 1024 * 1024 * 10); + defer env.close(); + + std.debug.print("Users:\n", .{}); + try list_users(env); + std.debug.print("User IDs:\n", .{}); + try list_user_ids(env); + std.debug.print("Sessions:\n", .{}); + try list_sessions(env); + std.debug.print("Posts:\n", .{}); + try list_posts(env); + + while (true) { + server.wait(); + while (true) { + const req = (server.next_request(&req_buffer) catch break) orelse break; + // handle_request(env, req) catch { + // try handle_error(env, req); + // }; + try handle_request(env, req); + } + } + // const ThreadCount = 1; + // var ts: [ThreadCount]std.Thread = undefined; + + // for (0..ThreadCount) |i| { + // ts[i] = try std.Thread.spawn(.{}, handle_connection, .{ &server, &env }); + // } + // for (0..ThreadCount) |i| { + // ts[i].join(); + // } + + std.debug.print("done\n", .{}); +} + +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.? }); + + // reponse + var res = http.Response.init(req, &res_head_buffer, &res_body_buffer); + + // check session token + const logged_in: ?Login = try check_login(env, req, &res); + + // html + if (req.method == .GET) { + try write_start(&res); + try write_header(&res, logged_in); + + const txn = try env.txn(); + defer txn.abort(); + + const get = GET{ + .txn = txn, + .req = req, + .res = &res, + .logged_in = logged_in, + }; + if (try get.handle()) {} else { + try res.redirect("/"); + } + + 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 (!res.has_header("Location")) { + if (req.get_header("Referer")) |ref| { + try res.redirect(ref); + } else { + try res.redirect("/"); } } + try res.send(); } }