From 7506a06d7f56d73fae2dfdad70741834f65cfb4a Mon Sep 17 00:00:00 2001 From: Candygoblen123 Date: Tue, 17 Mar 2026 16:37:31 -0400 Subject: [PATCH] DiscordKit: can now get to ready event --- Package.resolved | 2 +- Package.swift | 4 +- Sources/DiscordKit/Bot.swift | 18 +++++-- Sources/DiscordKit/GatewayClient.swift | 69 +++++++++++++++++++++++--- Sources/DiscordKit/Models.swift | 49 ++++++++++++++++-- Sources/zundamon/Zundamon.swift | 36 ++++++-------- 6 files changed, 139 insertions(+), 39 deletions(-) diff --git a/Package.resolved b/Package.resolved index 2867933..e1cdc08 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "219f2158581681a4b5edbd171ed8aebcd120092f145806d33067fb0cfbb93848", + "originHash" : "5244b11ed61f26bb7868e246b3fa58e4ccca64cf7a3b26777a9a1447030fa349", "pins" : [ { "identity" : "async-http-client", diff --git a/Package.swift b/Package.swift index 5b7dce7..3367724 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/DiscordBM/DiscordBM.git", branch: "main"), .package(url: "https://github.com/thebarndog/swift-dotenv.git", from: "2.1.0"), - .package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.18.0") + .package(url: "https://github.com/CoreOffice/XMLCoder.git", from: "0.18.0"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -24,6 +24,8 @@ let package = Package( ), .target( name: "DiscordKit", + dependencies: [ + ], ), ] ) diff --git a/Sources/DiscordKit/Bot.swift b/Sources/DiscordKit/Bot.swift index 6440f89..9107849 100644 --- a/Sources/DiscordKit/Bot.swift +++ b/Sources/DiscordKit/Bot.swift @@ -3,15 +3,23 @@ import Foundation import FoundationNetworking #endif -public struct Bot { +public actor Bot { let client: ApiClient + let gateway: GatewayClient + let intents: Intents - public init(token: String) async throws { + public init(token: String, intents: Intents) async throws { client = ApiClient(token: token) - + self.intents = intents let gatewayURL = try await client.getGatewayURL() + gateway = GatewayClient(gatewayURL: gatewayURL, token: token) + } - let gateway = GatewayClient(gatewayURL: gatewayURL) - try await gateway.openConnection() + public func connect() async throws { + try await gateway.openConnection(intents: intents) + } + + public var events: AsyncStream { + get async { await gateway.events } } } diff --git a/Sources/DiscordKit/GatewayClient.swift b/Sources/DiscordKit/GatewayClient.swift index c43dd3f..5588869 100644 --- a/Sources/DiscordKit/GatewayClient.swift +++ b/Sources/DiscordKit/GatewayClient.swift @@ -3,25 +3,60 @@ import Foundation import FoundationNetworking #endif -class GatewayClient { - let ws: URLSessionWebSocketTask +actor GatewayClient { + private let ws: URLSessionWebSocketTask + private let token: String + private(set) var open = false var sequenceNum: Int? = nil - init(gatewayURL: URL) { + init(gatewayURL: URL, token: String) { ws = URLSession.shared.webSocketTask(with: gatewayURL.appending(component: "?v=10&encoding=json")) + self.token = token } - func openConnection() async throws { + func openConnection(intents: Intents) async throws { ws.resume() + open = true guard case .hello(let helloMessage) = try await getMessage().d else { throw GatewayError.mismatchedOpcode } dump(helloMessage) - Timer.scheduledTimer(withTimeInterval: (helloMessage.heartbeat_interval / 1000.0), repeats: true) { - + + let heartbeatTask = Task() { + try await Task.sleep(for: .milliseconds(Int.random(in: 0...helloMessage.heartbeat_interval))) + try await sendHeartbeat() + + while !Task.isCancelled { + try await Task.sleep(for: .milliseconds(helloMessage.heartbeat_interval)) + try await sendHeartbeat() + } } + + try await sendIdentify(intents: intents) + //dump(try await getMessage()) + //print("got here") + _ = await heartbeatTask.result + } + + func sendIdentify(intents: Intents) async throws { + let payload = """ + { + "op": 2, + "d": { + "token": "\(token)", + "intents": \(intents.rawValue), + "properties": { + "os": "linux", + "browser": "discordkit", + "device": "discordkit" + } + } + } + """ // Im lazy + try await ws.send(.string(payload)) } func getMessage() async throws -> GatewayMessage { - let wsMessage = try await ws.receive() + let wsMessage = try! await ws.receive() + print(wsMessage) guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage } let json = JSONDecoder() let gwMessage = try json.decode(GatewayMessage.self, from: Data(str.utf8)) @@ -29,6 +64,26 @@ class GatewayClient { return gwMessage } + private func sendHeartbeat() async throws { + let hbMessage = "{\"op\":1,\"d\":\(sequenceNum == nil ? "null" : String(sequenceNum!))}" + print(hbMessage) + try await ws.send(.string(hbMessage)) + } + + var events: AsyncStream { + AsyncStream { [self] in + guard await open else { return nil } + do { + let event = try await getMessage() + if event.op == 1 { try await sendHeartbeat() } + return event + } catch { + print("Error listening to gateway: \(error)") + return nil + } + } + } + } public enum GatewayError: Error { diff --git a/Sources/DiscordKit/Models.swift b/Sources/DiscordKit/Models.swift index 9789800..0bda4fc 100644 --- a/Sources/DiscordKit/Models.swift +++ b/Sources/DiscordKit/Models.swift @@ -3,24 +3,55 @@ import Foundation import FoundationNetworking #endif -public struct GetGatewayResponse: Codable { +/// https://discord.com/developers/docs/topics/gateway#gateway-intents +public struct Intents: OptionSet, Sendable { + public var rawValue: UInt32 + + public static let guilds = Intents(rawValue: 1 << 0) + public static let guildMembers = Intents(rawValue: 1 << 1) + public static let guildModeration = Intents(rawValue: 1 << 2) + public static let guildEmojisAndStickers = Intents(rawValue: 1 << 3) + public static let guildIntegrations = Intents(rawValue: 1 << 4) + public static let guildWebhooks = Intents(rawValue: 1 << 5) + public static let guildInvites = Intents(rawValue: 1 << 6) + public static let guildVoiceStates = Intents(rawValue: 1 << 7) + public static let guildPresences = Intents(rawValue: 1 << 8) + public static let guildMessages = Intents(rawValue: 1 << 9) + public static let guildMessageReactions = Intents(rawValue: 1 << 10) + public static let guildMessageTyping = Intents(rawValue: 1 << 11) + public static let directMessages = Intents(rawValue: 1 << 12) + public static let directMessageReactions = Intents(rawValue: 1 << 13) + public static let directMessageTyping = Intents(rawValue: 1 << 14) + public static let messageContent = Intents(rawValue: 1 << 15) + public static let guildScheduledEvents = Intents(rawValue: 1 << 16) + public static let autoModerationConfiguration = Intents(rawValue: 1 << 20) + public static let autoModerationExecution = Intents(rawValue: 1 << 21) + public static let guildMessagePolls = Intents(rawValue: 1 << 24) + public static let directMessagePolls = Intents(rawValue: 1 << 25) + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } +} + +public struct GetGatewayResponse: Codable, Sendable { let url: URL let shards: Int let session_start_limit: SessionStartLimit } -public struct SessionStartLimit: Codable { +public struct SessionStartLimit: Codable, Sendable { let total: Int let remaining: Int let reset_after: Int let max_concurrency: Int } -public enum GatewayPayload: Decodable { +public enum GatewayPayload: Decodable, Sendable { case hello(HelloPayload) } -public struct GatewayMessage: Decodable { +public struct GatewayMessage: Decodable, Sendable { let op: Int let d: GatewayPayload? let s: Int? @@ -47,8 +78,16 @@ public struct GatewayMessage: Decodable { break } } + + public init(op: Int, d: GatewayPayload?, s: Int?, t: String?) { + self.op = op + self.d = d + self.s = s + self.t = t + } } -public struct HelloPayload: Codable { +public struct HelloPayload: Codable, Sendable { let heartbeat_interval: Int } + diff --git a/Sources/zundamon/Zundamon.swift b/Sources/zundamon/Zundamon.swift index 6a7508b..a8e50b8 100644 --- a/Sources/zundamon/Zundamon.swift +++ b/Sources/zundamon/Zundamon.swift @@ -7,36 +7,32 @@ struct Zundamon { //nonisolated(unsafe) static private(set) var ownID: UserSnowflake? = nil static func main() async throws { - let tmp = Result { try Dotenv.configure() } - switch (tmp) { - case .success: - break - case .failure: - print("Failed to load .env file") - break + if case .failure = Result(catching: { try Dotenv.configure() }) { + print("Failed to load .env file") } let token = ProcessInfo.processInfo.environment["DISCORD_TOKEN"]! guard !token.isEmpty else { fatalError("Err: Empty DISCORD_TOKEN. Exiting...") } - let bot = try await DiscordKit.Bot(token: token) + let intents: Intents = [.guilds, .guildMessages, .messageContent, .guildMembers, .directMessages] + print(intents) + + let bot = try await DiscordKit.Bot(token: token, intents: intents) // let bot = await BotGatewayManager(token: token, intents: [.guildMessages, .messageContent]) - //await withTaskGroup(of: Void.self) { taskGroup in - // taskGroup.addTask { - // await bot.connect() - // let tmp = try! await bot.client.getOwnUser() - // ownID = try! tmp.decode().id - // } + await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + try await bot.connect() + } - // for await event in await bot.events { - // taskGroup.addTask { - // await EventHandler(event: event, client: bot.client).handleAsync() - // } - // } - //} + taskGroup.addTask { + for await event in await bot.events { + dump(event) + } + } + } } }