towards a gateway
This commit is contained in:
parent
188dd4be48
commit
49c6326086
68
README.md
68
README.md
@ -13,12 +13,42 @@ git clone https://github.com/jetzig-framework/zmpl.git ./lib/zmpl/
|
|||||||
or simply run ./install.sh
|
or simply run ./install.sh
|
||||||
|
|
||||||
# features
|
# features
|
||||||
* scalable
|
* supports sharding for large bots
|
||||||
* 100% API coverage, fully typed
|
* 100% API coverage, fully typed
|
||||||
* faster than any other Discord library
|
* faster than any other Discord library
|
||||||
* language-agnostic
|
* language-agnostic (may be used with JavaScript)
|
||||||
* implemented from scratch
|
* parses payloads using either zlib or zstd
|
||||||
* parses payloads using zlib
|
|
||||||
|
```zig
|
||||||
|
// Sample code
|
||||||
|
const Session = @import("discord.zig").Session;
|
||||||
|
const Discord = @import("discord.zig").Discord;
|
||||||
|
const Intents = Discord.Intents;
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const token = "Bot MTI5ODgzOTgzMDY3OTEzMDE4OA...";
|
||||||
|
|
||||||
|
fn message_create(message: Discord.Message) void {
|
||||||
|
// do whatever you want
|
||||||
|
std.debug.print("captured: {?s}\n", .{message.content});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
|
var handler = try Session.init(allocator, .{
|
||||||
|
.token = token,
|
||||||
|
.intents = Intents.fromRaw(37379),
|
||||||
|
.run = Session.GatewayDispatchEvent{ .message_create = &message_create },
|
||||||
|
});
|
||||||
|
errdefer handler.deinit();
|
||||||
|
|
||||||
|
const t = try std.Thread.spawn(.{}, Session.readMessage, .{ &handler, null });
|
||||||
|
defer t.join();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## event coverage roadmap
|
## event coverage roadmap
|
||||||
* application_command_permissions_update | ❌
|
* application_command_permissions_update | ❌
|
||||||
@ -90,33 +120,3 @@ or simply run ./install.sh
|
|||||||
* resumed | ❌
|
* resumed | ❌
|
||||||
* any: ?*const fn (data: []u8) void | ✅
|
* any: ?*const fn (data: []u8) void | ✅
|
||||||
|
|
||||||
```zig
|
|
||||||
// Sample code
|
|
||||||
const Session = @import("discord.zig").Session;
|
|
||||||
const Discord = @import("discord.zig").Discord;
|
|
||||||
const Intents = Discord.Intents;
|
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const token = "Bot MTI5ODgzOTgzMDY3OTEzMDE4OA...";
|
|
||||||
|
|
||||||
fn message_create(message: Discord.Message) void {
|
|
||||||
// do whatever you want
|
|
||||||
std.debug.print("captured: {?s}\n", .{message.content});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() !void {
|
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
|
||||||
defer arena.deinit();
|
|
||||||
const allocator = arena.allocator();
|
|
||||||
|
|
||||||
var handler = try Session.init(allocator, .{
|
|
||||||
.token = token,
|
|
||||||
.intents = Intents.fromRaw(37379),
|
|
||||||
.run = Session.GatewayDispatchEvent{ .message_create = &message_create },
|
|
||||||
});
|
|
||||||
errdefer handler.deinit();
|
|
||||||
|
|
||||||
const t = try std.Thread.spawn(.{}, Session.readMessage, .{ &handler, null });
|
|
||||||
defer t.join();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
14
build.zig
14
build.zig
@ -34,7 +34,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
.link_libc = true,
|
.link_libc = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zig_tls_http = b.createModule(.{
|
const zig_tls = b.createModule(.{
|
||||||
.root_source_file = b.path("lib/zig-tls12/src/entry.zig"),
|
.root_source_file = b.path("lib/zig-tls12/src/entry.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
@ -55,6 +55,11 @@ pub fn build(b: *std.Build) void {
|
|||||||
.root_source_file = b.path("lib/zmpl/src/zmpl.zig"),
|
.root_source_file = b.path("lib/zmpl/src/zmpl.zig"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deque = b.dependency("zig-deque", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
});
|
||||||
|
|
||||||
const srcs = &.{
|
const srcs = &.{
|
||||||
"lib/zlib/adler32.c",
|
"lib/zlib/adler32.c",
|
||||||
"lib/zlib/compress.c",
|
"lib/zlib/compress.c",
|
||||||
@ -79,18 +84,19 @@ pub fn build(b: *std.Build) void {
|
|||||||
zlib_zig.addIncludePath(b.path("lib/zlib/"));
|
zlib_zig.addIncludePath(b.path("lib/zlib/"));
|
||||||
|
|
||||||
websocket.addImport("zlib", zlib_zig);
|
websocket.addImport("zlib", zlib_zig);
|
||||||
websocket.addImport("tls12", zig_tls_http);
|
websocket.addImport("tls12", zig_tls);
|
||||||
|
|
||||||
// now install your own executable after it's built correctly
|
// now install your own executable after it's built correctly
|
||||||
|
|
||||||
exe.root_module.addImport("ws", websocket);
|
exe.root_module.addImport("ws", websocket);
|
||||||
exe.root_module.addImport("tls12", zig_tls_http);
|
exe.root_module.addImport("tls12", zig_tls);
|
||||||
exe.root_module.addImport("zlib", zlib_zig);
|
exe.root_module.addImport("zlib", zlib_zig);
|
||||||
exe.root_module.addImport("zmpl", zmpl);
|
exe.root_module.addImport("zmpl", zmpl);
|
||||||
|
exe.root_module.addImport("deque", deque.module("zig-deque"));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
test_comp.root_module.addImport("ws", websocket);
|
test_comp.root_module.addImport("ws", websocket);
|
||||||
test_comp.root_module.addImport("tls12", zig_tls_http);
|
test_comp.root_module.addImport("tls12", zig_tls);
|
||||||
test_comp.root_module.addImport("zlib", zlib_zig);
|
test_comp.root_module.addImport("zlib", zlib_zig);
|
||||||
|
|
||||||
const run_test_comp = b.addRunArtifact(test_comp);
|
const run_test_comp = b.addRunArtifact(test_comp);
|
||||||
|
39
build.zig.zon
Normal file
39
build.zig.zon
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
.{
|
||||||
|
// This is the default name used by packages depending on this one. For
|
||||||
|
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||||
|
// as the key in the `dependencies` table. Although the user can choose a
|
||||||
|
// different name, most users will stick with this provided value.
|
||||||
|
//
|
||||||
|
// It is redundant to include "zig" in this name because it is already
|
||||||
|
// within the Zig package namespace.
|
||||||
|
.name = "discord.zig",
|
||||||
|
|
||||||
|
// This is a [Semantic Version](https://semver.org/).
|
||||||
|
// In a future version of Zig it will be used for package deduplication.
|
||||||
|
.version = "0.0.0",
|
||||||
|
|
||||||
|
// This field is optional.
|
||||||
|
// This is currently advisory only; Zig does not yet do anything
|
||||||
|
// with this value.
|
||||||
|
//.minimum_zig_version = "0.11.0",
|
||||||
|
|
||||||
|
// This field is optional.
|
||||||
|
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||||
|
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||||
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
|
// internet connectivity.
|
||||||
|
.dependencies = .{
|
||||||
|
.@"zig-deque" = .{
|
||||||
|
.url = "https://github.com/magurotuna/zig-deque/archive/refs/heads/main.zip",
|
||||||
|
.hash = "1220d1bedf7d5cfc7475842b3d4e8f03f1308be2e724a036677cceb5c4db13c3da3d",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.paths = .{
|
||||||
|
"build.zig",
|
||||||
|
"build.zig.zon",
|
||||||
|
"src",
|
||||||
|
// For example...
|
||||||
|
//"LICENSE",
|
||||||
|
//"README.md",
|
||||||
|
},
|
||||||
|
}
|
663
src/discord.zig
663
src/discord.zig
@ -1,659 +1,4 @@
|
|||||||
const std = @import("std");
|
pub const Discord = @import("types.zig");
|
||||||
const json = std.json;
|
pub const Shard = @import("shard.zig");
|
||||||
const mem = std.mem;
|
pub const Internal = @import("internal.zig");
|
||||||
const http = std.http;
|
pub const Session = @import("session.zig");
|
||||||
const ws = @import("ws");
|
|
||||||
const builtin = @import("builtin");
|
|
||||||
const HttpClient = @import("tls12").HttpClient;
|
|
||||||
const net = std.net;
|
|
||||||
const crypto = std.crypto;
|
|
||||||
const tls = std.crypto.tls;
|
|
||||||
// todo use this to read compressed messages
|
|
||||||
const zlib = @import("zlib");
|
|
||||||
const zmpl = @import("zmpl");
|
|
||||||
|
|
||||||
const Discord = @import("raw_types.zig");
|
|
||||||
const Parser = @import("parser.zig");
|
|
||||||
|
|
||||||
const debug = std.log.scoped(.@"discord.zig");
|
|
||||||
const Self = @This();
|
|
||||||
|
|
||||||
const GatewayPayload = Discord.GatewayPayload;
|
|
||||||
const Opcode = Discord.GatewayOpcodes;
|
|
||||||
const Intents = Discord.Intents;
|
|
||||||
|
|
||||||
const ShardSocketCloseCodes = enum(u16) {
|
|
||||||
Shutdown = 3000,
|
|
||||||
ZombiedConnection = 3010,
|
|
||||||
};
|
|
||||||
|
|
||||||
const BASE_URL = "https://discord.com/api/v10";
|
|
||||||
|
|
||||||
pub const GatewayDispatchEvent = 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 (message: Discord.Message) void = undefined,
|
|
||||||
message_update: ?*const fn (message: Discord.Message) void = undefined,
|
|
||||||
message_delete: ?*const fn (log: Discord.MessageDelete) void = undefined,
|
|
||||||
message_delete_bulk: ?*const fn (log: Discord.MessageDeleteBulk) void = undefined,
|
|
||||||
// TODO: implement // message_delete_bulk: null = null,
|
|
||||||
// TODO: implement // message_reaction_add: null = null,
|
|
||||||
// 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 (data: Discord.Ready) void = undefined,
|
|
||||||
// TODO: implement // resumed: null = null,
|
|
||||||
any: ?*const fn (data: []u8) void = undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FetchReq = struct {
|
|
||||||
allocator: mem.Allocator,
|
|
||||||
token: []const u8,
|
|
||||||
client: HttpClient,
|
|
||||||
body: std.ArrayList(u8),
|
|
||||||
|
|
||||||
pub fn init(allocator: mem.Allocator, token: []const u8) FetchReq {
|
|
||||||
const client = HttpClient{ .allocator = allocator };
|
|
||||||
return FetchReq{
|
|
||||||
.allocator = allocator,
|
|
||||||
.client = client,
|
|
||||||
.body = std.ArrayList(u8).init(allocator),
|
|
||||||
.token = token,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *FetchReq) void {
|
|
||||||
self.client.deinit();
|
|
||||||
self.body.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn makeRequest(self: *FetchReq, method: http.Method, path: []const u8, body: ?[]const u8) !HttpClient.FetchResult {
|
|
||||||
var fetch_options = HttpClient.FetchOptions{
|
|
||||||
.location = HttpClient.FetchOptions.Location{
|
|
||||||
.url = path,
|
|
||||||
},
|
|
||||||
.extra_headers = &[_]http.Header{
|
|
||||||
http.Header{ .name = "Accept", .value = "application/json" },
|
|
||||||
http.Header{ .name = "Content-Type", .value = "application/json" },
|
|
||||||
http.Header{ .name = "Authorization", .value = self.token },
|
|
||||||
},
|
|
||||||
.method = method,
|
|
||||||
.response_storage = .{ .dynamic = &self.body },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (body != null) {
|
|
||||||
fetch_options.payload = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = try self.client.fetch(fetch_options);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
///
|
|
||||||
/// https://discord.com/developers/docs/topics/gateway#get-gateway
|
|
||||||
///
|
|
||||||
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
|
|
||||||
///
|
|
||||||
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
|
|
||||||
///
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
const _default_properties = IdentifyProperties{
|
|
||||||
.os = @tagName(builtin.os.tag),
|
|
||||||
.browser = "discord.zig",
|
|
||||||
.device = "discord.zig",
|
|
||||||
};
|
|
||||||
|
|
||||||
const Heart = struct {
|
|
||||||
heartbeatInterval: u64,
|
|
||||||
ack: bool,
|
|
||||||
/// useful for calculating ping
|
|
||||||
lastBeat: u64,
|
|
||||||
};
|
|
||||||
|
|
||||||
client: ws.Client,
|
|
||||||
token: []const u8,
|
|
||||||
intents: Intents,
|
|
||||||
//heart: Heart =
|
|
||||||
allocator: mem.Allocator,
|
|
||||||
resume_gateway_url: ?[]const u8 = null,
|
|
||||||
info: GatewayBotInfo,
|
|
||||||
|
|
||||||
session_id: ?[]const u8,
|
|
||||||
sequence: isize,
|
|
||||||
heart: Heart = .{ .heartbeatInterval = 45000, .ack = false, .lastBeat = 0 },
|
|
||||||
|
|
||||||
///
|
|
||||||
handler: GatewayDispatchEvent,
|
|
||||||
packets: std.ArrayList(u8),
|
|
||||||
inflator: zlib.Decompressor,
|
|
||||||
|
|
||||||
///useful for closing the conn
|
|
||||||
mutex: std.Thread.Mutex = .{},
|
|
||||||
log: Log = .no,
|
|
||||||
|
|
||||||
fn parseJson(self: *Self, raw: []const u8) !zmpl.Data {
|
|
||||||
var data = zmpl.Data.init(self.allocator);
|
|
||||||
try data.fromJson(raw);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn resumable(self: *Self) bool {
|
|
||||||
return self.resume_gateway_url != null and
|
|
||||||
self.session_id != null and
|
|
||||||
self.getSequence() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume_(self: *Self) !void {
|
|
||||||
const data = .{ .op = @intFromEnum(Opcode.Resume), .d = .{
|
|
||||||
.token = self.token,
|
|
||||||
.session_id = self.session_id,
|
|
||||||
.seq = self.getSequence(),
|
|
||||||
} };
|
|
||||||
|
|
||||||
try self.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn gatewayUrl(self: ?*Self) []const u8 {
|
|
||||||
return if (self) |s| (s.resume_gateway_url orelse s.info.url)["wss://".len..] else "gateway.discord.gg";
|
|
||||||
}
|
|
||||||
|
|
||||||
// identifies in order to connect to Discord and get the online status, this shall be done on hello perhaps
|
|
||||||
fn identify(self: *Self) !void {
|
|
||||||
self.logif("intents: {d}", .{self.intents.toRaw()});
|
|
||||||
const data = .{
|
|
||||||
.op = @intFromEnum(Opcode.Identify),
|
|
||||||
.d = .{
|
|
||||||
//.compress = false,
|
|
||||||
.intents = self.intents.toRaw(),
|
|
||||||
.properties = Self._default_properties,
|
|
||||||
.token = self.token,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// try posting our shitty data
|
|
||||||
try self.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Log = union(enum) { yes, no };
|
|
||||||
|
|
||||||
// asks /gateway/bot initializes both the ws client and the http client
|
|
||||||
pub fn init(allocator: mem.Allocator, args: struct {
|
|
||||||
token: []const u8,
|
|
||||||
intents: Intents,
|
|
||||||
run: GatewayDispatchEvent,
|
|
||||||
log: Log,
|
|
||||||
}) !Self {
|
|
||||||
var req = FetchReq.init(allocator, args.token);
|
|
||||||
defer req.deinit();
|
|
||||||
|
|
||||||
const res = try req.makeRequest(.GET, BASE_URL ++ "/gateway/bot", null);
|
|
||||||
const body = try req.body.toOwnedSlice();
|
|
||||||
defer allocator.free(body);
|
|
||||||
|
|
||||||
// check status idk
|
|
||||||
if (res.status != http.Status.ok) {
|
|
||||||
@panic("we are cooked\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = try json.parseFromSlice(GatewayBotInfo, allocator, body, .{});
|
|
||||||
const url = parsed.value.url["wss://".len..];
|
|
||||||
defer parsed.deinit();
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.allocator = allocator,
|
|
||||||
.token = args.token,
|
|
||||||
.intents = args.intents,
|
|
||||||
// maybe there is a better way to do this
|
|
||||||
.client = try Self._connect_ws(allocator, url),
|
|
||||||
.session_id = undefined,
|
|
||||||
.sequence = 0,
|
|
||||||
.info = parsed.value,
|
|
||||||
.handler = args.run,
|
|
||||||
.log = args.log,
|
|
||||||
.packets = std.ArrayList(u8).init(allocator),
|
|
||||||
.inflator = try zlib.Decompressor.init(allocator, .{ .header = .zlib_or_gzip }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn _connect_ws(allocator: mem.Allocator, url: []const u8) !ws.Client {
|
|
||||||
var conn = try ws.Client.init(allocator, .{
|
|
||||||
.tls = true, // important: zig.http doesn't support this, type shit
|
|
||||||
.port = 443,
|
|
||||||
.host = url,
|
|
||||||
});
|
|
||||||
|
|
||||||
// maybe change this to a buffer
|
|
||||||
var buf: [0x100]u8 = undefined;
|
|
||||||
const host = try std.fmt.bufPrint(&buf, "host: {s}", .{url});
|
|
||||||
|
|
||||||
conn.handshake("/?v=10&encoding=json&compress=zlib-stream", .{
|
|
||||||
.timeout_ms = 1000,
|
|
||||||
.headers = host,
|
|
||||||
}) catch unreachable;
|
|
||||||
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
|
||||||
self.client.deinit();
|
|
||||||
self.logif("killing the whole bot", .{});
|
|
||||||
}
|
|
||||||
|
|
||||||
// listens for messages
|
|
||||||
pub fn readMessage(self: *Self, _: anytype) !void {
|
|
||||||
try self.client.readTimeout(0);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const msg = (try self.client.read()) orelse
|
|
||||||
continue;
|
|
||||||
|
|
||||||
defer self.client.done(msg);
|
|
||||||
|
|
||||||
// self.logif("received: {?s}\n", .{msg.data});
|
|
||||||
try self.packets.appendSlice(msg.data);
|
|
||||||
|
|
||||||
// end of zlib
|
|
||||||
if (!std.mem.endsWith(u8, msg.data, &[4]u8{ 0x00, 0x00, 0xFF, 0xFF }))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// self.logif("{b}\n", .{self.packets.items});
|
|
||||||
const buf = try self.packets.toOwnedSlice();
|
|
||||||
const decompressed = try self.inflator.decompressAllAlloc(buf);
|
|
||||||
|
|
||||||
const raw = try json.parseFromSlice(struct {
|
|
||||||
/// opcode for the payload
|
|
||||||
op: isize,
|
|
||||||
/// Event data
|
|
||||||
d: json.Value,
|
|
||||||
/// Sequence isize, used for resuming sessions and heartbeats
|
|
||||||
s: ?i64,
|
|
||||||
/// The event name for this payload
|
|
||||||
t: ?[]const u8,
|
|
||||||
}, self.allocator, decompressed, .{});
|
|
||||||
|
|
||||||
const payload = raw.value;
|
|
||||||
|
|
||||||
switch (@as(Opcode, @enumFromInt(payload.op))) {
|
|
||||||
Opcode.Dispatch => {
|
|
||||||
self.setSequence(payload.s orelse 0);
|
|
||||||
// maybe use threads and call it instead from there
|
|
||||||
if (payload.t) |name| try self.handleEvent(name, decompressed);
|
|
||||||
},
|
|
||||||
Opcode.Hello => {
|
|
||||||
{
|
|
||||||
const HelloPayload = struct { heartbeat_interval: u64, _trace: [][]const u8 };
|
|
||||||
const parsed = try json.parseFromValue(HelloPayload, self.allocator, payload.d, .{});
|
|
||||||
const helloPayload = parsed.value;
|
|
||||||
|
|
||||||
// PARSE NEW URL IN READY
|
|
||||||
|
|
||||||
self.heart = Heart{
|
|
||||||
// TODO: fix bug
|
|
||||||
.heartbeatInterval = helloPayload.heartbeat_interval,
|
|
||||||
.ack = false,
|
|
||||||
.lastBeat = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.logif("starting heart beater. seconds:{d}...", .{self.heart.heartbeatInterval});
|
|
||||||
|
|
||||||
try self.heartbeat();
|
|
||||||
|
|
||||||
var prng = std.Random.DefaultPrng.init(0);
|
|
||||||
const jitter = std.Random.float(prng.random(), f64);
|
|
||||||
|
|
||||||
const thread = try std.Thread.spawn(.{}, Self.heartbeat_wait, .{ self, jitter });
|
|
||||||
thread.detach();
|
|
||||||
|
|
||||||
if (self.resumable()) {
|
|
||||||
try self.resume_();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try self.identify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Opcode.HeartbeatACK => {
|
|
||||||
// perhaps this needs a mutex?
|
|
||||||
self.logif("got heartbeat ack", .{});
|
|
||||||
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
self.heart.ack = true;
|
|
||||||
},
|
|
||||||
Opcode.Heartbeat => {
|
|
||||||
self.logif("sending requested heartbeat", .{});
|
|
||||||
try self.heartbeat();
|
|
||||||
},
|
|
||||||
Opcode.Reconnect => {
|
|
||||||
self.logif("reconnecting", .{});
|
|
||||||
try self.reconnect();
|
|
||||||
},
|
|
||||||
Opcode.Resume => {
|
|
||||||
const WithSequence = struct {
|
|
||||||
token: []const u8,
|
|
||||||
session_id: []const u8,
|
|
||||||
seq: ?isize,
|
|
||||||
};
|
|
||||||
{
|
|
||||||
const parsed = try json.parseFromValue(WithSequence, self.allocator, payload.d, .{});
|
|
||||||
const resume_payload = parsed.value;
|
|
||||||
|
|
||||||
self.setSequence(resume_payload.seq orelse 0);
|
|
||||||
self.session_id = resume_payload.session_id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Opcode.InvalidSession => {},
|
|
||||||
else => {
|
|
||||||
self.logif("Unhandled {d} -- {s}", .{ payload.op, "none" });
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn heartbeat(self: *Self) !void {
|
|
||||||
const data = .{ .op = @intFromEnum(Opcode.Heartbeat), .d = if (self.getSequence() > 0) self.getSequence() else null };
|
|
||||||
|
|
||||||
try self.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn heartbeat_wait(self: *Self, jitter: f64) !void {
|
|
||||||
if (jitter == 1.0) {
|
|
||||||
// self.logif("zzz for {d}", .{self.heart.heartbeatInterval});
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * self.heart.heartbeatInterval);
|
|
||||||
} else {
|
|
||||||
const timeout = @as(f64, @floatFromInt(self.heart.heartbeatInterval)) * jitter;
|
|
||||||
self.logif("zzz for {d} and jitter {d}", .{ @as(u64, @intFromFloat(timeout)), jitter });
|
|
||||||
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intFromFloat(timeout)));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logif(">> ♥ and ack received: {}", .{self.heart.ack});
|
|
||||||
|
|
||||||
if (self.heart.ack) {
|
|
||||||
self.logif("sending unrequested heartbeat", .{});
|
|
||||||
try self.heartbeat();
|
|
||||||
try self.client.readTimeout(1000);
|
|
||||||
} else {
|
|
||||||
self.close(ShardSocketCloseCodes.ZombiedConnection, "Zombied connection") catch unreachable;
|
|
||||||
@panic("zombied conn\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return heartbeat_wait(self, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn reconnect(self: *Self) !void {
|
|
||||||
try self.disconnect();
|
|
||||||
try self.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn connect(self: *Self) !void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
//std.time.sleep(std.time.ms_per_s * 5);
|
|
||||||
self.client = try Self._connect_ws(self.allocator, self.gatewayUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disconnect(self: *Self) !void {
|
|
||||||
try self.close(ShardSocketCloseCodes.Shutdown, "Shard down request");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close(self: *Self, code: ShardSocketCloseCodes, reason: []const u8) !void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
self.logif("cooked closing ws conn...\n", .{});
|
|
||||||
// Implement reconnection logic here
|
|
||||||
try self.client.close(.{
|
|
||||||
.code = @intFromEnum(code), //u16
|
|
||||||
.reason = reason, //[]const u8
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send(self: *Self, data: anytype) !void {
|
|
||||||
var buf: [1000]u8 = undefined;
|
|
||||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
|
||||||
var string = std.ArrayList(u8).init(fba.allocator());
|
|
||||||
try std.json.stringify(data, .{}, string.writer());
|
|
||||||
|
|
||||||
//self.logif("{s}\n", .{string.items});
|
|
||||||
|
|
||||||
try self.client.write(string.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn getSequence(self: *Self) isize {
|
|
||||||
return self.sequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub inline fn setSequence(self: *Self, new: isize) void {
|
|
||||||
self.mutex.lock();
|
|
||||||
defer self.mutex.unlock();
|
|
||||||
|
|
||||||
self.sequence = new;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handleEvent(self: *Self, name: []const u8, payload: []const u8) !void {
|
|
||||||
if (std.ascii.eqlIgnoreCase(name, "ready")) {
|
|
||||||
var attempt = try self.parseJson(payload);
|
|
||||||
defer attempt.deinit();
|
|
||||||
|
|
||||||
const obj = attempt.getT(.object, "d").?;
|
|
||||||
|
|
||||||
self.resume_gateway_url = obj.getT(.string, "resume_gateway_url");
|
|
||||||
|
|
||||||
self.logif("new gateway url: {s}", .{self.gatewayUrl()});
|
|
||||||
|
|
||||||
const application = obj.getT(.object, "application").?;
|
|
||||||
const user = Parser.parseUser(obj.getT(.object, "user").?);
|
|
||||||
|
|
||||||
var ready = Discord.Ready{
|
|
||||||
.v = @as(isize, @intCast(obj.getT(.integer, "v").?)),
|
|
||||||
.user = user,
|
|
||||||
.shard = null,
|
|
||||||
.session_id = obj.getT(.string, "session_id").?,
|
|
||||||
.guilds = &[0]Discord.UnavailableGuild{},
|
|
||||||
.resume_gateway_url = obj.getT(.string, "resume_gateway_url").?,
|
|
||||||
.application = .{
|
|
||||||
// todo
|
|
||||||
.name = null,
|
|
||||||
.description = null,
|
|
||||||
.rpc_origins = null,
|
|
||||||
.terms_of_service_url = null,
|
|
||||||
.privacy_policy_url = null,
|
|
||||||
.verify_key = null,
|
|
||||||
.primary_sku_id = null,
|
|
||||||
.slug = null,
|
|
||||||
.icon = null,
|
|
||||||
.bot_public = null,
|
|
||||||
.bot_require_code_grant = null,
|
|
||||||
.owner = null,
|
|
||||||
.team = null,
|
|
||||||
.guild_id = null,
|
|
||||||
.guild = null,
|
|
||||||
.cover_image = null,
|
|
||||||
.tags = null,
|
|
||||||
.install_params = null,
|
|
||||||
.integration_types_config = null,
|
|
||||||
.custom_install_url = null,
|
|
||||||
.role_connections_verification_url = null,
|
|
||||||
.approximate_guild_count = null,
|
|
||||||
.approximate_user_install_count = null,
|
|
||||||
.bot = null,
|
|
||||||
.redirect_uris = null,
|
|
||||||
.interactions_endpoint_url = null,
|
|
||||||
.flags = .{ .Embedded = true }, //@as(Discord.ApplicationFlags, @bitCast(@as(u25, @intCast(application.getT(.integer, "flags").?)))),
|
|
||||||
.id = application.getT(.string, "id").?,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const shard = obj.getT(.array, "shard");
|
|
||||||
|
|
||||||
if (shard) |s| {
|
|
||||||
for (&ready.shard.?, s.items()) |*rs, ss| rs.* = switch (ss.*) {
|
|
||||||
.integer => |v| @as(isize, @intCast(v.value)),
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (self.handler.ready) |event| @call(.auto, event, .{ready});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase(name, "message_delete")) {
|
|
||||||
const attempt = try self.parseJson(payload);
|
|
||||||
const obj = attempt.getT(.object, "d").?;
|
|
||||||
const data = Discord.MessageDelete{
|
|
||||||
.id = obj.getT(.string, "id").?,
|
|
||||||
.channel_id = obj.getT(.string, "channel_id").?,
|
|
||||||
.guild_id = obj.getT(.string, "guild_id"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.handler.message_delete) |event| @call(.auto, event, .{data});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase(name, "message_delete_bulk")) {
|
|
||||||
const attempt = try self.parseJson(payload);
|
|
||||||
const obj = attempt.getT(.object, "d").?;
|
|
||||||
var ids = std.ArrayList([]const u8).init(self.allocator);
|
|
||||||
|
|
||||||
while (obj.getT(.array, "ids").?.iterator().next()) |id| {
|
|
||||||
ids.append(id.string.value) catch unreachable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = Discord.MessageDeleteBulk{
|
|
||||||
.ids = ids.items,
|
|
||||||
.channel_id = obj.getT(.string, "channel_id").?,
|
|
||||||
.guild_id = obj.getT(.string, "guild_id"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (self.handler.message_delete_bulk) |event| @call(.auto, event, .{data});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase(name, "message_update")) {
|
|
||||||
const attempt = try self.parseJson(payload);
|
|
||||||
const obj = attempt.getT(.object, "d").?;
|
|
||||||
|
|
||||||
const message = Parser.parseMessage(obj);
|
|
||||||
|
|
||||||
if (self.handler.message_update) |event| @call(.auto, event, .{message});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std.ascii.eqlIgnoreCase(name, "message_create")) {
|
|
||||||
const attempt = try self.parseJson(payload);
|
|
||||||
const obj = attempt.getT(.object, "d").?;
|
|
||||||
|
|
||||||
const message = Parser.parseMessage(obj);
|
|
||||||
|
|
||||||
if (self.handler.message_create) |event| @call(.auto, event, .{message});
|
|
||||||
} else {}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fn logif(self: *Self, comptime format: []const u8, args: anytype) void {
|
|
||||||
switch (self.log) {
|
|
||||||
.yes => debug.info(format, args),
|
|
||||||
.no => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
69
src/internal.zig
Normal file
69
src/internal.zig
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const Deque = @import("deque");
|
||||||
|
|
||||||
|
/// inspired from:
|
||||||
|
/// https://github.com/tiramisulabs/seyfert/blob/main/src/websocket/structures/timeout.ts
|
||||||
|
pub const ConnectQueue = struct {
|
||||||
|
dequeue: Deque(*const fn () void),
|
||||||
|
allocator: mem.Allocator,
|
||||||
|
remaining: usize,
|
||||||
|
interval_time: u64 = 5000,
|
||||||
|
running: bool,
|
||||||
|
concurrency: usize = 1,
|
||||||
|
|
||||||
|
pub fn init(allocator: mem.Allocator, concurrency: usize, interval_time: u64) !ConnectQueue {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.dequeue = try Deque(*const fn () void).init(allocator),
|
||||||
|
.remaining = concurrency,
|
||||||
|
.interval_time = interval_time,
|
||||||
|
.concurrency = concurrency,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *ConnectQueue) void {
|
||||||
|
self.dequeue.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(self: *ConnectQueue, callback: *const fn () void) !void {
|
||||||
|
if (self.remaining == 0) {
|
||||||
|
return self.dequeue.pushBack(callback);
|
||||||
|
}
|
||||||
|
self.remaining -= 1;
|
||||||
|
|
||||||
|
if (!self.running) {
|
||||||
|
self.startInterval();
|
||||||
|
self.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dequeue.items.len < self.concurrency) {
|
||||||
|
@call(.auto, callback, .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.dequeue.pushBack(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startInterval(self: *ConnectQueue) void {
|
||||||
|
while (self.running) {
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * (self.interval_time / self.concurrency));
|
||||||
|
const callback: ?*const fn () void = self.dequeue.popFront();
|
||||||
|
|
||||||
|
while (self.dequeue.items.len == 0 and callback == null) {}
|
||||||
|
|
||||||
|
if (callback) |cb| {
|
||||||
|
@call(.auto, cb, .{});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.remaining < self.concurrency) {
|
||||||
|
self.remaining += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.dequeue.len() == 0) {
|
||||||
|
self.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
13
src/main.zig
13
src/main.zig
@ -1,8 +1,9 @@
|
|||||||
const Session = @import("discord.zig");
|
const Session = @import("shard.zig");
|
||||||
const Intents = @import("raw_types.zig").Intents;
|
const Discord = @import("types.zig");
|
||||||
const Discord = @import("raw_types.zig");
|
const Intents = Discord.Intents;
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const TOKEN = "Bot MTI5ODgzOTgzMDY3OTEzMDE4OA.GNojts.iyblGKK0xTWU57QCG5n3hr2Be1whyylTGr44P0";
|
const Thread = std.Thread;
|
||||||
|
const token = "Bot MTI5ODgzOTgzMDY3OTEzMDE4OA.GNojts.iyblGKK0xTWU57QCG5n3hr2Be1whyylTGr44P0";
|
||||||
|
|
||||||
fn ready(payload: Discord.Ready) void {
|
fn ready(payload: Discord.Ready) void {
|
||||||
std.debug.print("logged in as {s}\n", .{payload.user.username});
|
std.debug.print("logged in as {s}\n", .{payload.user.username});
|
||||||
@ -18,7 +19,7 @@ pub fn main() !void {
|
|||||||
const allocator = arena.allocator();
|
const allocator = arena.allocator();
|
||||||
|
|
||||||
var handler = try Session.init(allocator, .{
|
var handler = try Session.init(allocator, .{
|
||||||
.token = TOKEN,
|
.token = token,
|
||||||
.intents = Intents.fromRaw(37379),
|
.intents = Intents.fromRaw(37379),
|
||||||
.run = Session.GatewayDispatchEvent{
|
.run = Session.GatewayDispatchEvent{
|
||||||
.message_create = &message_create,
|
.message_create = &message_create,
|
||||||
@ -28,6 +29,6 @@ pub fn main() !void {
|
|||||||
});
|
});
|
||||||
errdefer handler.deinit();
|
errdefer handler.deinit();
|
||||||
|
|
||||||
const t = try std.Thread.spawn(.{}, Session.readMessage, .{ &handler, null });
|
const t = try Thread.spawn(.{}, Session.readMessage, .{ &handler, null });
|
||||||
defer t.join();
|
defer t.join();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const zmpl = @import("zmpl");
|
const zmpl = @import("zmpl");
|
||||||
const Discord = @import("raw_types.zig");
|
const Discord = @import("types.zig");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
pub fn parseUser(obj: *zmpl.Data.Object) Discord.User {
|
pub fn parseUser(obj: *zmpl.Data.Object) Discord.User {
|
||||||
|
5064
src/raw_types.zig
5064
src/raw_types.zig
File diff suppressed because it is too large
Load Diff
46
src/session.zig
Normal file
46
src/session.zig
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const ConnectQueue = @import("internal.zig").ConnectQueue;
|
||||||
|
const Intents = @import("types.zig").Intents;
|
||||||
|
const GatewayBotInfo = @import("shared.zig").GatewayBotInfo;
|
||||||
|
const IdentifyProperties = @import("shared.zig").IdentifyProperties;
|
||||||
|
|
||||||
|
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.
|
||||||
|
/// @default 10
|
||||||
|
///
|
||||||
|
version: ?usize = 10,
|
||||||
|
///
|
||||||
|
/// The calculated intent value of the events which the shard should receive.
|
||||||
|
///
|
||||||
|
intents: Intents,
|
||||||
|
///
|
||||||
|
/// Identify properties to use
|
||||||
|
///
|
||||||
|
properties: ?IdentifyProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
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!!!
|
||||||
|
spawnShardDelay: ?u64 = 5300,
|
||||||
|
/// Total amount of shards your bot uses. Useful for zero-downtime updates or resharding.
|
||||||
|
totalShards: ?usize = 1,
|
||||||
|
shardStart: ?usize,
|
||||||
|
shardEnd: ?usize,
|
||||||
|
///
|
||||||
|
/// The payload handlers for messages on the shard.
|
||||||
|
/// TODO:
|
||||||
|
/// handlePayload: (shardId: number, packet: GatewayDispatchPayload): unknown;
|
||||||
|
///
|
||||||
|
resharding: ?struct {
|
||||||
|
interval: u64,
|
||||||
|
percentage: usize,
|
||||||
|
},
|
||||||
|
};
|
608
src/shard.zig
Normal file
608
src/shard.zig
Normal file
@ -0,0 +1,608 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json = std.json;
|
||||||
|
const mem = std.mem;
|
||||||
|
const http = std.http;
|
||||||
|
const ws = @import("ws");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const HttpClient = @import("tls12").HttpClient;
|
||||||
|
const net = std.net;
|
||||||
|
const crypto = std.crypto;
|
||||||
|
const tls = std.crypto.tls;
|
||||||
|
// todo use this to read compressed messages
|
||||||
|
const zlib = @import("zlib");
|
||||||
|
const zmpl = @import("zmpl");
|
||||||
|
|
||||||
|
const Discord = @import("types.zig");
|
||||||
|
const Parser = @import("parser.zig");
|
||||||
|
|
||||||
|
const debug = std.log.scoped(.@"discord.zig");
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
const GatewayPayload = Discord.GatewayPayload;
|
||||||
|
const Opcode = Discord.GatewayOpcodes;
|
||||||
|
const Intents = Discord.Intents;
|
||||||
|
const Shared = @import("shared.zig");
|
||||||
|
const IdentifyProperties = Shared.IdentifyProperties;
|
||||||
|
const GatewayInfo = Shared.GatewayInfo;
|
||||||
|
const GatewayBotInfo = Shared.GatewayBotInfo;
|
||||||
|
const GatewaySessionStartLimit = Shared.GatewaySessionStartLimit;
|
||||||
|
|
||||||
|
const ShardSocketCloseCodes = enum(u16) {
|
||||||
|
Shutdown = 3000,
|
||||||
|
ZombiedConnection = 3010,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_URL = "https://discord.com/api/v10";
|
||||||
|
|
||||||
|
pub const GatewayDispatchEvent = 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 (message: Discord.Message) void = undefined,
|
||||||
|
message_update: ?*const fn (message: Discord.Message) void = undefined,
|
||||||
|
message_delete: ?*const fn (log: Discord.MessageDelete) void = undefined,
|
||||||
|
message_delete_bulk: ?*const fn (log: Discord.MessageDeleteBulk) void = undefined,
|
||||||
|
// TODO: implement // message_delete_bulk: null = null,
|
||||||
|
// TODO: implement // message_reaction_add: null = null,
|
||||||
|
// 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 (data: Discord.Ready) void = undefined,
|
||||||
|
// TODO: implement // resumed: null = null,
|
||||||
|
any: ?*const fn (data: []u8) void = undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FetchReq = struct {
|
||||||
|
allocator: mem.Allocator,
|
||||||
|
token: []const u8,
|
||||||
|
client: HttpClient,
|
||||||
|
body: std.ArrayList(u8),
|
||||||
|
|
||||||
|
pub fn init(allocator: mem.Allocator, token: []const u8) FetchReq {
|
||||||
|
const client = HttpClient{ .allocator = allocator };
|
||||||
|
return FetchReq{
|
||||||
|
.allocator = allocator,
|
||||||
|
.client = client,
|
||||||
|
.body = std.ArrayList(u8).init(allocator),
|
||||||
|
.token = token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *FetchReq) void {
|
||||||
|
self.client.deinit();
|
||||||
|
self.body.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn makeRequest(self: *FetchReq, method: http.Method, path: []const u8, body: ?[]const u8) !HttpClient.FetchResult {
|
||||||
|
var fetch_options = HttpClient.FetchOptions{
|
||||||
|
.location = HttpClient.FetchOptions.Location{
|
||||||
|
.url = path,
|
||||||
|
},
|
||||||
|
.extra_headers = &[_]http.Header{
|
||||||
|
http.Header{ .name = "Accept", .value = "application/json" },
|
||||||
|
http.Header{ .name = "Content-Type", .value = "application/json" },
|
||||||
|
http.Header{ .name = "Authorization", .value = self.token },
|
||||||
|
},
|
||||||
|
.method = method,
|
||||||
|
.response_storage = .{ .dynamic = &self.body },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
fetch_options.payload = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = try self.client.fetch(fetch_options);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _default_properties = IdentifyProperties{
|
||||||
|
.os = @tagName(builtin.os.tag),
|
||||||
|
.browser = "discord.zig",
|
||||||
|
.device = "discord.zig",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Heart = struct {
|
||||||
|
heartbeatInterval: u64,
|
||||||
|
ack: bool,
|
||||||
|
/// useful for calculating ping
|
||||||
|
lastBeat: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
client: ws.Client,
|
||||||
|
token: []const u8,
|
||||||
|
intents: Intents,
|
||||||
|
//heart: Heart =
|
||||||
|
allocator: mem.Allocator,
|
||||||
|
resume_gateway_url: ?[]const u8 = null,
|
||||||
|
info: GatewayBotInfo,
|
||||||
|
|
||||||
|
session_id: ?[]const u8,
|
||||||
|
sequence: isize,
|
||||||
|
heart: Heart = .{ .heartbeatInterval = 45000, .ack = false, .lastBeat = 0 },
|
||||||
|
|
||||||
|
///
|
||||||
|
handler: GatewayDispatchEvent,
|
||||||
|
packets: std.ArrayList(u8),
|
||||||
|
inflator: zlib.Decompressor,
|
||||||
|
|
||||||
|
///useful for closing the conn
|
||||||
|
mutex: std.Thread.Mutex = .{},
|
||||||
|
log: Log = .no,
|
||||||
|
|
||||||
|
fn parseJson(self: *Self, raw: []const u8) !zmpl.Data {
|
||||||
|
var data = zmpl.Data.init(self.allocator);
|
||||||
|
try data.fromJson(raw);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn resumable(self: *Self) bool {
|
||||||
|
return self.resume_gateway_url != null and
|
||||||
|
self.session_id != null and
|
||||||
|
self.getSequence() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume_(self: *Self) !void {
|
||||||
|
const data = .{ .op = @intFromEnum(Opcode.Resume), .d = .{
|
||||||
|
.token = self.token,
|
||||||
|
.session_id = self.session_id,
|
||||||
|
.seq = self.getSequence(),
|
||||||
|
} };
|
||||||
|
|
||||||
|
try self.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn gatewayUrl(self: ?*Self) []const u8 {
|
||||||
|
return if (self) |s| (s.resume_gateway_url orelse s.info.url)["wss://".len..] else "gateway.discord.gg";
|
||||||
|
}
|
||||||
|
|
||||||
|
// identifies in order to connect to Discord and get the online status, this shall be done on hello perhaps
|
||||||
|
fn identify(self: *Self) !void {
|
||||||
|
self.logif("intents: {d}", .{self.intents.toRaw()});
|
||||||
|
const data = .{
|
||||||
|
.op = @intFromEnum(Opcode.Identify),
|
||||||
|
.d = .{
|
||||||
|
//.compress = false,
|
||||||
|
.intents = self.intents.toRaw(),
|
||||||
|
.properties = Self._default_properties,
|
||||||
|
.token = self.token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// try posting our shitty data
|
||||||
|
try self.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Log = union(enum) { yes, no };
|
||||||
|
|
||||||
|
// asks /gateway/bot initializes both the ws client and the http client
|
||||||
|
pub fn init(allocator: mem.Allocator, args: struct {
|
||||||
|
token: []const u8,
|
||||||
|
intents: Intents,
|
||||||
|
run: GatewayDispatchEvent,
|
||||||
|
log: Log,
|
||||||
|
}) !Self {
|
||||||
|
var req = FetchReq.init(allocator, args.token);
|
||||||
|
defer req.deinit();
|
||||||
|
|
||||||
|
const res = try req.makeRequest(.GET, BASE_URL ++ "/gateway/bot", null);
|
||||||
|
const body = try req.body.toOwnedSlice();
|
||||||
|
defer allocator.free(body);
|
||||||
|
|
||||||
|
// check status idk
|
||||||
|
if (res.status != http.Status.ok) {
|
||||||
|
@panic("we are cooked\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = try json.parseFromSlice(GatewayBotInfo, allocator, body, .{});
|
||||||
|
const url = parsed.value.url["wss://".len..];
|
||||||
|
defer parsed.deinit();
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.token = args.token,
|
||||||
|
.intents = args.intents,
|
||||||
|
// maybe there is a better way to do this
|
||||||
|
.client = try Self._connect_ws(allocator, url),
|
||||||
|
.session_id = undefined,
|
||||||
|
.sequence = 0,
|
||||||
|
.info = parsed.value,
|
||||||
|
.handler = args.run,
|
||||||
|
.log = args.log,
|
||||||
|
.packets = std.ArrayList(u8).init(allocator),
|
||||||
|
.inflator = try zlib.Decompressor.init(allocator, .{ .header = .zlib_or_gzip }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn _connect_ws(allocator: mem.Allocator, url: []const u8) !ws.Client {
|
||||||
|
var conn = try ws.Client.init(allocator, .{
|
||||||
|
.tls = true, // important: zig.http doesn't support this, type shit
|
||||||
|
.port = 443,
|
||||||
|
.host = url,
|
||||||
|
});
|
||||||
|
|
||||||
|
// maybe change this to a buffer
|
||||||
|
var buf: [0x100]u8 = undefined;
|
||||||
|
const host = try std.fmt.bufPrint(&buf, "host: {s}", .{url});
|
||||||
|
|
||||||
|
conn.handshake("/?v=10&encoding=json&compress=zlib-stream", .{
|
||||||
|
.timeout_ms = 1000,
|
||||||
|
.headers = host,
|
||||||
|
}) catch unreachable;
|
||||||
|
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self) void {
|
||||||
|
self.client.deinit();
|
||||||
|
self.logif("killing the whole bot", .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
// listens for messages
|
||||||
|
pub fn readMessage(self: *Self, _: anytype) !void {
|
||||||
|
try self.client.readTimeout(0);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const msg = (try self.client.read()) orelse
|
||||||
|
continue;
|
||||||
|
|
||||||
|
defer self.client.done(msg);
|
||||||
|
|
||||||
|
// self.logif("received: {?s}\n", .{msg.data});
|
||||||
|
try self.packets.appendSlice(msg.data);
|
||||||
|
|
||||||
|
// end of zlib
|
||||||
|
if (!std.mem.endsWith(u8, msg.data, &[4]u8{ 0x00, 0x00, 0xFF, 0xFF }))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// self.logif("{b}\n", .{self.packets.items});
|
||||||
|
const buf = try self.packets.toOwnedSlice();
|
||||||
|
const decompressed = try self.inflator.decompressAllAlloc(buf);
|
||||||
|
|
||||||
|
const raw = try json.parseFromSlice(struct {
|
||||||
|
/// opcode for the payload
|
||||||
|
op: isize,
|
||||||
|
/// Event data
|
||||||
|
d: json.Value,
|
||||||
|
/// Sequence isize, used for resuming sessions and heartbeats
|
||||||
|
s: ?i64,
|
||||||
|
/// The event name for this payload
|
||||||
|
t: ?[]const u8,
|
||||||
|
}, self.allocator, decompressed, .{});
|
||||||
|
|
||||||
|
const payload = raw.value;
|
||||||
|
|
||||||
|
switch (@as(Opcode, @enumFromInt(payload.op))) {
|
||||||
|
Opcode.Dispatch => {
|
||||||
|
self.setSequence(payload.s orelse 0);
|
||||||
|
// maybe use threads and call it instead from there
|
||||||
|
if (payload.t) |name| try self.handleEvent(name, decompressed);
|
||||||
|
},
|
||||||
|
Opcode.Hello => {
|
||||||
|
{
|
||||||
|
const HelloPayload = struct { heartbeat_interval: u64, _trace: [][]const u8 };
|
||||||
|
const parsed = try json.parseFromValue(HelloPayload, self.allocator, payload.d, .{});
|
||||||
|
const helloPayload = parsed.value;
|
||||||
|
|
||||||
|
// PARSE NEW URL IN READY
|
||||||
|
|
||||||
|
self.heart = Heart{
|
||||||
|
// TODO: fix bug
|
||||||
|
.heartbeatInterval = helloPayload.heartbeat_interval,
|
||||||
|
.ack = false,
|
||||||
|
.lastBeat = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.logif("starting heart beater. seconds:{d}...", .{self.heart.heartbeatInterval});
|
||||||
|
|
||||||
|
try self.heartbeat();
|
||||||
|
|
||||||
|
var prng = std.Random.DefaultPrng.init(0);
|
||||||
|
const jitter = std.Random.float(prng.random(), f64);
|
||||||
|
|
||||||
|
const thread = try std.Thread.spawn(.{}, Self.heartbeat_wait, .{ self, jitter });
|
||||||
|
thread.detach();
|
||||||
|
|
||||||
|
if (self.resumable()) {
|
||||||
|
try self.resume_();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try self.identify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Opcode.HeartbeatACK => {
|
||||||
|
// perhaps this needs a mutex?
|
||||||
|
self.logif("got heartbeat ack", .{});
|
||||||
|
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
self.heart.ack = true;
|
||||||
|
},
|
||||||
|
Opcode.Heartbeat => {
|
||||||
|
self.logif("sending requested heartbeat", .{});
|
||||||
|
try self.heartbeat();
|
||||||
|
},
|
||||||
|
Opcode.Reconnect => {
|
||||||
|
self.logif("reconnecting", .{});
|
||||||
|
try self.reconnect();
|
||||||
|
},
|
||||||
|
Opcode.Resume => {
|
||||||
|
const WithSequence = struct {
|
||||||
|
token: []const u8,
|
||||||
|
session_id: []const u8,
|
||||||
|
seq: ?isize,
|
||||||
|
};
|
||||||
|
{
|
||||||
|
const parsed = try json.parseFromValue(WithSequence, self.allocator, payload.d, .{});
|
||||||
|
const resume_payload = parsed.value;
|
||||||
|
|
||||||
|
self.setSequence(resume_payload.seq orelse 0);
|
||||||
|
self.session_id = resume_payload.session_id;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Opcode.InvalidSession => {},
|
||||||
|
else => {
|
||||||
|
self.logif("Unhandled {d} -- {s}", .{ payload.op, "none" });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn heartbeat(self: *Self) !void {
|
||||||
|
const data = .{ .op = @intFromEnum(Opcode.Heartbeat), .d = if (self.getSequence() > 0) self.getSequence() else null };
|
||||||
|
|
||||||
|
try self.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn heartbeat_wait(self: *Self, jitter: f64) !void {
|
||||||
|
if (jitter == 1.0) {
|
||||||
|
// self.logif("zzz for {d}", .{self.heart.heartbeatInterval});
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * self.heart.heartbeatInterval);
|
||||||
|
} else {
|
||||||
|
const timeout = @as(f64, @floatFromInt(self.heart.heartbeatInterval)) * jitter;
|
||||||
|
self.logif("zzz for {d} and jitter {d}", .{ @as(u64, @intFromFloat(timeout)), jitter });
|
||||||
|
std.Thread.sleep(std.time.ns_per_ms * @as(u64, @intFromFloat(timeout)));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logif(">> ♥ and ack received: {}", .{self.heart.ack});
|
||||||
|
|
||||||
|
if (self.heart.ack) {
|
||||||
|
self.logif("sending unrequested heartbeat", .{});
|
||||||
|
try self.heartbeat();
|
||||||
|
try self.client.readTimeout(1000);
|
||||||
|
} else {
|
||||||
|
self.close(ShardSocketCloseCodes.ZombiedConnection, "Zombied connection") catch unreachable;
|
||||||
|
@panic("zombied conn\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return heartbeat_wait(self, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn reconnect(self: *Self) !void {
|
||||||
|
try self.disconnect();
|
||||||
|
try self.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect(self: *Self) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
//std.time.sleep(std.time.ms_per_s * 5);
|
||||||
|
self.client = try Self._connect_ws(self.allocator, self.gatewayUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disconnect(self: *Self) !void {
|
||||||
|
try self.close(ShardSocketCloseCodes.Shutdown, "Shard down request");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: *Self, code: ShardSocketCloseCodes, reason: []const u8) !void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
self.logif("cooked closing ws conn...\n", .{});
|
||||||
|
// Implement reconnection logic here
|
||||||
|
try self.client.close(.{
|
||||||
|
.code = @intFromEnum(code), //u16
|
||||||
|
.reason = reason, //[]const u8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(self: *Self, data: anytype) !void {
|
||||||
|
var buf: [1000]u8 = undefined;
|
||||||
|
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||||
|
var string = std.ArrayList(u8).init(fba.allocator());
|
||||||
|
try std.json.stringify(data, .{}, string.writer());
|
||||||
|
|
||||||
|
//self.logif("{s}\n", .{string.items});
|
||||||
|
|
||||||
|
try self.client.write(string.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn getSequence(self: *Self) isize {
|
||||||
|
return self.sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setSequence(self: *Self, new: isize) void {
|
||||||
|
self.mutex.lock();
|
||||||
|
defer self.mutex.unlock();
|
||||||
|
|
||||||
|
self.sequence = new;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handleEvent(self: *Self, name: []const u8, payload: []const u8) !void {
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "ready")) {
|
||||||
|
var attempt = try self.parseJson(payload);
|
||||||
|
defer attempt.deinit();
|
||||||
|
|
||||||
|
const obj = attempt.getT(.object, "d").?;
|
||||||
|
|
||||||
|
self.resume_gateway_url = obj.getT(.string, "resume_gateway_url");
|
||||||
|
|
||||||
|
self.logif("new gateway url: {s}", .{self.gatewayUrl()});
|
||||||
|
|
||||||
|
const application = obj.getT(.object, "application").?;
|
||||||
|
const user = Parser.parseUser(obj.getT(.object, "user").?);
|
||||||
|
|
||||||
|
var ready = Discord.Ready{
|
||||||
|
.v = @as(isize, @intCast(obj.getT(.integer, "v").?)),
|
||||||
|
.user = user,
|
||||||
|
.shard = null,
|
||||||
|
.session_id = obj.getT(.string, "session_id").?,
|
||||||
|
.guilds = &[0]Discord.UnavailableGuild{},
|
||||||
|
.resume_gateway_url = obj.getT(.string, "resume_gateway_url").?,
|
||||||
|
.application = .{
|
||||||
|
// todo
|
||||||
|
.name = null,
|
||||||
|
.description = null,
|
||||||
|
.rpc_origins = null,
|
||||||
|
.terms_of_service_url = null,
|
||||||
|
.privacy_policy_url = null,
|
||||||
|
.verify_key = null,
|
||||||
|
.primary_sku_id = null,
|
||||||
|
.slug = null,
|
||||||
|
.icon = null,
|
||||||
|
.bot_public = null,
|
||||||
|
.bot_require_code_grant = null,
|
||||||
|
.owner = null,
|
||||||
|
.team = null,
|
||||||
|
.guild_id = null,
|
||||||
|
.guild = null,
|
||||||
|
.cover_image = null,
|
||||||
|
.tags = null,
|
||||||
|
.install_params = null,
|
||||||
|
.integration_types_config = null,
|
||||||
|
.custom_install_url = null,
|
||||||
|
.role_connections_verification_url = null,
|
||||||
|
.approximate_guild_count = null,
|
||||||
|
.approximate_user_install_count = null,
|
||||||
|
.bot = null,
|
||||||
|
.redirect_uris = null,
|
||||||
|
.interactions_endpoint_url = null,
|
||||||
|
.flags = .{ .Embedded = true }, //@as(Discord.ApplicationFlags, @bitCast(@as(u25, @intCast(application.getT(.integer, "flags").?)))),
|
||||||
|
.id = application.getT(.string, "id").?,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const shard = obj.getT(.array, "shard");
|
||||||
|
|
||||||
|
if (shard) |s| {
|
||||||
|
for (&ready.shard.?, s.items()) |*rs, ss| rs.* = switch (ss.*) {
|
||||||
|
.integer => |v| @as(isize, @intCast(v.value)),
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (self.handler.ready) |event| @call(.auto, event, .{ready});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "message_delete")) {
|
||||||
|
const attempt = try self.parseJson(payload);
|
||||||
|
const obj = attempt.getT(.object, "d").?;
|
||||||
|
const data = Discord.MessageDelete{
|
||||||
|
.id = obj.getT(.string, "id").?,
|
||||||
|
.channel_id = obj.getT(.string, "channel_id").?,
|
||||||
|
.guild_id = obj.getT(.string, "guild_id"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.handler.message_delete) |event| @call(.auto, event, .{data});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "message_delete_bulk")) {
|
||||||
|
const attempt = try self.parseJson(payload);
|
||||||
|
const obj = attempt.getT(.object, "d").?;
|
||||||
|
var ids = std.ArrayList([]const u8).init(self.allocator);
|
||||||
|
|
||||||
|
while (obj.getT(.array, "ids").?.iterator().next()) |id| {
|
||||||
|
ids.append(id.string.value) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = Discord.MessageDeleteBulk{
|
||||||
|
.ids = ids.items,
|
||||||
|
.channel_id = obj.getT(.string, "channel_id").?,
|
||||||
|
.guild_id = obj.getT(.string, "guild_id"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (self.handler.message_delete_bulk) |event| @call(.auto, event, .{data});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "message_update")) {
|
||||||
|
const attempt = try self.parseJson(payload);
|
||||||
|
const obj = attempt.getT(.object, "d").?;
|
||||||
|
|
||||||
|
const message = Parser.parseMessage(obj);
|
||||||
|
|
||||||
|
if (self.handler.message_update) |event| @call(.auto, event, .{message});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std.ascii.eqlIgnoreCase(name, "message_create")) {
|
||||||
|
const attempt = try self.parseJson(payload);
|
||||||
|
const obj = attempt.getT(.object, "d").?;
|
||||||
|
|
||||||
|
const message = Parser.parseMessage(obj);
|
||||||
|
|
||||||
|
if (self.handler.message_create) |event| @call(.auto, event, .{message});
|
||||||
|
} else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn logif(self: *Self, comptime format: []const u8, args: anytype) void {
|
||||||
|
switch (self.log) {
|
||||||
|
.yes => debug.info(format, args),
|
||||||
|
.no => {},
|
||||||
|
}
|
||||||
|
}
|
49
src/shared.zig
Normal file
49
src/shared.zig
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
};
|
5064
src/types.zig
5064
src/types.zig
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user