}
// 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
}
const Chirp = struct {
+ const PostsPerPage = 10;
+ const UsersPerPage = 10;
+
pub fn hash_password(password: []const u8) !PasswordHash {
var hash_buffer = try PasswordHash.init(128);
const posts = try Db.posts(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,
// }}}
// 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);
+ }
+
+ if (it.idx == null) {
+ return error.InvalidIterator;
+ }
+
+ 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("<a href=\"{s}?starting_at={x}\">Prev</a> ", .{ self.res.req.target, @intFromEnum(oldest_idx) });
+ }
+
+ if (next_idx) |kv| {
+ try self.res.write("<a href=\"{s}?starting_at={x}\">Next</a>", .{ self.res.req.target, @intFromEnum(kv.key) });
+ }
+ }
+ };
+}
fn html_form(res: *http.Response, action: []const u8, inputs: anytype) !void {
try res.write("<form action=\"{s}\" method=\"post\">", .{action});
\\<html>
\\<head>
\\<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐣</text></svg>">
+ \\<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\\<style>
\\ form {
\\ display: inline-block;
const user = try users.get(post.user_id);
try res.write(
- \\<div>
+ \\<div id="{x}">
\\<span><a href="/user/{s}">{s}</a>
- , .{ user.name.constSlice(), user.display_name.constSlice() });
+ , .{ @intFromEnum(post_id), user.name.constSlice(), user.display_name.constSlice() });
if (post.parent_id) |id| {
try res.write(" <a href=\"/post/{x}\">..</a>", .{@intFromEnum(id)});
}
\\<span>{s}</span><br />
, .{ time_str(post.time).constSlice(), post.text.constSlice() });
+ if (logged_in != null and post.user_id == logged_in.?.user.id) {
+ // Votes
+ try res.write(
+ \\<small>
+ \\<a href="/upvotes/{0x}">{1} Upvotes</a>
+ \\<a href="/downvotes/{0x}">{2} Downvotes</a>
+ \\</small>
+ \\<br />
+ , .{ @intFromEnum(post_id), post.upvotes, post.downvotes });
+ }
+
if (post.quote_id) |quote_id| {
try res.write("<div style=\"border: 1px solid black;\">", .{});
if (options.recurse > 0) {
// TODO: mark lists that already contain post
while (it.next()) |kv| {
const name = kv.val.name;
- const id = kv.val.list.idx.?;
- try res.write("<option value=\"{x}\">{s}</option>", .{ id, name.constSlice() });
+ const id = kv.val.list.base.idx.?;
+ const list_view = try kv.val.list.open(txn);
+ try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(post_id) catch false) " *" else "" });
}
try res.write("</select>", .{});
try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\"></input>", .{@intFromEnum(post_id)});
var it = comments_view.iterator();
var count: u8 = 0;
while (it.next()) |comment_id| {
- try write_post(res, txn, logged_in, comment_id, .{ .recurse = options.recurse - 1 });
+ try write_post(res, txn, logged_in, comment_id.key, .{ .recurse = options.recurse - 1 });
try res.write("<br />", .{});
if (options.recurse == 1) {
count += 1;
// follow/unfollow
if (try followers.has(login.user.id)) {
try html_form(res, "/follow", .{
- .{ "type=\"hidden\" name=\"user.id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
+ .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
\\type="submit" value="Unfollow"
});
} else {
try html_form(res, "/follow", .{
- .{ "type=\"hidden\" name=\"user.id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
+ .{ "type=\"hidden\" name=\"user_id\" value=\"{x}\"", .{@intFromEnum(user.id)} },
\\type="submit" value="Follow"
});
}
var it = feeds_view.iterator();
while (it.next()) |kv| {
const name = kv.val.name;
- const id = kv.val.list.idx.?;
- try res.write("<option value=\"{x}\">{s}</option>", .{ id, name.constSlice() });
+ const id = kv.val.list.base.idx.?;
+ const list_view = try kv.val.list.open(txn);
+ try res.write("<option value=\"{x}\">{s}{s}</option>", .{ id, name.constSlice(), if (list_view.has(user.id) catch false) " *" else "" });
}
try res.write("</select>", .{});
- try res.write("<input type=\"hidden\" name=\"user.id\" value=\"{x}\"></input>", .{@intFromEnum(user.id)});
+ try res.write("<input type=\"hidden\" name=\"user_id\" value=\"{x}\"></input>", .{@intFromEnum(user.id)});
try res.write("<input type=\"submit\" value=\"Add to feed\"></input>", .{});
try res.write("</form>", .{});
}
}) !void {
const posts_view = try post_list.open(txn);
- var it = posts_view.reverse_iterator();
- while (it.next()) |post_id| {
+ var paginate = try Paginate(PostList).init(res, posts_view, Chirp.PostsPerPage);
+
+ while (paginate.next()) |post_id| {
const posts = try Db.posts(txn);
- const post = try posts.get(post_id);
+ const post = try posts.get(post_id.key);
if ((options.show_posts and (post.parent_id == null and post.quote_id == null)) or
(options.show_quotes and (post.quote_id != null)) or
(options.show_comments and (post.parent_id != null)))
{
- try write_post(res, txn, logged_in, post_id, .{ .recurse = 1 });
+ try write_post(res, txn, logged_in, post_id.key, .{ .recurse = 1 });
try res.write("<br />", .{});
}
}
+
+ try paginate.write_navigation();
}
fn write_timeline(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, user_list: UserList) !void {
const users = try Db.users(txn);
var following_it = following.iterator();
while (following_it.next()) |following_id| {
- const followed_user = try users.get(following_id);
+ const followed_user = try users.get(following_id.key);
const followed_posts = try followed_user.posts.open(txn);
if (followed_posts.len() == 0) {
var followed_posts_it = followed_posts.reverse_iterator();
while (followed_posts_it.next()) |followed_post_id| {
- const p = try posts.get(followed_post_id);
+ const p = try posts.get(followed_post_id.key);
if ((prev_newest_post == null or p.time < prev_newest_post.?.time) and (newest_post == null or newest_post.?.time < p.time)) {
newest_post = p;
try res.write("<br />", .{});
}
}
+fn write_user(res: *http.Response, txn: lmdb.Txn, user_id: UserId) !void {
+ const users = try Db.users(txn);
+ const user = try users.get(user_id);
+ try res.write(
+ \\<a href="/user/{s}">{s}</a>
+ , .{ user.name.constSlice(), user.display_name.constSlice() });
+}
+fn write_votes(res: *http.Response, txn: lmdb.Txn, votes: VoteList, options: struct {
+ show_upvotes: bool = true,
+ show_downvotes: bool = true,
+}) !void {
+ const votes_view = try votes.open(txn);
+
+ var paginate = try Paginate(VoteList).init(res, votes_view, Chirp.UsersPerPage);
+
+ while (paginate.next()) |kv| {
+ const user_id = kv.key;
+ const vote = kv.val;
+
+ if ((options.show_upvotes and vote.kind == .Up) or
+ (options.show_downvotes and vote.kind == .Down))
+ {
+ try write_user(res, txn, user_id);
+ try res.write(" <small>{s}</small><br />", .{time_str(vote.time).constSlice()});
+ }
+ }
+
+ try paginate.write_navigation();
+}
fn check_login(env: lmdb.Env, req: http.Request, res: *http.Response) !?Login {
var result: ?Login = null;
const users = try Db.users(self.txn);
const user = try users.get(user_id);
- const following = try user.following.open(self.txn);
- var it = following.iterator();
+ 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(
\\<h2><a href="/user/{s}">{s}</a> follows:</h2>
, .{ user.name.constSlice(), user.display_name.constSlice() });
- while (it.next()) |following_id| {
- const following_user = try users.get(following_id);
+ while (paginate.next()) |following_id| {
+ const following_user = try users.get(following_id.key);
try self.res.write(
\\<a href="/user/{s}">{s}</a><br />
, .{ following_user.name.constSlice(), following_user.display_name.constSlice() });
}
+
+ try paginate.write_navigation();
} else |err| {
try self.res.write(
\\<p>User not found [{}]</p>
const users = try Db.users(self.txn);
const user = try users.get(user_id);
- const followers = try user.followers.open(self.txn);
- var it = followers.iterator();
+ 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(
\\<h2><a href="/user/{s}">{s}</a> followers:</h2>
, .{ user.name.constSlice(), user.display_name.constSlice() });
- while (it.next()) |follower_id| {
- const follower_user = try users.get(follower_id);
+ while (paginate.next()) |follower_id| {
+ const follower_user = try users.get(follower_id.key);
try self.res.write(
\\<a href="/user/{s}">{s}</a><br />
, .{ follower_user.name.constSlice(), follower_user.display_name.constSlice() });
}
+
+ try paginate.write_navigation();
} else |err| {
try self.res.write(
\\<p>User not found [{}]</p>
.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:<br />", .{post.upvotes});
+ try write_votes(self.res, self.txn, post.votes, .{});
+ }
pub fn @"/quoted/"(self: Self, args: struct { post_id: PostId }) !void {
const posts = try Db.posts(self.txn);
const post = try posts.get(args.post_id);
try html_form(self.res, "/quote", .{
.{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
.{ "type=\"hidden\" name=\"post_id\" value=\"{x}\"", .{@intFromEnum(post.id)} },
- "type=\"text\" name=\"text\" placeholder=\"Text\"",
+ "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
"type=\"submit\" value=\"Quote\"",
});
try self.res.write("<br />", .{});
}
- const quotes_view = try post.quotes.open(self.txn);
- var it = quotes_view.iterator();
- while (it.next()) |quote_id| {
- try write_post(self.res, self.txn, self.logged_in, quote_id, .{ .recurse = 1 });
- try self.res.write("<br />", .{});
- }
+ // TODO: show all bc this only contains quotes?
+ try write_posts(self.res, self.txn, self.logged_in, post.quotes, .{
+ .show_posts = false,
+ .show_quotes = true,
+ .show_comments = false,
+ });
}
- pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Index }) !void {
- try write_posts(self.res, self.txn, self.logged_in, PostList{ .idx = args.list_id }, .{
+ pub fn @"/list/"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
+ try write_posts(self.res, self.txn, self.logged_in, PostList{ .base = .{ .idx = args.list_id } }, .{
.show_posts = true,
.show_quotes = true,
.show_comments = true,
const name = kv.val.name;
const post_list = kv.val.list;
try self.res.write(
- \\<a href="/list/{x}">{s}</a>
- , .{ post_list.idx.?, name.constSlice() });
+ \\<a href="/list/{x}">{s}</a>
+ , .{ post_list.base.idx.?, name.constSlice() });
try html_form(self.res, "/delete_list", .{
.{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
"type=\"submit\" value=\"Delete\"",
try self.res.write("not logged in", .{});
}
}
- pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Index }) !void {
- try write_timeline(self.res, self.txn, self.logged_in, UserList{ .idx = args.feed_id });
+ pub fn @"/feed/"(self: Self, args: struct { feed_id: UserList.Base.Index }) !void {
+ try write_timeline(self.res, self.txn, self.logged_in, UserList{ .base = .{ .idx = args.feed_id } });
}
pub fn @"/feeds"(self: Self) !void {
if (self.logged_in) |login| {
const name = kv.val.name;
const user_list = kv.val.list;
try self.res.write(
- \\<a href="/feed/{x}">{s}</a>
- , .{ user_list.idx.?, name.constSlice() });
+ \\<a href="/feed/{x}">{s}</a>
+ , .{ user_list.base.idx.?, name.constSlice() });
try html_form(self.res, "/delete_feed", .{
.{ "type=\"hidden\" name=\"list_id\" value=\"{x}\"", .{kv.key} },
"type=\"submit\" value=\"Delete\"",
try html_form(self.res, "/post", .{
.{ "type=\"hidden\" name=\"referer\" value=\"{s}\"", .{referer} },
- "type=\"text\" name=\"text\"",
+ "type=\"text\" name=\"text\" placeholder=\"Text\" autofocus",
"type=\"submit\" value=\"Post\"",
});
} else {
try txn.commit();
}
}
- pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Index }) !void {
+ pub fn @"/delete_list"(self: Self, args: struct { list_id: PostList.Base.Index }) !void {
if (self.logged_in) |login| {
var post_list: ?PostList = null;
{
}
}
}
- pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.Index, post_id: PostId }) !void {
+ pub fn @"/list_add"(self: Self, args: struct { list_id: PostList.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 = PostList{ .idx = args.list_id };
+ const post_list = PostList{ .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);
try txn.commit();
}
}
- pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Index }) !void {
+ pub fn @"/delete_feed"(self: Self, args: struct { list_id: UserList.Base.Index }) !void {
if (self.logged_in) |login| {
var user_list: ?UserList = null;
}
}
}
- pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.Index, user_id: UserId }) !void {
+ pub fn @"/feed_add"(self: Self, args: struct { feed_id: UserList.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 = UserList{ .idx = args.feed_id };
+ const user_list = UserList{ .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);
fn handle_error(env: lmdb.Env, req: http.Request) !void {
_ = env;
- var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
+ 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);
// std.debug.print("[{}]: {s}\n", .{ req.method, req.head.? });
// reponse
- var res = http.Response.init(req.fd, &res_head_buffer, &res_body_buffer);
+ 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);