Move api code into methods namespace
This commit is contained in:
parent
39565bccf0
commit
b58266bdd8
27 changed files with 922 additions and 833 deletions
651
src/api/lib.zig
651
src/api/lib.zig
|
@ -7,17 +7,13 @@ const types = @import("./types.zig");
|
|||
const DateTime = util.DateTime;
|
||||
const Uuid = util.Uuid;
|
||||
|
||||
const default_avatar = "static/default_avi.png";
|
||||
|
||||
const QueryResult = types.QueryResult;
|
||||
|
||||
pub usingnamespace types;
|
||||
pub const Account = types.Account;
|
||||
pub const Account = types.accounts.Account;
|
||||
pub const Actor = types.actors.Actor;
|
||||
pub const Community = types.Community;
|
||||
pub const Invite = types.Invite;
|
||||
pub const Note = types.Note;
|
||||
pub const Token = types.Token;
|
||||
pub const Community = types.communities.Community;
|
||||
pub const Invite = types.invites.Invite;
|
||||
pub const Note = types.notes.Note;
|
||||
pub const Token = types.tokens.Token;
|
||||
|
||||
pub const ClusterMeta = struct {
|
||||
community_count: usize,
|
||||
|
@ -25,171 +21,11 @@ pub const ClusterMeta = struct {
|
|||
note_count: usize,
|
||||
};
|
||||
|
||||
pub const RegistrationOptions = struct {
|
||||
invite_code: ?[]const u8 = null,
|
||||
email: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const InviteOptions = struct {
|
||||
pub const Kind = Invite.Kind;
|
||||
|
||||
name: ?[]const u8 = null,
|
||||
lifespan: ?DateTime.Duration = null,
|
||||
max_uses: ?usize = null,
|
||||
|
||||
// admin only options
|
||||
kind: Kind = .user,
|
||||
to_community: ?Uuid = null,
|
||||
};
|
||||
|
||||
pub const UserResponse = struct {
|
||||
id: Uuid,
|
||||
|
||||
username: []const u8,
|
||||
host: []const u8,
|
||||
|
||||
display_name: ?[]const u8,
|
||||
bio: []const u8,
|
||||
|
||||
avatar_file_id: ?Uuid,
|
||||
avatar_url: []const u8,
|
||||
|
||||
header_file_id: ?Uuid,
|
||||
header_url: ?[]const u8,
|
||||
|
||||
profile_fields: []const Actor.ProfileField,
|
||||
|
||||
community_id: Uuid,
|
||||
|
||||
created_at: DateTime,
|
||||
updated_at: DateTime,
|
||||
};
|
||||
|
||||
pub const NoteResponse = struct {
|
||||
id: Uuid,
|
||||
author: struct {
|
||||
id: Uuid,
|
||||
username: []const u8,
|
||||
host: []const u8,
|
||||
},
|
||||
|
||||
content: []const u8,
|
||||
created_at: DateTime,
|
||||
};
|
||||
|
||||
const FollowQueryArgs = struct {
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const FollowQueryResult = struct {
|
||||
items: []services.follows.Follow,
|
||||
|
||||
prev_page: FollowQueryArgs,
|
||||
next_page: FollowQueryArgs,
|
||||
};
|
||||
|
||||
pub const FollowerQueryArgs = FollowQueryArgs;
|
||||
pub const FollowerQueryResult = FollowQueryResult;
|
||||
pub const FollowingQueryArgs = FollowQueryArgs;
|
||||
pub const FollowingQueryResult = FollowQueryResult;
|
||||
|
||||
pub const UploadFileArgs = struct {
|
||||
filename: []const u8,
|
||||
dir: []const u8,
|
||||
description: ?[]const u8,
|
||||
content_type: []const u8,
|
||||
sensitive: bool,
|
||||
};
|
||||
|
||||
pub const DriveEntry = union(enum) {
|
||||
const Kind = services.drive.Kind;
|
||||
dir: struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
|
||||
kind: Kind = .dir,
|
||||
|
||||
// If null = not enumerated
|
||||
children: ?[]const DriveEntry,
|
||||
},
|
||||
file: struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
|
||||
kind: Kind = .file,
|
||||
|
||||
meta: FileUpload,
|
||||
},
|
||||
};
|
||||
|
||||
pub const FileUpload = types.FileUpload;
|
||||
|
||||
pub const DriveGetResult = union(services.drive.Kind) {
|
||||
dir: struct {
|
||||
entry: DriveEntry,
|
||||
children: []DriveEntry,
|
||||
},
|
||||
file: struct {
|
||||
entry: DriveEntry,
|
||||
file: FileUpload,
|
||||
},
|
||||
};
|
||||
|
||||
pub const FileResult = struct {
|
||||
meta: FileUpload,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
pub const InviteResponse = struct {
|
||||
code: []const u8,
|
||||
kind: Invite.Kind,
|
||||
name: []const u8,
|
||||
creator: UserResponse,
|
||||
|
||||
url: []const u8,
|
||||
|
||||
community_id: Uuid,
|
||||
|
||||
created_at: DateTime,
|
||||
expires_at: ?DateTime,
|
||||
|
||||
times_used: usize,
|
||||
max_uses: ?usize,
|
||||
};
|
||||
|
||||
pub fn isAdminSetup(db: sql.Db) !bool {
|
||||
_ = services.communities.adminCommunityId(db) catch |err| switch (err) {
|
||||
const svc = services.Services(sql.Db){ .db = db };
|
||||
_ = svc.getAdminCommunityId() catch |err| switch (err) {
|
||||
error.NotFound => return false,
|
||||
else => return err,
|
||||
else => |e| return e,
|
||||
};
|
||||
|
||||
return true;
|
||||
|
@ -230,7 +66,7 @@ pub fn setupAdmin(db: sql.Db, origin: []const u8, username: []const u8, password
|
|||
pub const ApiSource = struct {
|
||||
db_conn_pool: *sql.ConnPool,
|
||||
|
||||
pub const Conn = ApiConn(sql.Db, services);
|
||||
pub const Conn = ApiConn(sql.Db, method_impl);
|
||||
|
||||
const root_username = "root";
|
||||
|
||||
|
@ -243,15 +79,17 @@ pub const ApiSource = struct {
|
|||
pub fn connectUnauthorized(self: *ApiSource, host: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||
const db = try self.db_conn_pool.acquire();
|
||||
errdefer db.releaseConnection();
|
||||
const community = try services.communities.getByHost(db, host, alloc);
|
||||
|
||||
return Conn{
|
||||
var conn = Conn{
|
||||
.db = db,
|
||||
.context = .{
|
||||
.community = community,
|
||||
.community = undefined,
|
||||
},
|
||||
.allocator = alloc,
|
||||
};
|
||||
|
||||
conn.context.community = try conn.getServices().getCommunityByHost(alloc, host);
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
||||
pub fn connectToken(self: *ApiSource, host: []const u8, token: []const u8, alloc: std.mem.Allocator) !Conn {
|
||||
|
@ -267,7 +105,7 @@ pub const ApiContext = struct {
|
|||
community: Community,
|
||||
|
||||
pub fn userId(self: ApiContext) ?Uuid {
|
||||
if (self.token_info) |t| return t.user_id else return null;
|
||||
if (self.token_info) |t| return t.account_id else return null;
|
||||
}
|
||||
|
||||
pub fn isAdmin(self: ApiContext) bool {
|
||||
|
@ -275,12 +113,18 @@ pub const ApiContext = struct {
|
|||
}
|
||||
};
|
||||
|
||||
const methods = struct {
|
||||
const method_impl = struct {
|
||||
const actors = @import("./methods/actors.zig");
|
||||
const auth = @import("./methods/auth.zig");
|
||||
const communities = @import("./methods/communities.zig");
|
||||
const drive = @import("./methods/drive.zig");
|
||||
const follows = @import("./methods/follows.zig");
|
||||
const invites = @import("./methods/invites.zig");
|
||||
const notes = @import("./methods/notes.zig");
|
||||
const timelines = @import("./methods/timelines.zig");
|
||||
};
|
||||
|
||||
fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
|
||||
fn ApiConn(comptime DbConn: type, comptime methods: anytype) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
const Services = @import("./services.zig").Services(DbConn);
|
||||
|
@ -295,131 +139,22 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
|
|||
self.db.releaseConnection();
|
||||
}
|
||||
|
||||
fn isAdmin(self: *Self) bool {
|
||||
// TODO
|
||||
return self.context.userId() != null and self.context.community.kind == .admin;
|
||||
}
|
||||
|
||||
fn getServices(self: *Self) Services {
|
||||
return Services{ .db = self.db };
|
||||
}
|
||||
|
||||
pub const AuthorizationInfo = struct {
|
||||
id: Uuid,
|
||||
username: []const u8,
|
||||
community_id: Uuid,
|
||||
host: []const u8,
|
||||
|
||||
issued_at: DateTime,
|
||||
};
|
||||
pub fn verifyAuthorization(self: *Self) !AuthorizationInfo {
|
||||
if (self.context.token_info) |info| {
|
||||
const user = try models.actors.get(self.db, info.user_id, self.allocator);
|
||||
defer util.deepFree(self.allocator, user);
|
||||
|
||||
const username = try util.deepClone(self.allocator, user.username);
|
||||
errdefer util.deepFree(self.allocator, username);
|
||||
|
||||
return AuthorizationInfo{
|
||||
.id = user.id,
|
||||
.username = username,
|
||||
.community_id = self.context.community.id,
|
||||
.host = try util.deepClone(self.allocator, self.context.community.host),
|
||||
|
||||
.issued_at = info.issued_at,
|
||||
};
|
||||
}
|
||||
|
||||
return error.TokenRequired;
|
||||
}
|
||||
|
||||
pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Community {
|
||||
if (!self.isAdmin()) {
|
||||
return error.PermissionDenied;
|
||||
}
|
||||
|
||||
const tx = try self.db.begin();
|
||||
errdefer tx.rollback();
|
||||
const community_id = try models.communities.create(
|
||||
tx,
|
||||
pub fn createCommunity(self: *Self, origin: []const u8, name: ?[]const u8) !Uuid {
|
||||
return try methods.communities.create(
|
||||
self.allocator,
|
||||
self.context,
|
||||
self.getServices(),
|
||||
origin,
|
||||
.{ .name = name },
|
||||
self.allocator,
|
||||
name,
|
||||
);
|
||||
|
||||
const community = models.communities.get(
|
||||
tx,
|
||||
community_id,
|
||||
self.allocator,
|
||||
) catch |err| return switch (err) {
|
||||
error.NotFound => error.DatabaseError,
|
||||
else => |err2| err2,
|
||||
};
|
||||
|
||||
try tx.commit();
|
||||
|
||||
return community;
|
||||
}
|
||||
|
||||
pub fn createInvite(self: *Self, options: InviteOptions) !InviteResponse {
|
||||
// Only logged in users can make invites
|
||||
const user_id = self.context.userId() orelse return error.TokenRequired;
|
||||
|
||||
const community_id = if (options.to_community) |id| blk: {
|
||||
// Only admins can send invites for other communities
|
||||
if (!self.isAdmin()) return error.PermissionDenied;
|
||||
|
||||
break :blk id;
|
||||
} else self.context.community.id;
|
||||
|
||||
// Users can only make user invites
|
||||
if (options.kind != .user and !self.isAdmin()) return error.PermissionDenied;
|
||||
|
||||
const invite_id = try models.invites.create(self.db, user_id, community_id, options.name orelse "", .{
|
||||
.lifespan = options.lifespan,
|
||||
.max_uses = options.max_uses,
|
||||
.kind = options.kind,
|
||||
}, self.allocator);
|
||||
|
||||
const invite = try models.invites.get(self.db, invite_id, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, invite);
|
||||
|
||||
const url = if (options.to_community) |cid| blk: {
|
||||
const community = try models.communities.get(self.db, cid, self.allocator);
|
||||
defer util.deepFree(self.allocator, community);
|
||||
|
||||
break :blk try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/invite/{s}",
|
||||
.{ @tagName(community.scheme), community.host, invite.code },
|
||||
);
|
||||
} else try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/invite/{s}",
|
||||
.{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
|
||||
);
|
||||
errdefer util.deepFree(self.allocator, url);
|
||||
|
||||
const user = try self.getUserUnchecked(self.db, user_id);
|
||||
|
||||
return InviteResponse{
|
||||
.code = invite.code,
|
||||
.kind = invite.kind,
|
||||
.name = invite.name,
|
||||
.creator = user,
|
||||
.url = url,
|
||||
.community_id = invite.community_id,
|
||||
.created_at = invite.created_at,
|
||||
.expires_at = invite.expires_at,
|
||||
.times_used = invite.times_used,
|
||||
.max_uses = invite.max_uses,
|
||||
};
|
||||
}
|
||||
|
||||
fn isInviteValid(invite: Invite) bool {
|
||||
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
|
||||
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
|
||||
return true;
|
||||
pub fn createInvite(self: *Self, options: types.invites.CreateOptions) !Uuid {
|
||||
return methods.invites.create(self.allocator, self.context, self.getServices(), options);
|
||||
}
|
||||
|
||||
pub fn login(self: *Self, username: []const u8, password: []const u8) !Token {
|
||||
|
@ -430,144 +165,53 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
|
|||
self: *Self,
|
||||
username: []const u8,
|
||||
password: []const u8,
|
||||
opt: methods.auth.RegistrationOptions,
|
||||
) !types.Actor {
|
||||
opt: types.auth.RegistrationOptions,
|
||||
) !Uuid {
|
||||
return methods.auth.register(self.allocator, self.context, self.getServices(), username, password, opt);
|
||||
}
|
||||
|
||||
fn getUserUnchecked(self: *Self, db: anytype, user_id: Uuid) !UserResponse {
|
||||
const user = try models.actors.get(db, user_id, self.allocator);
|
||||
|
||||
const avatar_url = if (user.avatar_file_id) |fid|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/media/{}",
|
||||
.{ @tagName(self.context.community.scheme), self.context.community.host, fid },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/{s}",
|
||||
.{ @tagName(self.context.community.scheme), self.context.community.host, default_avatar },
|
||||
);
|
||||
errdefer self.allocator.free(avatar_url);
|
||||
const header_url = if (user.header_file_id) |fid|
|
||||
try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/media/{}",
|
||||
.{ @tagName(self.context.community.scheme), self.context.community.host, fid },
|
||||
)
|
||||
else
|
||||
null;
|
||||
|
||||
return UserResponse{
|
||||
.id = user.id,
|
||||
|
||||
.username = user.username,
|
||||
.host = user.host,
|
||||
|
||||
.display_name = user.display_name,
|
||||
.bio = user.bio,
|
||||
|
||||
.avatar_file_id = user.avatar_file_id,
|
||||
.avatar_url = avatar_url,
|
||||
|
||||
.header_file_id = user.header_file_id,
|
||||
.header_url = header_url,
|
||||
|
||||
.profile_fields = user.profile_fields,
|
||||
|
||||
.community_id = user.community_id,
|
||||
|
||||
.created_at = user.created_at,
|
||||
.updated_at = user.updated_at,
|
||||
};
|
||||
pub fn getActor(self: *Self, user_id: Uuid) !Actor {
|
||||
return methods.actors.get(self.allocator, self.context, self.getServices(), user_id);
|
||||
}
|
||||
|
||||
pub fn getUser(self: *Self, user_id: Uuid) !UserResponse {
|
||||
const user = try self.getUserUnchecked(self.db, user_id);
|
||||
errdefer util.deepFree(self.allocator, user);
|
||||
|
||||
if (self.context.userId() == null) {
|
||||
if (!Uuid.eql(self.context.community.id, user.community_id)) return error.NotFound;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
pub fn createNote(self: *Self, content: []const u8) !Note {
|
||||
// You cannot post on admin accounts
|
||||
if (self.context.community.kind == .admin) return error.WrongCommunity;
|
||||
|
||||
// Only authenticated users can post
|
||||
const user_id = self.context.userId() orelse return error.TokenRequired;
|
||||
const note_id = try models.notes.create(self.db, user_id, content, self.allocator);
|
||||
|
||||
return self.getNote(note_id) catch |err| switch (err) {
|
||||
error.NotFound => error.Unexpected,
|
||||
else => err,
|
||||
};
|
||||
pub fn createNote(self: *Self, content: []const u8) !Uuid {
|
||||
return methods.notes.create(self.allocator, self.context, self.getServices(), content);
|
||||
}
|
||||
|
||||
pub fn getNote(self: *Self, note_id: Uuid) !Note {
|
||||
const note = try models.notes.get(self.db, note_id, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, note);
|
||||
|
||||
// Only serve community-specific notes on unauthenticated requests
|
||||
if (self.context.userId() == null) {
|
||||
if (!Uuid.eql(self.context.community.id, note.author.community_id)) return error.NotFound;
|
||||
}
|
||||
|
||||
return note;
|
||||
return methods.notes.get(self.allocator, self.context, self.getServices(), note_id);
|
||||
}
|
||||
|
||||
pub fn queryCommunities(self: *Self, args: Community.QueryArgs) !QueryResult(Community) {
|
||||
if (!self.isAdmin()) return error.PermissionDenied;
|
||||
return try models.communities.query(self.db, args, self.allocator);
|
||||
pub fn queryCommunities(self: *Self, args: types.communities.QueryArgs) !types.communities.QueryResult {
|
||||
return methods.communities.query(self.allocator, self.context, self.getServices(), args);
|
||||
}
|
||||
|
||||
pub fn globalTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
pub fn globalTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
return methods.timelines.globalTimeline(self.allocator, self.context, self.getServices(), args);
|
||||
}
|
||||
|
||||
pub fn localTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
pub fn localTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
return methods.timelines.localTimeline(self.allocator, self.context, self.getServices(), args);
|
||||
}
|
||||
|
||||
pub fn homeTimeline(self: *Self, args: services.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
pub fn homeTimeline(self: *Self, args: types.timelines.TimelineArgs) !methods.timelines.TimelineResult {
|
||||
return methods.timelines.homeTimeline(self.allocator, self.context, self.getServices(), args);
|
||||
}
|
||||
|
||||
pub fn queryFollowers(self: *Self, user_id: Uuid, args: FollowerQueryArgs) !FollowerQueryResult {
|
||||
var all_args = std.mem.zeroInit(models.follows.QueryArgs, args);
|
||||
all_args.followee_id = user_id;
|
||||
const result = try models.follows.query(self.db, all_args, self.allocator);
|
||||
return FollowerQueryResult{
|
||||
.items = result.items,
|
||||
.prev_page = FollowQueryArgs.from(result.prev_page),
|
||||
.next_page = FollowQueryArgs.from(result.next_page),
|
||||
};
|
||||
pub fn queryFollowers(self: *Self, user_id: Uuid, args: types.follows.FollowerQueryArgs) !types.follows.FollowerQueryResult {
|
||||
return methods.follows.queryFollowers(self.allocator, self.context, self.getServices(), user_id, args);
|
||||
}
|
||||
|
||||
pub fn queryFollowing(self: *Self, user_id: Uuid, args: FollowingQueryArgs) !FollowingQueryResult {
|
||||
var all_args = std.mem.zeroInit(models.follows.QueryArgs, args);
|
||||
all_args.followed_by_id = user_id;
|
||||
const result = try models.follows.query(self.db, all_args, self.allocator);
|
||||
return FollowingQueryResult{
|
||||
.items = result.items,
|
||||
.prev_page = FollowQueryArgs.from(result.prev_page),
|
||||
.next_page = FollowQueryArgs.from(result.next_page),
|
||||
};
|
||||
pub fn queryFollowing(self: *Self, user_id: Uuid, args: types.follows.FollowingQueryArgs) !types.follows.FollowingQueryResult {
|
||||
return methods.follows.queryFollowing(self.allocator, self.context, self.getServices(), user_id, args);
|
||||
}
|
||||
|
||||
pub fn follow(self: *Self, followee: Uuid) !void {
|
||||
const result = try models.follows.create(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
|
||||
defer util.deepFree(self.allocator, result);
|
||||
try methods.follows.follow(self.allocator, self.context, self.getServices(), followee);
|
||||
}
|
||||
|
||||
pub fn unfollow(self: *Self, followee: Uuid) !void {
|
||||
const result = try models.follows.delete(self.db, self.context.userId() orelse return error.NoToken, followee, self.allocator);
|
||||
defer util.deepFree(self.allocator, result);
|
||||
try methods.follows.unfollow(self.allocator, self.context, self.getServices(), followee);
|
||||
}
|
||||
|
||||
pub fn getClusterMeta(self: *Self) !ClusterMeta {
|
||||
|
@ -587,201 +231,40 @@ fn ApiConn(comptime DbConn: type, comptime models: anytype) type {
|
|||
);
|
||||
}
|
||||
|
||||
fn backendDriveEntryToFrontend(self: *Self, entry: models.drive.Entry, recurse: bool) !DriveEntry {
|
||||
return if (entry.file_id) |file_id| .{
|
||||
.file = .{
|
||||
.id = entry.id,
|
||||
.owner_id = entry.owner_id,
|
||||
.name = entry.name,
|
||||
.path = entry.path,
|
||||
.parent_directory_id = entry.parent_directory_id,
|
||||
|
||||
.meta = try models.files.get(self.db, file_id, self.allocator),
|
||||
},
|
||||
} else .{
|
||||
.dir = .{
|
||||
.id = entry.id,
|
||||
.owner_id = entry.owner_id,
|
||||
.name = entry.name,
|
||||
.path = entry.path,
|
||||
.parent_directory_id = entry.parent_directory_id,
|
||||
|
||||
.children = blk: {
|
||||
if (!recurse) break :blk null;
|
||||
|
||||
const children = try models.drive.list(self.db, entry.id, self.allocator);
|
||||
|
||||
const result = self.allocator.alloc(DriveEntry, children.len) catch |err| {
|
||||
util.deepFree(self.allocator, children);
|
||||
return err;
|
||||
};
|
||||
var count: usize = 0;
|
||||
errdefer for (children) |child, i| {
|
||||
if (i < count)
|
||||
util.deepFree(self.allocator, result[i])
|
||||
else
|
||||
util.deepFree(self.allocator, child);
|
||||
};
|
||||
defer self.allocator.free(children);
|
||||
errdefer self.allocator.free(result);
|
||||
|
||||
for (children) |child, i| {
|
||||
result[i] = try backendDriveEntryToFrontend(self, child, false);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
break :blk result;
|
||||
},
|
||||
},
|
||||
};
|
||||
pub fn driveUpload(self: *Self, meta: types.drive.UploadArgs, body: []const u8) !Uuid {
|
||||
return try methods.drive.upload(self.allocator, self.context, self.getServices(), meta, body);
|
||||
}
|
||||
|
||||
pub fn driveUpload(self: *Self, meta: UploadFileArgs, body: []const u8) !DriveEntry {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
const file_id = try models.files.create(self.db, user_id, .{
|
||||
.filename = meta.filename,
|
||||
.description = meta.description,
|
||||
.content_type = meta.content_type,
|
||||
.sensitive = meta.sensitive,
|
||||
}, body, self.allocator);
|
||||
|
||||
const entry = entry: {
|
||||
errdefer models.files.delete(self.db, file_id, self.allocator) catch |err| {
|
||||
std.log.err("Unable to delete file {}: {}", .{ file_id, err });
|
||||
};
|
||||
|
||||
break :entry models.drive.create(
|
||||
self.db,
|
||||
user_id,
|
||||
meta.dir,
|
||||
meta.filename,
|
||||
file_id,
|
||||
self.allocator,
|
||||
) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
var buf: [256]u8 = undefined;
|
||||
var split = std.mem.splitBackwards(u8, meta.filename, ".");
|
||||
const ext = split.first();
|
||||
const name = split.rest();
|
||||
const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext });
|
||||
|
||||
break :entry try models.drive.create(
|
||||
self.db,
|
||||
user_id,
|
||||
meta.dir,
|
||||
new_name,
|
||||
file_id,
|
||||
self.allocator,
|
||||
);
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
};
|
||||
errdefer util.deepFree(self.allocator, entry);
|
||||
|
||||
return try self.backendDriveEntryToFrontend(entry, true);
|
||||
}
|
||||
|
||||
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !DriveEntry {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
const entry = try models.drive.create(self.db, user_id, parent_path, name, null, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, entry);
|
||||
return try self.backendDriveEntryToFrontend(entry, true);
|
||||
pub fn driveMkdir(self: *Self, parent_path: []const u8, name: []const u8) !Uuid {
|
||||
return try methods.drive.mkdir(self.allocator, self.context, self.getServices(), parent_path, name);
|
||||
}
|
||||
|
||||
pub fn driveDelete(self: *Self, path: []const u8) !void {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
|
||||
defer util.deepFree(self.allocator, entry);
|
||||
try models.drive.delete(self.db, entry.id, self.allocator);
|
||||
if (entry.file_id) |file_id| try models.files.delete(self.db, file_id, self.allocator);
|
||||
return try methods.drive.delete(self.allocator, self.context, self.getServices(), path);
|
||||
}
|
||||
|
||||
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !DriveEntry {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
try models.drive.move(self.db, user_id, src, dest, self.allocator);
|
||||
|
||||
return try self.driveGet(dest);
|
||||
pub fn driveMove(self: *Self, src: []const u8, dest: []const u8) !void {
|
||||
return try methods.drive.move(self.allocator, self.context, self.getServices(), src, dest);
|
||||
}
|
||||
|
||||
pub fn driveGet(self: *Self, path: []const u8) !DriveEntry {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, entry);
|
||||
|
||||
return try self.backendDriveEntryToFrontend(entry, true);
|
||||
pub fn driveGet(self: *Self, path: []const u8) !types.drive.DriveEntry {
|
||||
return try methods.drive.get(self.allocator, self.context, self.getServices(), path);
|
||||
}
|
||||
|
||||
pub fn driveUpdate(self: *Self, path: []const u8, meta: FileUpload.UpdateArgs) !DriveEntry {
|
||||
const user_id = self.context.userId() orelse return error.NoToken;
|
||||
std.log.debug("{s}", .{path});
|
||||
const entry = try models.drive.stat(self.db, user_id, path, self.allocator);
|
||||
defer util.deepFree(self.allocator, entry);
|
||||
|
||||
std.log.debug("{}", .{entry.id});
|
||||
try models.files.update(self.db, entry.file_id orelse return error.NotAFile, meta, self.allocator);
|
||||
|
||||
return try self.driveGet(path);
|
||||
pub fn driveUpdate(self: *Self, path: []const u8, meta: types.files.UpdateArgs) !void {
|
||||
return try methods.drive.update(self.allocator, self.context, self.getServices(), path, meta);
|
||||
}
|
||||
|
||||
pub fn fileDereference(self: *Self, id: Uuid) !FileResult {
|
||||
const meta = try models.files.get(self.db, id, self.allocator);
|
||||
errdefer util.deepFree(self.allocator, meta);
|
||||
|
||||
return FileResult{
|
||||
.meta = meta,
|
||||
.data = try models.files.deref(self.allocator, id),
|
||||
};
|
||||
pub fn fileDereference(self: *Self, id: Uuid) !types.files.DerefResult {
|
||||
return try methods.drive.dereference(self.allocator, self.context, self.getServices(), id);
|
||||
}
|
||||
|
||||
pub fn updateUserProfile(self: *Self, id: Uuid, data: Actor.ProfileUpdateArgs) !void {
|
||||
if (!Uuid.eql(id, self.context.userId() orelse return error.NoToken)) return error.AccessDenied;
|
||||
try models.actors.updateProfile(self.db, id, data, self.allocator);
|
||||
pub fn updateUserProfile(self: *Self, id: Uuid, data: types.actors.ProfileUpdateArgs) !void {
|
||||
try methods.actors.updateProfile(self.allocator, self.context, self.getServices(), id, data);
|
||||
}
|
||||
|
||||
pub fn validateInvite(self: *Self, code: []const u8) !InviteResponse {
|
||||
const invite = models.invites.getByCode(
|
||||
self.db,
|
||||
code,
|
||||
self.context.community.id,
|
||||
self.allocator,
|
||||
) catch |err| switch (err) {
|
||||
error.NotFound => return error.InvalidInvite,
|
||||
else => return error.DatabaseFailure,
|
||||
};
|
||||
errdefer util.deepFree(self.allocator, invite);
|
||||
|
||||
if (!Uuid.eql(invite.community_id, self.context.community.id)) return error.InvalidInvite;
|
||||
if (!isInviteValid(invite)) return error.InvalidInvite;
|
||||
|
||||
const url = try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
"{s}://{s}/invite/{s}",
|
||||
.{ @tagName(self.context.community.scheme), self.context.community.host, invite.code },
|
||||
);
|
||||
errdefer util.deepFree(self.allocator, url);
|
||||
|
||||
const creator = self.getUserUnchecked(self.db, invite.created_by) catch |err| switch (err) {
|
||||
error.NotFound => return error.Unexpected,
|
||||
else => return error.DatabaseFailure,
|
||||
};
|
||||
|
||||
return InviteResponse{
|
||||
.code = invite.code,
|
||||
.name = invite.name,
|
||||
.kind = invite.kind,
|
||||
.creator = creator,
|
||||
|
||||
.url = url,
|
||||
|
||||
.community_id = invite.community_id,
|
||||
|
||||
.created_at = invite.created_at,
|
||||
.expires_at = invite.expires_at,
|
||||
|
||||
.times_used = invite.times_used,
|
||||
.max_uses = invite.max_uses,
|
||||
};
|
||||
pub fn validateInvite(self: *Self, code: []const u8) !Invite {
|
||||
return try methods.invites.getByCode(self.allocator, self.context, self.getServices(), code);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
79
src/api/methods/actors.zig
Normal file
79
src/api/methods/actors.zig
Normal file
|
@ -0,0 +1,79 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const services = @import("../services.zig");
|
||||
const pkg = @import("../lib.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
|
||||
const default_avatar = "static/default_avi.png";
|
||||
pub fn get(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
id: Uuid,
|
||||
) !pkg.Actor {
|
||||
const actor = try svcs.getActor(alloc, id);
|
||||
errdefer util.deepFree(alloc, actor);
|
||||
|
||||
if (!Uuid.eql(actor.community_id, ctx.community.id) and ctx.userId() == null) {
|
||||
return error.NotFound;
|
||||
}
|
||||
|
||||
const avatar_url = if (actor.avatar_file_id) |fid|
|
||||
try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}://{s}/media/{}",
|
||||
.{ @tagName(ctx.community.scheme), ctx.community.host, fid },
|
||||
)
|
||||
else
|
||||
try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}://{s}/{s}",
|
||||
.{ @tagName(ctx.community.scheme), ctx.community.host, default_avatar },
|
||||
);
|
||||
errdefer alloc.free(avatar_url);
|
||||
const header_url = if (actor.header_file_id) |fid|
|
||||
try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}://{s}/media/{}",
|
||||
.{ @tagName(ctx.community.scheme), ctx.community.host, fid },
|
||||
)
|
||||
else
|
||||
null;
|
||||
errdefer alloc.free(header_url);
|
||||
|
||||
return pkg.Actor{
|
||||
.id = actor.id,
|
||||
|
||||
.username = actor.username,
|
||||
.host = actor.host,
|
||||
|
||||
.display_name = actor.display_name,
|
||||
.bio = actor.bio,
|
||||
|
||||
.avatar_file_id = actor.avatar_file_id,
|
||||
.avatar_url = avatar_url,
|
||||
|
||||
.header_file_id = actor.header_file_id,
|
||||
.header_url = header_url,
|
||||
|
||||
.profile_fields = actor.profile_fields,
|
||||
|
||||
.community_id = actor.community_id,
|
||||
|
||||
.created_at = actor.created_at,
|
||||
.updated_at = actor.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateProfile(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
id: Uuid,
|
||||
data: pkg.actors.ProfileUpdateArgs,
|
||||
) !void {
|
||||
if (!Uuid.eql(id, ctx.userId() orelse return error.NoToken)) return error.AccessDenied;
|
||||
try svcs.updateActorProfile(alloc, id, data);
|
||||
}
|
|
@ -1,28 +1,19 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const types = @import("../types.zig");
|
||||
const pkg = @import("../lib.zig");
|
||||
const services = @import("../services.zig");
|
||||
const invites = @import("./invites.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const Invite = types.Invite;
|
||||
const Token = types.Token;
|
||||
const Invite = pkg.invites.Invite;
|
||||
const Token = pkg.tokens.Token;
|
||||
|
||||
const RegistrationOptions = struct {
|
||||
invite_code: ?[]const u8 = null,
|
||||
email: ?[]const u8 = null,
|
||||
};
|
||||
const RegistrationOptions = pkg.auth.RegistrationOptions;
|
||||
|
||||
const AccountCreateOptions = services.accounts.CreateOptions;
|
||||
|
||||
fn isInviteValid(invite: Invite) bool {
|
||||
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
|
||||
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
|
@ -30,7 +21,7 @@ pub fn register(
|
|||
username: []const u8,
|
||||
password: []const u8,
|
||||
opt: RegistrationOptions,
|
||||
) !types.Actor {
|
||||
) !Uuid {
|
||||
const tx = try svcs.beginTx();
|
||||
errdefer tx.rollbackTx();
|
||||
|
||||
|
@ -42,14 +33,14 @@ pub fn register(
|
|||
|
||||
if (maybe_invite) |invite| {
|
||||
if (!Uuid.eql(invite.community_id, ctx.community.id)) return error.WrongCommunity;
|
||||
if (!isInviteValid(invite)) return error.InvalidInvite;
|
||||
if (!invites.isValid(invite)) return error.InvalidInvite;
|
||||
}
|
||||
|
||||
const invite_kind = if (maybe_invite) |inv| inv.kind else .user;
|
||||
|
||||
if (ctx.community.kind == .admin) @panic("Unimplmented");
|
||||
|
||||
const user_id = try createLocalAccount(
|
||||
const account_id = try createLocalAccount(
|
||||
alloc,
|
||||
tx,
|
||||
username,
|
||||
|
@ -65,18 +56,12 @@ pub fn register(
|
|||
.user => {},
|
||||
.system => @panic("System user invites unimplemented"),
|
||||
.community_owner => {
|
||||
try tx.transferCommunityOwnership(ctx.community.id, user_id);
|
||||
try tx.transferCommunityOwnership(ctx.community.id, account_id);
|
||||
},
|
||||
}
|
||||
|
||||
const user = tx.getActor(alloc, user_id) catch |err| switch (err) {
|
||||
error.NotFound => return error.Unexpected,
|
||||
else => |e| return e,
|
||||
};
|
||||
errdefer util.deepFree(alloc, user);
|
||||
|
||||
try tx.commitTx();
|
||||
return user;
|
||||
return account_id;
|
||||
}
|
||||
|
||||
pub fn createLocalAccount(
|
||||
|
@ -108,7 +93,7 @@ pub fn verifyToken(alloc: std.mem.Allocator, ctx: ApiContext, svcs: anytype, tok
|
|||
const info = try svcs.getTokenByHash(alloc, hash, ctx.community.id);
|
||||
defer util.deepFree(alloc, info);
|
||||
|
||||
return .{ .user_id = info.account_id, .issued_at = info.issued_at };
|
||||
return .{ .account_id = info.account_id, .issued_at = info.issued_at };
|
||||
}
|
||||
|
||||
pub fn login(
|
||||
|
@ -160,7 +145,7 @@ pub fn login(
|
|||
return .{
|
||||
.value = token,
|
||||
.info = .{
|
||||
.user_id = info.account_id,
|
||||
.account_id = info.account_id,
|
||||
.issued_at = info.issued_at,
|
||||
},
|
||||
};
|
||||
|
@ -309,9 +294,9 @@ test "register" {
|
|||
}
|
||||
};
|
||||
const actors = struct {
|
||||
fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!types.Actor {
|
||||
fn get(_: *TestDb, id: Uuid, alloc: std.mem.Allocator) anyerror!pkg.Actor {
|
||||
try std.testing.expectEqual(uid, id);
|
||||
return try util.deepClone(alloc, std.mem.zeroInit(types.Actor, .{
|
||||
return try util.deepClone(alloc, std.mem.zeroInit(pkg.Actor, .{
|
||||
.id = id,
|
||||
.username = "root",
|
||||
.host = "example.com",
|
||||
|
|
|
@ -1,45 +1,48 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const types = @import("../types.zig");
|
||||
const pkg = @import("../lib.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const QueryResult = types.QueryResult;
|
||||
const Community = types.Community;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const Community = types.communities.Community;
|
||||
const QueryArgs = types.communities.QueryArgs;
|
||||
const QueryResult = types.communities.QueryResult;
|
||||
|
||||
pub fn methods(comptime models: type) type {
|
||||
return struct {
|
||||
pub fn createCommunity(self: anytype, origin: []const u8, name: ?[]const u8) !Community {
|
||||
if (!self.isAdmin()) {
|
||||
return error.PermissionDenied;
|
||||
}
|
||||
pub fn create(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
origin: []const u8,
|
||||
name: ?[]const u8,
|
||||
) !Uuid {
|
||||
if (!ctx.isAdmin()) {
|
||||
return error.PermissionDenied;
|
||||
}
|
||||
|
||||
const tx = try self.db.begin();
|
||||
errdefer tx.rollback();
|
||||
const community_id = try models.communities.create(
|
||||
tx,
|
||||
origin,
|
||||
.{ .name = name },
|
||||
self.allocator,
|
||||
);
|
||||
|
||||
const community = models.communities.get(
|
||||
tx,
|
||||
community_id,
|
||||
self.allocator,
|
||||
) catch |err| return switch (err) {
|
||||
error.NotFound => error.DatabaseError,
|
||||
else => |err2| err2,
|
||||
};
|
||||
|
||||
try tx.commit();
|
||||
|
||||
return community;
|
||||
}
|
||||
|
||||
pub fn queryCommunities(self: anytype, args: Community.QueryArgs) !QueryResult(Community) {
|
||||
if (!self.context.isAdmin()) return error.PermissionDenied;
|
||||
return try models.communities.query(self.db, args, self.allocator);
|
||||
}
|
||||
};
|
||||
return try svcs.createCommunity(
|
||||
alloc,
|
||||
origin,
|
||||
.{ .name = name },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
alloc: std.mem.Allocator,
|
||||
_: ApiContext,
|
||||
svcs: anytype,
|
||||
id: Uuid,
|
||||
) !Community {
|
||||
return try svcs.getCommunity(alloc, id);
|
||||
}
|
||||
|
||||
pub fn query(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
args: QueryArgs,
|
||||
) !QueryResult {
|
||||
if (!ctx.isAdmin()) return error.PermissionDenied;
|
||||
return try svcs.queryCommunities(alloc, args);
|
||||
}
|
||||
|
|
192
src/api/methods/drive.zig
Normal file
192
src/api/methods/drive.zig
Normal file
|
@ -0,0 +1,192 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const pkg = @import("../lib.zig");
|
||||
const services = @import("../services.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const DriveEntry = pkg.drive.DriveEntry;
|
||||
|
||||
pub fn upload(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
args: pkg.drive.UploadArgs,
|
||||
body: []const u8,
|
||||
) !Uuid {
|
||||
const owner = ctx.userId() orelse return error.NoToken;
|
||||
const file_id = try svcs.createFile(alloc, owner, .{
|
||||
.filename = args.filename,
|
||||
.description = args.description,
|
||||
.content_type = args.content_type,
|
||||
.sensitive = args.sensitive,
|
||||
}, body);
|
||||
|
||||
const entry_id = entry: {
|
||||
errdefer svcs.deleteFile(alloc, file_id) catch |err| {
|
||||
std.log.err("Unable to delete file {}: {}", .{ file_id, err });
|
||||
};
|
||||
|
||||
break :entry svcs.createDriveEntry(
|
||||
alloc,
|
||||
owner,
|
||||
args.dir,
|
||||
args.filename,
|
||||
file_id,
|
||||
) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
var buf: [256]u8 = undefined;
|
||||
var split = std.mem.splitBackwards(u8, args.filename, ".");
|
||||
const ext = split.first();
|
||||
const name = split.rest();
|
||||
const new_name = try std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ name, file_id, ext });
|
||||
|
||||
break :entry try svcs.createDriveEntry(
|
||||
alloc,
|
||||
owner,
|
||||
args.dir,
|
||||
new_name,
|
||||
file_id,
|
||||
);
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
};
|
||||
|
||||
return entry_id;
|
||||
}
|
||||
|
||||
pub fn mkdir(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
parent_path: []const u8,
|
||||
name: []const u8,
|
||||
) !Uuid {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
return try svcs.createDriveEntry(alloc, user_id, parent_path, name, null);
|
||||
}
|
||||
|
||||
pub fn delete(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
path: []const u8,
|
||||
) !void {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
const entry = try svcs.statDriveEntry(alloc, user_id, path);
|
||||
defer util.deepFree(alloc, entry);
|
||||
try svcs.deleteDriveEntry(alloc, entry.id);
|
||||
if (entry.file_id) |file_id| try svcs.deleteFile(alloc, file_id);
|
||||
}
|
||||
|
||||
pub fn move(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
src: []const u8,
|
||||
dest: []const u8,
|
||||
) !void {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
try svcs.moveDriveEntry(alloc, user_id, src, dest);
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
path: []const u8,
|
||||
) !pkg.drive.DriveEntry {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
const entry = try svcs.statDriveEntry(alloc, user_id, path);
|
||||
defer util.deepFree(alloc, entry);
|
||||
|
||||
return try convert(alloc, svcs, entry, true);
|
||||
}
|
||||
|
||||
// TODO: These next two functions are more about files than drive entries, consider refactor?
|
||||
|
||||
pub fn update(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
path: []const u8,
|
||||
meta: pkg.files.UpdateArgs,
|
||||
) !void {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
|
||||
const entry = try svcs.statDriveEntry(alloc, user_id, path);
|
||||
defer util.deepFree(alloc, entry);
|
||||
|
||||
try svcs.updateFileMetadata(alloc, entry.file_id orelse return error.NotAFile, meta);
|
||||
}
|
||||
|
||||
pub fn dereference(
|
||||
alloc: std.mem.Allocator,
|
||||
_: ApiContext,
|
||||
svcs: anytype,
|
||||
file_id: Uuid,
|
||||
) !pkg.files.DerefResult {
|
||||
const meta = try svcs.statFile(alloc, file_id);
|
||||
errdefer util.deepFree(alloc, meta);
|
||||
|
||||
return .{
|
||||
.meta = meta,
|
||||
.data = try svcs.derefFile(alloc, file_id),
|
||||
};
|
||||
}
|
||||
|
||||
fn convert(
|
||||
alloc: std.mem.Allocator,
|
||||
svcs: anytype,
|
||||
entry: services.drive.DriveEntry,
|
||||
recurse: bool,
|
||||
) !DriveEntry {
|
||||
if (entry.file_id) |file_id| return .{
|
||||
.file = .{
|
||||
.id = entry.id,
|
||||
.owner_id = entry.owner_id,
|
||||
.name = entry.name,
|
||||
.path = entry.path,
|
||||
.parent_directory_id = entry.parent_directory_id,
|
||||
|
||||
.meta = try svcs.statFile(alloc, file_id),
|
||||
},
|
||||
} else return .{
|
||||
.dir = .{
|
||||
.id = entry.id,
|
||||
.owner_id = entry.owner_id,
|
||||
.name = entry.name,
|
||||
.path = entry.path,
|
||||
.parent_directory_id = entry.parent_directory_id,
|
||||
|
||||
.children = blk: {
|
||||
if (!recurse) break :blk null;
|
||||
|
||||
const children = try svcs.listDriveEntry(alloc, entry.id);
|
||||
|
||||
const result = alloc.alloc(DriveEntry, children.len) catch |err| {
|
||||
util.deepFree(alloc, children);
|
||||
return err;
|
||||
};
|
||||
var count: usize = 0;
|
||||
errdefer for (children) |child, i| {
|
||||
if (i < count)
|
||||
util.deepFree(alloc, result[i])
|
||||
else
|
||||
util.deepFree(alloc, child);
|
||||
};
|
||||
defer alloc.free(children);
|
||||
errdefer alloc.free(result);
|
||||
|
||||
for (children) |child, i| {
|
||||
result[i] = try convert(alloc, svcs, child, false);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
break :blk result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
83
src/api/methods/follows.zig
Normal file
83
src/api/methods/follows.zig
Normal file
|
@ -0,0 +1,83 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const pkg = @import("../lib.zig");
|
||||
const services = @import("../services.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const QueryArgs = services.follows.QueryArgs;
|
||||
const FollowerQueryArgs = pkg.follows.FollowerQueryArgs;
|
||||
const FollowingQueryArgs = pkg.follows.FollowingQueryArgs;
|
||||
|
||||
pub fn follow(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
followee: Uuid,
|
||||
) !void {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
try svcs.createFollow(alloc, user_id, followee);
|
||||
}
|
||||
|
||||
pub fn unfollow(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
followee: Uuid,
|
||||
) !void {
|
||||
const user_id = ctx.userId() orelse return error.NoToken;
|
||||
try svcs.deleteFollow(alloc, user_id, followee);
|
||||
}
|
||||
|
||||
pub fn queryFollowers(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
of: Uuid,
|
||||
args: FollowerQueryArgs,
|
||||
) !pkg.follows.FollowerQueryResult {
|
||||
const user = try svcs.getActor(alloc, of);
|
||||
defer util.deepFree(alloc, user);
|
||||
if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound;
|
||||
|
||||
var all_args = std.mem.zeroInit(QueryArgs, args);
|
||||
all_args.followee_id = of;
|
||||
const result = try svcs.queryFollows(alloc, all_args);
|
||||
return .{
|
||||
.items = result.items,
|
||||
.prev_page = convert(FollowerQueryArgs, result.prev_page),
|
||||
.next_page = convert(FollowerQueryArgs, result.next_page),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn queryFollowing(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
of: Uuid,
|
||||
args: FollowingQueryArgs,
|
||||
) !pkg.follows.FollowingQueryResult {
|
||||
const user = try svcs.getActor(alloc, of);
|
||||
defer util.deepFree(alloc, user);
|
||||
if (!Uuid.eql(user.community_id, ctx.community.id) and ctx.userId() == null) return error.NotFound;
|
||||
|
||||
var all_args = std.mem.zeroInit(QueryArgs, args);
|
||||
all_args.followed_by_id = of;
|
||||
const result = try svcs.queryFollows(alloc, all_args);
|
||||
return .{
|
||||
.items = result.items,
|
||||
.prev_page = convert(FollowingQueryArgs, result.prev_page),
|
||||
.next_page = convert(FollowingQueryArgs, result.next_page),
|
||||
};
|
||||
}
|
||||
|
||||
fn convert(comptime T: type, args: QueryArgs) T {
|
||||
return .{
|
||||
.max_items = args.max_items,
|
||||
.order_by = args.order_by,
|
||||
.direction = args.direction,
|
||||
.prev = args.prev,
|
||||
.page_direction = args.page_direction,
|
||||
};
|
||||
}
|
107
src/api/methods/invites.zig
Normal file
107
src/api/methods/invites.zig
Normal file
|
@ -0,0 +1,107 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const services = @import("../services.zig");
|
||||
const pkg = @import("../lib.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const CreateOptions = pkg.invites.CreateOptions;
|
||||
const Invite = pkg.invites.Invite;
|
||||
|
||||
pub fn create(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
options: CreateOptions,
|
||||
) !Uuid {
|
||||
// Only logged in users can make invites
|
||||
const user_id = ctx.userId() orelse return error.TokenRequired;
|
||||
|
||||
const community_id = if (options.to_community) |id| blk: {
|
||||
// Only admins can send invites for other communities
|
||||
if (!ctx.isAdmin()) return error.PermissionDenied;
|
||||
|
||||
break :blk id;
|
||||
} else ctx.community.id;
|
||||
|
||||
// Users can only make user invites
|
||||
if (options.kind != .user and !ctx.isAdmin()) return error.PermissionDenied;
|
||||
|
||||
return try svcs.createInvite(alloc, .{
|
||||
.created_by = user_id,
|
||||
.community_id = community_id,
|
||||
.name = options.name,
|
||||
.lifespan = options.lifespan,
|
||||
.max_uses = options.max_uses,
|
||||
.kind = options.kind,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn isValid(invite: services.invites.Invite) bool {
|
||||
if (invite.max_uses != null and invite.times_used >= invite.max_uses.?) return false;
|
||||
if (invite.expires_at != null and DateTime.now().isAfter(invite.expires_at.?)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn getInviteImpl(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
invite: services.invites.Invite,
|
||||
) !Invite {
|
||||
errdefer util.deepFree(alloc, invite);
|
||||
|
||||
if (!Uuid.eql(invite.community_id, ctx.community.id) and !ctx.isAdmin()) return error.NotFound;
|
||||
if (!isValid(invite)) return error.NotFound;
|
||||
|
||||
const community = try svcs.getCommunity(alloc, invite.community_id);
|
||||
defer util.deepFree(alloc, community);
|
||||
|
||||
const url = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}://{s}/invite/{s}",
|
||||
.{ @tagName(community.scheme), community.host, invite.code },
|
||||
);
|
||||
errdefer util.deepFree(alloc, url);
|
||||
|
||||
return Invite{
|
||||
.id = invite.id,
|
||||
|
||||
.created_by = invite.created_by,
|
||||
.community_id = invite.community_id,
|
||||
.name = invite.name,
|
||||
.code = invite.code,
|
||||
.url = url,
|
||||
|
||||
.created_at = invite.created_at,
|
||||
.times_used = invite.times_used,
|
||||
|
||||
.expires_at = invite.expires_at,
|
||||
.max_uses = invite.max_uses,
|
||||
|
||||
.kind = invite.kind,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
id: Uuid,
|
||||
) !Invite {
|
||||
const invite = try svcs.getInvite(alloc, id);
|
||||
|
||||
return getInviteImpl(alloc, ctx, svcs, invite);
|
||||
}
|
||||
|
||||
pub fn getByCode(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
code: []const u8,
|
||||
) !Invite {
|
||||
const invite = try svcs.getInviteByCode(alloc, code, ctx.community.id);
|
||||
|
||||
return getInviteImpl(alloc, ctx, svcs, invite);
|
||||
}
|
38
src/api/methods/notes.zig
Normal file
38
src/api/methods/notes.zig
Normal file
|
@ -0,0 +1,38 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const services = @import("../services.zig");
|
||||
const pkg = @import("../lib.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const ApiContext = pkg.ApiContext;
|
||||
|
||||
pub fn create(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
content: []const u8,
|
||||
) !Uuid {
|
||||
// You cannot post on admin accounts
|
||||
if (ctx.community.kind == .admin) return error.WrongCommunity;
|
||||
|
||||
// Only authenticated users can post
|
||||
const user_id = ctx.userId() orelse return error.TokenRequired;
|
||||
return try svcs.createNote(alloc, user_id, content);
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
alloc: std.mem.Allocator,
|
||||
ctx: ApiContext,
|
||||
svcs: anytype,
|
||||
note_id: Uuid,
|
||||
) !pkg.Note {
|
||||
const note = try svcs.getNote(alloc, note_id);
|
||||
errdefer util.deepFree(alloc, note);
|
||||
|
||||
// Only serve community-specific notes on unauthenticated requests
|
||||
if (ctx.userId() == null) {
|
||||
if (!Uuid.eql(ctx.community.id, note.author.community_id)) return error.NotFound;
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const pkg = @import("../lib.zig");
|
||||
const types = @import("../types.zig");
|
||||
|
||||
const ApiContext = pkg.ApiContext;
|
||||
const Uuid = util.Uuid;
|
||||
|
@ -10,29 +11,17 @@ const services = @import("../services.zig");
|
|||
const Note = services.Note;
|
||||
const QueryArgs = services.notes.QueryArgs;
|
||||
|
||||
pub const TimelineArgs = struct {
|
||||
pub const PageDirection = QueryArgs.PageDirection;
|
||||
pub const Prev = QueryArgs.Prev;
|
||||
const TimelineArgs = types.timelines.TimelineArgs;
|
||||
|
||||
max_items: usize = 20,
|
||||
|
||||
created_before: ?DateTime = null,
|
||||
created_after: ?DateTime = null,
|
||||
|
||||
prev: ?Prev = null,
|
||||
|
||||
page_direction: PageDirection = .forward,
|
||||
|
||||
fn from(args: QueryArgs) 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,
|
||||
};
|
||||
}
|
||||
};
|
||||
fn timelineArgs(args: services.notes.QueryArgs) 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: []Note,
|
||||
|
@ -51,8 +40,8 @@ pub fn globalTimeline(
|
|||
const result = try svcs.queryNotes(alloc, all_args);
|
||||
return TimelineResult{
|
||||
.items = result.items,
|
||||
.prev_page = TimelineArgs.from(result.prev_page),
|
||||
.next_page = TimelineArgs.from(result.next_page),
|
||||
.prev_page = timelineArgs(result.prev_page),
|
||||
.next_page = timelineArgs(result.next_page),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -67,8 +56,8 @@ pub fn localTimeline(
|
|||
const result = try svcs.queryNotes(alloc, all_args);
|
||||
return TimelineResult{
|
||||
.items = result.items,
|
||||
.prev_page = TimelineArgs.from(result.prev_page),
|
||||
.next_page = TimelineArgs.from(result.next_page),
|
||||
.prev_page = timelineArgs(result.prev_page),
|
||||
.next_page = timelineArgs(result.next_page),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -85,7 +74,7 @@ pub fn homeTimeline(
|
|||
const result = try svcs.queryNotes(alloc, all_args);
|
||||
return TimelineResult{
|
||||
.items = result.items,
|
||||
.prev_page = TimelineArgs.from(result.prev_page),
|
||||
.next_page = TimelineArgs.from(result.next_page),
|
||||
.prev_page = timelineArgs(result.prev_page),
|
||||
.next_page = timelineArgs(result.next_page),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ pub fn Services(comptime Db: type) type {
|
|||
alloc: std.mem.Allocator,
|
||||
actor_id: Uuid,
|
||||
new: types.actors.ProfileUpdateArgs,
|
||||
) !Actor {
|
||||
) !void {
|
||||
return try impl.actors.updateProfile(self.db, actor_id, new, alloc);
|
||||
}
|
||||
|
||||
|
@ -129,14 +129,22 @@ pub fn Services(comptime Db: type) type {
|
|||
return try impl.communities.getByHost(self.db, host, alloc);
|
||||
}
|
||||
|
||||
pub fn transferCommunityOwnership(
|
||||
self: Self,
|
||||
community_id: Uuid,
|
||||
owner_id: Uuid,
|
||||
) !void {
|
||||
pub fn getAdminCommunityId(self: Self) !Uuid {
|
||||
return try impl.communities.adminCommunityId(self.db);
|
||||
}
|
||||
|
||||
pub fn transferCommunityOwnership(self: Self, community_id: Uuid, owner_id: Uuid) !void {
|
||||
return try impl.communities.transferOwnership(self.db, community_id, owner_id);
|
||||
}
|
||||
|
||||
pub fn queryCommunities(
|
||||
self: Self,
|
||||
alloc: std.mem.Allocator,
|
||||
args: types.communities.QueryArgs,
|
||||
) !types.communities.QueryResult {
|
||||
return try impl.communities.query(self.db, args, alloc);
|
||||
}
|
||||
|
||||
pub fn statDriveEntry(
|
||||
self: Self,
|
||||
alloc: std.mem.Allocator,
|
||||
|
@ -179,10 +187,9 @@ pub fn Services(comptime Db: type) type {
|
|||
pub fn listDriveEntry(
|
||||
self: Self,
|
||||
alloc: std.mem.Allocator,
|
||||
owner_id: Uuid,
|
||||
path: []const u8,
|
||||
entry_id: Uuid,
|
||||
) ![]DriveEntry {
|
||||
return try impl.drive.list(self.db, owner_id, path, alloc);
|
||||
return try impl.drive.list(self.db, entry_id, alloc);
|
||||
}
|
||||
|
||||
pub fn createFile(
|
||||
|
@ -224,7 +231,7 @@ pub fn Services(comptime Db: type) type {
|
|||
alloc: std.mem.Allocator,
|
||||
id: Uuid,
|
||||
meta: types.files.UpdateArgs,
|
||||
) !FileUpload {
|
||||
) !void {
|
||||
return try impl.files.update(self.db, id, meta, alloc);
|
||||
}
|
||||
|
||||
|
@ -257,12 +264,9 @@ pub fn Services(comptime Db: type) type {
|
|||
pub fn createInvite(
|
||||
self: Self,
|
||||
alloc: std.mem.Allocator,
|
||||
created_by: Uuid,
|
||||
community_id: Uuid,
|
||||
name: []const u8,
|
||||
options: types.invites.CreateOptions,
|
||||
) !Invite {
|
||||
return try impl.invites.create(self.db, created_by, community_id, name, options, alloc);
|
||||
) !Uuid {
|
||||
return try impl.invites.create(self.db, options, alloc);
|
||||
}
|
||||
|
||||
pub fn getInvite(
|
||||
|
|
|
@ -7,7 +7,7 @@ const types = @import("./types.zig");
|
|||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
pub const Actor = types.Actor;
|
||||
pub const Actor = types.actors.Actor;
|
||||
|
||||
pub const CreateError = error{
|
||||
UsernameTaken,
|
||||
|
@ -124,7 +124,7 @@ pub const max_fields = 32;
|
|||
pub const max_display_name_len = 128;
|
||||
pub const max_bio = 1 << 16;
|
||||
|
||||
pub fn updateProfile(db: anytype, id: Uuid, new: Actor.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
|
||||
pub fn updateProfile(db: anytype, id: Uuid, new: types.actors.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
|
||||
var builder = sql.QueryBuilder.init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
|
|
|
@ -1,30 +1,11 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const sql = @import("sql");
|
||||
const types = @import("./types.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub const DriveOwner = union(enum) {
|
||||
user_id: Uuid,
|
||||
community_id: Uuid,
|
||||
};
|
||||
|
||||
pub const Entry = struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
file_id: ?Uuid,
|
||||
kind: Kind,
|
||||
};
|
||||
|
||||
pub const Kind = enum {
|
||||
dir,
|
||||
file,
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
const Entry = types.drive.DriveEntry;
|
||||
|
||||
pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator) !Entry {
|
||||
return (db.queryRow(Entry,
|
||||
|
@ -42,7 +23,7 @@ pub fn stat(db: anytype, owner: Uuid, path: []const u8, alloc: std.mem.Allocator
|
|||
}
|
||||
|
||||
/// Creates a file or directory
|
||||
pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Entry {
|
||||
pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_id: ?Uuid, alloc: std.mem.Allocator) !Uuid {
|
||||
if (name.len == 0) return error.EmptyName;
|
||||
|
||||
const id = Uuid.randV4(util.getThreadPrng());
|
||||
|
@ -66,18 +47,7 @@ pub fn create(db: anytype, owner: Uuid, dir: []const u8, name: []const u8, file_
|
|||
|
||||
try tx.commit();
|
||||
|
||||
const path = try std.mem.join(alloc, "/", if (dir.len == 0) &.{ "", name } else &.{ "", dir, name });
|
||||
errdefer alloc.free(path);
|
||||
|
||||
return Entry{
|
||||
.id = id,
|
||||
.owner_id = owner,
|
||||
.name = try util.deepClone(alloc, name),
|
||||
.path = path,
|
||||
.parent_directory_id = parent.id,
|
||||
.file_id = file_id,
|
||||
.kind = if (file_id) |_| .file else .dir,
|
||||
};
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn delete(db: anytype, id: Uuid, alloc: std.mem.Allocator) !void {
|
||||
|
|
|
@ -5,7 +5,9 @@ const types = @import("./types.zig");
|
|||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const FileUpload = types.FileUpload;
|
||||
const FileUpload = types.files.FileUpload;
|
||||
const CreateOptions = types.files.CreateOptions;
|
||||
const UpdateArgs = types.files.UpdateArgs;
|
||||
|
||||
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload {
|
||||
return try db.queryRow(
|
||||
|
@ -30,7 +32,7 @@ pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) !FileUpload {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem.Allocator) !void {
|
||||
pub fn update(db: anytype, id: Uuid, meta: UpdateArgs, alloc: std.mem.Allocator) !void {
|
||||
var builder = sql.QueryBuilder.init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
|
@ -57,7 +59,7 @@ pub fn update(db: anytype, id: Uuid, meta: FileUpload.UpdateArgs, alloc: std.mem
|
|||
}, alloc);
|
||||
}
|
||||
|
||||
pub fn create(db: anytype, owner_id: Uuid, meta: FileUpload.CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid {
|
||||
pub fn create(db: anytype, owner_id: Uuid, meta: CreateOptions, data: []const u8, alloc: std.mem.Allocator) !Uuid {
|
||||
const id = Uuid.randV4(util.getThreadPrng());
|
||||
const now = DateTime.now();
|
||||
try db.insert("file_upload", .{
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const sql = @import("sql");
|
||||
|
||||
const common = @import("./common.zig");
|
||||
const types = @import("./types.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
pub const Follow = struct {
|
||||
id: Uuid,
|
||||
|
||||
followed_by_id: Uuid,
|
||||
followee_id: Uuid,
|
||||
|
||||
created_at: DateTime,
|
||||
};
|
||||
const QueryArgs = types.follows.QueryArgs;
|
||||
const QueryResult = types.follows.QueryResult;
|
||||
const Follow = types.follows.Follow;
|
||||
|
||||
pub fn create(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.mem.Allocator) !void {
|
||||
if (Uuid.eql(followed_by_id, followee_id)) return error.SelfFollow;
|
||||
|
@ -46,41 +39,6 @@ pub fn delete(db: anytype, followed_by_id: Uuid, followee_id: Uuid, alloc: std.m
|
|||
|
||||
const max_max_items = 100;
|
||||
|
||||
pub const QueryArgs = struct {
|
||||
pub const Direction = common.Direction;
|
||||
pub const PageDirection = common.PageDirection;
|
||||
pub const Prev = std.meta.Child(std.meta.fieldInfo(@This(), .prev).field_type);
|
||||
|
||||
pub const OrderBy = enum {
|
||||
created_at,
|
||||
};
|
||||
|
||||
max_items: usize = 20,
|
||||
|
||||
followed_by_id: ?Uuid = null,
|
||||
followee_id: ?Uuid = null,
|
||||
|
||||
order_by: OrderBy = .created_at,
|
||||
|
||||
direction: Direction = .descending,
|
||||
|
||||
prev: ?struct {
|
||||
id: Uuid,
|
||||
order_val: union(OrderBy) {
|
||||
created_at: DateTime,
|
||||
},
|
||||
} = null,
|
||||
|
||||
page_direction: PageDirection = .forward,
|
||||
};
|
||||
|
||||
pub const QueryResult = struct {
|
||||
items: []Follow,
|
||||
|
||||
prev_page: QueryArgs,
|
||||
next_page: QueryArgs,
|
||||
};
|
||||
|
||||
pub fn query(db: anytype, args: QueryArgs, alloc: std.mem.Allocator) !QueryResult {
|
||||
var builder = sql.QueryBuilder.init(alloc);
|
||||
defer builder.deinit();
|
||||
|
|
|
@ -5,7 +5,7 @@ const types = @import("./types.zig");
|
|||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const Invite = types.Invite;
|
||||
const Invite = types.invites.Invite;
|
||||
|
||||
// 9 random bytes = 12 random b64
|
||||
const rand_len = 8;
|
||||
|
@ -16,10 +16,7 @@ const Decoder = std.base64.url_safe.Decoder;
|
|||
|
||||
pub fn create(
|
||||
db: anytype,
|
||||
created_by: Uuid,
|
||||
community_id: Uuid,
|
||||
name: []const u8,
|
||||
options: Invite.InternalCreateOptions,
|
||||
options: types.invites.CreateOptions,
|
||||
alloc: std.mem.Allocator,
|
||||
) !Uuid {
|
||||
const id = Uuid.randV4(util.getThreadPrng());
|
||||
|
@ -38,9 +35,9 @@ pub fn create(
|
|||
.{
|
||||
.id = id,
|
||||
|
||||
.created_by = created_by,
|
||||
.community_id = community_id,
|
||||
.name = name,
|
||||
.created_by = options.created_by,
|
||||
.community_id = options.community_id,
|
||||
.name = options.name,
|
||||
.code = code,
|
||||
|
||||
.max_uses = options.max_uses,
|
||||
|
|
|
@ -5,9 +5,9 @@ const types = @import("./types.zig");
|
|||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
const Note = types.Note;
|
||||
const QueryArgs = Note.QueryArgs;
|
||||
const QueryResult = types.QueryResult(Note);
|
||||
const Note = types.notes.Note;
|
||||
const QueryArgs = types.notes.QueryArgs;
|
||||
const QueryResult = types.notes.QueryResult;
|
||||
|
||||
pub const CreateError = error{
|
||||
DatabaseFailure,
|
||||
|
|
|
@ -208,6 +208,15 @@ pub const drive = struct {
|
|||
|
||||
pub const files = struct {
|
||||
pub const FileUpload = struct {
|
||||
pub const Status = enum {
|
||||
uploading,
|
||||
uploaded,
|
||||
external,
|
||||
deleted,
|
||||
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
|
||||
id: Uuid,
|
||||
|
||||
owner_id: Uuid,
|
||||
|
@ -224,15 +233,6 @@ pub const files = struct {
|
|||
updated_at: DateTime,
|
||||
};
|
||||
|
||||
pub const Status = enum {
|
||||
uploading,
|
||||
uploaded,
|
||||
external,
|
||||
deleted,
|
||||
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
|
||||
pub const CreateOptions = struct {
|
||||
filename: []const u8,
|
||||
description: ?[]const u8,
|
||||
|
@ -249,7 +249,7 @@ pub const files = struct {
|
|||
};
|
||||
|
||||
pub const invites = struct {
|
||||
const UseCount = usize;
|
||||
pub const UseCount = usize;
|
||||
pub const Invite = struct {
|
||||
id: Uuid,
|
||||
|
||||
|
@ -273,15 +273,10 @@ pub const invites = struct {
|
|||
|
||||
pub const jsonStringify = util.jsonSerializeEnumAsString;
|
||||
};
|
||||
pub const CreateOptions = struct {
|
||||
name: ?[]const u8 = null,
|
||||
max_uses: ?UseCount = null,
|
||||
lifespan: ?DateTime.Duration = null,
|
||||
kind: Kind = .user,
|
||||
to_community: ?Uuid = null,
|
||||
};
|
||||
|
||||
pub const InternalCreateOptions = struct {
|
||||
pub const CreateOptions = struct {
|
||||
created_by: Uuid,
|
||||
community_id: Uuid,
|
||||
name: ?[]const u8 = null,
|
||||
max_uses: ?UseCount = null,
|
||||
lifespan: ?DateTime.Duration = null,
|
||||
|
@ -333,7 +328,7 @@ pub const notes = struct {
|
|||
pub const Note = struct {
|
||||
id: Uuid,
|
||||
|
||||
author_id: actors.Actor, // TODO
|
||||
author: actors.Actor, // TODO
|
||||
content: []const u8,
|
||||
created_at: DateTime,
|
||||
|
||||
|
|
|
@ -1,5 +1,199 @@
|
|||
const services = @import("../services.zig");
|
||||
const util = @import("util");
|
||||
const services = @import("./services.zig");
|
||||
|
||||
const Uuid = util.Uuid;
|
||||
const DateTime = util.DateTime;
|
||||
|
||||
fn QueryResult(comptime R: type, comptime A: type) type {
|
||||
return struct {
|
||||
items: []R,
|
||||
|
||||
next_page: A,
|
||||
prev_page: A,
|
||||
};
|
||||
}
|
||||
|
||||
pub const auth = struct {
|
||||
pub const RegistrationOptions = struct {
|
||||
invite_code: ?[]const u8 = null,
|
||||
email: ?[]const u8 = null,
|
||||
};
|
||||
};
|
||||
|
||||
pub const actors = struct {
|
||||
pub const Actor = services.actors.Actor;
|
||||
pub const Actor = struct {
|
||||
id: Uuid,
|
||||
|
||||
username: []const u8,
|
||||
host: []const u8,
|
||||
|
||||
display_name: ?[]const u8,
|
||||
bio: []const u8,
|
||||
|
||||
avatar_file_id: ?Uuid,
|
||||
avatar_url: []const u8,
|
||||
|
||||
header_file_id: ?Uuid,
|
||||
header_url: ?[]const u8,
|
||||
|
||||
profile_fields: []const ProfileField,
|
||||
|
||||
community_id: Uuid,
|
||||
|
||||
created_at: DateTime,
|
||||
updated_at: DateTime,
|
||||
};
|
||||
pub const ProfileField = services.actors.ProfileField;
|
||||
pub const ProfileUpdateArgs = services.actors.ProfileUpdateArgs;
|
||||
};
|
||||
|
||||
pub const communities = struct {
|
||||
pub const Community = services.communities.Community;
|
||||
pub const QueryArgs = services.communities.QueryArgs;
|
||||
pub const QueryResult = services.communities.QueryResult;
|
||||
};
|
||||
|
||||
pub const drive = struct {
|
||||
pub const DriveEntry = union(enum) {
|
||||
pub const Kind = services.drive.DriveEntry.Kind;
|
||||
dir: struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
|
||||
kind: Kind = .dir,
|
||||
|
||||
// If null = not enumerated
|
||||
children: ?[]const DriveEntry,
|
||||
},
|
||||
file: struct {
|
||||
id: Uuid,
|
||||
owner_id: Uuid,
|
||||
name: ?[]const u8,
|
||||
path: []const u8,
|
||||
parent_directory_id: ?Uuid,
|
||||
|
||||
kind: Kind = .file,
|
||||
|
||||
meta: files.FileUpload,
|
||||
},
|
||||
};
|
||||
|
||||
pub const UploadArgs = struct {
|
||||
filename: []const u8,
|
||||
dir: []const u8,
|
||||
description: ?[]const u8,
|
||||
content_type: []const u8,
|
||||
sensitive: bool,
|
||||
};
|
||||
};
|
||||
|
||||
pub const files = struct {
|
||||
pub const FileUpload = services.files.FileUpload;
|
||||
pub const UpdateArgs = services.files.UpdateArgs;
|
||||
|
||||
pub const DerefResult = struct {
|
||||
meta: FileUpload,
|
||||
data: []const u8,
|
||||
};
|
||||
};
|
||||
|
||||
pub const follows = struct {
|
||||
pub const Follow = services.follows.Follow;
|
||||
|
||||
const QueryArgs = struct {
|
||||
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,
|
||||
};
|
||||
|
||||
pub const FollowerQueryArgs = QueryArgs;
|
||||
pub const FollowingQueryArgs = QueryArgs;
|
||||
|
||||
pub const FollowerQueryResult = QueryResult(Follow, FollowerQueryArgs);
|
||||
pub const FollowingQueryResult = QueryResult(Follow, FollowingQueryArgs);
|
||||
};
|
||||
|
||||
pub const invites = struct {
|
||||
pub const UseCount = services.invites.UseCount;
|
||||
pub const Invite = struct {
|
||||
id: Uuid,
|
||||
|
||||
created_by: Uuid, // User ID
|
||||
community_id: Uuid,
|
||||
name: []const u8,
|
||||
code: []const u8,
|
||||
url: []const u8,
|
||||
|
||||
created_at: DateTime,
|
||||
times_used: UseCount,
|
||||
|
||||
expires_at: ?DateTime,
|
||||
max_uses: ?UseCount,
|
||||
|
||||
kind: Kind,
|
||||
};
|
||||
pub const Kind = services.invites.Kind;
|
||||
pub const CreateOptions = struct {
|
||||
name: ?[]const u8 = null,
|
||||
lifespan: ?DateTime.Duration = null,
|
||||
max_uses: ?usize = null,
|
||||
|
||||
// admin only options
|
||||
kind: Kind = .user,
|
||||
to_community: ?Uuid = null,
|
||||
};
|
||||
};
|
||||
|
||||
pub const notes = struct {
|
||||
pub const Note = services.notes.Note;
|
||||
pub const QueryArgs = services.notes.QueryArgs;
|
||||
};
|
||||
|
||||
pub const timelines = struct {
|
||||
pub const TimelineArgs = struct {
|
||||
pub const PageDirection = notes.QueryArgs.PageDirection;
|
||||
pub const Prev = notes.QueryArgs.Prev;
|
||||
|
||||
max_items: usize = 20,
|
||||
|
||||
created_before: ?DateTime = null,
|
||||
created_after: ?DateTime = null,
|
||||
|
||||
prev: ?Prev = null,
|
||||
|
||||
page_direction: PageDirection = .forward,
|
||||
};
|
||||
|
||||
pub const TimelineResult = struct {
|
||||
items: []notes.Note,
|
||||
|
||||
prev_page: TimelineArgs,
|
||||
next_page: TimelineArgs,
|
||||
};
|
||||
};
|
||||
|
||||
pub const tokens = struct {
|
||||
pub const Token = struct {
|
||||
pub const Info = struct {
|
||||
account_id: Uuid,
|
||||
issued_at: DateTime,
|
||||
};
|
||||
|
||||
value: []const u8,
|
||||
info: Info,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -282,7 +282,7 @@ pub const Response = struct {
|
|||
pub fn template(self: *Self, status_code: http.Status, srv: anytype, comptime templ: []const u8, data: anytype) !void {
|
||||
try self.headers.put("Content-Type", "text/html");
|
||||
|
||||
const user = if (srv.context.userId()) |uid| try srv.getUser(uid) else null;
|
||||
const user = if (srv.context.userId()) |uid| try srv.getActor(uid) else null;
|
||||
defer util.deepFree(srv.allocator, user);
|
||||
|
||||
var stream = try self.open(status_code);
|
||||
|
|
|
@ -24,8 +24,10 @@ pub const verify_login = struct {
|
|||
pub const path = "/auth/login";
|
||||
|
||||
pub fn handler(_: anytype, res: anytype, srv: anytype) !void {
|
||||
const info = try srv.verifyAuthorization();
|
||||
|
||||
try res.json(.ok, info);
|
||||
if (srv.context.token_info) |token| {
|
||||
return try res.json(.ok, token);
|
||||
} else {
|
||||
return try res.status(.unauthorized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ const api = @import("api");
|
|||
const util = @import("util");
|
||||
const controller_utils = @import("../../controllers.zig").helpers;
|
||||
|
||||
const QueryArgs = api.Community.QueryArgs;
|
||||
const QueryArgs = api.communities.QueryArgs;
|
||||
|
||||
pub const create = struct {
|
||||
pub const method = .POST;
|
||||
|
@ -25,9 +25,9 @@ pub const query = struct {
|
|||
pub const path = "/communities";
|
||||
|
||||
pub const Query = struct {
|
||||
const OrderBy = api.Community.QueryArgs.OrderBy;
|
||||
const Direction = api.Community.QueryArgs.Direction;
|
||||
const PageDirection = api.Community.QueryArgs.PageDirection;
|
||||
const OrderBy = QueryArgs.OrderBy;
|
||||
const Direction = QueryArgs.Direction;
|
||||
const PageDirection = QueryArgs.PageDirection;
|
||||
|
||||
// Max items to fetch
|
||||
max_items: usize = 20,
|
||||
|
@ -80,7 +80,7 @@ pub const query = struct {
|
|||
});
|
||||
|
||||
const convert = struct {
|
||||
fn func(args: api.Community.QueryArgs) Query {
|
||||
fn func(args: QueryArgs) Query {
|
||||
return .{
|
||||
.max_items = args.max_items,
|
||||
.owner_id = args.owner_id,
|
||||
|
|
|
@ -4,7 +4,7 @@ pub const create = struct {
|
|||
pub const method = .POST;
|
||||
pub const path = "/invites";
|
||||
|
||||
pub const Body = api.InviteOptions;
|
||||
pub const Body = api.invites.CreateOptions;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
// No need to free because it will be freed when the api conn
|
||||
|
|
|
@ -2,11 +2,13 @@ const std = @import("std");
|
|||
const api = @import("api");
|
||||
const controller_utils = @import("../../controllers.zig").helpers;
|
||||
|
||||
const TimelineArgs = api.timelines.TimelineArgs;
|
||||
|
||||
pub const global = struct {
|
||||
pub const method = .GET;
|
||||
pub const path = "/timelines/global";
|
||||
|
||||
pub const Query = api.TimelineArgs;
|
||||
pub const Query = TimelineArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const results = try srv.globalTimeline(req.query);
|
||||
|
@ -18,7 +20,7 @@ pub const local = struct {
|
|||
pub const method = .GET;
|
||||
pub const path = "/timelines/local";
|
||||
|
||||
pub const Query = api.TimelineArgs;
|
||||
pub const Query = TimelineArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const results = try srv.localTimeline(req.query);
|
||||
|
@ -30,7 +32,7 @@ pub const home = struct {
|
|||
pub const method = .GET;
|
||||
pub const path = "/timelines/home";
|
||||
|
||||
pub const Query = api.TimelineArgs;
|
||||
pub const Query = TimelineArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const results = try srv.homeTimeline(req.query);
|
||||
|
|
|
@ -35,7 +35,7 @@ pub const get = struct {
|
|||
};
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const result = try srv.getUser(req.args.id);
|
||||
const result = try srv.getActor(req.args.id);
|
||||
defer util.deepFree(srv.allocator, result);
|
||||
|
||||
try res.json(.ok, result);
|
||||
|
@ -50,13 +50,13 @@ pub const update_profile = struct {
|
|||
id: util.Uuid,
|
||||
};
|
||||
|
||||
pub const Body = api.Actor.ProfileUpdateArgs;
|
||||
pub const Body = api.actors.ProfileUpdateArgs;
|
||||
|
||||
// TODO: I don't like that the request body dn response body are substantially different
|
||||
// TODO: I don't like that the request body and response body are substantially different
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
try srv.updateUserProfile(req.args.id, req.body);
|
||||
|
||||
const result = try srv.getUser(req.args.id);
|
||||
const result = try srv.getActor(req.args.id);
|
||||
defer util.deepFree(srv.allocator, result);
|
||||
|
||||
try res.json(.ok, result);
|
||||
|
|
|
@ -42,7 +42,7 @@ pub const query_followers = struct {
|
|||
id: Uuid,
|
||||
};
|
||||
|
||||
pub const Query = api.FollowingQueryArgs;
|
||||
pub const Query = api.follows.FollowingQueryArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const results = try srv.queryFollowers(req.args.id, req.query);
|
||||
|
@ -59,7 +59,7 @@ pub const query_following = struct {
|
|||
id: Uuid,
|
||||
};
|
||||
|
||||
pub const Query = api.FollowerQueryArgs;
|
||||
pub const Query = api.follows.FollowerQueryArgs;
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const results = try srv.queryFollowing(req.args.id, req.query);
|
||||
|
|
|
@ -114,14 +114,19 @@ const signup = struct {
|
|||
srv: anytype,
|
||||
) !void {
|
||||
const invite = if (invite_code) |code| srv.validateInvite(code) catch |err| switch (err) {
|
||||
error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv),
|
||||
//error.InvalidInvite => return servePage(null, "Invite is not valid", .bad_request, res, srv),
|
||||
else => |e| return e,
|
||||
} else null;
|
||||
defer util.deepFree(srv.allocator, invite);
|
||||
const creator = if (invite) |inv| try srv.getActor(inv.created_by) else null;
|
||||
defer util.deepFree(srv.allocator, creator);
|
||||
|
||||
try res.template(status, srv, tmpl, .{
|
||||
.error_msg = error_msg,
|
||||
.invite = invite,
|
||||
.invite = if (invite) |inv| .{
|
||||
.meta = inv,
|
||||
.creator = creator.?,
|
||||
} else null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -223,7 +228,7 @@ const user_details = struct {
|
|||
};
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const user = try srv.getUser(req.args.id);
|
||||
const user = try srv.getActor(req.args.id);
|
||||
defer util.deepFree(srv.allocator, user);
|
||||
|
||||
try res.template(.ok, srv, tmpl, user);
|
||||
|
@ -397,7 +402,8 @@ const cluster = struct {
|
|||
};
|
||||
|
||||
pub fn handler(req: anytype, res: anytype, srv: anytype) !void {
|
||||
const community = try srv.createCommunity(req.body.origin, req.body.name);
|
||||
const cid = try srv.createCommunity(req.body.origin, req.body.name);
|
||||
const community = try srv.getCommunity(cid);
|
||||
defer util.deepFree(srv.allocator, community);
|
||||
|
||||
const invite = try srv.createInvite(.{
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<div>
|
||||
<div>You are about to accept an invite from:</div>
|
||||
{#template mini-user $invite.creator}
|
||||
{#if @isTag($invite.kind, community_owner) =}
|
||||
{#if @isTag($invite.meta.kind, community_owner) =}
|
||||
<div>This act will make your new account the owner of { %community.name }.</div>
|
||||
{/if =}
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@
|
|||
</div>
|
||||
</label>
|
||||
{#if .invite |$invite| =}
|
||||
<input style="display: none" type="text" name="invite_code" value="{$invite.code}" />
|
||||
<input style="display: none" type="text" name="invite_code" value="{$invite.meta.code}" />
|
||||
{/if =}
|
||||
<button type="submit">Sign up</button>
|
||||
</form>
|
||||
|
|
Loading…
Reference in a new issue