331 lines
13 KiB
Zig
331 lines
13 KiB
Zig
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,
|
|
};
|
|
}
|