const std = @import("std"); const mem = std.mem; const Deque = @import("deque").Deque; 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, }; dequeue: Deque(RequestWithShard), 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 = try Deque(RequestWithShard).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.pushBack(req); } self.remaining -= 1; if (!self.running) { try self.startInterval(); self.running = true; } if (self.dequeue.len() < self.concurrency) { // perhaps store this? const ptr = try self.allocator.create(RequestWithShard); ptr.* = req; try @call(.auto, req.callback, .{ptr}); return; } return self.dequeue.pushBack(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.popFront(); while (self.dequeue.len() == 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.len() == 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 { // TODO: implement // application_command_permissions_update: null = null, // TODO: implement // auto_moderation_rule_create: null = null, // TODO: implement // auto_moderation_rule_update: null = null, // TODO: implement // auto_moderation_rule_delete: null = null, // TODO: implement // auto_moderation_action_execution: null = null, // TODO: implement // channel_create: null = null, // TODO: implement // channel_update: null = null, // TODO: implement // channel_delete: null = null, // TODO: implement // channel_pins_update: null = null, // TODO: implement // thread_create: null = null, // TODO: implement // thread_update: null = null, // TODO: implement // thread_delete: null = null, // TODO: implement // thread_list_sync: null = null, // TODO: implement // thread_member_update: null = null, // TODO: implement // thread_members_update: null = null, // TODO: implement // guild_audit_log_entry_create: null = null, // TODO: implement // guild_create: null = null, // TODO: implement // guild_update: null = null, // TODO: implement // guild_delete: null = null, // TODO: implement // guild_ban_add: null = null, // TODO: implement // guild_ban_remove: null = null, // TODO: implement // guild_emojis_update: null = null, // TODO: implement // guild_stickers_update: null = null, // TODO: implement // guild_integrations_update: null = null, // TODO: implement // guild_member_add: null = null, // TODO: implement // guild_member_remove: null = null, // TODO: implement // guild_member_update: null = null, // TODO: implement // guild_members_chunk: null = null, // TODO: implement // guild_role_create: null = null, // TODO: implement // guild_role_update: null = null, // TODO: implement // guild_role_delete: null = null, // TODO: implement // guild_scheduled_event_create: null = null, // TODO: implement // guild_scheduled_event_update: null = null, // TODO: implement // guild_scheduled_event_delete: null = null, // TODO: implement // guild_scheduled_event_user_add: null = null, // TODO: implement // guild_scheduled_event_user_remove: null = null, // TODO: implement // integration_create: null = null, // TODO: implement // integration_update: null = null, // TODO: implement // integration_delete: null = null, // TODO: implement // interaction_create: null = null, // TODO: implement // invite_create: null = null, // TODO: implement // invite_delete: null = null, 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, // TODO: message_reaction_add: ?*const fn (save: T, log: Discord.MessageReactionAdd) anyerror!void = undefined, // TODO: implement // message_reaction_remove: null = null, // TODO: implement // message_reaction_remove_all: null = null, // TODO: implement // message_reaction_remove_emoji: null = null, // TODO: implement // presence_update: null = null, // TODO: implement // stage_instance_create: null = null, // TODO: implement // stage_instance_update: null = null, // TODO: implement // stage_instance_delete: null = null, // TODO: implement // typing_start: null = null, // TODO: implement // user_update: null = null, // TODO: implement // voice_channel_effect_send: null = null, // TODO: implement // voice_state_update: null = null, // TODO: implement // voice_server_update: null = null, // TODO: implement // webhooks_update: null = null, // TODO: implement // entitlement_create: null = null, // TODO: implement // entitlement_update: null = null, // TODO: implement // entitlement_delete: null = null, // TODO: implement // message_poll_vote_add: null = null, // TODO: implement // message_poll_vote_remove: null = null, ready: ?*const fn (save: T, data: Types.Ready) anyerror!void = undefined, // TODO: implement // resumed: null = null, any: ?*const fn (save: T, data: []const u8) anyerror!void = undefined, }; }