const std = @import("std"); const builtin = @import("builtin"); const util = @import("util"); const models = @import("../db/models.zig"); const getRandom = @import("../api.zig").getRandom; const Uuid = util.Uuid; const DateTime = util.DateTime; // 9 random bytes = 12 random b64 const rand_len = 8; const code_len = 12; const Encoder = std.base64.url_safe.Encoder; const Decoder = std.base64.url_safe.Decoder; pub const InviteType = enum { system, community_owner, user, pub const jsonStringify = defaultJsonStringify(@This()); }; fn defaultJsonStringify(comptime T: type) fn (T, std.json.StringifyOptions, anytype) anyerror!void { return struct { pub fn jsonStringify(s: T, _: std.json.StringifyOptions, writer: anytype) !void { return std.fmt.format(writer, "\"{s}\"", .{@tagName(s)}); } }.jsonStringify; } const InviteCount = u16; pub const Invite = struct { id: Uuid, created_by: Uuid, // User ID to_community: ?Uuid, name: []const u8, code: []const u8, created_at: DateTime, times_used: InviteCount, expires_at: ?DateTime, max_uses: ?InviteCount, invite_type: InviteType, }; const DbModel = struct { id: Uuid, created_by: Uuid, // User ID to_community: ?Uuid, name: []const u8, code: []const u8, created_at: DateTime, expires_at: ?DateTime, max_uses: ?InviteCount, @"type": InviteType, }; fn cloneStr(str: []const u8, alloc: std.mem.Allocator) ![]const u8 { const new = try alloc.alloc(u8, str.len); std.mem.copy(u8, new, str); return new; } pub const InviteOptions = struct { name: ?[]const u8 = null, max_uses: ?InviteCount = null, expires_at: ?DateTime = null, invite_type: InviteType = .user, }; pub fn create(db: anytype, created_by: Uuid, to_community: ?Uuid, options: InviteOptions, alloc: std.mem.Allocator) !Invite { var code_bytes: [rand_len]u8 = undefined; getRandom().bytes(&code_bytes); const code = try alloc.alloc(u8, code_len); errdefer alloc.free(code); _ = Encoder.encode(code, &code_bytes); const name = if (options.name) |name| try cloneStr(name, alloc) else try cloneStr(code, alloc); errdefer alloc.free(name); const id = Uuid.randV4(getRandom()); const created_at = DateTime.now(); try db.insert("invite", DbModel{ .id = id, .created_by = created_by, .to_community = to_community, .name = name, .code = code, .created_at = created_at, .expires_at = options.expires_at, .max_uses = options.max_uses, .@"type" = options.invite_type, }); return Invite{ .id = id, .created_by = created_by, .to_community = to_community, .name = name, .code = code, .created_at = created_at, .expires_at = options.expires_at, .times_used = 0, .max_uses = options.max_uses, .invite_type = options.invite_type, }; } pub fn getByCode(db: anytype, code: []const u8, alloc: std.mem.Allocator) !Invite { const code_clone = try cloneStr(code, alloc); const info = (try db.queryRow(std.meta.Tuple(&.{ Uuid, Uuid, Uuid, []const u8, DateTime, ?DateTime, InviteCount, ?InviteCount, InviteType }), \\SELECT \\ invite.id, invite.created_by, invite.to_community, invite.name, \\ invite.created_at, invite.expires_at, \\ COUNT(local_user.user_id) as uses, invite.max_uses, \\ invite.type \\FROM invite LEFT OUTER JOIN local_user ON invite.id = local_user.invite_id \\WHERE invite.code = $1 \\GROUP BY invite.id , .{code}, alloc)) orelse return error.NotFound; return Invite{ .id = info[0], .created_by = info[1], .to_community = info[2], .name = info[3], .code = code_clone, .created_at = info[4], .expires_at = info[5], .times_used = info[6], .max_uses = info[7], .invite_type = info[8], }; }