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,29 +305,38 @@ 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;
|
||||
|
||||
pub fn CustomisedSession(comptime Table: cache.TableTemplate) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
allocator: mem.Allocator,
|
||||
sharder: Sharder,
|
||||
sharder: Sharder(Table),
|
||||
token: []const u8,
|
||||
|
||||
pub fn init(allocator: mem.Allocator) Self {
|
||||
@ -348,18 +353,26 @@ pub fn deinit(self: *Self) void {
|
||||
|
||||
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,
|
||||
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,
|
||||
|
@ -1430,181 +1430,6 @@ pub const GuildFeatures = enum {
|
||||
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) {
|
||||
|
@ -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