Compare commits
10 Commits
acc539c342
...
b8c42bb22c
Author | SHA1 | Date | |
---|---|---|---|
b8c42bb22c | |||
195c5945eb | |||
![]() |
84bf0bac63 | ||
![]() |
cbc9245de8 | ||
d449cdeea1 | |||
ed968040fe | |||
4f1d1f7d69 | |||
21cf72449f | |||
fb8ce159d3 | |||
52a0394603 |
@ -39,6 +39,7 @@ pub fn main() !void {
|
||||
.intents = Discord.Intents.fromRaw(53608447),
|
||||
.token = std.posix.getenv("DISCORD_TOKEN").?,
|
||||
.run = .{ .message_create = &message_create, .ready = &ready },
|
||||
.cache = Discord.CacheTables.defaults(allocator),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
@ -16,9 +16,8 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
const zlib = b.dependency("zlib", .{});
|
||||
|
||||
|
||||
const dzig = b.addModule("discord.zig", .{
|
||||
.root_source_file = b.path("src/discord.zig"),
|
||||
.root_source_file = b.path("src/lib.zig"),
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
@ -48,7 +47,7 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "discord.zig",
|
||||
.root_source_file = b.path("src/discord.zig"),
|
||||
.root_source_file = b.path("lib/discord.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
@ -28,10 +28,7 @@
|
||||
.url = "https://github.com/yuzudev/zig-zlib/archive/refs/heads/main.zip",
|
||||
.hash = "zlib-0.1.0-AAAAAKm6QADNBB6NBPHanW9G0EOPTmgJsRO6NFT__arp",
|
||||
},
|
||||
.websocket = .{
|
||||
.url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.zip",
|
||||
.hash = "websocket-0.1.0-ZPISdYBIAwB1yO6AFDHRHLaZSmpdh4Bz4dCmaQUqNNWh"
|
||||
},
|
||||
.websocket = .{ .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.zip", .hash = "websocket-0.1.0-ZPISdYBIAwB1yO6AFDHRHLaZSmpdh4Bz4dCmaQUqNNWh" },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
|
201
src/cache/cache.zig
vendored
Normal file
201
src/cache/cache.zig
vendored
Normal file
@ -0,0 +1,201 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// defaults are to be overridden by the end user
|
||||
/// otherwise, simply do TableTemplate{}
|
||||
/// this is a template for the cache tables
|
||||
pub const TableTemplate = struct {
|
||||
comptime User: type = @import("../config.zig").StoredUser,
|
||||
comptime Guild: type = @import("../config.zig").StoredGuild,
|
||||
comptime Channel: type = @import("../config.zig").StoredChannel,
|
||||
comptime Emoji: type = @import("../config.zig").StoredEmoji,
|
||||
comptime Message: type = @import("../config.zig").StoredMessage,
|
||||
comptime Role: type = @import("../config.zig").StoredRole,
|
||||
comptime Sticker: type = @import("../config.zig").StoredSticker,
|
||||
comptime Reaction: type = @import("../config.zig").StoredReaction,
|
||||
comptime Member: type = @import("../config.zig").StoredMember,
|
||||
comptime Thread: type = @import("../config.zig").StoredChannel,
|
||||
};
|
||||
|
||||
// by default this caches everything
|
||||
// therefore we'll allow custom cache tables
|
||||
pub fn CacheTables(comptime Table: TableTemplate) type {
|
||||
return struct {
|
||||
const Snowflake = @import("../structures/snowflake.zig").Snowflake;
|
||||
|
||||
const StoredUser: type = Table.User;
|
||||
const StoredGuild: type = Table.Guild;
|
||||
const StoredChannel: type = Table.Channel;
|
||||
const StoredEmoji: type = Table.Emoji;
|
||||
const StoredMessage: type = Table.Message;
|
||||
const StoredRole: type = Table.Role;
|
||||
const StoredSticker: type = Table.Sticker;
|
||||
const StoredReaction: type = Table.Reaction;
|
||||
const StoredMember: type = Table.Member;
|
||||
const StoredThread: type = Table.Thread;
|
||||
|
||||
users: CacheLike(Snowflake, StoredUser),
|
||||
guilds: CacheLike(Snowflake, StoredGuild),
|
||||
channels: CacheLike(Snowflake, StoredChannel),
|
||||
emojis: CacheLike(Snowflake, StoredEmoji),
|
||||
messages: CacheLike(Snowflake, StoredMessage),
|
||||
roles: CacheLike(Snowflake, StoredRole),
|
||||
stickers: CacheLike(Snowflake, StoredSticker),
|
||||
reactions: CacheLike(Snowflake, StoredReaction),
|
||||
members: CacheLike(Snowflake, StoredMember),
|
||||
threads: CacheLike(Snowflake, StoredThread),
|
||||
|
||||
/// you can customize with your own cache
|
||||
pub fn defaults(allocator: std.mem.Allocator) CacheTables(Table) {
|
||||
var users = DefaultCache(Snowflake, StoredUser).init(allocator);
|
||||
var guilds = DefaultCache(Snowflake, StoredGuild).init(allocator);
|
||||
var channels = DefaultCache(Snowflake, StoredChannel).init(allocator);
|
||||
var emojis = DefaultCache(Snowflake, StoredEmoji).init(allocator);
|
||||
var messages = DefaultCache(Snowflake, StoredMessage).init(allocator);
|
||||
var roles = DefaultCache(Snowflake, StoredRole).init(allocator);
|
||||
var stickers = DefaultCache(Snowflake, StoredSticker).init(allocator);
|
||||
var reactions = DefaultCache(Snowflake, StoredReaction).init(allocator);
|
||||
var members = DefaultCache(Snowflake, StoredMember).init(allocator);
|
||||
var threads = DefaultCache(Snowflake, StoredChannel).init(allocator);
|
||||
|
||||
return .{
|
||||
.users = users.cache(),
|
||||
.guilds = guilds.cache(),
|
||||
.channels = channels.cache(),
|
||||
.emojis = emojis.cache(),
|
||||
.messages = messages.cache(),
|
||||
.roles = roles.cache(),
|
||||
.stickers = stickers.cache(),
|
||||
.reactions = reactions.cache(),
|
||||
.members = members.cache(),
|
||||
.threads = threads.cache(),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// this is an extensible cache, you may implement your own
|
||||
/// I recommend "zigache" to be used
|
||||
pub fn CacheLike(comptime K: type, comptime V: type) type {
|
||||
return struct {
|
||||
ptr: *anyopaque,
|
||||
putFn: *const fn(*anyopaque, K, V) anyerror!void,
|
||||
getFn: *const fn(*anyopaque, K) ?V,
|
||||
removeFn: *const fn(*anyopaque, K) void,
|
||||
containsFn: *const fn(*anyopaque, K) bool,
|
||||
countFn: *const fn(*anyopaque) usize,
|
||||
|
||||
pub fn put(self: CacheLike(K, V), key: K, value: V) !void {
|
||||
return self.putFn(self.ptr, key, value);
|
||||
}
|
||||
|
||||
pub fn get(self: CacheLike(K, V), key: K) ?V {
|
||||
return self.getFn(self.ptr, key);
|
||||
}
|
||||
|
||||
pub fn remove(self: CacheLike(K, V), key: K) void {
|
||||
self.removeFn(self.ptr, key);
|
||||
}
|
||||
|
||||
pub fn contains(self: CacheLike(K, V), key: K) bool {
|
||||
return self.containsFn(self.ptr, key);
|
||||
}
|
||||
|
||||
pub fn count(self: CacheLike(K, V)) usize {
|
||||
return self.countFn(self.ptr);
|
||||
}
|
||||
|
||||
pub fn init(ptr: *const anyopaque) CacheLike(K, V) {
|
||||
const T = @TypeOf(ptr);
|
||||
const ptr_info = @typeInfo(T);
|
||||
|
||||
const gen = struct {
|
||||
// we don't care about order
|
||||
map: std.AutoHashMap(K, V),
|
||||
allocator: *std.mem.Allocator,
|
||||
|
||||
pub fn put(pointer: *anyopaque, key: K, value: V) anyerror!void {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.put(self, key, value);
|
||||
}
|
||||
|
||||
pub fn get(pointer: *anyopaque, key: K) ?V {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.get(self, key);
|
||||
}
|
||||
|
||||
pub fn remove(pointer: *anyopaque, key: K) void {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.remove(self, key);
|
||||
}
|
||||
|
||||
pub fn contains(pointer: *anyopaque, key: K) bool {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.contains(self, key);
|
||||
}
|
||||
|
||||
pub fn count(pointer: *anyopaque) usize {
|
||||
const self: T = @ptrCast(@alignCast(pointer));
|
||||
return ptr_info.pointer.child.count(self);
|
||||
}
|
||||
};
|
||||
|
||||
return .{
|
||||
.ptr = ptr,
|
||||
.putFn = gen.put,
|
||||
.getFn = gen.get,
|
||||
.removeFn = gen.remove,
|
||||
.containsFn = gen.contains,
|
||||
.countFn = gen.count,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn DefaultCache(comptime K: type, comptime V: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
map: std.AutoHashMap(K, V),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) DefaultCache(K, V) {
|
||||
return .{ .allocator = allocator, .map = .init(allocator) };
|
||||
}
|
||||
|
||||
pub fn cache(self: *Self) CacheLike(K, V) {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.putFn = put,
|
||||
.getFn = get,
|
||||
.removeFn = remove,
|
||||
.containsFn = contains,
|
||||
.countFn = count,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn put(ptr: *anyopaque, key: K, value: V) !void {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
return self.map.put(key, value);
|
||||
}
|
||||
|
||||
pub fn get(ptr: *anyopaque, key: K) ?V {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
return self.map.get(key);
|
||||
}
|
||||
|
||||
pub fn remove(ptr: *anyopaque, key: K) void {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
_ = self.map.remove(key);
|
||||
}
|
||||
|
||||
pub fn contains(ptr: *anyopaque, key: K) bool {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
return self.map.contains(key);
|
||||
}
|
||||
|
||||
pub fn count(ptr: *anyopaque) usize {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
return self.map.count();
|
||||
}
|
||||
};
|
||||
}
|
300
src/config.zig
Normal file
300
src/config.zig
Normal file
@ -0,0 +1,300 @@
|
||||
// custom file for configurations you might want
|
||||
|
||||
const PremiumTypes = @import("./structures/shared.zig").PremiumTypes;
|
||||
const Snowflake = @import("./structures/snowflake.zig").Snowflake;
|
||||
const AvatarDecorationData = @import("./structures/user.zig").AvatarDecorationData;
|
||||
|
||||
|
||||
/// https://discord.com/developers/docs/resources/user#user-object
|
||||
/// modify this to your liking
|
||||
pub const StoredUser = struct {
|
||||
username: []const u8,
|
||||
global_name: ?[]const u8 = null,
|
||||
locale: ?[]const u8 = null,
|
||||
flags: ?isize = null,
|
||||
premium_type: ?PremiumTypes = null,
|
||||
public_flags: ?isize = null,
|
||||
accent_color: ?isize = null,
|
||||
id: Snowflake,
|
||||
discriminator: []const u8,
|
||||
avatar: ?[]const u8 = null,
|
||||
bot: ?bool = null,
|
||||
system: ?bool = null,
|
||||
mfa_enabled: ?bool = null,
|
||||
verified: ?bool = null,
|
||||
email: ?[]const u8 = null,
|
||||
banner: ?[]const u8 = null,
|
||||
avatar_decoration_data: ?AvatarDecorationData = null,
|
||||
clan: ?[]const u8 = null,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn transform(default: @import("./structures/user.zig").User) Self {
|
||||
return .{
|
||||
.username = default.username,
|
||||
.global_name = default.global_name,
|
||||
.locale = default.locale,
|
||||
.flags = default.flags,
|
||||
.premium_type = default.premium_type,
|
||||
.public_flags = default.public_flags,
|
||||
.accent_color = default.accent_color,
|
||||
.id = default.id,
|
||||
.discriminator = default.discriminator,
|
||||
.avatar = default.avatar,
|
||||
.bot = default.bot,
|
||||
.system = default.system,
|
||||
.mfa_enabled = default.mfa_enabled,
|
||||
.verified = default.verified,
|
||||
.email = default.email,
|
||||
.banner = default.banner,
|
||||
.avatar_decoration_data = default.avatar_decoration_data,
|
||||
.clan = default.clan,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const VerificationLevels = @import("./structures/shared.zig").VerificationLevels;
|
||||
const DefaultMessageNotificationLevels = @import("./structures/shared.zig").DefaultMessageNotificationLevels;
|
||||
const ExplicitContentFilterLevels = @import("./structures/shared.zig").ExplicitContentFilterLevels;
|
||||
const GuildFeatures = @import("./structures/shared.zig").GuildFeatures;
|
||||
const MfaLevels = @import("./structures/shared.zig").MfaLevels;
|
||||
const SystemChannelFlags = @import("./structures/shared.zig").SystemChannelFlags;
|
||||
const PremiumTiers = @import("./structures/shared.zig").PremiumTiers;
|
||||
const GuildNsfwLevel = @import("./structures/shared.zig").GuildNsfwLevel;
|
||||
const StageInstance = @import("./structures/channel.zig").StageInstance;
|
||||
const WelcomeScreen = @import("./structures/channel.zig").WelcomeScreen;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object
|
||||
/// modify this to your liking
|
||||
pub const StoredGuild = struct {
|
||||
name: []const u8,
|
||||
widget_enabled: ?bool = null,
|
||||
verification_level: VerificationLevels,
|
||||
default_message_notifications: DefaultMessageNotificationLevels,
|
||||
explicit_content_filter: ExplicitContentFilterLevels,
|
||||
features: []GuildFeatures,
|
||||
mfa_level: MfaLevels,
|
||||
system_channel_flags: SystemChannelFlags,
|
||||
large: ?bool = null,
|
||||
unavailable: ?bool = null,
|
||||
member_count: ?isize = null,
|
||||
max_presences: ?isize = null,
|
||||
max_members: ?isize = null,
|
||||
vanity_url_code: ?[]const u8 = null,
|
||||
description: ?[]const u8 = null,
|
||||
premium_tier: PremiumTiers,
|
||||
premium_subscription_count: ?isize = null,
|
||||
/// returned from the GET /guilds/id endpoint when with_counts is true
|
||||
approximate_member_count: ?isize = null,
|
||||
/// returned from the GET /guilds/id endpoint when with_counts is true
|
||||
approximate_presence_count: ?isize = null,
|
||||
nsfw_level: GuildNsfwLevel,
|
||||
id: Snowflake,
|
||||
icon: ?[]const u8 = null,
|
||||
icon_hash: ?[]const u8 = null,
|
||||
splash: ?[]const u8 = null,
|
||||
discovery_splash: ?[]const u8 = null,
|
||||
owner_id: Snowflake,
|
||||
permissions: ?[]const u8 = null,
|
||||
afk_channel_id: ?Snowflake = null,
|
||||
widget_channel_id: ?Snowflake = null,
|
||||
application_id: ?Snowflake = null,
|
||||
system_channel_id: ?Snowflake = null,
|
||||
rules_channel_id: ?Snowflake = null,
|
||||
banner: ?[]const u8 = null,
|
||||
preferred_locale: []const u8,
|
||||
public_updates_channel_id: ?Snowflake = null,
|
||||
welcome_screen: ?WelcomeScreen = null,
|
||||
stage_instances: ?[]StageInstance = null,
|
||||
safety_alerts_channel_id: ?Snowflake = null,
|
||||
};
|
||||
|
||||
const ChannelTypes = @import("./structures/shared.zig").ChannelTypes;
|
||||
const Overwrite = @import("./structures/channel.zig").Overwrite;
|
||||
const VideoQualityModes = @import("./structures/shared.zig").VideoQualityModes;
|
||||
const ThreadMetadata = @import("./structures/thread.zig").ThreadMetadata;
|
||||
const ChannelFlags = @import("./structures/shared.zig").ChannelFlags;
|
||||
const ForumTag = @import("./structures/channel.zig").ForumTag;
|
||||
const DefaultReactionEmoji = @import("./structures/channel.zig").DefaultReactionEmoji;
|
||||
const SortOrderTypes = @import("./structures/shared.zig").SortOrderTypes;
|
||||
const ForumLayout = @import("./structures/shared.zig").ForumLayout;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#channel-object
|
||||
/// modify this to your liking
|
||||
pub const StoredChannel = struct {
|
||||
id: Snowflake,
|
||||
type: ChannelTypes,
|
||||
guild_id: ?Snowflake = null,
|
||||
position: ?isize = null,
|
||||
permission_overwrites: ?[]Overwrite = null,
|
||||
name: ?[]const u8 = null,
|
||||
topic: ?[]const u8 = null,
|
||||
nsfw: ?bool = null,
|
||||
user_limit: ?isize = null,
|
||||
icon: ?[]const u8 = null,
|
||||
owner_id: ?Snowflake = null,
|
||||
application_id: ?Snowflake = null,
|
||||
managed: ?bool = null,
|
||||
parent_id: ?Snowflake = null,
|
||||
last_pin_timestamp: ?[]const u8 = null,
|
||||
rtc_region: ?[]const u8 = null,
|
||||
video_quality_mode: ?VideoQualityModes = null,
|
||||
message_count: ?isize = null,
|
||||
member_count: ?isize = null,
|
||||
/// Thread-specific fields not needed by other channels
|
||||
/// TODO: optimise this
|
||||
thread_metadata: ?ThreadMetadata = null,
|
||||
/// threads
|
||||
default_auto_archive_duration: ?isize = null,
|
||||
permissions: ?[]const u8 = null,
|
||||
flags: ?ChannelFlags = null,
|
||||
total_message_sent: ?isize = null,
|
||||
/// forum channels
|
||||
available_tags: ?[]ForumTag = null,
|
||||
/// forum channels
|
||||
applied_tags: ?[][]const u8 = null,
|
||||
/// forum channels
|
||||
default_reaction_emoji: ?DefaultReactionEmoji = null,
|
||||
/// threads and channels
|
||||
default_thread_rate_limit_per_user: ?isize = null,
|
||||
/// forum channels
|
||||
default_sort_order: ?SortOrderTypes = null,
|
||||
/// forum channels
|
||||
default_forum_layout: ?ForumLayout = null,
|
||||
/// When a thread is created this will be true on that channel payload for the thread.
|
||||
/// TODO: optimise this
|
||||
newly_created: ?bool = null,
|
||||
};
|
||||
|
||||
|
||||
/// https://discord.com/developers/docs/resources/emoji#emoji-object-emoji-structure
|
||||
/// modifyus to your liking
|
||||
pub const StoredEmoji = struct {
|
||||
name: ?[]const u8 = null,
|
||||
id: ?Snowflake = null,
|
||||
roles: ?[][]const u8 = null,
|
||||
user_id: ?Snowflake = null,
|
||||
require_colons: ?bool = null,
|
||||
managed: ?bool = null,
|
||||
animated: ?bool = null,
|
||||
available: ?bool = null,
|
||||
};
|
||||
|
||||
const MessageTypes = @import("./structures/shared.zig").MessageTypes;
|
||||
const Attachment = @import("./structures/attachment.zig").Attachment;
|
||||
const Embed = @import("./structures/embed.zig").Embed;
|
||||
const ChannelMention = @import("./structures/message.zig").ChannelMention;
|
||||
const MessageActivity = @import("./structures/message.zig").MessageActivity;
|
||||
const MessageFlags = @import("./structures/shared.zig").MessageFlags;
|
||||
const StickerItem = @import("./structures/sticker.zig").StickerItem;
|
||||
const Poll = @import("./structures/poll.zig").Poll;
|
||||
const MessageCall = @import("./structures/message.zig").MessageCall;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#message-object
|
||||
/// modify this to your liking
|
||||
pub const StoredMessage = struct {
|
||||
id: Snowflake,
|
||||
channel_id: Snowflake,
|
||||
guild_id: ?Snowflake = null,
|
||||
author_id: Snowflake,
|
||||
member_id: ?Snowflake = null,
|
||||
content: ?[]const u8 = null,
|
||||
timestamp: []const u8,
|
||||
edited_timestamp: ?[]const u8 = null,
|
||||
tts: bool,
|
||||
mention_everyone: bool,
|
||||
mentions: []Snowflake,
|
||||
mention_roles: ?[][]const u8 = null,
|
||||
mention_channels: ?[]ChannelMention = null,
|
||||
attachments: []Attachment,
|
||||
embeds: []Embed,
|
||||
reactions: ?[]Snowflake = null,
|
||||
// Used for validating a message was sent
|
||||
// nonce: ?union(enum) {int: isize,string: []const u8,} = null,
|
||||
pinned: bool,
|
||||
webhook_id: ?Snowflake = null,
|
||||
type: MessageTypes,
|
||||
// interactions or webhooks
|
||||
application_id: ?Snowflake = null,
|
||||
// Data showing the source of a crosspost, channel follow add, pin, or reply message
|
||||
// message_reference: ?Omit(MessageReference, .{"failIfNotExists"}) = null,
|
||||
flags: ?MessageFlags = null,
|
||||
referenced_message_id: ?Snowflake = null,
|
||||
// The thread that was started from this message, includes thread member object
|
||||
// thread: ?Omit(Channel, .{"member"}), //& { member: ThreadMember }; = null,
|
||||
components: ?[]Snowflake = null,
|
||||
sticker_items: ?[]StickerItem = null,
|
||||
/// threads, may be deleted?
|
||||
position: ?isize = null,
|
||||
poll: ?Poll = null,
|
||||
call: ?MessageCall = null,
|
||||
};
|
||||
|
||||
const RoleFlags = @import("./structures/shared.zig").RoleFlags;
|
||||
const RoleTags = @import("./structures/role.zig").RoleTags;
|
||||
|
||||
/// https://discord.com/developers/docs/topics/permissions#role-object-role-structure
|
||||
/// modify this to your liking
|
||||
pub const StoredRole = struct {
|
||||
id: Snowflake,
|
||||
hoist: bool,
|
||||
permissions: []const u8,
|
||||
managed: bool,
|
||||
mentionable: bool,
|
||||
tags: ?RoleTags = null,
|
||||
icon: ?[]const u8 = null,
|
||||
name: []const u8,
|
||||
color: isize,
|
||||
position: isize,
|
||||
unicode_emoji: ?[]const u8 = null,
|
||||
flags: RoleFlags,
|
||||
};
|
||||
|
||||
const StickerTypes = @import("./structures/shared.zig").StickerTypes;
|
||||
const StickerFormatTypes = @import("./structures/shared.zig").StickerFormatTypes;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure
|
||||
/// I don't know why you'd cache a sticker, but I deliver what I promise
|
||||
pub const StoredSticker = struct {
|
||||
id: Snowflake,
|
||||
pack_id: ?Snowflake = null,
|
||||
name: []const u8,
|
||||
description: []const u8,
|
||||
tags: []const u8,
|
||||
type: StickerTypes,
|
||||
format_type: StickerFormatTypes,
|
||||
available: ?bool = null,
|
||||
guild_id: ?Snowflake = null,
|
||||
user_id: ?Snowflake = null,
|
||||
sort_value: ?isize = null,
|
||||
};
|
||||
|
||||
const ReactionCountDetails = @import("./structures/message.zig").ReactionCountDetails;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#reaction-object
|
||||
/// This is actually beneficial to cache
|
||||
pub const StoredReaction = struct {
|
||||
count: isize,
|
||||
count_details: ReactionCountDetails,
|
||||
me: bool,
|
||||
me_burst: bool,
|
||||
emoji_id: []const u8,
|
||||
burst_colors: [][]const u8,
|
||||
};
|
||||
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||
/// modify this to your liking
|
||||
pub const StoredMember = struct {
|
||||
pending: ?bool = null,
|
||||
user_id: ?Snowflake = null,
|
||||
nick: ?[]const u8 = null,
|
||||
avatar: ?[]const u8 = null,
|
||||
roles: [][]const u8,
|
||||
joined_at: []const u8,
|
||||
premium_since: ?[]const u8 = null,
|
||||
permissions: ?[]const u8 = null,
|
||||
flags: isize,
|
||||
avatar_decoration_data: ?AvatarDecorationData = null,
|
||||
};
|
||||
|
249
src/core.zig
249
src/core.zig
@ -1,249 +0,0 @@
|
||||
//! ISC License
|
||||
//!
|
||||
//! Copyright (c) 2024-2025 Yuzu
|
||||
//!
|
||||
//! Permission to use, copy, modify, and/or distribute this software for any
|
||||
//! purpose with or without fee is hereby granted, provided that the above
|
||||
//! copyright notice and this permission notice appear in all copies.
|
||||
//!
|
||||
//! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
//! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
//! AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
//! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
//! LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
//! OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
//! PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
const Intents = @import("./structures/types.zig").Intents;
|
||||
const Snowflake = @import("./structures/snowflake.zig").Snowflake;
|
||||
const GatewayBotInfo = @import("internal.zig").GatewayBotInfo;
|
||||
const IdentifyProperties = @import("internal.zig").IdentifyProperties;
|
||||
const ShardDetails = @import("internal.zig").ShardDetails;
|
||||
const ConnectQueue = @import("internal.zig").ConnectQueue;
|
||||
const GatewayDispatchEvent = @import("internal.zig").GatewayDispatchEvent;
|
||||
const Log = @import("internal.zig").Log;
|
||||
const Shard = @import("shard.zig");
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const debug = @import("internal.zig").debug;
|
||||
|
||||
const Self = @This();
|
||||
|
||||
shard_details: ShardDetails,
|
||||
allocator: mem.Allocator,
|
||||
|
||||
/// Queue for managing shard connections
|
||||
connect_queue: ConnectQueue(Shard),
|
||||
shards: std.AutoArrayHashMap(usize, Shard),
|
||||
handler: GatewayDispatchEvent(*Shard),
|
||||
|
||||
/// where we dispatch work for every thread, threads must be spawned upon shard creation
|
||||
/// make sure the address of workers is stable
|
||||
workers: std.Thread.Pool = undefined,
|
||||
|
||||
/// configuration settings
|
||||
options: SessionOptions,
|
||||
log: Log,
|
||||
|
||||
pub const ShardData = struct {
|
||||
/// resume seq to resume connections
|
||||
resume_seq: ?usize,
|
||||
|
||||
/// resume_gateway_url is the url to resume the connection
|
||||
/// https://discord.com/developers/docs/topics/gateway#ready-event
|
||||
resume_gateway_url: ?[]const u8,
|
||||
|
||||
/// session_id is the unique session id of the gateway
|
||||
session_id: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const SessionOptions = struct {
|
||||
/// Important data which is used by the manager to connect shards to the gateway. */
|
||||
info: GatewayBotInfo,
|
||||
/// Delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 5100. YOU DON'T WANT TO HIT THE RATE LIMIT!!!
|
||||
spawn_shard_delay: ?u64 = 5300,
|
||||
/// Total amount of shards your bot uses. Useful for zero-downtime updates or resharding.
|
||||
total_shards: usize = 1,
|
||||
shard_start: usize = 0,
|
||||
shard_end: usize = 1,
|
||||
/// The payload handlers for messages on the shard.
|
||||
resharding: ?struct { interval: u64, percentage: usize } = null,
|
||||
/// worker threads
|
||||
workers_per_shard: usize = 1,
|
||||
/// The shard lifespan in milliseconds. If a shard is not connected within this time, it will be closed.
|
||||
shard_lifespan: ?u64 = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: mem.Allocator, settings: struct {
|
||||
token: []const u8,
|
||||
intents: Intents,
|
||||
options: SessionOptions,
|
||||
run: GatewayDispatchEvent(*Shard),
|
||||
log: Log,
|
||||
}) mem.Allocator.Error!Self {
|
||||
const concurrency = settings.options.info.session_start_limit.?.max_concurrency;
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.connect_queue = try ConnectQueue(Shard).init(allocator, concurrency, 5000),
|
||||
.shards = .init(allocator),
|
||||
.workers = undefined,
|
||||
.shard_details = ShardDetails{
|
||||
.token = settings.token,
|
||||
.intents = settings.intents,
|
||||
},
|
||||
.handler = settings.run,
|
||||
.options = .{
|
||||
.info = .{
|
||||
.url = settings.options.info.url,
|
||||
.shards = settings.options.info.shards,
|
||||
.session_start_limit = settings.options.info.session_start_limit,
|
||||
},
|
||||
.total_shards = settings.options.total_shards,
|
||||
.shard_start = settings.options.shard_start,
|
||||
.shard_end = settings.options.shard_end,
|
||||
.workers_per_shard = settings.options.workers_per_shard,
|
||||
},
|
||||
.log = settings.log,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.connect_queue.deinit();
|
||||
self.shards.deinit();
|
||||
}
|
||||
|
||||
pub fn forceIdentify(self: *Self, shard_id: usize) !void {
|
||||
self.logif("#{d} force identify", .{shard_id});
|
||||
const shard = try self.create(shard_id);
|
||||
|
||||
return shard.identify(null);
|
||||
}
|
||||
|
||||
pub fn disconnect(self: *Self, shard_id: usize) Shard.CloseError!void {
|
||||
return if (self.shards.get(shard_id)) |shard| shard.disconnect();
|
||||
}
|
||||
|
||||
pub fn disconnectAll(self: *Self) Shard.CloseError!void {
|
||||
while (self.shards.iterator().next()) |shard| shard.value_ptr.disconnect();
|
||||
}
|
||||
|
||||
/// spawn buckets in order
|
||||
/// Log bucket preparation
|
||||
/// Divide shards into chunks based on concurrency
|
||||
/// Assign each shard to a bucket
|
||||
/// Return list of buckets
|
||||
/// https://discord.com/developers/docs/events/gateway#sharding-max-concurrency
|
||||
fn spawnBuckets(self: *Self) ![][]Shard {
|
||||
const concurrency = self.options.info.session_start_limit.?.max_concurrency;
|
||||
|
||||
self.logif("{d}-{d}", .{ self.options.shard_start, self.options.shard_end });
|
||||
|
||||
const range = std.math.sub(usize, self.options.shard_start, self.options.shard_end) catch 1;
|
||||
const bucket_count = (range + concurrency - 1) / concurrency;
|
||||
|
||||
self.logif("#0 preparing buckets", .{});
|
||||
|
||||
const buckets = try self.allocator.alloc([]Shard, bucket_count);
|
||||
|
||||
for (buckets, 0..) |*bucket, i| {
|
||||
const bucket_size = if ((i + 1) * concurrency > range) range - (i * concurrency) else concurrency;
|
||||
|
||||
bucket.* = try self.allocator.alloc(Shard, bucket_size);
|
||||
|
||||
for (bucket.*, 0..) |*shard, j| {
|
||||
shard.* = try self.create(self.options.shard_start + i * concurrency + j);
|
||||
}
|
||||
}
|
||||
|
||||
self.logif("{d} buckets created", .{bucket_count});
|
||||
|
||||
// finally defihne threads
|
||||
|
||||
try self.workers.init(.{
|
||||
.allocator = self.allocator,
|
||||
.n_jobs = self.options.workers_per_shard * self.options.total_shards,
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/// creates a shard and stores it
|
||||
fn create(self: *Self, shard_id: usize) !Shard {
|
||||
if (self.shards.get(shard_id)) |s| return s;
|
||||
|
||||
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{
|
||||
.info = self.options.info,
|
||||
.ratelimit_options = .{},
|
||||
},
|
||||
.run = self.handler,
|
||||
.log = self.log,
|
||||
.sharder_pool = &self.workers,
|
||||
});
|
||||
|
||||
try self.shards.put(shard_id, shard);
|
||||
|
||||
return shard;
|
||||
}
|
||||
|
||||
pub fn resume_(self: *Self, shard_id: usize, shard_data: ShardData) void {
|
||||
if (self.shards.contains(shard_id)) return error.CannotOverrideExistingShard;
|
||||
|
||||
const shard = self.create(shard_id);
|
||||
|
||||
shard.data = shard_data;
|
||||
|
||||
return self.connect_queue.push(.{
|
||||
.shard = shard,
|
||||
.callback = &callback,
|
||||
});
|
||||
}
|
||||
|
||||
fn callback(self: *ConnectQueue(Shard).RequestWithShard) anyerror!void {
|
||||
try self.shard.connect();
|
||||
}
|
||||
|
||||
pub fn spawnShards(self: *Self) !void {
|
||||
const buckets = try self.spawnBuckets();
|
||||
|
||||
self.logif("Spawning shards", .{});
|
||||
|
||||
for (buckets) |bucket| {
|
||||
for (bucket) |shard| {
|
||||
self.logif("adding {d} to connect queue", .{shard.id});
|
||||
try self.connect_queue.push(.{
|
||||
.shard = shard,
|
||||
.callback = &callback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//self.startResharder();
|
||||
}
|
||||
|
||||
pub fn send(self: *Self, shard_id: usize, data: anytype) Shard.SendError!void {
|
||||
if (self.shards.get(shard_id)) |shard| try shard.send(data);
|
||||
}
|
||||
|
||||
// SPEC OF THE RESHARDER:
|
||||
// Class Self
|
||||
//
|
||||
// Method startResharder():
|
||||
// If resharding interval is not set or shard bounds are not valid:
|
||||
// Exit
|
||||
// Set up periodic check for resharding:
|
||||
// If new shards are required:
|
||||
// Log resharding process
|
||||
// Update options with new shard settings
|
||||
// Disconnect old shards and clear them from manager
|
||||
// Spawn shards again with updated configuration
|
||||
//
|
||||
|
||||
inline fn logif(self: *Self, comptime format: []const u8, args: anytype) void {
|
||||
switch (self.log) {
|
||||
.yes => debug.info(format, args),
|
||||
.no => {},
|
||||
}
|
||||
}
|
@ -29,5 +29,5 @@ pub const DiscordError = struct {
|
||||
};
|
||||
|
||||
pub fn Result(comptime T: type) type {
|
||||
return @import("json-helper.zig").OwnedEither(DiscordError, T);
|
||||
return @import("./utils/json.zig").OwnedEither(DiscordError, T);
|
||||
}
|
||||
|
0
src/http/api.zig
Normal file
0
src/http/api.zig
Normal file
@ -18,10 +18,10 @@ const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const http = std.http;
|
||||
const json = std.json;
|
||||
const json_helpers = @import("json-helper.zig");
|
||||
const json_helpers = @import("../utils/json.zig");
|
||||
|
||||
pub const Result = @import("errors.zig").Result;
|
||||
pub const DiscordError = @import("errors.zig").DiscordError;
|
||||
pub const Result = @import("../errors.zig").Result;
|
||||
pub const DiscordError = @import("../errors.zig").DiscordError;
|
||||
|
||||
pub const BASE_URL = "https://discord.com/api/v10";
|
||||
|
360
src/internal.zig
360
src/internal.zig
@ -1,360 +0,0 @@
|
||||
//! ISC License
|
||||
//!
|
||||
//! Copyright (c) 2024-2025 Yuzu
|
||||
//!
|
||||
//! Permission to use, copy, modify, and/or distribute this software for any
|
||||
//! purpose with or without fee is hereby granted, provided that the above
|
||||
//! copyright notice and this permission notice appear in all copies.
|
||||
//!
|
||||
//! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
//! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
//! AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
//! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
//! LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
//! OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
//! PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const builtin = @import("builtin");
|
||||
const Types = @import("./structures/types.zig");
|
||||
|
||||
pub const IdentifyProperties = struct {
|
||||
/// Operating system the shard runs on.
|
||||
os: []const u8,
|
||||
/// The "browser" where this shard is running on.
|
||||
browser: []const u8,
|
||||
/// The device on which the shard is running.
|
||||
device: []const u8,
|
||||
|
||||
system_locale: ?[]const u8 = null, // TODO parse this
|
||||
browser_user_agent: ?[]const u8 = null,
|
||||
browser_version: ?[]const u8 = null,
|
||||
os_version: ?[]const u8 = null,
|
||||
referrer: ?[]const u8 = null,
|
||||
referring_domain: ?[]const u8 = null,
|
||||
referrer_current: ?[]const u8 = null,
|
||||
referring_domain_current: ?[]const u8 = null,
|
||||
release_channel: ?[]const u8 = null,
|
||||
client_build_number: ?u64 = null,
|
||||
client_event_source: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#get-gateway
|
||||
pub const GatewayInfo = struct {
|
||||
/// The WSS URL that can be used for connecting to the gateway
|
||||
url: []const u8,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/events/gateway#session-start-limit-object
|
||||
pub const GatewaySessionStartLimit = struct {
|
||||
/// Total number of session starts the current user is allowed
|
||||
total: u32,
|
||||
/// Remaining number of session starts the current user is allowed
|
||||
remaining: u32,
|
||||
/// Number of milliseconds after which the limit resets
|
||||
reset_after: u32,
|
||||
/// Number of identify requests allowed per 5 seconds
|
||||
max_concurrency: u32,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#get-gateway-bot
|
||||
pub const GatewayBotInfo = struct {
|
||||
url: []const u8,
|
||||
/// The recommended number of shards to use when connecting
|
||||
///
|
||||
/// See https://discord.com/developers/docs/topics/gateway#sharding
|
||||
shards: u32,
|
||||
/// Information on the current session start limit
|
||||
///
|
||||
/// See https://discord.com/developers/docs/topics/gateway#session-start-limit-object
|
||||
session_start_limit: ?GatewaySessionStartLimit,
|
||||
};
|
||||
|
||||
pub const ShardDetails = struct {
|
||||
/// Bot token which is used to connect to Discord */
|
||||
token: []const u8,
|
||||
/// The URL of the gateway which should be connected to.
|
||||
url: []const u8 = "wss://gateway.discord.gg",
|
||||
/// The gateway version which should be used.
|
||||
version: ?usize = 10,
|
||||
/// The calculated intent value of the events which the shard should receive.
|
||||
intents: Types.Intents,
|
||||
/// Identify properties to use
|
||||
properties: IdentifyProperties = default_identify_properties,
|
||||
};
|
||||
|
||||
pub const debug = std.log.scoped(.@"discord.zig");
|
||||
|
||||
pub const Log = union(enum) { yes, no };
|
||||
|
||||
pub const default_identify_properties = IdentifyProperties{
|
||||
.os = @tagName(builtin.os.tag),
|
||||
.browser = "discord.zig",
|
||||
.device = "discord.zig",
|
||||
};
|
||||
|
||||
/// inspired from:
|
||||
/// https://github.com/tiramisulabs/seyfert/blob/main/src/websocket/structures/timeout.ts
|
||||
pub fn ConnectQueue(comptime T: type) type {
|
||||
return struct {
|
||||
pub const RequestWithShard = struct {
|
||||
callback: *const fn (self: *RequestWithShard) anyerror!void,
|
||||
shard: T,
|
||||
};
|
||||
|
||||
// ignore this function
|
||||
// so it becomes a regular dequeue
|
||||
fn eq(_: void, _: RequestWithShard, _: RequestWithShard) std.math.Order {
|
||||
return std.math.Order.eq;
|
||||
}
|
||||
|
||||
dequeue: std.PriorityDequeue(RequestWithShard, void, eq),
|
||||
allocator: mem.Allocator,
|
||||
remaining: usize,
|
||||
interval_time: u64 = 5000,
|
||||
running: bool = false,
|
||||
concurrency: usize = 1,
|
||||
|
||||
pub fn init(allocator: mem.Allocator, concurrency: usize, interval_time: u64) !ConnectQueue(T) {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.dequeue = std.PriorityDequeue(RequestWithShard, void, eq).init(allocator, {}),
|
||||
.remaining = concurrency,
|
||||
.interval_time = interval_time,
|
||||
.concurrency = concurrency,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ConnectQueue(T)) void {
|
||||
self.dequeue.deinit();
|
||||
}
|
||||
|
||||
pub fn push(self: *ConnectQueue(T), req: RequestWithShard) !void {
|
||||
if (self.remaining == 0) {
|
||||
return self.dequeue.add(req);
|
||||
}
|
||||
self.remaining -= 1;
|
||||
|
||||
if (!self.running) {
|
||||
try self.startInterval();
|
||||
self.running = true;
|
||||
}
|
||||
|
||||
if (self.dequeue.count() < self.concurrency) {
|
||||
// perhaps store this?
|
||||
const ptr = try self.allocator.create(RequestWithShard);
|
||||
ptr.* = req;
|
||||
try req.callback(ptr);
|
||||
return;
|
||||
}
|
||||
|
||||
return self.dequeue.add(req);
|
||||
}
|
||||
|
||||
fn startInterval(self: *ConnectQueue(T)) !void {
|
||||
while (self.running) {
|
||||
std.Thread.sleep(std.time.ns_per_ms * (self.interval_time / self.concurrency));
|
||||
const req: ?RequestWithShard = self.dequeue.removeMin(); // pop front
|
||||
|
||||
while (self.dequeue.count() == 0 and req == null) {}
|
||||
|
||||
if (req) |r| {
|
||||
const ptr = try self.allocator.create(RequestWithShard);
|
||||
ptr.* = r;
|
||||
try @call(.auto, r.callback, .{ptr});
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.remaining < self.concurrency) {
|
||||
self.remaining += 1;
|
||||
}
|
||||
|
||||
if (self.dequeue.count() == 0) {
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Bucket = struct {
|
||||
/// The queue of requests to acquire an available request. Mapped by (shardId, RequestWithPrio)
|
||||
queue: std.PriorityQueue(RequestWithPrio, void, Bucket.lessthan),
|
||||
|
||||
limit: usize,
|
||||
refill_interval: u64,
|
||||
refill_amount: usize,
|
||||
|
||||
/// The amount of requests that have been used up already.
|
||||
used: usize = 0,
|
||||
|
||||
/// Whether or not the queue is already processing.
|
||||
processing: bool = false,
|
||||
|
||||
/// Whether the timeout should be killed because there is already one running
|
||||
should_stop: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
|
||||
|
||||
/// The timestamp in milliseconds when the next refill is scheduled.
|
||||
refills_at: ?u64 = null,
|
||||
|
||||
pub const RequestWithPrio = struct {
|
||||
callback: *const fn () void,
|
||||
priority: u32 = 1,
|
||||
};
|
||||
|
||||
fn lessthan(_: void, a: RequestWithPrio, b: RequestWithPrio) std.math.Order {
|
||||
return std.math.order(a.priority, b.priority);
|
||||
}
|
||||
|
||||
pub fn init(allocator: mem.Allocator, limit: usize, refill_interval: u64, refill_amount: usize) Bucket {
|
||||
return .{
|
||||
.queue = std.PriorityQueue(RequestWithPrio, void, lessthan).init(allocator, {}),
|
||||
.limit = limit,
|
||||
.refill_interval = refill_interval,
|
||||
.refill_amount = refill_amount,
|
||||
};
|
||||
}
|
||||
|
||||
fn remaining(self: *Bucket) usize {
|
||||
if (self.limit < self.used) {
|
||||
return 0;
|
||||
} else {
|
||||
return self.limit - self.used;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refill(self: *Bucket) std.Thread.SpawnError!void {
|
||||
// Lower the used amount by the refill amount
|
||||
self.used = if (self.refill_amount > self.used) 0 else self.used - self.refill_amount;
|
||||
|
||||
// Reset the refills_at timestamp since it just got refilled
|
||||
self.refills_at = null;
|
||||
|
||||
if (self.used > 0) {
|
||||
if (self.should_stop.load(.monotonic) == true) {
|
||||
self.should_stop.store(false, .monotonic);
|
||||
}
|
||||
const thread = try std.Thread.spawn(.{}, Bucket.timeout, .{self});
|
||||
thread.detach;
|
||||
self.refills_at = std.time.milliTimestamp() + self.refill_interval;
|
||||
}
|
||||
}
|
||||
|
||||
fn timeout(self: *Bucket) void {
|
||||
while (!self.should_stop.load(.monotonic)) {
|
||||
self.refill();
|
||||
std.time.sleep(std.time.ns_per_ms * self.refill_interval);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processQueue(self: *Bucket) std.Thread.SpawnError!void {
|
||||
if (self.processing) return;
|
||||
|
||||
while (self.queue.remove()) |first_element| {
|
||||
if (self.remaining() != 0) {
|
||||
first_element.callback();
|
||||
self.used += 1;
|
||||
|
||||
if (!self.should_stop.load(.monotonic)) {
|
||||
const thread = try std.Thread.spawn(.{}, Bucket.timeout, .{self});
|
||||
thread.detach;
|
||||
self.refills_at = std.time.milliTimestamp() + self.refill_interval;
|
||||
}
|
||||
} else if (self.refills_at) |ra| {
|
||||
const now = std.time.milliTimestamp();
|
||||
if (ra > now) std.time.sleep(std.time.ns_per_ms * (ra - now));
|
||||
}
|
||||
}
|
||||
|
||||
self.processing = false;
|
||||
}
|
||||
|
||||
pub fn acquire(self: *Bucket, rq: RequestWithPrio) !void {
|
||||
try self.queue.add(rq);
|
||||
try self.processQueue();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn GatewayDispatchEvent(comptime T: type) type {
|
||||
return struct {
|
||||
application_command_permissions_update: ?*const fn (save: T, application_command_permissions: Types.ApplicationCommandPermissions) anyerror!void = undefined,
|
||||
auto_moderation_rule_create: ?*const fn (save: T, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_rule_update: ?*const fn (save: T, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_rule_delete: ?*const fn (save: T, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_action_execution: ?*const fn (save: T, action_execution: Types.AutoModerationActionExecution) anyerror!void = undefined,
|
||||
|
||||
channel_create: ?*const fn (save: T, chan: Types.Channel) anyerror!void = undefined,
|
||||
channel_update: ?*const fn (save: T, chan: Types.Channel) anyerror!void = undefined,
|
||||
/// this isn't send when the channel is not relevant to you
|
||||
channel_delete: ?*const fn (save: T, chan: Types.Channel) anyerror!void = undefined,
|
||||
channel_pins_update: ?*const fn (save: T, chan_pins_update: Types.ChannelPinsUpdate) anyerror!void = undefined,
|
||||
thread_create: ?*const fn (save: T, thread: Types.Channel) anyerror!void = undefined,
|
||||
thread_update: ?*const fn (save: T, thread: Types.Channel) anyerror!void = undefined,
|
||||
/// has `id`, `guild_id`, `parent_id`, and `type` fields.
|
||||
thread_delete: ?*const fn (save: T, thread: Types.Partial(Types.Channel)) anyerror!void = undefined,
|
||||
thread_list_sync: ?*const fn (save: T, data: Types.ThreadListSync) anyerror!void = undefined,
|
||||
thread_member_update: ?*const fn (save: T, guild_id: Types.ThreadMemberUpdate) anyerror!void = undefined,
|
||||
thread_members_update: ?*const fn (save: T, thread_data: Types.ThreadMembersUpdate) anyerror!void = undefined,
|
||||
// TODO: implement // guild_audit_log_entry_create: null = null,
|
||||
guild_create: ?*const fn (save: T, guild: Types.Guild) anyerror!void = undefined,
|
||||
guild_create_unavailable: ?*const fn (save: T, guild: Types.UnavailableGuild) anyerror!void = undefined,
|
||||
guild_update: ?*const fn (save: T, guild: Types.Guild) anyerror!void = undefined,
|
||||
/// this is not necessarily sent upon deletion of a guild
|
||||
/// but from when a user is *removed* therefrom
|
||||
guild_delete: ?*const fn (save: T, guild: Types.UnavailableGuild) anyerror!void = undefined,
|
||||
guild_ban_add: ?*const fn (save: T, gba: Types.GuildBanAddRemove) anyerror!void = undefined,
|
||||
guild_ban_remove: ?*const fn (save: T, gbr: Types.GuildBanAddRemove) anyerror!void = undefined,
|
||||
guild_emojis_update: ?*const fn (save: T, fields: Types.GuildEmojisUpdate) anyerror!void = undefined,
|
||||
guild_stickers_update: ?*const fn (save: T, fields: Types.GuildStickersUpdate) anyerror!void = undefined,
|
||||
guild_integrations_update: ?*const fn (save: T, fields: Types.GuildIntegrationsUpdate) anyerror!void = undefined,
|
||||
guild_member_add: ?*const fn (save: T, guild_id: Types.GuildMemberAdd) anyerror!void = undefined,
|
||||
guild_member_update: ?*const fn (save: T, fields: Types.GuildMemberUpdate) anyerror!void = undefined,
|
||||
guild_member_remove: ?*const fn (save: T, user: Types.GuildMemberRemove) anyerror!void = undefined,
|
||||
guild_members_chunk: ?*const fn (save: T, data: Types.GuildMembersChunk) anyerror!void = undefined,
|
||||
guild_role_create: ?*const fn (save: T, role: Types.GuildRoleCreate) anyerror!void = undefined,
|
||||
guild_role_delete: ?*const fn (save: T, role: Types.GuildRoleDelete) anyerror!void = undefined,
|
||||
guild_role_update: ?*const fn (save: T, role: Types.GuildRoleUpdate) anyerror!void = undefined,
|
||||
guild_scheduled_event_create: ?*const fn (save: T, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_update: ?*const fn (save: T, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_delete: ?*const fn (save: T, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_user_add: ?*const fn (save: T, data: Types.ScheduledEventUserAdd) anyerror!void = undefined,
|
||||
guild_scheduled_event_user_remove: ?*const fn (save: T, data: Types.ScheduledEventUserRemove) anyerror!void = undefined,
|
||||
integration_create: ?*const fn (save: T, guild_id: Types.IntegrationCreateUpdate) anyerror!void = undefined,
|
||||
integration_update: ?*const fn (save: T, guild_id: Types.IntegrationCreateUpdate) anyerror!void = undefined,
|
||||
integration_delete: ?*const fn (save: T, guild_id: Types.IntegrationDelete) anyerror!void = undefined,
|
||||
interaction_create: ?*const fn (save: T, interaction: Types.MessageInteraction) anyerror!void = undefined,
|
||||
invite_create: ?*const fn (save: T, data: Types.InviteCreate) anyerror!void = undefined,
|
||||
invite_delete: ?*const fn (save: T, data: Types.InviteDelete) anyerror!void = undefined,
|
||||
message_create: ?*const fn (save: T, message: Types.Message) anyerror!void = undefined,
|
||||
message_update: ?*const fn (save: T, message: Types.Message) anyerror!void = undefined,
|
||||
message_delete: ?*const fn (save: T, log: Types.MessageDelete) anyerror!void = undefined,
|
||||
message_delete_bulk: ?*const fn (save: T, log: Types.MessageDeleteBulk) anyerror!void = undefined,
|
||||
message_reaction_add: ?*const fn (save: T, log: Types.MessageReactionAdd) anyerror!void = undefined,
|
||||
message_reaction_remove_all: ?*const fn (save: T, data: Types.MessageReactionRemoveAll) anyerror!void = undefined,
|
||||
message_reaction_remove: ?*const fn (save: T, data: Types.MessageReactionRemove) anyerror!void = undefined,
|
||||
message_reaction_remove_emoji: ?*const fn (save: T, data: Types.MessageReactionRemoveEmoji) anyerror!void = undefined,
|
||||
presence_update: ?*const fn (save: T, presence: Types.PresenceUpdate) anyerror!void = undefined,
|
||||
stage_instance_create: ?*const fn (save: T, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
stage_instance_update: ?*const fn (save: T, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
stage_instance_delete: ?*const fn (save: T, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
typing_start: ?*const fn (save: T, data: Types.TypingStart) anyerror!void = undefined,
|
||||
/// remember this is only sent when you change your profile yourself/your bot does
|
||||
user_update: ?*const fn (save: T, user: Types.User) anyerror!void = undefined,
|
||||
// will do these someday, music is rather pointless at this point in time
|
||||
// TODO: implement // voice_channel_effect_send: null = null,
|
||||
// TODO: implement // voice_state_update: null = null,
|
||||
// TODO: implement // voice_server_update: null = null,
|
||||
webhooks_update: ?*const fn (save: T, fields: Types.WebhookUpdate) anyerror!void = undefined,
|
||||
entitlement_create: ?*const fn (save: T, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
entitlement_update: ?*const fn (save: T, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
/// discord claims this is infrequent, therefore not throughoutly tested - Yuzu
|
||||
entitlement_delete: ?*const fn (save: T, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
message_poll_vote_add: ?*const fn (save: T, poll: Types.PollVoteAdd) anyerror!void = undefined,
|
||||
message_poll_vote_remove: ?*const fn (save: T, poll: Types.PollVoteRemove) anyerror!void = undefined,
|
||||
|
||||
ready: ?*const fn (save: T, data: Types.Ready) anyerror!void = undefined,
|
||||
// TODO: implement // resumed: null = null,
|
||||
any: ?*const fn (save: T, data: std.json.Value) anyerror!void = undefined,
|
||||
};
|
||||
}
|
@ -67,8 +67,6 @@ pub const PermissionStrings = @import("structures/types.zig").PermissionStrings;
|
||||
pub const GatewayCloseEventCodes = @import("structures/types.zig").GatewayCloseEventCodes;
|
||||
pub const GatewayOpcodes = @import("structures/types.zig").GatewayOpcodes;
|
||||
pub const GatewayDispatchEventNames = @import("structures/types.zig").GatewayDispatchEventNames;
|
||||
pub const GatewayIntents = @import("structures/types.zig").GatewayIntents;
|
||||
pub const Intents = @import("structures/types.zig").Intents;
|
||||
pub const InteractionResponseTypes = @import("structures/types.zig").InteractionResponseTypes;
|
||||
pub const SortOrderTypes = @import("structures/types.zig").SortOrderTypes;
|
||||
pub const ForumLayout = @import("structures/types.zig").ForumLayout;
|
||||
@ -163,8 +161,6 @@ pub const ModifyGuildChannelPositions = @import("structures/types.zig").ModifyGu
|
||||
pub const CreateChannelInvite = @import("structures/types.zig").CreateChannelInvite;
|
||||
pub const ApplicationCommand = @import("structures/types.zig").ApplicationCommand;
|
||||
pub const CreateApplicationCommand = @import("structures/types.zig").CreateApplicationCommand;
|
||||
pub const LocaleMap = @import("structures/types.zig").LocaleMap;
|
||||
pub const InteractionEntryPointCommandHandlerType = @import("structures/types.zig").InteractionEntryPointCommandHandlerType;
|
||||
pub const ApplicationCommandOption = @import("structures/types.zig").ApplicationCommandOption;
|
||||
pub const ApplicationCommandOptionChoice = @import("structures/types.zig").ApplicationCommandOptionChoice;
|
||||
pub const GuildApplicationCommandPermissions = @import("structures/types.zig").GuildApplicationCommandPermissions;
|
||||
@ -309,57 +305,74 @@ pub const ApplicationWebhook = @import("structures/types.zig").ApplicationWebhoo
|
||||
pub const GatewayPayload = @import("structures/types.zig").GatewayPayload;
|
||||
// END USING NAMESPACE
|
||||
|
||||
pub const Shard = @import("shard.zig");
|
||||
pub const zjson = @compileError("Deprecated, use std.json instead.");
|
||||
pub const CacheTables = @import("cache/cache.zig").CacheTables;
|
||||
pub const CacheLike = @import("cache/cache.zig").CacheLike;
|
||||
pub const DefaultCache = @import("cache/cache.zig").DefaultCache;
|
||||
|
||||
pub const Internal = @import("internal.zig");
|
||||
pub const Permissions = @import("utils/permissions.zig").Permissions;
|
||||
pub const Shard = @import("shard/shard.zig").Shard;
|
||||
pub const zjson = @compileError("Deprecated.");
|
||||
|
||||
pub const Internal = @import("utils/core.zig");
|
||||
const GatewayDispatchEvent = Internal.GatewayDispatchEvent;
|
||||
const GatewayBotInfo = Internal.GatewayBotInfo;
|
||||
const GatewayBotInfo = @import("shard/util.zig").GatewayBotInfo;
|
||||
const Log = Internal.Log;
|
||||
|
||||
pub const Sharder = @import("core.zig");
|
||||
const SessionOptions = Sharder.SessionOptions;
|
||||
// sharder
|
||||
pub const Sharder = @import("shard/sharder.zig").ShardManager;
|
||||
|
||||
pub const FetchReq = @import("http.zig").FetchReq;
|
||||
pub const FileData = @import("http.zig").FileData;
|
||||
pub const cache = @import("cache/cache.zig");
|
||||
|
||||
pub const FetchReq = @import("http/http.zig").FetchReq;
|
||||
pub const FileData = @import("http/http.zig").FileData;
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const http = std.http;
|
||||
const json = std.json;
|
||||
|
||||
const Self = @This();
|
||||
pub fn CustomisedSession(comptime Table: cache.TableTemplate) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: mem.Allocator,
|
||||
sharder: Sharder,
|
||||
token: []const u8,
|
||||
allocator: mem.Allocator,
|
||||
sharder: Sharder(Table),
|
||||
token: []const u8,
|
||||
|
||||
pub fn init(allocator: mem.Allocator) Self {
|
||||
pub fn init(allocator: mem.Allocator) Self {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.sharder = undefined,
|
||||
.token = undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.sharder.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(self: *Self, settings: struct {
|
||||
pub fn start(self: *Self, settings: struct {
|
||||
token: []const u8,
|
||||
intents: Self.Intents,
|
||||
intents: Intents,
|
||||
options: struct {
|
||||
spawn_shard_delay: u64 = 5300,
|
||||
total_shards: usize = 1,
|
||||
shard_start: usize = 0,
|
||||
shard_end: usize = 1,
|
||||
},
|
||||
run: GatewayDispatchEvent(*Shard),
|
||||
run: GatewayDispatchEvent,
|
||||
log: Log,
|
||||
}) !void {
|
||||
cache: cache.TableTemplate,
|
||||
}) !void {
|
||||
if (!std.mem.startsWith(u8, settings.token, "Bot")) {
|
||||
var buffer = [_]u8{undefined} ** 128;
|
||||
const printed = try std.fmt.bufPrint(&buffer, "Bot {s}", .{settings.token});
|
||||
self.token = printed;
|
||||
} else {
|
||||
self.token = settings.token;
|
||||
var req = FetchReq.init(self.allocator, settings.token);
|
||||
}
|
||||
|
||||
var req = FetchReq.init(self.allocator, self.token);
|
||||
defer req.deinit();
|
||||
|
||||
const res = try req.makeRequest(.GET, "/gateway/bot", null);
|
||||
@ -374,11 +387,11 @@ pub fn start(self: *Self, settings: struct {
|
||||
const parsed = try json.parseFromSlice(GatewayBotInfo, self.allocator, body, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
self.sharder = try Sharder.init(self.allocator, .{
|
||||
.token = settings.token,
|
||||
self.sharder = try Sharder(Table).init(self.allocator, .{
|
||||
.token = self.token,
|
||||
.intents = settings.intents,
|
||||
.run = settings.run,
|
||||
.options = SessionOptions{
|
||||
.options = Sharder(Table).SessionOptions{
|
||||
.info = parsed.value,
|
||||
.shard_start = settings.options.shard_start,
|
||||
.shard_end = @intCast(parsed.value.shards),
|
||||
@ -386,7 +399,41 @@ pub fn start(self: *Self, settings: struct {
|
||||
.spawn_shard_delay = settings.options.spawn_shard_delay,
|
||||
},
|
||||
.log = settings.log,
|
||||
.cache = settings.cache,
|
||||
});
|
||||
|
||||
try self.sharder.spawnShards();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// defaults
|
||||
const DefaultTable = cache.TableTemplate{};
|
||||
pub const Session = CustomisedSession(DefaultTable);
|
||||
|
||||
pub fn init(allocator: mem.Allocator) Session {
|
||||
return Session.init(allocator);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Session) void {
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
pub const GatewayIntents = @import("./shard/intents.zig").GatewayIntents;
|
||||
pub const Intents = @import("./shard/intents.zig").Intents;
|
||||
|
||||
pub fn start(self: *Session, settings: struct {
|
||||
token: []const u8,
|
||||
intents: Intents,
|
||||
options: struct {
|
||||
spawn_shard_delay: u64 = 5300,
|
||||
total_shards: usize = 1,
|
||||
shard_start: usize = 0,
|
||||
shard_end: usize = 1,
|
||||
},
|
||||
run: GatewayDispatchEvent,
|
||||
log: Log,
|
||||
cache: cache.TableTemplate,
|
||||
}) !void {
|
||||
return self.start(settings);
|
||||
}
|
2874
src/shard.zig
2874
src/shard.zig
File diff suppressed because it is too large
Load Diff
106
src/shard/bucket.zig
Normal file
106
src/shard/bucket.zig
Normal file
@ -0,0 +1,106 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const math = std.math;
|
||||
const time = std.time;
|
||||
const atomic = std.atomic;
|
||||
const Thread = std.Thread;
|
||||
const PriorityQueue = std.PriorityQueue;
|
||||
|
||||
pub const Bucket = struct {
|
||||
pub const RequestWithPrio = struct {
|
||||
callback: *const fn () void,
|
||||
priority: u32 = 1,
|
||||
};
|
||||
|
||||
fn lessthan(_: void, a: RequestWithPrio, b: RequestWithPrio) math.Order {
|
||||
return math.order(a.priority, b.priority);
|
||||
}
|
||||
|
||||
/// The queue of requests to acquire an available request.
|
||||
/// Mapped by (shardId, RequestWithPrio)
|
||||
queue: PriorityQueue(RequestWithPrio, void, lessthan),
|
||||
|
||||
limit: usize,
|
||||
refill_interval: u64,
|
||||
refill_amount: usize,
|
||||
|
||||
/// The amount of requests that have been used up already.
|
||||
used: usize = 0,
|
||||
|
||||
/// Whether or not the queue is already processing.
|
||||
processing: bool = false,
|
||||
|
||||
/// Whether the timeout should be killed because there is already one running
|
||||
should_stop: atomic.Value(bool) = .init(false),
|
||||
|
||||
/// The timestamp in milliseconds when the next refill is scheduled.
|
||||
refills_at: ?u64 = null,
|
||||
|
||||
pub fn init(allocator: mem.Allocator, limit: usize, refill_interval: u64, refill_amount: usize) Bucket {
|
||||
return .{
|
||||
.queue = PriorityQueue(RequestWithPrio, void, lessthan).init(allocator, {}),
|
||||
.limit = limit,
|
||||
.refill_interval = refill_interval,
|
||||
.refill_amount = refill_amount,
|
||||
};
|
||||
}
|
||||
|
||||
fn remaining(self: *Bucket) usize {
|
||||
if (self.limit < self.used) {
|
||||
return 0;
|
||||
} else {
|
||||
return self.limit - self.used;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refill(self: *Bucket) Thread.SpawnError!void {
|
||||
// Lower the used amount by the refill amount
|
||||
self.used = if (self.refill_amount > self.used) 0 else self.used - self.refill_amount;
|
||||
|
||||
// Reset the refills_at timestamp since it just got refilled
|
||||
self.refills_at = null;
|
||||
|
||||
if (self.used > 0) {
|
||||
if (self.should_stop.load(.monotonic) == true) {
|
||||
self.should_stop.store(false, .monotonic);
|
||||
}
|
||||
const thread = try Thread.spawn(.{}, Bucket.timeout, .{self});
|
||||
thread.detach;
|
||||
self.refills_at = time.milliTimestamp() + self.refill_interval;
|
||||
}
|
||||
}
|
||||
|
||||
fn timeout(self: *Bucket) void {
|
||||
while (!self.should_stop.load(.monotonic)) {
|
||||
self.refill();
|
||||
time.sleep(time.ns_per_ms * self.refill_interval);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn processQueue(self: *Bucket) std.Thread.SpawnError!void {
|
||||
if (self.processing) return;
|
||||
|
||||
while (self.queue.remove()) |first_element| {
|
||||
if (self.remaining() != 0) {
|
||||
first_element.callback();
|
||||
self.used += 1;
|
||||
|
||||
if (!self.should_stop.load(.monotonic)) {
|
||||
const thread = try Thread.spawn(.{}, Bucket.timeout, .{self});
|
||||
thread.detach;
|
||||
self.refills_at = time.milliTimestamp() + self.refill_interval;
|
||||
}
|
||||
} else if (self.refills_at) |ra| {
|
||||
const now = time.milliTimestamp();
|
||||
if (ra > now) time.sleep(time.ns_per_ms * (ra - now));
|
||||
}
|
||||
}
|
||||
|
||||
self.processing = false;
|
||||
}
|
||||
|
||||
pub fn acquire(self: *Bucket, rq: RequestWithPrio) !void {
|
||||
try self.queue.add(rq);
|
||||
try self.processQueue();
|
||||
}
|
||||
};
|
87
src/shard/connect_queue.zig
Normal file
87
src/shard/connect_queue.zig
Normal file
@ -0,0 +1,87 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
|
||||
/// inspired from:
|
||||
/// https://github.com/tiramisulabs/seyfert/blob/main/src/websocket/structures/timeout.ts
|
||||
pub fn ConnectQueue(comptime T: type) type {
|
||||
return struct {
|
||||
pub const RequestWithShard = struct {
|
||||
callback: *const fn (self: *RequestWithShard) anyerror!void,
|
||||
shard: T,
|
||||
};
|
||||
|
||||
// ignore this function
|
||||
// so it becomes a regular dequeue
|
||||
fn eq(_: void, _: RequestWithShard, _: RequestWithShard) std.math.Order {
|
||||
return std.math.Order.eq;
|
||||
}
|
||||
|
||||
dequeue: std.PriorityDequeue(RequestWithShard, void, eq),
|
||||
allocator: mem.Allocator,
|
||||
remaining: usize,
|
||||
interval_time: u64 = 5000,
|
||||
running: bool = false,
|
||||
concurrency: usize = 1,
|
||||
|
||||
pub fn init(allocator: mem.Allocator, concurrency: usize, interval_time: u64) !ConnectQueue(T) {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.dequeue = std.PriorityDequeue(RequestWithShard, void, eq).init(allocator, {}),
|
||||
.remaining = concurrency,
|
||||
.interval_time = interval_time,
|
||||
.concurrency = concurrency,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ConnectQueue(T)) void {
|
||||
self.dequeue.deinit();
|
||||
}
|
||||
|
||||
pub fn push(self: *ConnectQueue(T), req: RequestWithShard) !void {
|
||||
if (self.remaining == 0) {
|
||||
return self.dequeue.add(req);
|
||||
}
|
||||
self.remaining -= 1;
|
||||
|
||||
if (!self.running) {
|
||||
try self.startInterval();
|
||||
self.running = true;
|
||||
}
|
||||
|
||||
if (self.dequeue.count() < self.concurrency) {
|
||||
// perhaps store this?
|
||||
const ptr = try self.allocator.create(RequestWithShard);
|
||||
ptr.* = req;
|
||||
try req.callback(ptr);
|
||||
return;
|
||||
}
|
||||
|
||||
return self.dequeue.add(req);
|
||||
}
|
||||
|
||||
fn startInterval(self: *ConnectQueue(T)) !void {
|
||||
while (self.running) {
|
||||
std.Thread.sleep(std.time.ns_per_ms * (self.interval_time / self.concurrency));
|
||||
const req: ?RequestWithShard = self.dequeue.removeMin(); // pop front
|
||||
|
||||
while (self.dequeue.count() == 0 and req == null) {}
|
||||
|
||||
if (req) |r| {
|
||||
const ptr = try self.allocator.create(RequestWithShard);
|
||||
ptr.* = r;
|
||||
try @call(.auto, r.callback, .{ptr});
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.remaining < self.concurrency) {
|
||||
self.remaining += 1;
|
||||
}
|
||||
|
||||
if (self.dequeue.count() == 0) {
|
||||
self.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
179
src/shard/intents.zig
Normal file
179
src/shard/intents.zig
Normal file
@ -0,0 +1,179 @@
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const json = std.json;
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#list-of-intents
|
||||
pub const GatewayIntents = packed struct {
|
||||
pub fn toRaw(self: GatewayIntents) u32 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
pub fn fromRaw(raw: u32) GatewayIntents {
|
||||
return @bitCast(raw);
|
||||
}
|
||||
|
||||
pub fn jsonParse(allocator: mem.Allocator, src: anytype, _: json.ParseOptions) !@This() {
|
||||
const value = try json.innerParse(json.Value, allocator, src, .{
|
||||
.ignore_unknown_fields = true,
|
||||
.max_value_len = 0x1000,
|
||||
});
|
||||
if (value != .integer) @panic("Invalid value for bitfield");
|
||||
|
||||
return fromRaw(@intCast(value.integer));
|
||||
}
|
||||
|
||||
pub fn jsonParseFromValue(_: mem.Allocator, src: json.Value, _: json.ParseOptions) @This() {
|
||||
if (src != .integer) @panic("Invalid value for bitfield");
|
||||
return fromRaw(@intCast(src.integer));
|
||||
}
|
||||
|
||||
///
|
||||
/// - GUILD_CREATE
|
||||
/// - GUILD_UPDATE
|
||||
/// - GUILD_DELETE
|
||||
/// - GUILD_ROLE_CREATE
|
||||
/// - GUILD_ROLE_UPDATE
|
||||
/// - GUILD_ROLE_DELETE
|
||||
/// - CHANNEL_CREATE
|
||||
/// - CHANNEL_UPDATE
|
||||
/// - CHANNEL_DELETE
|
||||
/// - CHANNEL_PINS_UPDATE
|
||||
/// - THREAD_CREATE
|
||||
/// - THREAD_UPDATE
|
||||
/// - THREAD_DELETE
|
||||
/// - THREAD_LIST_SYNC
|
||||
/// - THREAD_MEMBER_UPDATE
|
||||
/// - THREAD_MEMBERS_UPDATE
|
||||
/// - STAGE_INSTANCE_CREATE
|
||||
/// - STAGE_INSTANCE_UPDATE
|
||||
/// - STAGE_INSTANCE_DELETE
|
||||
////
|
||||
Guilds: bool = false,
|
||||
///
|
||||
/// - GUILD_MEMBER_ADD
|
||||
/// - GUILD_MEMBER_UPDATE
|
||||
/// - GUILD_MEMBER_REMOVE
|
||||
/// - THREAD_MEMBERS_UPDATE
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
GuildMembers: bool = false,
|
||||
///
|
||||
/// - GUILD_AUDIT_LOG_ENTRY_CREATE
|
||||
/// - GUILD_BAN_ADD
|
||||
/// - GUILD_BAN_REMOVE
|
||||
////
|
||||
GuildModeration: bool = false,
|
||||
///
|
||||
/// - GUILD_EMOJIS_UPDATE
|
||||
/// - GUILD_STICKERS_UPDATE
|
||||
////
|
||||
GuildEmojisAndStickers: bool = false,
|
||||
///
|
||||
/// - GUILD_INTEGRATIONS_UPDATE
|
||||
/// - INTEGRATION_CREATE
|
||||
/// - INTEGRATION_UPDATE
|
||||
/// - INTEGRATION_DELETE
|
||||
////
|
||||
GuildIntegrations: bool = false,
|
||||
///
|
||||
/// - WEBHOOKS_UPDATE
|
||||
////
|
||||
GuildWebhooks: bool = false,
|
||||
///
|
||||
/// - INVITE_CREATE
|
||||
/// - INVITE_DELETE
|
||||
////
|
||||
GuildInvites: bool = false,
|
||||
///
|
||||
/// - VOICE_STATE_UPDATE
|
||||
/// - VOICE_CHANNEL_EFFECT_SEND
|
||||
////
|
||||
GuildVoiceStates: bool = false,
|
||||
///
|
||||
/// - PRESENCE_UPDATE
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
GuildPresences: bool = false,
|
||||
///
|
||||
/// - MESSAGE_CREATE
|
||||
/// - MESSAGE_UPDATE
|
||||
/// - MESSAGE_DELETE
|
||||
/// - MESSAGE_DELETE_BULK
|
||||
///
|
||||
/// The messages do not contain content by default.
|
||||
/// If you want to receive their content too, you need to turn on the privileged `MESSAGE_CONTENT` intent. */
|
||||
GuildMessages: bool = false,
|
||||
///
|
||||
/// - MESSAGE_REACTION_ADD
|
||||
/// - MESSAGE_REACTION_REMOVE
|
||||
/// - MESSAGE_REACTION_REMOVE_ALL
|
||||
/// - MESSAGE_REACTION_REMOVE_EMOJI
|
||||
////
|
||||
GuildMessageReactions: bool = false,
|
||||
///
|
||||
/// - TYPING_START
|
||||
////
|
||||
GuildMessageTyping: bool = false,
|
||||
///
|
||||
/// - CHANNEL_CREATE
|
||||
/// - MESSAGE_CREATE
|
||||
/// - MESSAGE_UPDATE
|
||||
/// - MESSAGE_DELETE
|
||||
/// - CHANNEL_PINS_UPDATE
|
||||
////
|
||||
DirectMessages: bool = false,
|
||||
///
|
||||
/// - MESSAGE_REACTION_ADD
|
||||
/// - MESSAGE_REACTION_REMOVE
|
||||
/// - MESSAGE_REACTION_REMOVE_ALL
|
||||
/// - MESSAGE_REACTION_REMOVE_EMOJI
|
||||
////
|
||||
DirectMessageReactions: bool = false,
|
||||
///
|
||||
/// - TYPING_START
|
||||
////
|
||||
DirectMessageTyping: bool = false,
|
||||
///
|
||||
/// This intent will add all content related values to message events.
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
MessageContent: bool = false,
|
||||
///
|
||||
/// - GUILD_SCHEDULED_EVENT_CREATE
|
||||
/// - GUILD_SCHEDULED_EVENT_UPDATE
|
||||
/// - GUILD_SCHEDULED_EVENT_DELETE
|
||||
/// - GUILD_SCHEDULED_EVENT_USER_ADD this is experimental and unstable.
|
||||
/// - GUILD_SCHEDULED_EVENT_USER_REMOVE this is experimental and unstable.
|
||||
////
|
||||
GuildScheduledEvents: bool = false,
|
||||
_pad: u4 = 0,
|
||||
///
|
||||
/// - AUTO_MODERATION_RULE_CREATE
|
||||
/// - AUTO_MODERATION_RULE_UPDATE
|
||||
/// - AUTO_MODERATION_RULE_DELETE
|
||||
////
|
||||
AutoModerationConfiguration: bool = false,
|
||||
///
|
||||
/// - AUTO_MODERATION_ACTION_EXECUTION
|
||||
////
|
||||
AutoModerationExecution: bool = false,
|
||||
_pad2: u3 = 0,
|
||||
///
|
||||
/// - MESSAGE_POLL_VOTE_ADD
|
||||
/// - MESSAGE_POLL_VOTE_REMOVE
|
||||
////
|
||||
GuildMessagePolls: bool = false,
|
||||
///
|
||||
/// - MESSAGE_POLL_VOTE_ADD
|
||||
/// - MESSAGE_POLL_VOTE_REMOVE
|
||||
////
|
||||
DirectMessagePolls: bool = false,
|
||||
_pad3: u4 = 0,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#list-of-intents
|
||||
/// alias
|
||||
pub const Intents = GatewayIntents;
|
2914
src/shard/shard.zig
Normal file
2914
src/shard/shard.zig
Normal file
File diff suppressed because it is too large
Load Diff
262
src/shard/sharder.zig
Normal file
262
src/shard/sharder.zig
Normal file
@ -0,0 +1,262 @@
|
||||
//! ISC License
|
||||
//!
|
||||
//! Copyright (c) 2024-2025 Yuzu
|
||||
//!
|
||||
//! Permission to use, copy, modify, and/or distribute this software for any
|
||||
//! purpose with or without fee is hereby granted, provided that the above
|
||||
//! copyright notice and this permission notice appear in all copies.
|
||||
//!
|
||||
//! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
//! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
//! AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
//! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
//! LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
//! OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
//! PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
const Intents = @import("intents.zig").Intents;
|
||||
const Snowflake = @import("../structures/snowflake.zig").Snowflake;
|
||||
const GatewayBotInfo = @import("util.zig").GatewayBotInfo;
|
||||
const IdentifyProperties = @import("util.zig").IdentifyProperties;
|
||||
const internalLogif = @import("../utils/core.zig").logif;
|
||||
const ShardDetails = @import("util.zig").ShardDetails;
|
||||
const ConnectQueue = @import("connect_queue.zig").ConnectQueue;
|
||||
const GatewayDispatchEvent = @import("../utils/core.zig").GatewayDispatchEvent;
|
||||
const Log = @import("../utils/core.zig").Log;
|
||||
const Shard = @import("shard.zig").Shard;
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const debug = @import("../utils/core.zig").debug;
|
||||
const TableTemplate = @import("../cache/cache.zig").TableTemplate;
|
||||
const CacheTables = @import("../cache/cache.zig").CacheTables;
|
||||
|
||||
pub fn ShardManager(comptime Table: TableTemplate) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
shard_details: ShardDetails,
|
||||
allocator: mem.Allocator,
|
||||
|
||||
/// Queue for managing shard connections
|
||||
connect_queue: ConnectQueue(Shard(Table)),
|
||||
shards: std.AutoArrayHashMap(usize, Shard(Table)),
|
||||
handler: GatewayDispatchEvent,
|
||||
|
||||
/// where we dispatch work for every thread, threads must be spawned upon shard creation
|
||||
/// make sure the address of workers is stable
|
||||
workers: std.Thread.Pool = undefined,
|
||||
|
||||
/// configuration settings
|
||||
options: SessionOptions,
|
||||
log: Log,
|
||||
|
||||
// must be initialised
|
||||
cache: *CacheTables(Table),
|
||||
|
||||
pub const ShardData = struct {
|
||||
/// resume seq to resume connections
|
||||
resume_seq: ?usize,
|
||||
|
||||
/// resume_gateway_url is the url to resume the connection
|
||||
/// https://discord.com/developers/docs/topics/gateway#ready-event
|
||||
resume_gateway_url: ?[]const u8,
|
||||
|
||||
/// session_id is the unique session id of the gateway
|
||||
session_id: ?[]const u8,
|
||||
};
|
||||
|
||||
pub const SessionOptions = struct {
|
||||
/// Important data which is used by the manager to connect shards to the gateway. */
|
||||
info: GatewayBotInfo,
|
||||
/// Delay in milliseconds to wait before spawning next shard. OPTIMAL IS ABOVE 5100. YOU DON'T WANT TO HIT THE RATE LIMIT!!!
|
||||
spawn_shard_delay: ?u64 = 5300,
|
||||
/// Total amount of shards your bot uses. Useful for zero-downtime updates or resharding.
|
||||
total_shards: usize = 1,
|
||||
shard_start: usize = 0,
|
||||
shard_end: usize = 1,
|
||||
/// The payload handlers for messages on the shard.
|
||||
resharding: ?struct { interval: u64, percentage: usize } = null,
|
||||
/// worker threads
|
||||
workers_per_shard: usize = 1,
|
||||
/// The shard lifespan in milliseconds. If a shard is not connected within this time, it will be closed.
|
||||
shard_lifespan: ?u64 = null,
|
||||
};
|
||||
|
||||
pub fn init(allocator: mem.Allocator, settings: struct {
|
||||
token: []const u8,
|
||||
intents: Intents,
|
||||
options: SessionOptions,
|
||||
run: GatewayDispatchEvent,
|
||||
log: Log,
|
||||
cache: TableTemplate,
|
||||
}) mem.Allocator.Error!Self {
|
||||
const concurrency = settings.options.info.session_start_limit.?.max_concurrency;
|
||||
const cache = try allocator.create(CacheTables(Table));
|
||||
cache.* = CacheTables(Table).defaults(allocator);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.connect_queue = try ConnectQueue(Shard(Table)).init(allocator, concurrency, 5000),
|
||||
.shards = .init(allocator),
|
||||
.workers = undefined,
|
||||
.shard_details = ShardDetails{
|
||||
.token = settings.token,
|
||||
.intents = settings.intents,
|
||||
},
|
||||
.handler = settings.run,
|
||||
.options = .{
|
||||
.info = .{
|
||||
.url = settings.options.info.url,
|
||||
.shards = settings.options.info.shards,
|
||||
.session_start_limit = settings.options.info.session_start_limit,
|
||||
},
|
||||
.total_shards = settings.options.total_shards,
|
||||
.shard_start = settings.options.shard_start,
|
||||
.shard_end = settings.options.shard_end,
|
||||
.workers_per_shard = settings.options.workers_per_shard,
|
||||
},
|
||||
.log = settings.log,
|
||||
.cache = cache,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.connect_queue.deinit();
|
||||
self.shards.deinit();
|
||||
}
|
||||
|
||||
pub fn forceIdentify(self: *Self, shard_id: usize) !void {
|
||||
self.logif("#{d} force identify", .{shard_id});
|
||||
const shard = try self.create(shard_id);
|
||||
|
||||
return shard.identify(null);
|
||||
}
|
||||
|
||||
pub fn disconnect(self: *Self, shard_id: usize) Shard(Table).CloseError!void {
|
||||
return if (self.shards.get(shard_id)) |shard| shard.disconnect();
|
||||
}
|
||||
|
||||
pub fn disconnectAll(self: *Self) Shard(Table).CloseError!void {
|
||||
while (self.shards.iterator().next()) |shard| shard.value_ptr.disconnect();
|
||||
}
|
||||
|
||||
/// spawn buckets in order
|
||||
/// Log bucket preparation
|
||||
/// Divide shards into chunks based on concurrency
|
||||
/// Assign each shard to a bucket
|
||||
/// Return list of buckets
|
||||
/// https://discord.com/developers/docs/events/gateway#sharding-max-concurrency
|
||||
fn spawnBuckets(self: *Self) ![][]Shard(Table) {
|
||||
const concurrency = self.options.info.session_start_limit.?.max_concurrency;
|
||||
|
||||
self.logif("{d}-{d}", .{ self.options.shard_start, self.options.shard_end });
|
||||
|
||||
const range = std.math.sub(usize, self.options.shard_start, self.options.shard_end) catch 1;
|
||||
const bucket_count = (range + concurrency - 1) / concurrency;
|
||||
|
||||
self.logif("#0 preparing buckets", .{});
|
||||
|
||||
const buckets = try self.allocator.alloc([]Shard(Table), bucket_count);
|
||||
|
||||
for (buckets, 0..) |*bucket, i| {
|
||||
const bucket_size = if ((i + 1) * concurrency > range) range - (i * concurrency) else concurrency;
|
||||
|
||||
bucket.* = try self.allocator.alloc(Shard(Table), bucket_size);
|
||||
|
||||
for (bucket.*, 0..) |*shard, j| {
|
||||
shard.* = try self.create(self.options.shard_start + i * concurrency + j);
|
||||
}
|
||||
}
|
||||
|
||||
self.logif("{d} buckets created", .{bucket_count});
|
||||
|
||||
// finally defihne threads
|
||||
|
||||
try self.workers.init(.{
|
||||
.allocator = self.allocator,
|
||||
.n_jobs = self.options.workers_per_shard * self.options.total_shards,
|
||||
});
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/// creates a shard and stores it
|
||||
fn create(self: *Self, shard_id: usize) !Shard(Table) {
|
||||
if (self.shards.get(shard_id)) |s| return s;
|
||||
|
||||
const shard: Shard(Table) = try .init(self.allocator, shard_id, self.options.total_shards, .{
|
||||
.token = self.shard_details.token,
|
||||
.intents = self.shard_details.intents,
|
||||
.options = Shard(Table).ShardOptions{
|
||||
.info = self.options.info,
|
||||
.ratelimit_options = .{},
|
||||
},
|
||||
.run = self.handler,
|
||||
.log = self.log,
|
||||
.cache = self.cache,
|
||||
.sharder_pool = &self.workers,
|
||||
});
|
||||
|
||||
try self.shards.put(shard_id, shard);
|
||||
|
||||
return shard;
|
||||
}
|
||||
|
||||
pub fn resume_(self: *Self, shard_id: usize, shard_data: ShardData) void {
|
||||
if (self.shards.contains(shard_id)) return error.CannotOverrideExistingShard;
|
||||
|
||||
const shard = self.create(shard_id);
|
||||
|
||||
shard.data = shard_data;
|
||||
|
||||
return self.connect_queue.push(.{
|
||||
.shard = shard,
|
||||
.callback = &callback,
|
||||
});
|
||||
}
|
||||
|
||||
fn callback(self: *ConnectQueue(Shard(Table)).RequestWithShard) anyerror!void {
|
||||
try self.shard.connect();
|
||||
}
|
||||
|
||||
pub fn spawnShards(self: *Self) !void {
|
||||
const buckets = try self.spawnBuckets();
|
||||
|
||||
self.logif("Spawning shards", .{});
|
||||
|
||||
for (buckets) |bucket| {
|
||||
for (bucket) |shard| {
|
||||
self.logif("adding {d} to connect queue", .{shard.id});
|
||||
try self.connect_queue.push(.{
|
||||
.shard = shard,
|
||||
.callback = &callback,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//self.startResharder();
|
||||
}
|
||||
|
||||
pub fn send(self: *Self, shard_id: usize, data: anytype) Shard(Table).SendError!void {
|
||||
if (self.shards.get(shard_id)) |shard| try shard.send(data);
|
||||
}
|
||||
|
||||
// SPEC OF THE RESHARDER:
|
||||
// Class Self
|
||||
//
|
||||
// Method startResharder():
|
||||
// If resharding interval is not set or shard bounds are not valid:
|
||||
// Exit
|
||||
// Set up periodic check for resharding:
|
||||
// If new shards are required:
|
||||
// Log resharding process
|
||||
// Update options with new shard settings
|
||||
// Disconnect old shards and clear them from manager
|
||||
// Spawn shards again with updated configuration
|
||||
//
|
||||
|
||||
inline fn logif(self: *Self, comptime format: []const u8, args: anytype) void {
|
||||
internalLogif(self.log, format, args);
|
||||
}
|
||||
};
|
||||
}
|
59
src/shard/util.zig
Normal file
59
src/shard/util.zig
Normal file
@ -0,0 +1,59 @@
|
||||
pub const IdentifyProperties = struct {
|
||||
/// Operating system the shard runs on.
|
||||
os: []const u8,
|
||||
/// The "browser" where this shard is running on.
|
||||
browser: []const u8,
|
||||
/// The device on which the shard is running.
|
||||
device: []const u8,
|
||||
};
|
||||
|
||||
pub const default_identify_properties = IdentifyProperties{
|
||||
.os = @tagName(@import("builtin").os.tag),
|
||||
.browser = "discord.zig",
|
||||
.device = "discord.zig",
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#get-gateway
|
||||
pub const GatewayInfo = struct {
|
||||
/// The WSS URL that can be used for connecting to the gateway
|
||||
url: []const u8,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/events/gateway#session-start-limit-object
|
||||
pub const GatewaySessionStartLimit = struct {
|
||||
/// Total number of session starts the current user is allowed
|
||||
total: u32,
|
||||
/// Remaining number of session starts the current user is allowed
|
||||
remaining: u32,
|
||||
/// Number of milliseconds after which the limit resets
|
||||
reset_after: u32,
|
||||
/// Number of identify requests allowed per 5 seconds
|
||||
max_concurrency: u32,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#get-gateway-bot
|
||||
pub const GatewayBotInfo = struct {
|
||||
url: []const u8,
|
||||
/// The recommended number of shards to use when connecting
|
||||
///
|
||||
/// See https://discord.com/developers/docs/topics/gateway#sharding
|
||||
shards: u32,
|
||||
/// Information on the current session start limit
|
||||
///
|
||||
/// See https://discord.com/developers/docs/topics/gateway#session-start-limit-object
|
||||
session_start_limit: ?GatewaySessionStartLimit,
|
||||
};
|
||||
|
||||
pub const ShardDetails = struct {
|
||||
/// Bot token which is used to connect to Discord */
|
||||
token: []const u8,
|
||||
/// The URL of the gateway which should be connected to.
|
||||
url: []const u8 = "wss://gateway.discord.gg",
|
||||
/// The gateway version which should be used.
|
||||
version: ?usize = 10,
|
||||
/// The calculated intent value of the events which the shard should receive.
|
||||
intents: @import("./intents.zig").Intents,
|
||||
/// Identify properties to use
|
||||
properties: IdentifyProperties = default_identify_properties,
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ const Partial = @import("partial.zig").Partial;
|
||||
const User = @import("user.zig").User;
|
||||
const Team = @import("team.zig").Team;
|
||||
const Guild = @import("guild.zig").Guild;
|
||||
const AssociativeArray = @import("../json-helper.zig").AssociativeArray;
|
||||
const AssociativeArray = @import("../utils/json.zig").AssociativeArray;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/application#application-object
|
||||
pub const Application = struct {
|
||||
|
@ -32,7 +32,7 @@
|
||||
const InteractionResponseTypes = @import("shared.zig").InteractionResponseTypes;
|
||||
const InteractionContextType = @import("command.zig").InteractionContextType;
|
||||
const Entitlement = @import("monetization.zig").Entitlement;
|
||||
const Record = @import("../json-helper.zig").Record;
|
||||
const Record = @import("../utils/json.zig").Record;
|
||||
|
||||
pub const Interaction = struct {
|
||||
/// Id of the interaction
|
||||
|
@ -15,12 +15,13 @@
|
||||
//! PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = std.builtin;
|
||||
|
||||
pub fn Partial(comptime T: type) type {
|
||||
const info = @typeInfo(T);
|
||||
switch (info) {
|
||||
.@"struct" => |s| {
|
||||
comptime var fields: []const std.builtin.Type.StructField = &[_]std.builtin.Type.StructField{};
|
||||
comptime var fields: []const builtin.Type.StructField = &[_]builtin.Type.StructField{};
|
||||
inline for (s.fields) |field| {
|
||||
if (field.is_comptime) {
|
||||
@compileError("Cannot make Partial of " ++ @typeName(T) ++ ", it has a comptime field " ++ field.name);
|
||||
@ -31,7 +32,7 @@ pub fn Partial(comptime T: type) type {
|
||||
};
|
||||
const default_value: optional_type = null;
|
||||
const aligned_ptr: *align(field.alignment) const anyopaque = @alignCast(@ptrCast(&default_value));
|
||||
const optional_field: [1]std.builtin.Type.StructField = [_]std.builtin.Type.StructField{.{
|
||||
const optional_field: [1]builtin.Type.StructField = [_]builtin.Type.StructField{.{
|
||||
.alignment = field.alignment,
|
||||
.default_value_ptr = aligned_ptr,
|
||||
.is_comptime = false,
|
||||
@ -40,9 +41,9 @@ pub fn Partial(comptime T: type) type {
|
||||
}};
|
||||
fields = fields ++ optional_field;
|
||||
}
|
||||
const partial_type_info: std.builtin.Type = .{ .@"struct" = .{
|
||||
const partial_type_info: builtin.Type = .{ .@"struct" = .{
|
||||
.backing_integer = s.backing_integer,
|
||||
.decls = &[_]std.builtin.Type.Declaration{},
|
||||
.decls = &[_]builtin.Type.Declaration{},
|
||||
.fields = fields,
|
||||
.is_tuple = s.is_tuple,
|
||||
.layout = s.layout,
|
||||
|
@ -665,18 +665,18 @@ pub const GuildFeatures = enum {
|
||||
RAID_ALERTS_DISABLED,
|
||||
/// Guild is using the old permissions configuration behavior
|
||||
APPLICATION_COMMAND_PERMISSIONS_V2,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-mfa-level
|
||||
pub const MfaLevels = enum {
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-mfa-level
|
||||
pub const MfaLevels = enum {
|
||||
/// Guild has no MFA/2FA requirement for moderation actions
|
||||
None,
|
||||
/// Guild has a 2FA requirement for moderation actions
|
||||
Elevated,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags
|
||||
pub const SystemChannelFlags = packed struct {
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags
|
||||
pub const SystemChannelFlags = packed struct {
|
||||
pub fn toRaw(self: SystemChannelFlags) u8 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
@ -710,10 +710,10 @@ pub const GuildFeatures = enum {
|
||||
/// Hide member join sticker reply buttons
|
||||
SuppressJoinNotificationReplies: bool = false,
|
||||
_pad: u4 = 0,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-premium-tier
|
||||
pub const PremiumTiers = enum {
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-premium-tier
|
||||
pub const PremiumTiers = enum {
|
||||
/// Guild has not unlocked any Server Boost perks
|
||||
None,
|
||||
/// Guild has unlocked Server Boost level 1 perks
|
||||
@ -722,18 +722,18 @@ pub const GuildFeatures = enum {
|
||||
Tier2,
|
||||
/// Guild has unlocked Server Boost level 3 perks
|
||||
Tier3,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level
|
||||
pub const GuildNsfwLevel = enum {
|
||||
/// https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level
|
||||
pub const GuildNsfwLevel = enum {
|
||||
Default,
|
||||
Explicit,
|
||||
Safe,
|
||||
AgeRestricted,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
pub const ChannelTypes = packed struct {
|
||||
/// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
pub const ChannelTypes = packed struct {
|
||||
pub fn toRaw(self: ChannelTypes) u32 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
@ -786,32 +786,32 @@ pub const GuildFeatures = enum {
|
||||
/// Channel that can only contain threads, similar to GUILD_FORUM channels
|
||||
GuildMedia: bool = false,
|
||||
_pad1: u15 = 0,
|
||||
};
|
||||
};
|
||||
|
||||
pub const OverwriteTypes = enum {
|
||||
pub const OverwriteTypes = enum {
|
||||
Role,
|
||||
Member,
|
||||
};
|
||||
};
|
||||
|
||||
pub const VideoQualityModes = enum(u4) {
|
||||
pub const VideoQualityModes = enum(u4) {
|
||||
/// Discord chooses the quality for optimal performance
|
||||
Auto = 1,
|
||||
/// 720p
|
||||
Full,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-types
|
||||
pub const ActivityTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/topics/gateway-events#activity-object-activity-types
|
||||
pub const ActivityTypes = enum(u4) {
|
||||
Playing = 0,
|
||||
Streaming = 1,
|
||||
Listening = 2,
|
||||
Watching = 3,
|
||||
Custom = 4,
|
||||
Competing = 5,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
pub const MessageTypes = enum(u8) {
|
||||
/// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
pub const MessageTypes = enum(u8) {
|
||||
Default,
|
||||
RecipientAdd,
|
||||
RecipientRemove,
|
||||
@ -849,43 +849,43 @@ pub const GuildFeatures = enum {
|
||||
GuildIncidentReportFalseAlarm,
|
||||
PurchaseNotification = 44,
|
||||
PollResult = 46,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/channel#message-object-message-activity-types
|
||||
pub const MessageActivityTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/resources/channel#message-object-message-activity-types
|
||||
pub const MessageActivityTypes = enum(u4) {
|
||||
Join = 1,
|
||||
Spectate = 2,
|
||||
Listen = 3,
|
||||
JoinRequest = 5,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types
|
||||
pub const StickerTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types
|
||||
pub const StickerTypes = enum(u4) {
|
||||
/// an official sticker in a pack
|
||||
Standard = 1,
|
||||
/// a sticker uploaded to a guild for the guild's members
|
||||
Guild = 2,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types
|
||||
pub const StickerFormatTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types
|
||||
pub const StickerFormatTypes = enum(u4) {
|
||||
Png = 1,
|
||||
APng,
|
||||
Lottie,
|
||||
Gif,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype
|
||||
pub const InteractionTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#interaction-interactiontype
|
||||
pub const InteractionTypes = enum(u4) {
|
||||
Ping = 1,
|
||||
ApplicationCommand = 2,
|
||||
MessageComponent = 3,
|
||||
ApplicationCommandAutocomplete = 4,
|
||||
ModalSubmit = 5,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype
|
||||
pub const ApplicationCommandOptionTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptiontype
|
||||
pub const ApplicationCommandOptionTypes = enum(u4) {
|
||||
SubCommand = 1,
|
||||
SubCommandGroup,
|
||||
String,
|
||||
@ -897,10 +897,10 @@ pub const GuildFeatures = enum {
|
||||
Mentionable,
|
||||
Number,
|
||||
Attachment,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events
|
||||
pub const AuditLogEvents = enum(u4) {
|
||||
/// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events
|
||||
pub const AuditLogEvents = enum(u4) {
|
||||
/// Server settings were updated
|
||||
GuildUpdate = 1,
|
||||
/// Channel was created
|
||||
@ -1027,33 +1027,33 @@ pub const GuildFeatures = enum {
|
||||
HomeSettingsCreate = 190,
|
||||
/// Guild Server Guide was updated
|
||||
HomeSettingsUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ScheduledEventPrivacyLevel = enum(u4) {
|
||||
pub const ScheduledEventPrivacyLevel = enum(u4) {
|
||||
/// the scheduled event is only accessible to guild members
|
||||
GuildOnly = 2,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ScheduledEventEntityType = enum(u4) {
|
||||
pub const ScheduledEventEntityType = enum(u4) {
|
||||
StageInstance = 1,
|
||||
Voice,
|
||||
External,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ScheduledEventStatus = enum(u4) {
|
||||
pub const ScheduledEventStatus = enum(u4) {
|
||||
Scheduled = 1,
|
||||
Active,
|
||||
Completed,
|
||||
Canceled,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/resources/invite#invite-object-target-user-types
|
||||
pub const TargetTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/resources/invite#invite-object-target-user-types
|
||||
pub const TargetTypes = enum(u4) {
|
||||
Stream = 1,
|
||||
EmbeddedApplication,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ApplicationCommandTypes = enum(u4) {
|
||||
pub const ApplicationCommandTypes = enum(u4) {
|
||||
/// A text-based command that shows up when a user types `/`
|
||||
ChatInput = 1,
|
||||
/// A UI-based command that shows up when you right click or tap on a user
|
||||
@ -1062,17 +1062,17 @@ pub const GuildFeatures = enum {
|
||||
Message,
|
||||
/// A UI-based command that represents the primary way to invoke an app's Activity
|
||||
PrimaryEntryPoint,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ApplicationCommandPermissionTypes = enum(u4) {
|
||||
pub const ApplicationCommandPermissionTypes = enum(u4) {
|
||||
Role = 1,
|
||||
User,
|
||||
Channel,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
|
||||
/// Permissions v2
|
||||
pub const BitwisePermissionFlags = packed struct {
|
||||
/// 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);
|
||||
}
|
||||
@ -1196,9 +1196,9 @@ pub const GuildFeatures = enum {
|
||||
/// 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: bool = false,
|
||||
_pad: u15 = 0,
|
||||
};
|
||||
};
|
||||
|
||||
pub const PermissionStrings = union(enum) {
|
||||
pub const PermissionStrings = union(enum) {
|
||||
/// Allows creation of instant invites
|
||||
CREATE_INSTANT_INVITE,
|
||||
/// Allows kicking members
|
||||
@ -1297,10 +1297,10 @@ pub const GuildFeatures = enum {
|
||||
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) {
|
||||
/// https://discord.com/developers/docs/topics/opcodes-and-status-codes#opcodes-and-status-codes
|
||||
pub const GatewayCloseEventCodes = enum(u16) {
|
||||
/// A normal closure of the gateway. You may attempt to reconnect.
|
||||
NormalClosure = 1000,
|
||||
/// We're not sure what went wrong. Try reconnecting?
|
||||
@ -1331,10 +1331,10 @@ pub const GuildFeatures = enum {
|
||||
InvalidIntents,
|
||||
/// You sent a disallowed intent for a [Gateway Intent](https://discord.com/developers/docs/topics/gateway#gateway-intents). You may have tried to specify an intent that you [have not enabled or are not approved for](https://discord.com/developers/docs/topics/gateway#privileged-intents).
|
||||
DisallowedIntents,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes
|
||||
pub const GatewayOpcodes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes
|
||||
pub const GatewayOpcodes = enum(u4) {
|
||||
/// An event was dispatched.
|
||||
Dispatch,
|
||||
/// Fired periodically by the client to keep the connection alive.
|
||||
@ -1357,9 +1357,9 @@ pub const GuildFeatures = enum {
|
||||
Hello,
|
||||
/// Sent in response to receiving a heartbeat to acknowledge that it has been received.
|
||||
HeartbeatACK,
|
||||
};
|
||||
};
|
||||
|
||||
pub const GatewayDispatchEventNames = union(enum) {
|
||||
pub const GatewayDispatchEventNames = union(enum) {
|
||||
APPLICATION_COMMAND_PERMISSIONS_UPDATE,
|
||||
AUTO_MODERATION_RULE_CREATE,
|
||||
AUTO_MODERATION_RULE_UPDATE,
|
||||
@ -1428,186 +1428,11 @@ pub const GuildFeatures = enum {
|
||||
|
||||
READY,
|
||||
RESUMED,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#list-of-intents
|
||||
pub const GatewayIntents = packed struct {
|
||||
pub fn toRaw(self: GatewayIntents) u32 {
|
||||
return @bitCast(self);
|
||||
}
|
||||
|
||||
pub fn fromRaw(raw: u32) GatewayIntents {
|
||||
return @bitCast(raw);
|
||||
}
|
||||
|
||||
pub fn jsonParse(allocator: std.mem.Allocator, src: anytype, _: std.json.ParseOptions) !@This() {
|
||||
const value = try std.json.innerParse(std.json.Value, allocator, src, .{
|
||||
.ignore_unknown_fields = true,
|
||||
.max_value_len = 0x1000,
|
||||
});
|
||||
if (value != .integer) @panic("Invalid value for bitfield");
|
||||
|
||||
return fromRaw(@intCast(value.integer));
|
||||
}
|
||||
|
||||
pub fn jsonParseFromValue(_: std.mem.Allocator, src: std.json.Value, _: std.json.ParseOptions) @This() {
|
||||
if (src != .integer) @panic("Invalid value for bitfield");
|
||||
return fromRaw(@intCast(src.integer));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
///
|
||||
/// - GUILD_CREATE
|
||||
/// - GUILD_UPDATE
|
||||
/// - GUILD_DELETE
|
||||
/// - GUILD_ROLE_CREATE
|
||||
/// - GUILD_ROLE_UPDATE
|
||||
/// - GUILD_ROLE_DELETE
|
||||
/// - CHANNEL_CREATE
|
||||
/// - CHANNEL_UPDATE
|
||||
/// - CHANNEL_DELETE
|
||||
/// - CHANNEL_PINS_UPDATE
|
||||
/// - THREAD_CREATE
|
||||
/// - THREAD_UPDATE
|
||||
/// - THREAD_DELETE
|
||||
/// - THREAD_LIST_SYNC
|
||||
/// - THREAD_MEMBER_UPDATE
|
||||
/// - THREAD_MEMBERS_UPDATE
|
||||
/// - STAGE_INSTANCE_CREATE
|
||||
/// - STAGE_INSTANCE_UPDATE
|
||||
/// - STAGE_INSTANCE_DELETE
|
||||
////
|
||||
Guilds: bool = false,
|
||||
///
|
||||
/// - GUILD_MEMBER_ADD
|
||||
/// - GUILD_MEMBER_UPDATE
|
||||
/// - GUILD_MEMBER_REMOVE
|
||||
/// - THREAD_MEMBERS_UPDATE
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
GuildMembers: bool = false,
|
||||
///
|
||||
/// - GUILD_AUDIT_LOG_ENTRY_CREATE
|
||||
/// - GUILD_BAN_ADD
|
||||
/// - GUILD_BAN_REMOVE
|
||||
////
|
||||
GuildModeration: bool = false,
|
||||
///
|
||||
/// - GUILD_EMOJIS_UPDATE
|
||||
/// - GUILD_STICKERS_UPDATE
|
||||
////
|
||||
GuildEmojisAndStickers: bool = false,
|
||||
///
|
||||
/// - GUILD_INTEGRATIONS_UPDATE
|
||||
/// - INTEGRATION_CREATE
|
||||
/// - INTEGRATION_UPDATE
|
||||
/// - INTEGRATION_DELETE
|
||||
////
|
||||
GuildIntegrations: bool = false,
|
||||
///
|
||||
/// - WEBHOOKS_UPDATE
|
||||
////
|
||||
GuildWebhooks: bool = false,
|
||||
///
|
||||
/// - INVITE_CREATE
|
||||
/// - INVITE_DELETE
|
||||
////
|
||||
GuildInvites: bool = false,
|
||||
///
|
||||
/// - VOICE_STATE_UPDATE
|
||||
/// - VOICE_CHANNEL_EFFECT_SEND
|
||||
////
|
||||
GuildVoiceStates: bool = false,
|
||||
///
|
||||
/// - PRESENCE_UPDATE
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
GuildPresences: bool = false,
|
||||
///
|
||||
/// - MESSAGE_CREATE
|
||||
/// - MESSAGE_UPDATE
|
||||
/// - MESSAGE_DELETE
|
||||
/// - MESSAGE_DELETE_BULK
|
||||
///
|
||||
/// The messages do not contain content by default.
|
||||
/// If you want to receive their content too, you need to turn on the privileged `MESSAGE_CONTENT` intent. */
|
||||
GuildMessages: bool = false,
|
||||
///
|
||||
/// - MESSAGE_REACTION_ADD
|
||||
/// - MESSAGE_REACTION_REMOVE
|
||||
/// - MESSAGE_REACTION_REMOVE_ALL
|
||||
/// - MESSAGE_REACTION_REMOVE_EMOJI
|
||||
////
|
||||
GuildMessageReactions: bool = false,
|
||||
///
|
||||
/// - TYPING_START
|
||||
////
|
||||
GuildMessageTyping: bool = false,
|
||||
///
|
||||
/// - CHANNEL_CREATE
|
||||
/// - MESSAGE_CREATE
|
||||
/// - MESSAGE_UPDATE
|
||||
/// - MESSAGE_DELETE
|
||||
/// - CHANNEL_PINS_UPDATE
|
||||
////
|
||||
DirectMessages: bool = false,
|
||||
///
|
||||
/// - MESSAGE_REACTION_ADD
|
||||
/// - MESSAGE_REACTION_REMOVE
|
||||
/// - MESSAGE_REACTION_REMOVE_ALL
|
||||
/// - MESSAGE_REACTION_REMOVE_EMOJI
|
||||
////
|
||||
DirectMessageReactions: bool = false,
|
||||
///
|
||||
/// - TYPING_START
|
||||
////
|
||||
DirectMessageTyping: bool = false,
|
||||
///
|
||||
/// This intent will add all content related values to message events.
|
||||
///
|
||||
/// This is a privileged intent.
|
||||
////
|
||||
MessageContent: bool = false,
|
||||
///
|
||||
/// - GUILD_SCHEDULED_EVENT_CREATE
|
||||
/// - GUILD_SCHEDULED_EVENT_UPDATE
|
||||
/// - GUILD_SCHEDULED_EVENT_DELETE
|
||||
/// - GUILD_SCHEDULED_EVENT_USER_ADD this is experimental and unstable.
|
||||
/// - GUILD_SCHEDULED_EVENT_USER_REMOVE this is experimental and unstable.
|
||||
////
|
||||
GuildScheduledEvents: bool = false,
|
||||
_pad: u4 = 0,
|
||||
///
|
||||
/// - AUTO_MODERATION_RULE_CREATE
|
||||
/// - AUTO_MODERATION_RULE_UPDATE
|
||||
/// - AUTO_MODERATION_RULE_DELETE
|
||||
////
|
||||
AutoModerationConfiguration: bool = false,
|
||||
///
|
||||
/// - AUTO_MODERATION_ACTION_EXECUTION
|
||||
////
|
||||
AutoModerationExecution: bool = false,
|
||||
_pad2: u3 = 0,
|
||||
///
|
||||
/// - MESSAGE_POLL_VOTE_ADD
|
||||
/// - MESSAGE_POLL_VOTE_REMOVE
|
||||
////
|
||||
GuildMessagePolls: bool = false,
|
||||
///
|
||||
/// - MESSAGE_POLL_VOTE_ADD
|
||||
/// - MESSAGE_POLL_VOTE_REMOVE
|
||||
////
|
||||
DirectMessagePolls: bool = false,
|
||||
_pad3: u4 = 0,
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/topics/gateway#list-of-intents
|
||||
pub const Intents = GatewayIntents;
|
||||
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype
|
||||
pub const InteractionResponseTypes = enum(u4) {
|
||||
/// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionresponsetype
|
||||
pub const InteractionResponseTypes = enum(u4) {
|
||||
/// ACK a `Ping`
|
||||
Pong = 1,
|
||||
/// Respond to an interaction with a message
|
||||
@ -1633,27 +1458,27 @@ pub const GuildFeatures = enum {
|
||||
/// @remarks
|
||||
/// Only available for apps with Activities enabled
|
||||
LaunchActivity = 12,
|
||||
};
|
||||
};
|
||||
|
||||
pub const SortOrderTypes = enum {
|
||||
pub const SortOrderTypes = enum {
|
||||
/// Sort forum posts by activity
|
||||
LatestActivity,
|
||||
/// Sort forum posts by creation time (from most recent to oldest)
|
||||
CreationDate,
|
||||
};
|
||||
};
|
||||
|
||||
pub const ForumLayout = enum {
|
||||
pub const ForumLayout = enum {
|
||||
/// No default has been set for forum channel.
|
||||
NotSet,
|
||||
/// Display posts as a list.
|
||||
ListView,
|
||||
/// Display posts as a collection of tiles.
|
||||
GalleryView,
|
||||
};
|
||||
};
|
||||
|
||||
/// https://discord.com/developers/docs/reference#image-formatting
|
||||
/// json is only for stickers
|
||||
pub const ImageFormat = union(enum) {
|
||||
/// https://discord.com/developers/docs/reference#image-formatting
|
||||
/// json is only for stickers
|
||||
pub const ImageFormat = union(enum) {
|
||||
jpg,
|
||||
jpeg,
|
||||
png,
|
||||
|
@ -66,8 +66,6 @@
|
||||
pub const GatewayCloseEventCodes = @import("shared.zig").GatewayCloseEventCodes;
|
||||
pub const GatewayOpcodes = @import("shared.zig").GatewayOpcodes;
|
||||
pub const GatewayDispatchEventNames = @import("shared.zig").GatewayDispatchEventNames;
|
||||
pub const GatewayIntents = @import("shared.zig").GatewayIntents;
|
||||
pub const Intents = @import("shared.zig").Intents;
|
||||
pub const InteractionResponseTypes = @import("shared.zig").InteractionResponseTypes;
|
||||
pub const SortOrderTypes = @import("shared.zig").SortOrderTypes;
|
||||
pub const ForumLayout = @import("shared.zig").ForumLayout;
|
||||
|
@ -20,7 +20,7 @@
|
||||
const OAuth2Scope = @import("shared.zig").OAuth2Scope;
|
||||
const Integration = @import("integration.zig").Integration;
|
||||
const Partial = @import("partial.zig").Partial;
|
||||
const Record = @import("../json-helper.zig").Record;
|
||||
const Record = @import("../utils/json.zig").Record;
|
||||
|
||||
/// https://discord.com/developers/docs/resources/user#user-object
|
||||
pub const User = struct {
|
||||
|
110
src/utils/core.zig
Normal file
110
src/utils/core.zig
Normal file
@ -0,0 +1,110 @@
|
||||
//! ISC License
|
||||
//!
|
||||
//! Copyright (c) 2024-2025 Yuzu
|
||||
//!
|
||||
//! Permission to use, copy, modify, and/or distribute this software for any
|
||||
//! purpose with or without fee is hereby granted, provided that the above
|
||||
//! copyright notice and this permission notice appear in all copies.
|
||||
//!
|
||||
//! THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
//! REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
//! AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
//! INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
//! LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
//! OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
//! PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
const std = @import("std");
|
||||
const Types = @import("../structures/types.zig");
|
||||
|
||||
pub const debug = std.log.scoped(.@"discord.zig");
|
||||
|
||||
pub const Log = union(enum) { yes, no };
|
||||
|
||||
pub inline fn logif(log: Log, comptime format: []const u8, args: anytype) void {
|
||||
switch (log) {
|
||||
.yes => debug.info(format, args),
|
||||
.no => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub const GatewayDispatchEvent = struct {
|
||||
application_command_permissions_update: ?*const fn (save: *anyopaque, application_command_permissions: Types.ApplicationCommandPermissions) anyerror!void = undefined,
|
||||
auto_moderation_rule_create: ?*const fn (save: *anyopaque, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_rule_update: ?*const fn (save: *anyopaque, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_rule_delete: ?*const fn (save: *anyopaque, rule: Types.AutoModerationRule) anyerror!void = undefined,
|
||||
auto_moderation_action_execution: ?*const fn (save: *anyopaque, action_execution: Types.AutoModerationActionExecution) anyerror!void = undefined,
|
||||
|
||||
channel_create: ?*const fn (save: *anyopaque, chan: Types.Channel) anyerror!void = undefined,
|
||||
channel_update: ?*const fn (save: *anyopaque, chan: Types.Channel) anyerror!void = undefined,
|
||||
/// this isn't send when the channel is not relevant to you
|
||||
channel_delete: ?*const fn (save: *anyopaque, chan: Types.Channel) anyerror!void = undefined,
|
||||
channel_pins_update: ?*const fn (save: *anyopaque, chan_pins_update: Types.ChannelPinsUpdate) anyerror!void = undefined,
|
||||
thread_create: ?*const fn (save: *anyopaque, thread: Types.Channel) anyerror!void = undefined,
|
||||
thread_update: ?*const fn (save: *anyopaque, thread: Types.Channel) anyerror!void = undefined,
|
||||
/// has `id`, `guild_id`, `parent_id`, and `type` fields.
|
||||
thread_delete: ?*const fn (save: *anyopaque, thread: Types.Partial(Types.Channel)) anyerror!void = undefined,
|
||||
thread_list_sync: ?*const fn (save: *anyopaque, data: Types.ThreadListSync) anyerror!void = undefined,
|
||||
thread_member_update: ?*const fn (save: *anyopaque, guild_id: Types.ThreadMemberUpdate) anyerror!void = undefined,
|
||||
thread_members_update: ?*const fn (save: *anyopaque, thread_data: Types.ThreadMembersUpdate) anyerror!void = undefined,
|
||||
// TODO: implement // guild_audit_log_entry_create: null = null,
|
||||
guild_create: ?*const fn (save: *anyopaque, guild: Types.Guild) anyerror!void = undefined,
|
||||
guild_create_unavailable: ?*const fn (save: *anyopaque, guild: Types.UnavailableGuild) anyerror!void = undefined,
|
||||
guild_update: ?*const fn (save: *anyopaque, guild: Types.Guild) anyerror!void = undefined,
|
||||
/// this is not necessarily sent upon deletion of a guild
|
||||
/// but from when a user is *removed* therefrom
|
||||
guild_delete: ?*const fn (save: *anyopaque, guild: Types.UnavailableGuild) anyerror!void = undefined,
|
||||
guild_ban_add: ?*const fn (save: *anyopaque, gba: Types.GuildBanAddRemove) anyerror!void = undefined,
|
||||
guild_ban_remove: ?*const fn (save: *anyopaque, gbr: Types.GuildBanAddRemove) anyerror!void = undefined,
|
||||
guild_emojis_update: ?*const fn (save: *anyopaque, fields: Types.GuildEmojisUpdate) anyerror!void = undefined,
|
||||
guild_stickers_update: ?*const fn (save: *anyopaque, fields: Types.GuildStickersUpdate) anyerror!void = undefined,
|
||||
guild_integrations_update: ?*const fn (save: *anyopaque, fields: Types.GuildIntegrationsUpdate) anyerror!void = undefined,
|
||||
guild_member_add: ?*const fn (save: *anyopaque, guild_id: Types.GuildMemberAdd) anyerror!void = undefined,
|
||||
guild_member_update: ?*const fn (save: *anyopaque, fields: Types.GuildMemberUpdate) anyerror!void = undefined,
|
||||
guild_member_remove: ?*const fn (save: *anyopaque, user: Types.GuildMemberRemove) anyerror!void = undefined,
|
||||
guild_members_chunk: ?*const fn (save: *anyopaque, data: Types.GuildMembersChunk) anyerror!void = undefined,
|
||||
guild_role_create: ?*const fn (save: *anyopaque, role: Types.GuildRoleCreate) anyerror!void = undefined,
|
||||
guild_role_delete: ?*const fn (save: *anyopaque, role: Types.GuildRoleDelete) anyerror!void = undefined,
|
||||
guild_role_update: ?*const fn (save: *anyopaque, role: Types.GuildRoleUpdate) anyerror!void = undefined,
|
||||
guild_scheduled_event_create: ?*const fn (save: *anyopaque, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_update: ?*const fn (save: *anyopaque, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_delete: ?*const fn (save: *anyopaque, s_event: Types.ScheduledEvent) anyerror!void = undefined,
|
||||
guild_scheduled_event_user_add: ?*const fn (save: *anyopaque, data: Types.ScheduledEventUserAdd) anyerror!void = undefined,
|
||||
guild_scheduled_event_user_remove: ?*const fn (save: *anyopaque, data: Types.ScheduledEventUserRemove) anyerror!void = undefined,
|
||||
integration_create: ?*const fn (save: *anyopaque, guild_id: Types.IntegrationCreateUpdate) anyerror!void = undefined,
|
||||
integration_update: ?*const fn (save: *anyopaque, guild_id: Types.IntegrationCreateUpdate) anyerror!void = undefined,
|
||||
integration_delete: ?*const fn (save: *anyopaque, guild_id: Types.IntegrationDelete) anyerror!void = undefined,
|
||||
interaction_create: ?*const fn (save: *anyopaque, interaction: Types.MessageInteraction) anyerror!void = undefined,
|
||||
invite_create: ?*const fn (save: *anyopaque, data: Types.InviteCreate) anyerror!void = undefined,
|
||||
invite_delete: ?*const fn (save: *anyopaque, data: Types.InviteDelete) anyerror!void = undefined,
|
||||
message_create: ?*const fn (save: *anyopaque, message: Types.Message) anyerror!void = undefined,
|
||||
message_update: ?*const fn (save: *anyopaque, message: Types.Message) anyerror!void = undefined,
|
||||
message_delete: ?*const fn (save: *anyopaque, log: Types.MessageDelete) anyerror!void = undefined,
|
||||
message_delete_bulk: ?*const fn (save: *anyopaque, log: Types.MessageDeleteBulk) anyerror!void = undefined,
|
||||
message_reaction_add: ?*const fn (save: *anyopaque, log: Types.MessageReactionAdd) anyerror!void = undefined,
|
||||
message_reaction_remove_all: ?*const fn (save: *anyopaque, data: Types.MessageReactionRemoveAll) anyerror!void = undefined,
|
||||
message_reaction_remove: ?*const fn (save: *anyopaque, data: Types.MessageReactionRemove) anyerror!void = undefined,
|
||||
message_reaction_remove_emoji: ?*const fn (save: *anyopaque, data: Types.MessageReactionRemoveEmoji) anyerror!void = undefined,
|
||||
presence_update: ?*const fn (save: *anyopaque, presence: Types.PresenceUpdate) anyerror!void = undefined,
|
||||
stage_instance_create: ?*const fn (save: *anyopaque, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
stage_instance_update: ?*const fn (save: *anyopaque, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
stage_instance_delete: ?*const fn (save: *anyopaque, stage_instance: Types.StageInstance) anyerror!void = undefined,
|
||||
typing_start: ?*const fn (save: *anyopaque, data: Types.TypingStart) anyerror!void = undefined,
|
||||
/// remember this is only sent when you change your profile yourself/your bot does
|
||||
user_update: ?*const fn (save: *anyopaque, user: Types.User) anyerror!void = undefined,
|
||||
// will do these someday, music is rather pointless at this point in time
|
||||
// TODO: implement // voice_channel_effect_send: null = null,
|
||||
// TODO: implement // voice_state_update: null = null,
|
||||
// TODO: implement // voice_server_update: null = null,
|
||||
webhooks_update: ?*const fn (save: *anyopaque, fields: Types.WebhookUpdate) anyerror!void = undefined,
|
||||
entitlement_create: ?*const fn (save: *anyopaque, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
entitlement_update: ?*const fn (save: *anyopaque, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
/// discord claims this is infrequent, therefore not throughoutly tested - Yuzu
|
||||
entitlement_delete: ?*const fn (save: *anyopaque, entitlement: Types.Entitlement) anyerror!void = undefined,
|
||||
message_poll_vote_add: ?*const fn (save: *anyopaque, poll: Types.PollVoteAdd) anyerror!void = undefined,
|
||||
message_poll_vote_remove: ?*const fn (save: *anyopaque, poll: Types.PollVoteRemove) anyerror!void = undefined,
|
||||
|
||||
ready: ?*const fn (save: *anyopaque, data: Types.Ready) anyerror!void = undefined,
|
||||
// TODO: implement // resumed: null = null,
|
||||
any: ?*const fn (save: *anyopaque, data: std.json.Value) anyerror!void = undefined,
|
||||
};
|
114
src/utils/permissions.zig
Normal file
114
src/utils/permissions.zig
Normal file
@ -0,0 +1,114 @@
|
||||
const BitwisePermissionFlags = @import("../structures/shared.zig").BitwisePermissionFlags;
|
||||
const IntegerBitSet = @import("std").StaticBitSet(usize);
|
||||
|
||||
pub const Permissions = struct {
|
||||
bitset: IntegerBitSet = .initEmpty(),
|
||||
const bitfield = BitwisePermissionFlags{};
|
||||
|
||||
pub const none = Permissions{};
|
||||
|
||||
pub fn all() Permissions {
|
||||
return .{ .bitset = .initFull() };
|
||||
}
|
||||
|
||||
pub fn has(self: Permissions, permission: BitwisePermissionFlags) bool {
|
||||
return self.bitset.isSet(@intCast(permission));
|
||||
}
|
||||
|
||||
pub fn missing(self: Permissions, permission: BitwisePermissionFlags) bool {
|
||||
return !self.bitset.isSet(@intCast(permission));
|
||||
}
|
||||
|
||||
pub fn isAdmin(self: Permissions) bool {
|
||||
return self.bitset.isSet(@intCast(BitwisePermissionFlags.ADMIN));
|
||||
}
|
||||
|
||||
pub fn set(self: Permissions, permission: BitwisePermissionFlags) void {
|
||||
self.bitset.set(@intCast(permission));
|
||||
}
|
||||
|
||||
pub fn eql(self: Permissions, other: Permissions) bool {
|
||||
return self.bitset.eql(other.bitset);
|
||||
}
|
||||
|
||||
pub fn any(self: Permissions, other: Permissions) bool {
|
||||
return self.bitset.supersetOf(other.bitset);
|
||||
}
|
||||
|
||||
pub fn toRaw(self: Permissions) u64 {
|
||||
return @intCast(self.bitset.mask);
|
||||
}
|
||||
|
||||
pub fn fromRaw(raw: u64) Permissions {
|
||||
return .{ .bitset = .{ .mask = raw } };
|
||||
}
|
||||
|
||||
/// std.json stringify
|
||||
pub fn jsonStringify(permissions: Permissions, writer: anytype) !void {
|
||||
try writer.print("<Permissions 0x{x}>", .{permissions.toRaw()});
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("std").testing;
|
||||
|
||||
test {
|
||||
const all_permissions = Permissions.all();
|
||||
|
||||
// 2^40 - 1 = 1099511627775
|
||||
const ALL: u64 = 1099511627775;
|
||||
|
||||
testing.expectEqual(all_permissions.toRaw(), ALL);
|
||||
}
|
||||
|
||||
test "is admin and set works" {
|
||||
var permissions = Permissions.none;
|
||||
permissions.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
testing.expect(permissions.isAdmin());
|
||||
}
|
||||
|
||||
test "eql works" {
|
||||
var permissions1 = Permissions.none;
|
||||
var permissions2 = Permissions.none;
|
||||
permissions1.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
permissions2.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
testing.expect(permissions1.eql(permissions2));
|
||||
}
|
||||
|
||||
test "has permission" {
|
||||
var permissions = Permissions.none;
|
||||
permissions.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
testing.expect(permissions.has(Permissions.bitfield.ADMINISTRATOR));
|
||||
testing.expect(!permissions.has(Permissions.bitfield.MANAGE_ROLES));
|
||||
}
|
||||
|
||||
test "missing permission" {
|
||||
var permissions = Permissions.none;
|
||||
permissions.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
testing.expect(!permissions.missing(Permissions.bitfield.ADMINISTRATOR));
|
||||
testing.expect(permissions.missing(Permissions.bitfield.MANAGE_ROLES));
|
||||
}
|
||||
|
||||
test "missing multiple permissions" {
|
||||
var permissions1 = Permissions.none;
|
||||
permissions1.set(Permissions.bitfield.KICK_MEMBERS); // only has kick members
|
||||
|
||||
var permissions2 = Permissions.none; // to check
|
||||
permissions2.set(Permissions.bitfield.BAN_MEMBERS);
|
||||
permissions2.set(Permissions.bitfield.KICK_MEMBERS);
|
||||
|
||||
// has both permissions
|
||||
testing.expect(permissions1.missing(permissions2));
|
||||
}
|
||||
|
||||
test "any permissions" {
|
||||
var permissions1 = Permissions.none;
|
||||
var permissions2 = Permissions.none;
|
||||
permissions1.set(Permissions.bitfield.ADMINISTRATOR);
|
||||
permissions2.set(Permissions.bitfield.MANAGE_GUILD);
|
||||
|
||||
testing.expect(!permissions1.any(permissions2));
|
||||
|
||||
permissions2.set(Permissions.bitfield.MANAGE_GUILD);
|
||||
|
||||
testing.expect(permissions1.any(permissions2));
|
||||
}
|
@ -20,12 +20,16 @@ const Shard = Discord.Shard;
|
||||
const Intents = Discord.Intents;
|
||||
|
||||
const INTENTS = 53608447;
|
||||
const Cache = Discord.cache.TableTemplate{};
|
||||
|
||||
fn ready(_: *Shard, payload: Discord.Ready) !void {
|
||||
fn ready(_: *anyopaque, payload: Discord.Ready) !void {
|
||||
std.debug.print("logged in as {s}\n", .{payload.user.username});
|
||||
}
|
||||
|
||||
fn message_create(session: *Shard, message: Discord.Message) !void {
|
||||
fn message_create(shard_ptr: *anyopaque, message: Discord.Message) !void {
|
||||
// set custom cache
|
||||
const session: *Shard(Cache) = @ptrCast(@alignCast(shard_ptr));
|
||||
|
||||
if (message.content != null and std.ascii.eqlIgnoreCase(message.content.?, "!hi")) {
|
||||
var result = try session.sendMessage(message.channel_id, .{ .content = "hi :)" });
|
||||
defer result.deinit();
|
||||
@ -42,11 +46,20 @@ pub fn main() !void {
|
||||
var handler = Discord.init(allocator);
|
||||
defer handler.deinit();
|
||||
|
||||
const env_map = try allocator.create(std.process.EnvMap);
|
||||
env_map.* = try std.process.getEnvMap(allocator);
|
||||
defer env_map.deinit();
|
||||
|
||||
const token = env_map.get("DISCORD_TOKEN") orelse {
|
||||
@panic("DISCORD_TOKEN not found in environment variables");
|
||||
};
|
||||
|
||||
try handler.start(.{
|
||||
.token = std.posix.getenv("DISCORD_TOKEN").?,
|
||||
.token = token,
|
||||
.intents = Intents.fromRaw(INTENTS),
|
||||
.run = .{ .message_create = &message_create, .ready = &ready },
|
||||
.log = .yes,
|
||||
.options = .{},
|
||||
.cache = Cache,
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user