fediglam/src/api/lib.zig

541 lines
18 KiB
Zig
Raw Normal View History

2022-07-13 03:40:48 +00:00
const std = @import("std");
2022-07-13 04:16:33 +00:00
const util = @import("util");
2022-09-15 01:12:07 +00:00
const sql = @import("sql");
2022-07-13 03:40:48 +00:00
2022-10-04 02:41:59 +00:00
const DateTime = util.DateTime;
const Uuid = util.Uuid;
2022-07-13 03:40:48 +00:00
2022-09-07 23:14:52 +00:00
const services = struct {
2022-10-08 20:47:54 +00:00
const communities = @import("./services/communities.zig");
const actors = @import("./services/actors.zig");
2022-10-08 20:47:54 +00:00
const auth = @import("./services/auth.zig");
2022-12-03 15:09:29 +00:00
const drive = @import("./services/files.zig");
2022-10-08 20:47:54 +00:00
const invites = @import("./services/invites.zig");
const notes = @import("./services/notes.zig");
2022-11-14 08:14:29 +00:00
const follows = @import("./services/follows.zig");
2022-09-08 05:10:58 +00:00
};
2022-11-19 11:13:05 +00:00
pub const ClusterMeta = struct {
community_count: usize,
user_count: usize,
note_count: usize,
};
2022-10-11 03:28:23 +00:00
pub const RegistrationOptions = struct {
invite_code: ?[]const u8 = null,
2022-09-08 07:06:55 +00:00
email: ?[]const u8 = null,
2022-09-08 06:56:29 +00:00
};
2022-10-11 03:28:23 +00:00
pub const InviteOptions = struct {
2022-10-04 02:41:59 +00:00
pub const Kind = services.invites.Kind;
2022-09-08 05:10:58 +00:00
name: ?[]const u8 = null,
2022-10-04 02:41:59 +00:00
lifespan: ?DateTime.Duration = null,
2022-09-15 01:12:07 +00:00
max_uses: ?u16 = null,
2022-09-08 05:10:58 +00:00
2022-10-04 05:57:09 +00:00
// admin only options
kind: Kind = .user,
to_community: ?Uuid = null,
2022-09-07 23:14:52 +00:00
};
2022-10-04 02:41:59 +00:00
pub const LoginResponse = services.auth.LoginResult;
2022-09-08 07:52:23 +00:00
pub const UserResponse = struct {
id: Uuid,
username: []const u8,
host: []const u8,
created_at: DateTime,
};
pub const NoteResponse = struct {
id: Uuid,
author: struct {
id: Uuid,
username: []const u8,
host: []const u8,
},
content: []const u8,
created_at: DateTime,
};
pub const Community = services.communities.Community;
2022-10-08 07:51:22 +00:00
pub const CommunityQueryArgs = services.communities.QueryArgs;
2022-10-11 05:19:58 +00:00
pub const CommunityQueryResult = services.communities.QueryResult;
2022-10-08 07:51:22 +00:00
2022-11-14 07:00:20 +00:00
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.NoteDetailed,
2022-11-14 07:00:20 +00:00
prev_page: TimelineArgs,
next_page: TimelineArgs,
};
2022-11-14 09:03:11 +00:00
const FollowQueryArgs = struct {
2022-11-14 08:14:29 +00:00
pub const OrderBy = services.follows.QueryArgs.OrderBy;
pub const Direction = services.follows.QueryArgs.Direction;
pub const PageDirection = services.follows.QueryArgs.PageDirection;
pub const Prev = services.follows.QueryArgs.Prev;
max_items: usize = 20,
order_by: OrderBy = .created_at,
direction: Direction = .descending,
prev: ?Prev = null,
page_direction: PageDirection = .forward,
fn from(args: services.follows.QueryArgs) FollowQueryArgs {
return .{
.max_items = args.max_items,
.order_by = args.order_by,
.direction = args.direction,
.prev = args.prev,
.page_direction = args.page_direction,
};
}
};
2022-11-14 09:03:11 +00:00
const FollowQueryResult = struct {
2022-11-14 08:14:29 +00:00
items: []services.follows.Follow,
prev_page: FollowQueryArgs,
next_page: FollowQueryArgs,
};
2022-11-14 09:03:11 +00:00
pub const FollowerQueryArgs = FollowQueryArgs;
pub const FollowerQueryResult = FollowQueryResult;
pub const FollowingQueryArgs = FollowQueryArgs;
pub const FollowingQueryResult = FollowQueryResult;
2022-12-03 15:09:29 +00:00
pub const UploadFileArgs = struct {
filename: []const u8,
dir: ?[]const u8,
description: ?[]const u8,
content_type: []const u8,
sensitive: bool,
};
2022-10-04 05:41:22 +00:00
pub fn isAdminSetup(db: sql.Db) !bool {
2022-09-29 21:52:01 +00:00
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
error.NotFound => return false,
else => return err,
};
return true;
}
2022-10-04 05:41:22 +00:00
pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password: []const u8, allocator: std.mem.Allocator) anyerror!void {
2022-09-29 21:52:01 +00:00
const tx = try db.begin();
errdefer tx.rollback();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
try tx.setConstraintMode(.deferred);
2022-10-04 02:41:59 +00:00
const community_id = try services.communities.create(
2022-09-29 21:52:01 +00:00
tx,
origin,
.{ .name = "Cluster Admin", .kind = .admin },
2022-10-04 02:41:59 +00:00
arena.allocator(),
2022-09-29 21:52:01 +00:00
);
2022-10-04 02:41:59 +00:00
const user = try services.auth.register(tx, username, password, community_id, .{ .kind = .admin }, arena.allocator());
2022-09-29 21:52:01 +00:00
2022-10-04 02:41:59 +00:00
try services.communities.transferOwnership(tx, community_id, user);
2022-09-29 21:52:01 +00:00
try tx.commit();
std.log.info(
"Created admin user {s} (id {}) with cluster admin origin {s} (id {})",
2022-10-04 02:41:59 +00:00
.{ username, user, origin, community_id },
2022-09-29 21:52:01 +00:00
);
}
2022-07-26 02:07:05 +00:00
pub const ApiSource = struct {
2022-10-13 06:19:59 +00:00
db_conn_pool: *sql.ConnPool,
2022-07-15 00:58:08 +00:00
2022-10-04 05:41:22 +00:00
pub const Conn = ApiConn(sql.Db);
2022-07-26 02:07:05 +00:00
2022-09-05 08:52:49 +00:00
const root_username = "root";
2022-10-13 06:19:59 +00:00
pub fn init(pool: *sql.ConnPool) !ApiSource {
2022-09-29 21:52:01 +00:00
return ApiSource{
2022-10-13 06:19:59 +00:00
.db_conn_pool = pool,
2022-09-05 08:52:49 +00:00
};
2022-09-07 23:14:52 +00:00
}
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
2022-10-13 06:19:59 +00:00
const db = try self.db_conn_pool.acquire();
2022-10-16 12:48:12 +00:00
errdefer db.releaseConnection();
const community = try services.communities.getByHost(db, host, alloc);
2022-09-05 08:52:49 +00:00
2022-07-26 02:07:05 +00:00
return Conn{
2022-10-04 05:41:22 +00:00
.db = db,
2022-09-07 23:14:52 +00:00
.user_id = null,
2022-09-29 21:52:01 +00:00
.community = community,
.allocator = alloc,
2022-07-26 02:07:05 +00:00
};
}
2022-09-05 08:52:49 +00:00
pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn {
2022-10-13 06:19:59 +00:00
const db = try self.db_conn_pool.acquire();
2022-10-16 12:48:12 +00:00
errdefer db.releaseConnection();
const community = try services.communities.getByHost(db, host, alloc);
2022-07-17 23:21:03 +00:00
2022-10-04 02:41:59 +00:00
const token_info = try services.auth.verifyToken(
2022-10-04 05:41:22 +00:00
db,
2022-10-04 02:41:59 +00:00
token,
community.id,
alloc,
2022-10-04 02:41:59 +00:00
);
2022-07-17 23:21:03 +00:00
2022-09-07 23:14:52 +00:00
return Conn{
2022-10-04 05:41:22 +00:00
.db = db,
2022-10-04 02:41:59 +00:00
.token_info = token_info,
.user_id = token_info.user_id,
2022-09-29 21:52:01 +00:00
.community = community,
.allocator = alloc,
2022-09-07 23:14:52 +00:00
};
2022-07-13 03:40:48 +00:00
}
2022-07-26 02:07:05 +00:00
};
2022-07-13 03:40:48 +00:00
2022-07-26 02:07:05 +00:00
fn ApiConn(comptime DbConn: type) type {
return struct {
const Self = @This();
2022-07-18 07:37:10 +00:00
2022-07-26 02:07:05 +00:00
db: DbConn,
2022-10-04 02:41:59 +00:00
token_info: ?services.auth.TokenInfo = null,
user_id: ?Uuid = null,
2022-09-29 21:52:01 +00:00
community: services.communities.Community,
allocator: std.mem.Allocator,
2022-07-18 06:11:42 +00:00
2022-07-26 02:07:05 +00:00
pub fn close(self: *Self) void {
util.deepFree(self.allocator, self.community);
2022-10-13 06:19:59 +00:00
self.db.releaseConnection();
2022-07-26 02:07:05 +00:00
}
2022-07-18 06:11:42 +00:00
2022-09-08 05:10:58 +00:00
fn isAdmin(self: *Self) bool {
// TODO
2022-09-29 21:52:01 +00:00
return self.user_id != null and self.community.kind == .admin;
2022-09-08 05:10:58 +00:00
}
2022-09-08 07:52:23 +00:00
pub fn login(self: *Self, username: []const u8, password: []const u8) !LoginResponse {
2022-10-04 02:41:59 +00:00
return services.auth.login(
self.db,
username,
self.community.id,
password,
self.allocator,
2022-10-04 02:41:59 +00:00
);
2022-07-26 02:07:05 +00:00
}
2022-10-04 02:41:59 +00:00
pub const AuthorizationInfo = struct {
id: Uuid,
2022-09-05 10:33:54 +00:00
username: []const u8,
2022-10-04 02:41:59 +00:00
community_id: Uuid,
host: []const u8,
issued_at: DateTime,
2022-09-05 10:33:54 +00:00
};
2022-10-04 02:41:59 +00:00
pub fn verifyAuthorization(self: *Self) !AuthorizationInfo {
if (self.token_info) |info| {
const user = try services.actors.get(self.db, info.user_id, self.allocator);
defer util.deepFree(self.allocator, user);
2022-10-04 02:41:59 +00:00
return AuthorizationInfo{
.id = user.id,
.username = try util.deepClone(self.allocator, user.username),
2022-10-04 02:41:59 +00:00
.community_id = self.community.id,
.host = self.community.host,
.issued_at = info.issued_at,
2022-09-05 10:33:54 +00:00
};
}
2022-10-08 07:51:22 +00:00
return error.TokenRequired;
2022-09-05 10:33:54 +00:00
}
2022-09-07 23:14:52 +00:00
pub fn createCommunity(self: *Self, origin: []const u8) !services.communities.Community {
2022-09-08 05:10:58 +00:00
if (!self.isAdmin()) {
return error.PermissionDenied;
2022-09-07 23:14:52 +00:00
}
2022-07-26 02:07:05 +00:00
2022-10-04 02:41:59 +00:00
const tx = try self.db.begin();
errdefer tx.rollback();
const community_id = try services.communities.create(
tx,
origin,
.{},
self.allocator,
2022-10-04 02:41:59 +00:00
);
const community = services.communities.get(
tx,
community_id,
self.allocator,
2022-10-04 02:41:59 +00:00
) catch |err| return switch (err) {
error.NotFound => error.DatabaseError,
else => |err2| err2,
};
try tx.commit();
return community;
2022-07-26 02:07:05 +00:00
}
2022-07-27 05:02:09 +00:00
2022-10-11 03:28:23 +00:00
pub fn createInvite(self: *Self, options: InviteOptions) !services.invites.Invite {
2022-09-08 05:10:58 +00:00
// Only logged in users can make invites
2022-09-08 07:52:23 +00:00
const user_id = self.user_id orelse return error.TokenRequired;
2022-07-27 05:02:09 +00:00
2022-10-04 05:57:09 +00:00
const community_id = if (options.to_community) |id| blk: {
// Only admins can send invites for other communities
2022-09-08 05:10:58 +00:00
if (!self.isAdmin()) return error.PermissionDenied;
2022-07-27 05:02:09 +00:00
2022-10-04 05:57:09 +00:00
break :blk id;
2022-09-29 21:52:01 +00:00
} else self.community.id;
2022-07-27 05:02:09 +00:00
2022-09-08 05:10:58 +00:00
// Users can only make user invites
2022-10-04 02:41:59 +00:00
if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied;
2022-07-27 05:02:09 +00:00
2022-10-04 02:41:59 +00:00
const invite_id = try services.invites.create(self.db, user_id, community_id, .{
2022-09-08 05:10:58 +00:00
.name = options.name,
2022-10-04 02:41:59 +00:00
.lifespan = options.lifespan,
2022-07-27 05:02:09 +00:00
.max_uses = options.max_uses,
2022-10-04 02:41:59 +00:00
.kind = options.kind,
}, self.allocator);
2022-10-04 02:41:59 +00:00
return try services.invites.get(self.db, invite_id, self.allocator);
2022-07-27 05:02:09 +00:00
}
2022-09-08 06:56:29 +00:00
2022-10-11 03:28:23 +00:00
pub fn register(self: *Self, username: []const u8, password: []const u8, opt: RegistrationOptions) !UserResponse {
const tx = try self.db.beginOrSavepoint();
2022-10-11 03:28:23 +00:00
const maybe_invite = if (opt.invite_code) |code|
try services.invites.getByCode(tx, code, self.community.id, self.allocator)
2022-10-11 03:28:23 +00:00
else
null;
defer if (maybe_invite) |inv| util.deepFree(self.allocator, inv);
2022-10-11 03:28:23 +00:00
if (maybe_invite) |invite| {
if (!Uuid.eql(invite.community_id, self.community.id)) return error.WrongCommunity;
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return error.InviteExpired;
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return error.InviteExpired;
}
2022-09-08 06:56:29 +00:00
2022-10-11 03:28:23 +00:00
const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
2022-09-08 06:56:29 +00:00
2022-09-29 21:52:01 +00:00
if (self.community.kind == .admin) @panic("Unimplmented");
2022-09-08 06:56:29 +00:00
2022-10-04 05:50:09 +00:00
const user_id = try services.auth.register(
tx,
2022-10-11 03:28:23 +00:00
username,
password,
2022-10-04 05:50:09 +00:00
self.community.id,
2022-11-10 09:53:09 +00:00
.{
.invite_id = if (maybe_invite) |inv| @as(?Uuid, inv.id) else null,
.email = opt.email,
},
self.allocator,
2022-10-04 05:50:09 +00:00
);
2022-09-08 06:56:29 +00:00
2022-10-11 03:28:23 +00:00
switch (invite_kind) {
2022-09-08 06:56:29 +00:00
.user => {},
.system => @panic("System user invites unimplemented"),
.community_owner => {
try services.communities.transferOwnership(tx, self.community.id, user_id);
2022-09-08 06:56:29 +00:00
},
}
2022-09-08 07:52:23 +00:00
return self.getUser(user_id) catch |err| switch (err) {
2022-09-08 06:56:29 +00:00
error.NotFound => error.Unexpected,
else => err,
};
}
2022-09-08 07:52:23 +00:00
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
const user = try services.actors.get(self.db, user_id, self.allocator);
2022-09-08 07:52:23 +00:00
if (self.user_id == null) {
2022-09-29 21:52:01 +00:00
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
2022-09-08 07:52:23 +00:00
}
return UserResponse{
.id = user.id,
.username = user.username,
.host = user.host,
.created_at = user.created_at,
};
}
pub fn createNote(self: *Self, content: []const u8) !NoteResponse {
2022-10-08 07:51:22 +00:00
// You cannot post on admin accounts
2022-09-29 21:52:01 +00:00
if (self.community.kind == .admin) return error.WrongCommunity;
2022-09-08 07:52:23 +00:00
2022-10-08 07:51:22 +00:00
// Only authenticated users can post
const user_id = self.user_id orelse return error.TokenRequired;
const note_id = try services.notes.create(self.db, user_id, content, self.allocator);
2022-09-08 07:52:23 +00:00
return self.getNote(note_id) catch |err| switch (err) {
error.NotFound => error.Unexpected,
else => err,
};
}
pub fn getNote(self: *Self, note_id: Uuid) !NoteResponse {
const note = try services.notes.get(self.db, note_id, self.allocator);
const user = try services.actors.get(self.db, note.author_id, self.allocator);
2022-09-08 07:52:23 +00:00
// Only serve community-specific notes on unauthenticated requests
if (self.user_id == null) {
2022-09-29 21:52:01 +00:00
if (!Uuid.eql(self.community.id, user.community_id)) return error.NotFound;
2022-09-08 07:52:23 +00:00
}
return NoteResponse{
.id = note.id,
.author = .{
.id = user.id,
.username = user.username,
.host = user.host,
},
.content = note.content,
.created_at = note.created_at,
};
}
2022-09-10 04:02:51 +00:00
2022-10-11 05:19:58 +00:00
pub fn queryCommunities(self: *Self, args: services.communities.QueryArgs) !CommunityQueryResult {
2022-09-10 04:02:51 +00:00
if (!self.isAdmin()) return error.PermissionDenied;
return try services.communities.query(self.db, args, self.allocator);
2022-09-10 04:02:51 +00:00
}
2022-11-12 12:39:49 +00:00
2022-11-14 07:00:20 +00:00
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.allocator);
2022-11-14 07:00:20 +00:00
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
2022-11-12 12:39:49 +00:00
}
2022-11-12 13:23:55 +00:00
2022-11-14 07:00:20 +00:00
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.allocator);
2022-11-14 07:00:20 +00:00
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
2022-11-12 13:23:55 +00:00
}
2022-11-14 08:14:29 +00:00
2022-11-14 23:00:01 +00:00
pub fn homeTimeline(self: *Self, args: TimelineArgs) !TimelineResult {
if (self.user_id == null) return error.NoToken;
var all_args = std.mem.zeroInit(services.notes.QueryArgs, args);
all_args.followed_by = self.user_id;
const result = try services.notes.query(self.db, all_args, self.allocator);
2022-11-14 23:00:01 +00:00
return TimelineResult{
.items = result.items,
.prev_page = TimelineArgs.from(result.prev_page),
.next_page = TimelineArgs.from(result.next_page),
};
}
2022-11-14 09:03:11 +00:00
pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult {
2022-11-14 08:14:29 +00:00
var all_args = std.mem.zeroInit(services.follows.QueryArgs, args);
2022-11-15 04:25:59 +00:00
all_args.followee_id = user_id;
const result = try services.follows.query(self.db, all_args, self.allocator);
2022-11-14 09:03:11 +00:00
return FollowerQueryResult{
2022-11-14 08:14:29 +00:00
.items = result.items,
.prev_page = FollowQueryArgs.from(result.prev_page),
.next_page = FollowQueryArgs.from(result.next_page),
};
}
2022-11-14 09:03:11 +00:00
pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult {
2022-11-14 08:14:29 +00:00
var all_args = std.mem.zeroInit(services.follows.QueryArgs, args);
2022-11-15 04:25:59 +00:00
all_args.followed_by_id = user_id;
const result = try services.follows.query(self.db, all_args, self.allocator);
2022-11-14 09:03:11 +00:00
return FollowingQueryResult{
2022-11-14 08:14:29 +00:00
.items = result.items,
.prev_page = FollowQueryArgs.from(result.prev_page),
.next_page = FollowQueryArgs.from(result.next_page),
};
}
2022-11-14 09:03:11 +00:00
pub fn follow(self: *Self, followee: Uuid) !void {
const result = try services.follows.create(self.db, self.user_id orelse return error.NoToken, followee, self.allocator);
defer util.deepFree(self.allocator, result);
2022-11-14 09:03:11 +00:00
}
2022-11-19 11:13:05 +00:00
2022-11-19 11:33:35 +00:00
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.allocator);
defer util.deepFree(self.allocator, result);
2022-11-19 11:33:35 +00:00
}
2022-11-19 11:13:05 +00:00
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.allocator,
2022-11-19 11:13:05 +00:00
);
}
2022-12-03 07:44:27 +00:00
2022-12-03 15:09:29 +00:00
pub fn uploadFile(self: *Self, meta: UploadFileArgs, body: []const u8) !void {
const user_id = self.user_id orelse return error.NoToken;
return try services.drive.createFile(self.db, .{
.dir = meta.dir orelse "/",
.filename = meta.filename,
.owner = .{ .user_id = user_id },
.created_by = user_id,
.description = meta.description,
.content_type = meta.content_type,
.sensitive = meta.sensitive,
}, body, self.allocator);
}
pub fn driveMkdir(self: *Self, path: []const u8) !void {
2022-12-03 07:44:27 +00:00
const user_id = self.user_id orelse return error.NoToken;
2022-12-03 15:09:29 +00:00
try services.drive.mkdir(self.db, .{ .user_id = user_id }, path, self.allocator);
2022-12-03 07:44:27 +00:00
}
2022-07-26 02:07:05 +00:00
};
}