From e8fbc724853c057290a1e3ced120752bfa46c953 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Fri, 13 Dec 2024 05:02:21 -0500 Subject: [PATCH] add components :) --- README.md | 33 ++--- build.zig | 2 +- src/core.zig | 2 +- src/json.zig | 2 +- src/shard.zig | 12 +- src/structures/component.zig | 236 +++++++++++++++++++++++++++++++++++ src/structures/events.zig | 2 +- src/structures/message.zig | 4 +- src/structures/types.zig | 1 + test/test.zig | 15 ++- 10 files changed, 273 insertions(+), 36 deletions(-) create mode 100644 src/structures/component.zig diff --git a/README.md b/README.md index cf7f6ed..986d473 100644 --- a/README.md +++ b/README.md @@ -9,39 +9,40 @@ A high-performance bleeding edge Discord library in Zig, featuring full API cove ```zig const std = @import("std"); -const Discord = @import("discord.zig"); +const Discord = @import("discord"); const Shard = Discord.Shard; -const Intents = Discord.Intents; fn ready(_: *Shard, payload: Discord.Ready) !void { std.debug.print("logged in as {s}\n", .{payload.user.username}); } fn message_create(session: *Shard, message: Discord.Message) !void { - if (message.content) |mc| if (std.ascii.eqlIgnoreCase(mc, "!hi")) { - var result = try session.sendMessage(message.channel_id, .{ .content = "discord.zig best lib" }); + if (std.ascii.eqlIgnoreCase(message.content.?, "!hi")) { + var result = try session.sendMessage(message.channel_id, .{ + .content = "hello world from discord.zig", + }); defer result.deinit(); - switch (result.value) { - .left => |e| std.debug.panic("Error: {d}\r{s}\n", .{ e.code, e.message }), // or you may tell the end user the error - .right => |m| std.debug.print("Sent: {?s} sent by {s}\n", .{ m.content, m.author.username }), - } - }; + const m = result.value.unwrap(); + std.debug.print("sent: {?s}\n", .{m.content}); + } } pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 9999 }){}; - var handler = Discord.init(gpa.allocator()); + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + const allocator = gpa.allocator(); + + var handler = Discord.init(allocator); + defer handler.deinit(); + try handler.start(.{ - .token = std.posix.getenv("TOKEN").?, // or your token - .intents = Intents.fromRaw(53608447), // all intents + .intents = Discord.Intents.fromRaw(53608447); + .token = std.posix.getenv("DISCORD_TOKEN").?, .run = .{ .message_create = &message_create, .ready = &ready }, - .log = .yes, - .options = .{}, }); - errdefer handler.deinit(); } ``` + ## Installation ```zig // In your build.zig file diff --git a/build.zig b/build.zig index 00a534d..f57f8ff 100644 --- a/build.zig +++ b/build.zig @@ -38,7 +38,7 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); - marin.root_module.addImport("discord.zig", dzig); + marin.root_module.addImport("discord", dzig); marin.root_module.addImport("ws", websocket.module("websocket")); marin.root_module.addImport("zlib", zlib.module("zlib")); marin.root_module.addImport("deque", deque.module("zig-deque")); diff --git a/src/core.zig b/src/core.zig index 9b221fb..4d897dc 100644 --- a/src/core.zig +++ b/src/core.zig @@ -154,7 +154,7 @@ fn spawnBuckets(self: *Self) ![][]Shard { fn create(self: *Self, shard_id: usize) !Shard { if (self.shards.get(shard_id)) |s| return s; - const shard: Shard = try Shard.init(self.allocator, shard_id, .{ + const shard: Shard = try .init(self.allocator, shard_id, self.options.total_shards, .{ .token = self.shard_details.token, .intents = self.shard_details.intents, .options = Shard.ShardOptions{ diff --git a/src/json.zig b/src/json.zig index 916c96c..f522607 100644 --- a/src/json.zig +++ b/src/json.zig @@ -1011,7 +1011,7 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er const fieldname = switch (value) { .string => |slice| slice, - else => @panic("can only cast strings for untagged union"), + else => @panic("can only cast strings for tagged union"), }; inline for (unionInfo.fields) |u_field| { diff --git a/src/shard.zig b/src/shard.zig index 6006631..ee8cfe2 100644 --- a/src/shard.zig +++ b/src/shard.zig @@ -74,6 +74,7 @@ pub const ShardOptions = struct { ratelimit_options: RatelimitOptions = .{}, }; +total_shards: usize, id: usize, client: ws.Client, @@ -128,6 +129,7 @@ pub fn identify(self: *Self, properties: ?IdentifyProperties) SendError!void { .intents = self.details.intents.toRaw(), .properties = properties orelse default_identify_properties, .token = self.details.token, + .shard = &.{ self.id, self.total_shards }, }, }; try self.send(false, data); @@ -144,7 +146,7 @@ pub fn identify(self: *Self, properties: ?IdentifyProperties) SendError!void { } } -pub fn init(allocator: mem.Allocator, shard_id: usize, settings: struct { +pub fn init(allocator: mem.Allocator, shard_id: usize, total_shards: usize, settings: struct { token: []const u8, intents: Intents, options: ShardOptions, @@ -161,6 +163,7 @@ pub fn init(allocator: mem.Allocator, shard_id: usize, settings: struct { .ratelimit_options = settings.options.ratelimit_options, }, .id = shard_id, + .total_shards = total_shards, .allocator = allocator, .details = ShardDetails{ .token = settings.token, @@ -2799,12 +2802,7 @@ pub fn createSticker( defer req.deinit(); var files = .{file}; - return req.post2( - Types.Sticker, - path, - sticker, - &files, - ); + return req.post2(Types.Sticker, path, sticker, &files); } /// Modify the given sticker. diff --git a/src/structures/component.zig b/src/structures/component.zig new file mode 100644 index 0000000..e5d527e --- /dev/null +++ b/src/structures/component.zig @@ -0,0 +1,236 @@ +const Partial = @import("partial.zig").Partial; +const Snowflake = @import("snowflake.zig").Snowflake; +const Emoji = @import("emoji.zig").Emoji; +const ButtonStyles = @import("shared.zig").ButtonStyles; +const ChannelTypes = @import("shared.zig").ChannelTypes; +const MessageComponentTypes = @import("shared.zig").MessageComponentTypes; + +const zjson = @import("../json.zig"); +const std = @import("std"); + +/// https://discord.com/developers/docs/interactions/message-components#buttons +pub const Button = struct { + /// 2 for a button + type: MessageComponentTypes, + /// A button style + style: ButtonStyles, + /// Text that appears on the button; max 80 characters + label: ?[]const u8, + /// name, id, and animated + emoji: Partial(Emoji), + /// Developer-defined identifier for the button; max 100 characters + custom_id: ?[]const u8, + /// Identifier for a purchasable SKU, only available when using premium-style buttons + sku_id: ?Snowflake, + /// URL for link-style buttons + url: ?[]const u8, + /// Whether the button is disabled (defaults to false) + disabled: ?bool, +}; + +pub const SelectOption = struct { + /// User-facing name of the option; max 100 characters + label: []const u8, + /// Dev-defined value of the option; max 100 characters + value: []const u8, + /// Additional description of the option; max 100 characters + description: ?[]const u8, + /// id, name, and animated + emoji: ?Partial(Emoji), + /// Will show this option as selected by default + default: ?bool, +}; + +pub const DefaultValue = struct { + /// ID of a user, role, or channel + id: Snowflake, + /// Type of value that id represents. Either "user", "role", or "channel" + type: union(enum) { user, role, channel }, +}; + +/// https://discord.com/developers/docs/interactions/message-components#select-menus +pub const SelectMenuString = struct { + /// Type of select menu component (text: 3, user: 5, role: 6, mentionable: 7, channels: 8) + type: MessageComponentTypes, + /// ID for the select menu; max 100 characters + custom_id: []const u8, + /// Specified choices in a select menu (only required and available for string selects (type 3); max 25 + /// * options is required for string select menus (component type 3), and unavailable for all other select menu components. + options: ?[]SelectOption, + /// Placeholder text if nothing is selected; max 150 characters + placeholder: ?[]const u8, + /// Minimum number of items that must be chosen (defaults to 1); min 0, max 25 + min_values: ?usize, + /// Maximum number of items that can be chosen (defaults to 1); max 25 + max_values: ?usize, + /// Whether select menu is disabled (defaults to false) + disabled: ?bool, +}; + +/// https://discord.com/developers/docs/interactions/message-components#select-menus +pub const SelectMenuUsers = struct { + /// Type of select menu component (text: 3, user: 5, role: 6, mentionable: 7, channels: 8) + type: MessageComponentTypes, + /// ID for the select menu; max 100 characters + custom_id: []const u8, + /// Placeholder text if nothing is selected; max 150 characters + placeholder: ?[]const u8, + /// List of default values for auto-populated select menu components; number of default values must be in the range defined by min_values and max_values + /// *** default_values is only available for auto-populated select menu components, which include user (5), role (6), mentionable (7), and channel (8) components. + default_values: ?[]DefaultValue, + /// Minimum number of items that must be chosen (defaults to 1); min 0, max 25 + min_values: ?usize, + /// Maximum number of items that can be chosen (defaults to 1); max 25 + max_values: ?usize, + /// Whether select menu is disabled (defaults to false) + disabled: ?bool, +}; + +/// https://discord.com/developers/docs/interactions/message-components#select-menus +pub const SelectMenuRoles = struct { + /// Type of select menu component (text: 3, user: 5, role: 6, mentionable: 7, channels: 8) + type: MessageComponentTypes, + /// ID for the select menu; max 100 characters + custom_id: []const u8, + /// Placeholder text if nothing is selected; max 150 characters + placeholder: ?[]const u8, + /// List of default values for auto-populated select menu components; number of default values must be in the range defined by min_values and max_values + /// *** default_values is only available for auto-populated select menu components, which include user (5), role (6), mentionable (7), and channel (8) components. + default_values: ?[]DefaultValue, + /// Minimum number of items that must be chosen (defaults to 1); min 0, max 25 + min_values: ?usize, + /// Maximum number of items that can be chosen (defaults to 1); max 25 + max_values: ?usize, + /// Whether select menu is disabled (defaults to false) + disabled: ?bool, +}; + +/// https://discord.com/developers/docs/interactions/message-components#select-menus +pub const SelectMenuUsersAndRoles = struct { + /// Type of select menu component (text: 3, user: 5, role: 6, mentionable: 7, channels: 8) + type: MessageComponentTypes, + /// ID for the select menu; max 100 characters + custom_id: []const u8, + /// Placeholder text if nothing is selected; max 150 characters + placeholder: ?[]const u8, + /// List of default values for auto-populated select menu components; number of default values must be in the range defined by min_values and max_values + /// *** default_values is only available for auto-populated select menu components, which include user (5), role (6), mentionable (7), and channel (8) components. + default_values: ?[]DefaultValue, + /// Minimum number of items that must be chosen (defaults to 1); min 0, max 25 + min_values: ?usize, + /// Maximum number of items that can be chosen (defaults to 1); max 25 + max_values: ?usize, + /// Whether select menu is disabled (defaults to false) + disabled: ?bool, +}; + +/// https://discord.com/developers/docs/interactions/message-components#select-menus +pub const SelectMenuChannels = struct { + /// Type of select menu component (text: 3, user: 5, role: 6, mentionable: 7, channels: 8) + type: MessageComponentTypes, + /// ID for the select menu; max 100 characters + custom_id: []const u8, + /// List of channel types to include in the channel select component (type 8) + /// ** channel_types can only be used for channel select menu components. + channel_types: ?[]ChannelTypes, + /// Placeholder text if nothing is selected; max 150 characters + placeholder: ?[]const u8, + /// List of default values for auto-populated select menu components; number of default values must be in the range defined by min_values and max_values + /// *** default_values is only available for auto-populated select menu components, which include user (5), role (6), mentionable (7), and channel (8) components. + default_values: ?[]DefaultValue, + /// Minimum number of items that must be chosen (defaults to 1); min 0, max 25 + min_values: ?usize, + /// Maximum number of items that can be chosen (defaults to 1); max 25 + max_values: ?usize, + /// Whether select menu is disabled (defaults to false) + disabled: ?bool, +}; + +pub const SelectMenu = union(MessageComponentTypes) { + SelectMenu: SelectMenuString, + SelectMenuUsers: SelectMenuUsers, + SelectMenuRoles: SelectMenuRoles, + SelectMenuUsersAndRoles: SelectMenuUsersAndRoles, + SelectMenuChannels: SelectMenuChannels, + + pub fn toJson(allocator: std.mem.Allocator, value: zjson.JsonType) !@This() { + if (!value.is(.object)) + @panic("coulnd't match against non-object type"); + + switch (value.object.get("type") orelse @panic("couldn't find property `type`")) { + .number => |num| switch (num) { + .integer => |int| return switch (@as(MessageComponentTypes, @enumFromInt(int))) { + .SelectMenu => .{ .SelectMenu = try zjson.parseInto(SelectMenuString, allocator, value) }, + .SelectMenuUsers => .{ .SelectMenuUsers = try zjson.parseInto(SelectMenuUsers, allocator, value) }, + .SelectMenuRoles => .{ .SelectMenuRoles = try zjson.parseInto(SelectMenuRoles, allocator, value) }, + .SelectMenuUsersAndRoles => .{ .SelectMenuUsersAndRoles = try zjson.parseInto(SelectMenuUsersAndRoles, allocator, value) }, + .SelectMenuChannels => .{ .SelectMenuChannels = try zjson.parseInto(SelectMenuChannels, allocator, value) }, + else => unreachable, + }, + else => unreachable, + }, + else => @panic("got type but couldn't match against non enum member `type`"), + } + unreachable; + } +}; + +pub const InputTextStyles = enum(u4) { + Short = 1, + Paragraph, +}; + +pub const InputText = struct { + /// 4 for a text input + type: MessageComponentTypes, + /// Developer-defined identifier for the input; max 100 characters + custom_id: []const u8, + /// The Text Input Style + style: InputTextStyles, + /// Label for this component; max 45 characters + label: []const u8, + /// Minimum input length for a text input; min 0, max 4000 + min_length: ?usize, + /// Maximum input length for a text input; min 1, max 4000 + max_length: ?usize, + /// Whether this component is required to be filled (defaults to true) + required: ?bool, + /// Pre-filled value for this component; max 4000 characters + value: ?[]const u8, + /// Custom placeholder text if the input is empty; max 100 characters + placeholder: ?[]const u8, +}; + +pub const MessageComponent = union(MessageComponentTypes) { + ActionRow: []MessageComponent, + Button: Button, + SelectMenu: SelectMenuString, + InputText: InputText, + SelectMenuUsers: SelectMenuUsers, + SelectMenuRoles: SelectMenuRoles, + SelectMenuUsersAndRoles: SelectMenuUsersAndRoles, + SelectMenuChannels: SelectMenuChannels, + + pub fn toJson(allocator: std.mem.Allocator, value: zjson.JsonType) !@This() { + if (!value.is(.object)) + @panic("coulnd't match against non-object type"); + + switch (value.object.get("type") orelse @panic("couldn't find property `type`")) { + .number => |num| switch (num) { + .integer => |int| return switch (@as(MessageComponentTypes, @enumFromInt(int))) { + .ActionRow => .{ .ActionRow = try zjson.parseInto([]MessageComponent, allocator, value) }, + .Button => .{ .Button = try zjson.parseInto(Button, allocator, value) }, + .SelectMenu => .{ .SelectMenu = try zjson.parseInto(SelectMenuString, allocator, value) }, + .InputText => .{ .InputText = try zjson.parseInto(InputText, allocator, value) }, + .SelectMenuUsers => .{ .SelectMenuUsers = try zjson.parseInto(SelectMenuUsers, allocator, value) }, + .SelectMenuRoles => .{ .SelectMenuRoles = try zjson.parseInto(SelectMenuRoles, allocator, value) }, + .SelectMenuUsersAndRoles => .{ .SelectMenuUsersAndRoles = try zjson.parseInto(SelectMenuUsersAndRoles, allocator, value) }, + .SelectMenuChannels => .{ .SelectMenuChannels = try zjson.parseInto(SelectMenuChannels, allocator, value) }, + }, + else => unreachable, + }, + else => @panic("got type but couldn't match against non enum member `type`"), + } + unreachable; + } +}; diff --git a/src/structures/events.zig b/src/structures/events.zig index f2a8d7d..8019bb1 100644 --- a/src/structures/events.zig +++ b/src/structures/events.zig @@ -35,7 +35,7 @@ const ThreadMember = @import("thread.zig").ThreadMember; const Embed = @import("embed.zig").Embed; const WelcomeScreenChannel = @import("channel.zig").WelcomeScreenChannel; const AllowedMentions = @import("channel.zig").AllowedMentions; -const MessageComponent = @import("message.zig").MessageComponent; +const MessageComponent = @import("component.zig").MessageComponent; const Sticker = @import("sticker.zig").Sticker; const Partial = @import("partial.zig").Partial; const ReactionType = @import("message.zig").ReactionType; diff --git a/src/structures/message.zig b/src/structures/message.zig index 842bbb0..47bd5db 100644 --- a/src/structures/message.zig +++ b/src/structures/message.zig @@ -33,9 +33,7 @@ const Poll = @import("poll.zig").Poll; const AvatarDecorationData = @import("user.zig").AvatarDecorationData; const MessageActivityTypes = @import("shared.zig").MessageActivityTypes; const Partial = @import("partial.zig").Partial; - -/// TODO: fix this -pub const MessageComponent = isize; +const MessageComponent = @import("component.zig").MessageComponent; /// https://discord.com/developers/docs/resources/channel#message-object pub const Message = struct { diff --git a/src/structures/types.zig b/src/structures/types.zig index 3ef4781..fc762c1 100644 --- a/src/structures/types.zig +++ b/src/structures/types.zig @@ -27,6 +27,7 @@ pub usingnamespace @import("auditlog.zig"); pub usingnamespace @import("automod.zig"); pub usingnamespace @import("channel.zig"); pub usingnamespace @import("command.zig"); +pub usingnamespace @import("component.zig"); pub usingnamespace @import("embed.zig"); pub usingnamespace @import("emoji.zig"); pub usingnamespace @import("gateway.zig"); diff --git a/test/test.zig b/test/test.zig index abf3c04..b26fcb2 100644 --- a/test/test.zig +++ b/test/test.zig @@ -15,7 +15,7 @@ //! PERFORMANCE OF THIS SOFTWARE. const std = @import("std"); -const Discord = @import("discord.zig"); +const Discord = @import("discord"); const Shard = Discord.Shard; const Intents = Discord.Intents; @@ -26,18 +26,22 @@ fn ready(_: *Shard, payload: Discord.Ready) !void { } fn message_create(session: *Shard, message: Discord.Message) !void { - if (message.content) |mc| if (std.ascii.eqlIgnoreCase(mc, "!hi")) { + if (message.content != null and std.ascii.eqlIgnoreCase(message.content.?, "!hi")) { var result = try session.sendMessage(message.channel_id, .{ .content = "hi :)" }); defer result.deinit(); const m = result.value.unwrap(); std.debug.print("sent: {?s}\n", .{m.content}); - }; + } } pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ .stack_trace_frames = 9999 }){}; - var handler = Discord.init(gpa.allocator()); + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + const allocator = gpa.allocator(); + + var handler = Discord.init(allocator); + defer handler.deinit(); + try handler.start(.{ .token = std.posix.getenv("DISCORD_TOKEN").?, .intents = Intents.fromRaw(INTENTS), @@ -45,5 +49,4 @@ pub fn main() !void { .log = .yes, .options = .{}, }); - errdefer handler.deinit(); }