fediglam/src/api/services/actors.zig

183 lines
5.4 KiB
Zig

const std = @import("std");
const util = @import("util");
const sql = @import("sql");
const common = @import("./common.zig");
const files = @import("./files.zig");
const types = @import("./types.zig");
const Uuid = util.Uuid;
const DateTime = util.DateTime;
pub const Actor = types.actors.Actor;
pub const CreateError = error{
UsernameTaken,
UsernameContainsInvalidChar,
UsernameTooLong,
UsernameEmpty,
DatabaseFailure,
};
pub const LookupError = error{
DatabaseFailure,
};
pub fn lookupByUsername(
db: anytype,
username: []const u8,
community_id: Uuid,
alloc: std.mem.Allocator,
) LookupError!?Uuid {
const row = db.queryRow(
std.meta.Tuple(&.{Uuid}),
\\SELECT id
\\FROM actor
\\WHERE username = $1 AND community_id = $2
\\LIMIT 1
,
.{ username, community_id },
alloc,
) catch |err| return switch (err) {
error.NoRows => null,
else => error.DatabaseFailure,
};
return row[0];
}
pub const max_username_chars = 32;
pub const UsernameValidationError = error{
UsernameContainsInvalidChar,
UsernameTooLong,
UsernameEmpty,
};
/// Usernames must satisfy:
/// - Be at least 1 character
/// - Be no more than 32 characters
/// - All characters are in [A-Za-z0-9_]
pub fn validateUsername(username: []const u8, lax: bool) UsernameValidationError!void {
if (username.len == 0) return error.UsernameEmpty;
if (username.len > max_username_chars) return error.UsernameTooLong;
for (username) |ch| {
const valid = std.ascii.isAlNum(ch) or ch == '_' or (lax and ch == '.');
if (!valid) return error.UsernameContainsInvalidChar;
}
}
pub fn create(
db: anytype,
username: []const u8,
community_id: Uuid,
lax_username: bool,
alloc: std.mem.Allocator,
) CreateError!Uuid {
const id = Uuid.randV4(util.getThreadPrng());
try validateUsername(username, lax_username);
db.insert("actor", .{
.id = id,
.username = username,
.community_id = community_id,
.created_at = DateTime.now(),
}, alloc) catch |err| return switch (err) {
error.UniqueViolation => error.UsernameTaken,
else => error.DatabaseFailure,
};
return id;
}
pub const GetError = error{ NotFound, DatabaseFailure };
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
return db.queryRow(
Actor,
\\SELECT
\\ actor.id,
\\ actor.username,
\\ community.host,
\\ actor.display_name,
\\ actor.bio,
\\ actor.avatar_file_id,
\\ actor.header_file_id,
\\ actor.profile_fields,
\\ actor.community_id,
\\ actor.created_at,
\\ actor.updated_at
\\FROM actor JOIN community
\\ ON actor.community_id = community.id
\\WHERE actor.id = $1
\\LIMIT 1
,
.{id},
alloc,
) catch |err| switch (err) {
error.NoRows => return error.NotFound,
else => |e| {
std.log.err("{}, {?}", .{ e, @errorReturnTrace() });
return error.DatabaseFailure;
},
};
}
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: types.actors.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
var builder = sql.QueryBuilder.init(alloc);
defer builder.deinit();
try builder.appendSlice("UPDATE actor");
if (new.display_name) |_| try builder.set("display_name", "$2");
if (new.bio) |_| try builder.set("bio", "$3");
if (new.avatar_file_id) |_| try builder.set("avatar_file_id", "$4");
if (new.header_file_id) |_| try builder.set("header_file_id", "$5");
if (new.profile_fields) |_| try builder.set("profile_fields", "$6");
if (builder.set_statements_appended == 0) return error.NoChange;
try builder.set("updated_at", "$7");
try builder.andWhere("id = $1");
const profile_fields = if (new.profile_fields) |pf| try std.json.stringifyAlloc(alloc, pf, .{}) else null;
defer if (profile_fields) |pf| alloc.free(pf);
const tx = try db.begin();
errdefer tx.rollback();
if (new.display_name) |maybe_dn| if (maybe_dn) |dn| {
if (dn.len > max_display_name_len) return error.DisplayNameTooLong;
};
if (new.bio) |b| if (b.len > max_bio) return error.BioTooLong;
if (new.avatar_file_id) |maybe_file_id| if (maybe_file_id) |file_id| {
const info = try files.get(tx, file_id, alloc);
defer util.deepFree(alloc, info);
if (!Uuid.eql(id, info.owner_id)) return error.FileAccessDenied;
if (info.status != .uploaded) return error.FileNotReady;
};
if (new.header_file_id) |maybe_file_id| if (maybe_file_id) |file_id| {
const info = try files.get(tx, file_id, alloc);
defer util.deepFree(alloc, info);
if (!Uuid.eql(id, info.owner_id)) return error.FileAccessDenied;
if (info.status != .uploaded) return error.FileNotReady;
};
if (new.profile_fields) |f| if (f.len > max_fields) return error.TooManyFields;
try tx.execWithOptions(try builder.terminate(), .{
id,
new.display_name orelse null,
new.bio orelse null,
new.avatar_file_id orelse null,
new.header_file_id orelse null,
profile_fields,
DateTime.now(),
}, .{ .allocator = alloc, .ignore_unused_arguments = true });
try tx.commit();
}