diff --git a/src/api/lib.zig b/src/api/lib.zig index 4b2a434..21f9249 100644 --- a/src/api/lib.zig +++ b/src/api/lib.zig @@ -14,6 +14,12 @@ const services = struct { const follows = @import("./services/follows.zig"); }; +pub const ClusterMeta = struct { + community_count: usize, + user_count: usize, + note_count: usize, +}; + pub const RegistrationOptions = struct { invite_code: ?[]const u8 = null, email: ?[]const u8 = null, @@ -485,5 +491,27 @@ fn ApiConn(comptime DbConn: type) type { const result = try services.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.arena.allocator()); defer util.deepFree(self.arena.allocator(), result); } + + pub fn unfollow(self: *Self, followee: Uuid) !void { + const result = try services.follows.delete(self.db, self.user_id orelse return error.NoToken, followee, self.arena.allocator()); + defer util.deepFree(self.arena.allocator(), result); + } + + pub fn getClusterMeta(self: *Self) !ClusterMeta { + return try self.db.queryRow( + ClusterMeta, + \\SELECT + \\ COUNT(DISTINCT note.id) AS note_count, + \\ COUNT(DISTINCT actor.id) AS user_count, + \\ COUNT(DISTINCT community.id) AS community_count + \\FROM note, actor, community + \\WHERE + \\ actor.community_id = community.id AND + \\ community.kind != 'admin' + , + .{}, + self.arena.allocator(), + ); + } }; } diff --git a/src/api/services/follows.zig b/src/api/services/follows.zig index dc4f5c7..7da65b7 100644 --- a/src/api/services/follows.zig +++ b/src/api/services/follows.zig @@ -33,6 +33,17 @@ pub fn create(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.m }; } +pub fn delete(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void { + // TODO: Measure count and report success + db.exec( + \\DELETE FROM follow + \\WHERE followed_by_id = $1 AND followee_id = $2 + , + .{ followed_by_id, followee_id }, + alloc, + ) catch return error.DatabaseFailure; +} + const max_max_items = 100; pub const QueryArgs = struct { diff --git a/src/main/controllers.zig b/src/main/controllers.zig index 38c81ae..e88ed3b 100644 --- a/src/main/controllers.zig +++ b/src/main/controllers.zig @@ -50,6 +50,7 @@ const routes = .{ timelines.local, timelines.home, follows.create, + follows.delete, follows.query_followers, follows.query_following, } ++ web.routes; diff --git a/src/main/controllers/api/users/follows.zig b/src/main/controllers/api/users/follows.zig index 8665718..6e3e6e7 100644 --- a/src/main/controllers/api/users/follows.zig +++ b/src/main/controllers/api/users/follows.zig @@ -19,6 +19,21 @@ pub const create = struct { } }; +pub const delete = struct { + pub const method = .DELETE; + pub const path = "/api/v0/users/:id/follow"; + + pub const Args = struct { + id: Uuid, + }; + + pub fn handler(req: anytype, res: anytype, srv: anytype) !void { + try srv.unfollow(req.args.id); + + try res.json(.ok, .{}); + } +}; + pub const query_followers = struct { pub const method = .GET; pub const path = "/api/v0/users/:id/followers"; diff --git a/src/main/controllers/web.zig b/src/main/controllers/web.zig index 183c3ce..9ee8ac4 100644 --- a/src/main/controllers/web.zig +++ b/src/main/controllers/web.zig @@ -5,6 +5,7 @@ pub const routes = .{ about, login, global_timeline, + cluster.overview, }; const index = struct { @@ -70,3 +71,18 @@ const global_timeline = struct { }); } }; + +const cluster = struct { + const overview = struct { + pub const path = "/cluster/overview"; + pub const method = .GET; + + pub fn handler(_: anytype, res: anytype, srv: anytype) !void { + const meta = try srv.getClusterMeta(); + try res.template(.ok, @embedFile("./web/cluster/overview.tmpl.html"), .{ + .community = srv.community, + .meta = meta, + }); + } + }; +}; diff --git a/src/main/controllers/web/cluster/overview.tmpl.html b/src/main/controllers/web/cluster/overview.tmpl.html new file mode 100644 index 0000000..3b55caf --- /dev/null +++ b/src/main/controllers/web/cluster/overview.tmpl.html @@ -0,0 +1,24 @@ + + +
+ +