diff --git a/src/api/lib.zig b/src/api/lib.zig index fb8f67e..98f0af2 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -57,6 +57,39 @@ pub const Community = services.communities.Community; pub const CommunityQueryArgs = services.communities.QueryArgs; pub const CommunityQueryResult = services.communities.QueryResult; +pub const NoteQueryArgs = services.notes.QueryArgs; + +pub const TimelineArgs = struct { + pub const PageDirection = NoteQueryArgs.PageDirection; + pub const Prev = NoteQueryArgs.Prev; + + max_items: usize = 20, + + created_before: ?DateTime = null, + created_after: ?DateTime = null, + + prev: ?Prev = null, + + page_direction: PageDirection = .forward, + + fn from(args: NoteQueryArgs) TimelineArgs { + return .{ + .max_items = args.max_items, + .created_before = args.created_before, + .created_after = args.created_after, + .prev = args.prev, + .page_direction = args.page_direction, + }; + } +}; + +pub const TimelineResult = struct { + items: []services.notes.Note, + + prev_page: TimelineArgs, + next_page: TimelineArgs, +}; + pub fn isAdminSetup(db: sql.Db) !bool { _ = services.communities.adminCommunityId(db) catch |err| switch (err) { error.NotFound => return false, @@ -352,18 +385,25 @@ fn ApiConn(comptime DbConn: type) type { return try services.communities.query(self.db, args, self.arena.allocator()); } - pub fn globalTimeline(self: *Self) ![]services.notes.Note { - const result = try services.notes.query(self.db, .{}, self.arena.allocator()); - return result.items; + pub fn globalTimeline(self: *Self, args: TimelineArgs) !TimelineResult { + const all_args = std.mem.zeroInit(NoteQueryArgs, args); + const result = try services.notes.query(self.db, all_args, self.arena.allocator()); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; } - pub fn localTimeline(self: *Self) ![]services.notes.Note { - const result = try services.notes.query( - self.db, - .{ .community_id = self.community.id }, - self.arena.allocator(), - ); - return result.items; + pub fn localTimeline(self: *Self, args: TimelineArgs) !TimelineResult { + var all_args = std.mem.zeroInit(NoteQueryArgs, args); + all_args.community_id = self.community.id; + const result = try services.notes.query(self.db, all_args, self.arena.allocator()); + return TimelineResult{ + .items = result.items, + .prev_page = TimelineArgs.from(result.prev_page), + .next_page = TimelineArgs.from(result.next_page), + }; } }; } diff --git a/src/api/services/notes.zig b/src/api/services/notes.zig index 6fda4f6..2a2d7f0 100644 --- a/src/api/services/notes.zig +++ b/src/api/services/notes.zig @@ -63,7 +63,7 @@ const max_max_items = 100; pub const QueryArgs = struct { pub const PageDirection = common.PageDirection; - pub const Prev = std.meta.Child(std.meta.field(@This(), .prev).field_type); + pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type); max_items: usize = 20, diff --git a/src/main/controllers/timelines.zig b/src/main/controllers/timelines.zig index 092d7c8..9a68da8 100644 --- a/src/main/controllers/timelines.zig +++ b/src/main/controllers/timelines.zig @@ -1,11 +1,27 @@ +const std = @import("std"); +const api = @import("api"); +const query_utils = @import("../query.zig"); + pub const global = struct { pub const method = .GET; pub const path = "/timelines/global"; - pub fn handler(_: anytype, res: anytype, srv: anytype) !void { - const results = try srv.globalTimeline(); + pub const Query = api.TimelineArgs; - try res.json(.ok, results); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const results = try srv.globalTimeline(req.query); + + var link = std.ArrayList(u8).init(req.allocator); + const link_writer = link.writer(); + defer link.deinit(); + + try writeLink(link_writer, srv.community, path, results.next_page, "next"); + try link_writer.writeByte(','); + try writeLink(link_writer, srv.community, path, results.prev_page, "prev"); + + try res.headers.put("Link", link.items); + + try res.json(.ok, results.items); } }; @@ -13,9 +29,45 @@ pub const local = struct { pub const method = .GET; pub const path = "/timelines/local"; - pub fn handler(_: anytype, res: anytype, srv: anytype) !void { - const results = try srv.localTimeline(); + pub const Query = api.TimelineArgs; - try res.json(.ok, results); + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + const results = try srv.localTimeline(req.query); + + var link = std.ArrayList(u8).init(req.allocator); + const link_writer = link.writer(); + defer link.deinit(); + + try writeLink(link_writer, srv.community, path, results.next_page, "next"); + try link_writer.writeByte(','); + try writeLink(link_writer, srv.community, path, results.prev_page, "prev"); + + try res.headers.put("Link", link.items); + + try res.json(.ok, results.items); } }; + +// TOOD: unify with communities.zig +fn writeLink( + writer: anytype, + community: api.Community, + path: []const u8, + params: anytype, + rel: []const u8, +) !void { + // TODO: percent-encode + try std.fmt.format( + writer, + "<{s}://{s}/{s}?", + .{ @tagName(community.scheme), community.host, path }, + ); + + try query_utils.formatQuery(params, writer); + + try std.fmt.format( + writer, + ">; rel=\"{s}\"", + .{rel}, + ); +} diff --git a/src/main/query.zig b/src/main/query.zig index d79ada8..857eff0 100644 --- a/src/main/query.zig +++ b/src/main/query.zig @@ -279,7 +279,6 @@ fn format(comptime prefix: []const u8, comptime name: []const u8, params: anytyp } }, .Union => { - //inline for (std.meta.tags(T)) |tag| { inline for (std.meta.fields(T)) |field| { const tag = @field(std.meta.Tag(T), field.name); const tag_name = field.name;