From 519dd143b4445665f4fd6b816578c33db034fd86 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Tue, 10 Dec 2024 12:55:59 -0500 Subject: [PATCH] add X-Audit-Log-Reason header --- build.zig | 42 ++++++--- src/core.zig | 7 -- src/http.zig | 5 +- src/json.zig | 60 +++++++++---- src/shard.zig | 151 +++++++++++++++++++++++++------- src/structures/events.zig | 10 +-- src/structures/interaction.zig | 6 +- src/structures/monetization.zig | 2 +- src/structures/shared.zig | 120 ++++++++++++++++++++++--- src/structures/snowflake.zig | 21 ++++- 10 files changed, 328 insertions(+), 96 deletions(-) diff --git a/build.zig b/build.zig index a861601..8952a59 100644 --- a/build.zig +++ b/build.zig @@ -9,12 +9,6 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast }); - // this is your own program - const dzig = b.addModule("discord.zig", .{ - .root_source_file = b.path("src/discord.zig"), - .link_libc = true, - }); - const websocket = b.dependency("websocket", .{ .target = target, .optimize = optimize, @@ -27,6 +21,15 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const dzig = b.addModule("discord.zig", .{ + .root_source_file = b.path("src/discord.zig"), + .link_libc = true, + }); + + dzig.addImport("ws", websocket.module("websocket")); + dzig.addImport("zlib", zlib.module("zlib")); + dzig.addImport("deque", deque.module("zig-deque")); + const marin = b.addExecutable(.{ .name = "marin", .root_source_file = b.path("src/test.zig"), @@ -35,12 +38,6 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); - // now install your own executable after it's built correctly - - dzig.addImport("ws", websocket.module("websocket")); - dzig.addImport("zlib", zlib.module("zlib")); - dzig.addImport("deque", deque.module("zig-deque")); - marin.root_module.addImport("discord.zig", dzig); marin.root_module.addImport("ws", websocket.module("websocket")); marin.root_module.addImport("zlib", zlib.module("zlib")); @@ -54,4 +51,25 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + const lib = b.addStaticLibrary(.{ + .name = "discord.zig", + .root_source_file = b.path("src/discord.zig"), + .target = target, + .optimize = optimize, + }); + + lib.root_module.addImport("ws", websocket.module("websocket")); + lib.root_module.addImport("zlib", zlib.module("zlib")); + lib.root_module.addImport("deque", deque.module("zig-deque")); + + // docs + const docs_step = b.step("docs", "Generate documentation"); + const docs_install = b.addInstallDirectory(.{ + .source_dir = lib.getEmittedDocs(), + .install_dir = .prefix, + .install_subdir = "docs", + }); + + docs_step.dependOn(&docs_install.step); } diff --git a/src/core.zig b/src/core.zig index 8be9f53..b38f7fb 100644 --- a/src/core.zig +++ b/src/core.zig @@ -27,18 +27,11 @@ const std = @import("std"); const mem = std.mem; const debug = @import("internal.zig").debug; -pub const discord_epoch = 1420070400000; - /// Calculate and return the shard ID for a given guild ID pub inline fn calculateShardId(guild_id: Snowflake, shards: ?usize) u64 { return (guild_id.into() >> 22) % shards orelse 1; } -/// Convert a timestamp to a snowflake. -pub inline fn snowflakeToTimestamp(id: Snowflake) u64 { - return (id.into() >> 22) + discord_epoch; -} - const Self = @This(); shard_details: ShardDetails, diff --git a/src/http.zig b/src/http.zig index 2c3f87e..f9a27f1 100644 --- a/src/http.zig +++ b/src/http.zig @@ -61,8 +61,9 @@ pub const FetchReq = struct { self.body.deinit(); } - pub fn addHeader(self: *FetchReq, name: []const u8, value: []const u8) !void { - try self.extra_headers.append(http.Header{ .name = name, .value = value }); + pub fn addHeader(self: *FetchReq, name: []const u8, value: ?[]const u8) !void { + if (value) |some| + try self.extra_headers.append(http.Header{ .name = name, .value = some }); } pub fn addQueryParam(self: *FetchReq, name: []const u8, value: anytype) !void { diff --git a/src/json.zig b/src/json.zig index d62ac41..a6da487 100644 --- a/src/json.zig +++ b/src/json.zig @@ -941,8 +941,6 @@ pub fn repetition(comptime T: type, parser: Parser(T)) Parser([]T) { pub const Error = std.mem.Allocator.Error || ParserError; -/// doesn't work yet -/// but will someday pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Error!T { switch (@typeInfo(T)) { .void => return {}, @@ -950,15 +948,15 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er return value.bool; }, .int, .comptime_int => { - // std.debug.assert(value.number.is(.integer)); + std.debug.assert(value.number.is(.integer)); // attempting to cast an int against a non-int return value.number.cast(T); }, .float, .comptime_float => { - // std.debug.assert(value.number.is(.float)); + std.debug.assert(value.number.is(.float)); // attempting to cast a float against a non-float return value.number.cast(T); }, .null => { - // std.debug.assert(value.is(.null)); + std.debug.assert(value.is(.null)); // nullable or required property marked explicitly as null return null; }, .optional => |optionalInfo| { @@ -969,13 +967,38 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er if (std.meta.hasFn(T, "toJson")) { return try T.toJson(allocator, value); } - if (unionInfo.tag_type == null) - @compileError("Unable to parse into untagged union '" ++ @typeName(T) ++ "'"); var result: ?T = null; + + if (unionInfo.tag_type == null) { + switch (value) { + .string => |string| inline for (unionInfo.fields) |u_field| { + if (u_field.type == []const u8) + result = @unionInit(T, u_field.name, string); + }, + .bool => |bool_| inline for (unionInfo.fields) |u_field| { + if (u_field.type == bool) + result = @unionInit(T, u_field.name, bool_); + }, + .number => |number| inline for (unionInfo.fields) |u_field| { + switch (number) { + .integer => |i| if (u_field.type == @TypeOf(i)) { + result = @unionInit(T, u_field.name, i); + }, + .float => |f| if (u_field.type == @TypeOf(f)) { + result = @unionInit(T, u_field.name, f); + }, + } + }, + else => return error.TypeMismatch, // may only cast string, bool, number + } + + return result.?; + } + const fieldname = switch (value) { .string => |slice| slice, - else => @panic("can only cast strings"), + else => @panic("can only cast strings for untagged union"), }; inline for (unionInfo.fields) |u_field| { @@ -983,7 +1006,7 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er if (u_field.type == void) { result = @unionInit(T, u_field.name, {}); } else { - @panic("unions may only contain empty values"); + @panic("tagged unions may only contain empty values"); } } } @@ -1027,8 +1050,7 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er .array => |arrayInfo| { switch (value) { .string => |string| { - if (arrayInfo.child != u8) - return error.TypeMismatch; + if (arrayInfo.child != u8) return error.TypeMismatch; // attempting to cast an array of T against a string var r: T = undefined; var i: usize = 0; while (i < arrayInfo.len) : (i += 1) @@ -1047,8 +1069,7 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er }, .pointer => |ptrInfo| switch (ptrInfo.size) { .One => { - // we simply allocate the type and return an address instead - // of just returning the type + // we simply allocate the type and return an address instead of just returning the type const r: *ptrInfo.child = try allocator.create(ptrInfo.child); r.* = try parseInto(ptrInfo.child, allocator, value); return r; @@ -1056,8 +1077,8 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er .Slice => switch (value) { .array => |array| { var arraylist: std.ArrayList(ptrInfo.child) = .init(allocator); + try arraylist.ensureUnusedCapacity(array.len); for (array) |jsonval| { - try arraylist.ensureUnusedCapacity(1); const item = try parseInto(ptrInfo.child, allocator, jsonval); arraylist.appendAssumeCapacity(item); } @@ -1070,14 +1091,16 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er .string => |string| { if (ptrInfo.child == u8) { var arraylist: std.ArrayList(u8) = .init(allocator); - for (string) |char| { - try arraylist.ensureUnusedCapacity(1); + try arraylist.ensureUnusedCapacity(string.len); + + for (string) |char| arraylist.appendAssumeCapacity(char); - } + if (ptrInfo.sentinel) |some| { const sentinel = @as(*align(1) const ptrInfo.child, @ptrCast(some)).*; return try arraylist.toOwnedSliceSentinel(sentinel); } + if (ptrInfo.is_const) { arraylist.deinit(); return string; @@ -1089,11 +1112,12 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er return try arraylist.toOwnedSlice(); } }, - else => return error.TypeMismatch, + else => return error.TypeMismatch, // may only cast string, array }, else => { if (std.meta.hasFn(T, "toJson")) return T.toJson(allocator, value); + return error.TypeMismatch; // unsupported type }, }, else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"), diff --git a/src/shard.zig b/src/shard.zig index 8094066..94e9895 100644 --- a/src/shard.zig +++ b/src/shard.zig @@ -1508,13 +1508,15 @@ pub fn createChannel(self: *Self, guild_id: Snowflake, create_channel: Types.Cre /// Method to fetch a guild /// Returns the guild object for the given id. /// If `with_counts` is set to true, this endpoint will also return `approximate_member_count` and `approximate_presence_count` for the guild. -pub fn fetchGuild(self: *Self, guild_id: Snowflake) RequestFailedError!zjson.Owned(Types.Guild) { +pub fn fetchGuild(self: *Self, guild_id: Snowflake, with_counts: ?bool) RequestFailedError!zjson.Owned(Types.Guild) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addQueryParam("with_counts", with_counts); + const res = try req.get(Types.Guild, path); return res; } @@ -1582,7 +1584,6 @@ pub fn editGuildChannelPositions(self: *Self, guild_id: Snowflake, edit_guild_ch /// Method to get a guild's active threads /// Returns all active threads in the guild, including public and private threads. /// Threads are ordered by their `id`, in descending order. -/// TODO: implement query string parameters pub fn fetchGuildActiveThreads(self: *Self, guild_id: Snowflake) RequestFailedError!zjson.Owned(Types.Channel) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/threads/active", .{guild_id.into()}); @@ -1607,36 +1608,50 @@ pub fn fetchMember(self: *Self, guild_id: Snowflake, user_id: Snowflake) Request return res; } +pub const ListGuildMembersQuery = struct { + /// max number of members to return (1-1000) + limit: u16 = 1, + /// the highest user id in the previous page + after: Snowflake = Snowflake.from(0), +}; + /// Method to get the members of a guild /// Returns a list of guild member objects that are members of the guild. -/// TODO: implement query string parameters -pub fn fetchMembers(self: *Self, guild_id: Snowflake) RequestFailedError!zjson.Owned([]Types.Member) { +pub fn fetchMembers(self: *Self, guild_id: Snowflake, query: ListGuildMembersQuery) RequestFailedError!zjson.Owned([]Types.Member) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addQueryParam("limit", query.limit); + try req.addQueryParam("after", query.after); + const res = try req.get([]Types.Member, path); return res; } +pub const SearchGuildMembersQuery = struct { + /// Query string to match username(s) and nickname(s) against + query: []const u8, + /// max number of members to return (1-1000) + limit: u16, +}; + /// Method to find members /// Returns a list of guild member objects whose username or nickname starts with a provided string. -pub fn searchMembers(self: *Self, guild_id: Snowflake, query: struct { - query: []const u8, - limit: usize, -}) RequestFailedError!zjson.Owned([]Types.Member) { +pub fn searchMembers(self: *Self, guild_id: Snowflake, query: SearchGuildMembersQuery) RequestFailedError!zjson.Owned([]Types.Member) { var buf: [256]u8 = undefined; - const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/search?query={s}&limit={d}", .{ + const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/search", .{ guild_id.into(), - query.query, - query.limit, }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addQueryParam("query", query.query); + try req.addQueryParam("limit", query.limit); + const res = try req.get([]Types.Member, path); return res; } @@ -1663,49 +1678,63 @@ pub fn addMember(self: *Self, guild_id: Snowflake, user_id: Snowflake, credentia /// Returns a 200 OK with the guild member as the body. /// Fires a Guild Member Update Gateway event. If the channel_id is set to null, /// this will force the target user to be disconnected from voice. -pub fn editMember(self: *Self, guild_id: Snowflake, user_id: Snowflake, attributes: Types.ModifyGuildMember) RequestFailedError!?zjson.Owned(Types.Member) { +pub fn editMember( + self: *Self, + guild_id: Snowflake, + user_id: Snowflake, + attributes: Types.ModifyGuildMember, + reason: ?[]const u8, +) RequestFailedError!?zjson.Owned(Types.Member) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/{d}", .{ guild_id.into(), user_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const res = try req.patch(Types.Member, path, attributes); return res; } -pub fn editCurrentMember(self: *Self, guild_id: Snowflake, attributes: Types.ModifyGuildMember) RequestFailedError!?zjson.Owned(Types.Member) { +pub fn editCurrentMember(self: *Self, guild_id: Snowflake, attributes: Types.ModifyGuildMember, reason: ?[]const u8) RequestFailedError!?zjson.Owned(Types.Member) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/@me", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const res = try req.patch(Types.Member, path, attributes); return res; } /// change's someones's nickname -pub fn changeNickname(self: *Self, guild_id: Snowflake, user_id: Snowflake, attributes: Types.ModifyGuildMember) RequestFailedError!void { +pub fn changeNickname(self: *Self, guild_id: Snowflake, user_id: Snowflake, nick: []const u8, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/{d}", .{ guild_id.into(), user_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); - const res = try req.patch(Types.Member, path, attributes); + try req.addHeader("X-Audit-Log-Reason", reason); + + const res = try req.patch(Types.Member, path, .{ .nick = nick }); defer res.deinit(); } /// change's someones's nickname -pub fn changeMyNickname(self: *Self, guild_id: Snowflake, attributes: Types.ModifyGuildMember) RequestFailedError!void { +pub fn changeMyNickname(self: *Self, guild_id: Snowflake, nick: []const u8, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/@me", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); - const res = try req.patch(Types.Member, path, attributes); + try req.addHeader("X-Audit-Log-Reason", reason); + + const res = try req.patch(Types.Member, path, .{ .nick = nick }); defer res.deinit(); } @@ -1717,6 +1746,7 @@ pub fn addRole( guild_id: Snowflake, user_id: Snowflake, role_id: Snowflake, + reason: ?[]const u8, ) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/{d}/roles/{d}", .{ @@ -1728,6 +1758,8 @@ pub fn addRole( var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.put3(path); } @@ -1740,6 +1772,7 @@ pub fn removeRole( guild_id: Snowflake, user_id: Snowflake, role_id: Snowflake, + reason: ?[]const u8, ) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/{d}/roles/{d}", .{ @@ -1751,6 +1784,8 @@ pub fn removeRole( var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(path); } @@ -1762,6 +1797,7 @@ pub fn kickMember( self: *Self, guild_id: Snowflake, user_id: Snowflake, + reason: ?[]const u8, ) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/members/{d}", .{ guild_id.into(), user_id.into() }); @@ -1769,9 +1805,20 @@ pub fn kickMember( var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(path); } +/// Provide a user id to before and after for pagination. +/// Users will always be returned in ascending order by user.id. +/// If both before and after are provided, only before is respected. +pub const GetGuildBansQuery = struct { + limit: ?u16 = 1000, + before: ?Snowflake, + after: ?Snowflake, +}; + /// Returns a list of ban objects for the users banned from this guild. /// Requires the `BAN_MEMBERS` permission. /// TODO: add query params @@ -1806,26 +1853,30 @@ pub fn fetchBan(self: *Self, guild_id: Snowflake, user_id: Snowflake) RequestFai /// Requires the `BAN_MEMBERS` permission. /// Returns a 204 empty response on success. /// Fires a Guild Ban Add Gateway event. -pub fn ban(self: *Self, guild_id: Snowflake, user_id: Snowflake) RequestFailedError!void { +pub fn ban(self: *Self, guild_id: Snowflake, user_id: Snowflake, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/bans/{d}", .{ guild_id.into(), user_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.put3(path); } /// Remove the ban for a user. Requires the `BAN_MEMBERS` permissions. /// Returns a 204 empty response on success. /// Fires a Guild Ban Remove Gateway event. -pub fn unban(self: *Self, guild_id: Snowflake, user_id: Snowflake) RequestFailedError!void { +pub fn unban(self: *Self, guild_id: Snowflake, user_id: Snowflake, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/bans/{d}", .{ guild_id.into(), user_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(path); } @@ -1833,13 +1884,15 @@ pub fn unban(self: *Self, guild_id: Snowflake, user_id: Snowflake) RequestFailed /// Requires both the `BAN_MEMBERS` and `MANAGE_GUILD` permissions. /// Returns a 200 response on success, including the fields banned_users with the IDs of the banned users /// and failed_users with IDs that could not be banned or were already banned. -pub fn bulkBan(self: *Self, guild_id: Snowflake, bulk_ban: Types.CreateGuildBan) RequestFailedError!zjson.Owned(Types.BulkBan) { +pub fn bulkBan(self: *Self, guild_id: Snowflake, bulk_ban: Types.CreateGuildBan, reason: ?[]const u8) RequestFailedError!zjson.Owned(Types.BulkBan) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/bulk-ban", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const res = try req.post(Types.BulkBan, path, bulk_ban); return res; } @@ -1889,13 +1942,15 @@ pub fn createGuild(self: *Self, create_guild: Partial(Types.CreateGuild)) Reques /// Returns the new role object on success. /// Fires a Guild Role Create Gateway event. /// All JSON params are optional. -pub fn createRole(self: *Self, guild_id: Snowflake, create_role: Partial(Types.CreateGuildRole)) RequestFailedError!zjson.Owned(Types.Role) { +pub fn createRole(self: *Self, guild_id: Snowflake, create_role: Partial(Types.CreateGuildRole), reason: ?[]const u8) RequestFailedError!zjson.Owned(Types.Role) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/roles", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const res = try req.post(Types.Role, path, create_role); return res; } @@ -1904,13 +1959,21 @@ pub fn createRole(self: *Self, guild_id: Snowflake, create_role: Partial(Types.C /// Requires the `MANAGE_ROLES` permission. /// Returns a list of all of the guild's role objects on success. /// Fires multiple Guild Role Update Gateway events. -pub fn editRole(self: *Self, guild_id: Snowflake, role_id: Snowflake, edit_role: Partial(Types.ModifyGuildRole)) RequestFailedError!zjson.Owned(Types.Role) { +pub fn editRole( + self: *Self, + guild_id: Snowflake, + role_id: Snowflake, + edit_role: Partial(Types.ModifyGuildRole), + reason: ?[]const u8, +) RequestFailedError!zjson.Owned(Types.Role) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/roles/{d}", .{ guild_id.into(), role_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const res = try req.patch(Types.Role, path, edit_role); return res; } @@ -1919,13 +1982,15 @@ pub fn editRole(self: *Self, guild_id: Snowflake, role_id: Snowflake, edit_role: /// Requires guild ownership. /// Returns the updated level on success. /// Fires a Guild Update Gateway event. -pub fn modifyMFALevel(self: *Self, guild_id: Snowflake) RequestFailedError!void { +pub fn modifyMFALevel(self: *Self, guild_id: Snowflake, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/mfa", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(Types.Role, path); } @@ -1933,13 +1998,15 @@ pub fn modifyMFALevel(self: *Self, guild_id: Snowflake) RequestFailedError!void /// Requires the `MANAGE_ROLES` permission. /// Returns a 204 empty response on success. /// Fires a Guild Role Delete Gateway event. -pub fn deleteRole(self: *Self, guild_id: Snowflake, role_id: Snowflake) RequestFailedError!void { +pub fn deleteRole(self: *Self, guild_id: Snowflake, role_id: Snowflake, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/roles/{d}", .{ guild_id.into(), role_id.into() }); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(Types.Role, path); } @@ -1948,14 +2015,16 @@ pub fn deleteRole(self: *Self, guild_id: Snowflake, role_id: Snowflake) RequestF /// By default, prune will not remove users with roles. /// You can optionally include specific roles in your prune by providing the include_roles parameter. /// Any inactive user that has a subset of the provided role(s) will be counted in the prune and users with additional roles will not. -/// TODO: implement query -pub fn fetchPruneCount(self: *Self, guild_id: Snowflake, _: Types.GetGuildPruneCountQuery) RequestFailedError!zjson.Owned(struct { pruned: isize }) { +pub fn fetchPruneCount(self: *Self, guild_id: Snowflake, query: Types.GetGuildPruneCountQuery) RequestFailedError!zjson.Owned(struct { pruned: isize }) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/prune", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addQueryParam("days", query.days); + try req.addQueryParam("include_roles", query.include_roles); // needs fixing perhaps + const pruned = try req.get(struct { pruned: isize }, path); return pruned; } @@ -1969,13 +2038,20 @@ pub fn fetchPruneCount(self: *Self, guild_id: Snowflake, _: Types.GetGuildPruneC /// By default, prune will not remove users with roles. /// You can optionally include specific roles in your prune by providing the `include_roles` parameter. /// Any inactive user that has a subset of the provided role(s) will be included in the prune and users with additional roles will not. -pub fn beginGuildPrune(self: *Self, guild_id: Snowflake, params: Types.BeginGuildPrune) RequestFailedError!zjson.Owned(struct { pruned: isize }) { +pub fn beginGuildPrune( + self: *Self, + guild_id: Snowflake, + params: Types.BeginGuildPrune, + reason: ?[]const u8, +) RequestFailedError!zjson.Owned(struct { pruned: isize }) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/prune", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const pruned = try req.post(struct { pruned: isize }, path, params); return pruned; } @@ -2021,11 +2097,7 @@ pub fn fetchIntegrations(self: *Self, guild_id: Snowflake) RequestFailedError!zj /// Returns a list of integration objects for the guild. /// Requires the `MANAGE_GUILD` permission. -pub fn deleteIntegration( - self: *Self, - guild_id: Snowflake, - integration_id: Snowflake, -) RequestFailedError!void { +pub fn deleteIntegration(self: *Self, guild_id: Snowflake, integration_id: Snowflake, reason: ?[]const u8) RequestFailedError!void { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/integrations/{d}", .{ guild_id.into(), @@ -2035,6 +2107,8 @@ pub fn deleteIntegration( var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + try req.delete(path); } @@ -2056,13 +2130,15 @@ pub fn fetchWidgetSettings(self: *Self, guild_id: Snowflake) RequestFailedError! /// Requires the `MANAGE_GUILD` permission. /// Returns the updated guild widget settings object. /// Fires a Guild Update Gateway event. -pub fn editWidget(self: *Self, guild_id: Snowflake, attributes: Partial(Types.GuildWidget)) RequestFailedError!zjson.Owned(Types.GuildWidget) { +pub fn editWidget(self: *Self, guild_id: Snowflake, attributes: Partial(Types.GuildWidget), reason: ?[]const u8) RequestFailedError!zjson.Owned(Types.GuildWidget) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/widget", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const widget = try req.patch(Types.GuildWidget, path, attributes); return widget; } @@ -2129,13 +2205,20 @@ pub fn fetchOnboarding(self: *Self, guild_id: Snowflake) RequestFailedError!zjso } /// Returns the Onboarding object for the guild. -pub fn editOnboarding(self: *Self, guild_id: Snowflake, onboarding: Types.GuildOnboardingPromptOption) RequestFailedError!zjson.Owned(Types.GuildOnboarding) { +pub fn editOnboarding( + self: *Self, + guild_id: Snowflake, + onboarding: Types.GuildOnboardingPromptOption, + reason: ?[]const u8, +) RequestFailedError!zjson.Owned(Types.GuildOnboarding) { var buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/guilds/{d}/onboarding", .{guild_id.into()}); var req = FetchReq.init(self.allocator, self.details.token); defer req.deinit(); + try req.addHeader("X-Audit-Log-Reason", reason); + const ob = try req.put(Types.GuildOnboarding, path, onboarding); return ob; } diff --git a/src/structures/events.zig b/src/structures/events.zig index 763f6b8..f2a8d7d 100644 --- a/src/structures/events.zig +++ b/src/structures/events.zig @@ -159,7 +159,7 @@ pub const VoiceChannelEffectSend = struct { /// The ID of the emoji animation, for emoji reaction and soundboard effects animation_id: ?isize, /// The ID of the soundboard sound, for soundboard effects - sound_id: union(enum) { + sound_id: union { string: ?[]const u8, integer: isize, }, @@ -168,11 +168,11 @@ pub const VoiceChannelEffectSend = struct { }; /// https://discord.com/developers/docs/topics/gateway-events#voice-channel-effect-send-animation-types -pub const VoiceChannelEffectAnimationType = enum(u4) { +pub const VoiceChannelEffectAnimationType = enum { /// A fun animation, sent by a Nitro subscriber - Premium = 0, + Premium, /// The standard animation - Basic = 1, + Basic, }; /// https://discord.com/developers/docs/topics/gateway#invite-create @@ -663,7 +663,7 @@ pub const CreateMessage = struct { /// The components you would like to have sent in this message components: ?[]MessageComponent, /// IDs of up to 3 stickers in the server to send in the message - stickerIds: ?union(enum) { one: struct { []const u8 }, two: struct { []const u8 }, three: struct { []const u8 } }, + stickerIds: ?[][]const u8, }; /// https://discord.com/developers/docs/resources/guild#modify-guild-welcome-screen diff --git a/src/structures/interaction.zig b/src/structures/interaction.zig index 46a2525..8463422 100644 --- a/src/structures/interaction.zig +++ b/src/structures/interaction.zig @@ -208,11 +208,7 @@ pub const InteractionDataOption = struct { /// Value of application command option type type: ApplicationCommandOptionTypes, /// Value of the option resulting from user input - value: ?union(enum) { - string: []const u8, - bool: bool, - integer: isize, - }, + value: ?union { string: []const u8, bool: bool, integer: isize }, /// Present if this option is a group or subcommand options: ?[]InteractionDataOption, /// `true` if this option is the currently focused option for autocomplete diff --git a/src/structures/monetization.zig b/src/structures/monetization.zig index 26e6988..c971c67 100644 --- a/src/structures/monetization.zig +++ b/src/structures/monetization.zig @@ -95,7 +95,7 @@ pub const CreateTestEntitlement = struct { /// ID of the guild or user to grant the entitlement top owner_id: []const u8, /// 1 for a guild subscription, 2 for a user subscription - owner_type: enum(u8) { + owner_type: enum(u4) { guild_subscription = 1, user_subscription = 2, }, diff --git a/src/structures/shared.zig b/src/structures/shared.zig index ade0a63..0de15b9 100644 --- a/src/structures/shared.zig +++ b/src/structures/shared.zig @@ -345,9 +345,9 @@ pub const ActivityFlags = packed struct { }; /// https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors -pub const IntegrationExpireBehaviors = enum(u4) { - RemoveRole = 0, - Kick = 1, +pub const IntegrationExpireBehaviors = enum { + RemoveRole, + Kick, }; /// https://discord.com/developers/docs/topics/teams#data-models-membership-state-enum @@ -939,6 +939,7 @@ pub const ApplicationCommandPermissionTypes = enum(u4) { }; /// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags +/// Permissions v2 pub const BitwisePermissionFlags = packed struct { pub fn toRaw(self: BitwisePermissionFlags) u64 { return @bitCast(self); @@ -1053,7 +1054,106 @@ pub const BitwisePermissionFlags = packed struct { _pad: u15 = 0, }; -pub const PermissionStrings = BitwisePermissionFlags; +pub const PermissionStrings = union(enum) { + /// Allows creation of instant invites + CREATE_INSTANT_INVITE, + /// Allows kicking members + KICK_MEMBERS, + /// Allows banning members + BAN_MEMBERS, + /// Allows all permissions and bypasses channel permission overwrites + ADMINISTRATOR, + /// Allows management and editing of channels + MANAGE_CHANNELS, + /// Allows management and editing of the guild + MANAGE_GUILD, + /// Allows for the addition of reactions to messages + ADD_REACTIONS, + /// Allows for viewing of audit logs + VIEW_AUDIT_LOG, + /// Allows for using priority speaker in a voice channel + PRIORITY_SPEAKER, + /// Allows the user to go live + STREAM, + /// Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels + VIEW_CHANNEL, + /// Allows for sending messages in a channel. (does not allow sending messages in threads) + SEND_MESSAGES, + /// Allows for sending of /tts messages + SEND_TTS_MESSAGES, + /// Allows for deletion of other users messages + MANAGE_MESSAGES, + /// Links sent by users with this permission will be auto-embedded + EMBED_LINKS, + /// Allows for uploading images and files + ATTACH_FILES, + /// Allows for reading of message history + READ_MESSAGE_HISTORY, + /// Allows for using the \@everyone tag to notify all users in a channel, and the \@here tag to notify all online users in a channel + MENTION_EVERYONE, + /// Allows the usage of custom emojis from other servers + USE_EXTERNAL_EMOJIS, + /// Allows for viewing guild insights + VIEW_GUILD_INSIGHTS, + /// Allows for joining of a voice channel + CONNECT, + /// Allows for speaking in a voice channel + SPEAK, + /// Allows for muting members in a voice channel + MUTE_MEMBERS, + /// Allows for deafening of members in a voice channel + DEAFEN_MEMBERS, + /// Allows for moving of members between voice channels + MOVE_MEMBERS, + /// Allows for using voice-activity-detection in a voice channel + USE_VAD, + /// Allows for modification of own nickname + CHANGE_NICKNAME, + /// Allows for modification of other users nicknames + MANAGE_NICKNAMES, + /// Allows management and editing of roles + MANAGE_ROLES, + /// Allows management and editing of webhooks + MANAGE_WEBHOOKS, + /// Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users + MANAGE_GUILD_EXPRESSIONS, + /// Allows members to use application commands in text channels + USE_SLASH_COMMANDS, + /// Allows for requesting to speak in stage channels. + REQUEST_TO_SPEAK, + /// Allows for editing and deleting scheduled events created by all users + MANAGE_EVENTS, + /// Allows for deleting and archiving threads, and viewing all private threads + MANAGE_THREADS, + /// Allows for creating public and announcement threads + CREATE_PUBLIC_THREADS, + /// Allows for creating private threads + CREATE_PRIVATE_THREADS, + /// Allows the usage of custom stickers from other servers + USE_EXTERNAL_STICKERS, + /// Allows for sending messages in threads + SEND_MESSAGES_IN_THREADS, + /// Allows for launching activities (applications with the `EMBEDDED` flag) in a voice channel. + USE_EMBEDDED_ACTIVITIES, + /// Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels + MODERATE_MEMBERS, + /// Allows for viewing role subscription insights. + VIEW_CREATOR_MONETIZATION_ANALYTICS, + /// Allows for using soundboard in a voice channel. + USE_SOUNDBOARD, + /// Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user + CREATE_GUILD_EXPRESSIONS, + /// Allows for creating scheduled events, and editing and deleting those created by the current user + CREATE_EVENTS, + /// Allows the usage of custom soundboards sounds from other servers + USE_EXTERNAL_SOUNDS, + /// Allows sending voice messages + SEND_VOICE_MESSAGES, + /// Allows sending polls + SEND_POLLS, + /// Allows user-installed apps to send public responses. When disabled, users will still be allowed to use their apps but the responses will be ephemeral. This only applies to apps not also installed to the server. + USE_EXTERNAL_APPS, +}; /// https://discord.com/developers/docs/topics/opcodes-and-status-codes#opcodes-and-status-codes pub const GatewayCloseEventCodes = enum(u16) { @@ -1385,13 +1485,13 @@ pub const SortOrderTypes = enum { CreationDate, }; -pub const ForumLayout = enum(u4) { +pub const ForumLayout = enum { /// No default has been set for forum channel. - NotSet = 0, + NotSet, /// Display posts as a list. - ListView = 1, + ListView, /// Display posts as a collection of tiles. - GalleryView = 2, + GalleryView, }; /// https://discord.com/developers/docs/reference#image-formatting @@ -1408,7 +1508,7 @@ pub const ImageFormat = union(enum) { /// https://discord.com/developers/docs/reference#image-formatting pub const ImageSize = isize; -pub const Locales = enum { +pub const Locales = union(enum) { id, da, de, @@ -1444,7 +1544,7 @@ pub const Locales = enum { }; /// https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes -pub const OAuth2Scope = enum { +pub const OAuth2Scope = union(enum) { /// /// Allows your app to fetch data from a user's "Now Playing/Recently Played" list /// diff --git a/src/structures/snowflake.zig b/src/structures/snowflake.zig index 912cd28..3c9e448 100644 --- a/src/structures/snowflake.zig +++ b/src/structures/snowflake.zig @@ -17,6 +17,13 @@ const std = @import("std"); const zjson = @import("../json.zig"); +/// Milliseconds since Discord Epoch, the first second of 2015 or 1420070400000. +pub const discord_epoch = 1420070400000; + +/// Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs). +/// These IDs are guaranteed to be unique across all of Discord, except in some unique scenarios in which child objects share their parent's ID. +/// Because Snowflake IDs are up to 64 bits in size (e.g. a uint64), they are always returned as strings in the HTTP API to prevent integer overflows in some languages. +/// See Gateway ETF/JSON for more information regarding Gateway encoding. pub const Snowflake = enum(u64) { _, @@ -46,14 +53,24 @@ pub const Snowflake = enum(u64) { return array.toOwnedSlice(); } + /// zjson parse pub fn toJson(_: std.mem.Allocator, value: zjson.JsonType) !@This() { if (value.is(.string)) return Snowflake.fromRaw(value.string) catch std.debug.panic("invalid snowflake: {s}\n", .{value.string}); unreachable; } + /// print pub fn format(self: Snowflake, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { - var buf: [256]u8 = undefined; - try writer.writeAll(try std.fmt.bufPrint(&buf, "{d}\n", .{self.into()})); + try writer.print("{d}", .{self.into()}); + } + + /// std.json stringify + pub fn jsonStringify(self: Snowflake, _: std.json.StringifyOptions, writer: anytype) !void { + try writer.print("\"{d}\"", .{self.into()}); + } + + pub fn toTimestamp(self: Snowflake) u64 { + return (self.into() >> 22) + discord_epoch; } };