2022-09-07 23:14:52 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const util = @import("util");
|
2022-12-07 09:59:46 +00:00
|
|
|
const sql = @import("sql");
|
|
|
|
const common = @import("./common.zig");
|
|
|
|
const files = @import("./files.zig");
|
2023-01-03 01:21:08 +00:00
|
|
|
const types = @import("./types.zig");
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
const Uuid = util.Uuid;
|
2022-09-08 06:56:29 +00:00
|
|
|
const DateTime = util.DateTime;
|
2023-01-04 19:03:23 +00:00
|
|
|
pub const Actor = types.actors.Actor;
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
pub const CreateError = error{
|
|
|
|
UsernameTaken,
|
2022-10-04 02:41:59 +00:00
|
|
|
UsernameContainsInvalidChar,
|
|
|
|
UsernameTooLong,
|
|
|
|
UsernameEmpty,
|
|
|
|
DatabaseFailure,
|
2022-09-07 23:14:52 +00:00
|
|
|
};
|
|
|
|
|
2022-10-02 05:18:24 +00:00
|
|
|
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(
|
2022-09-29 21:52:01 +00:00
|
|
|
std.meta.Tuple(&.{Uuid}),
|
2022-10-02 05:18:24 +00:00
|
|
|
\\SELECT id
|
2022-10-12 03:06:29 +00:00
|
|
|
\\FROM actor
|
2022-10-02 05:18:24 +00:00
|
|
|
\\WHERE username = $1 AND community_id = $2
|
|
|
|
\\LIMIT 1
|
|
|
|
,
|
|
|
|
.{ username, community_id },
|
|
|
|
alloc,
|
|
|
|
) catch |err| return switch (err) {
|
|
|
|
error.NoRows => null,
|
|
|
|
else => error.DatabaseFailure,
|
|
|
|
};
|
2022-09-07 23:14:52 +00:00
|
|
|
|
2022-10-02 05:18:24 +00:00
|
|
|
return row[0];
|
2022-09-07 23:14:52 +00:00
|
|
|
}
|
|
|
|
|
2022-10-04 02:41:59 +00:00
|
|
|
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
|
2022-10-12 05:48:08 +00:00
|
|
|
/// - All characters are in [A-Za-z0-9_]
|
2022-12-10 06:33:39 +00:00
|
|
|
pub fn validateUsername(username: []const u8, lax: bool) UsernameValidationError!void {
|
2022-10-04 02:41:59 +00:00
|
|
|
if (username.len == 0) return error.UsernameEmpty;
|
|
|
|
if (username.len > max_username_chars) return error.UsernameTooLong;
|
|
|
|
|
|
|
|
for (username) |ch| {
|
2022-12-10 06:33:39 +00:00
|
|
|
const valid = std.ascii.isAlNum(ch) or ch == '_' or (lax and ch == '.');
|
2022-10-04 02:41:59 +00:00
|
|
|
if (!valid) return error.UsernameContainsInvalidChar;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-07 23:14:52 +00:00
|
|
|
pub fn create(
|
|
|
|
db: anytype,
|
|
|
|
username: []const u8,
|
2022-09-29 21:52:01 +00:00
|
|
|
community_id: Uuid,
|
2022-12-10 06:33:39 +00:00
|
|
|
lax_username: bool,
|
2022-10-02 05:18:24 +00:00
|
|
|
alloc: std.mem.Allocator,
|
2022-09-07 23:14:52 +00:00
|
|
|
) CreateError!Uuid {
|
2022-10-08 20:47:54 +00:00
|
|
|
const id = Uuid.randV4(util.getThreadPrng());
|
2022-09-07 23:14:52 +00:00
|
|
|
|
2022-12-10 06:33:39 +00:00
|
|
|
try validateUsername(username, lax_username);
|
2022-10-04 02:41:59 +00:00
|
|
|
|
2022-10-12 03:06:29 +00:00
|
|
|
db.insert("actor", .{
|
2022-09-07 23:14:52 +00:00
|
|
|
.id = id,
|
|
|
|
.username = username,
|
|
|
|
.community_id = community_id,
|
2022-10-12 02:19:34 +00:00
|
|
|
.created_at = DateTime.now(),
|
2022-10-02 05:18:24 +00:00
|
|
|
}, alloc) catch |err| return switch (err) {
|
|
|
|
error.UniqueViolation => error.UsernameTaken,
|
|
|
|
else => error.DatabaseFailure,
|
|
|
|
};
|
2022-09-07 23:14:52 +00:00
|
|
|
|
|
|
|
return id;
|
|
|
|
}
|
2022-09-08 06:56:29 +00:00
|
|
|
|
2022-11-10 09:53:09 +00:00
|
|
|
pub const GetError = error{ NotFound, DatabaseFailure };
|
|
|
|
pub fn get(db: anytype, id: Uuid, alloc: std.mem.Allocator) GetError!Actor {
|
2022-10-02 05:18:24 +00:00
|
|
|
return db.queryRow(
|
2022-10-12 03:06:29 +00:00
|
|
|
Actor,
|
2022-10-04 02:41:59 +00:00
|
|
|
\\SELECT
|
2022-10-12 03:06:29 +00:00
|
|
|
\\ actor.id,
|
|
|
|
\\ actor.username,
|
2022-10-04 02:41:59 +00:00
|
|
|
\\ community.host,
|
2022-12-07 07:12:11 +00:00
|
|
|
\\ actor.display_name,
|
|
|
|
\\ actor.bio,
|
|
|
|
\\ actor.avatar_file_id,
|
|
|
|
\\ actor.header_file_id,
|
|
|
|
\\ actor.profile_fields,
|
2022-10-12 03:06:29 +00:00
|
|
|
\\ actor.community_id,
|
2022-12-07 07:12:11 +00:00
|
|
|
\\ actor.created_at,
|
|
|
|
\\ actor.updated_at
|
2022-10-12 03:06:29 +00:00
|
|
|
\\FROM actor JOIN community
|
|
|
|
\\ ON actor.community_id = community.id
|
|
|
|
\\WHERE actor.id = $1
|
2022-09-08 06:56:29 +00:00
|
|
|
\\LIMIT 1
|
|
|
|
,
|
|
|
|
.{id},
|
|
|
|
alloc,
|
2022-10-02 05:18:24 +00:00
|
|
|
) catch |err| switch (err) {
|
2022-12-10 09:29:12 +00:00
|
|
|
error.NoRows => return error.NotFound,
|
|
|
|
else => |e| {
|
|
|
|
std.log.err("{}, {?}", .{ e, @errorReturnTrace() });
|
|
|
|
return error.DatabaseFailure;
|
|
|
|
},
|
2022-09-08 06:56:29 +00:00
|
|
|
};
|
|
|
|
}
|
2022-12-07 07:12:11 +00:00
|
|
|
|
|
|
|
pub const max_fields = 32;
|
2022-12-07 09:59:46 +00:00
|
|
|
pub const max_display_name_len = 128;
|
|
|
|
pub const max_bio = 1 << 16;
|
|
|
|
|
2023-01-04 19:03:23 +00:00
|
|
|
pub fn updateProfile(db: anytype, id: Uuid, new: types.actors.ProfileUpdateArgs, alloc: std.mem.Allocator) !void {
|
2022-12-07 09:59:46 +00:00
|
|
|
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();
|
|
|
|
}
|