feat: sending messages works

This commit is contained in:
Andrew Glaze
2026-03-20 17:53:29 -04:00
parent df3cb95c91
commit 4d39ed8053
8 changed files with 240 additions and 157 deletions

View File

@@ -4,12 +4,12 @@ import FoundationNetworking
#endif
public actor Bot {
let client: ApiClient
public let client: DiscordClient
let gateway: GatewayClient
let intents: Intents
public init(token: String, intents: Intents) async throws {
client = ApiClient(token: token)
client = DiscordClient(token: token)
self.intents = intents
let gatewayURL = try await client.getGatewayURL()
gateway = GatewayClient(gatewayURL: gatewayURL, token: token)

View File

@@ -3,7 +3,7 @@ import Foundation
import FoundationNetworking
#endif
struct ApiClient {
public struct DiscordClient: Sendable {
static let apiUrl: URL = URL(string: "https://discord.com/api/v10")!
let token: String
@@ -19,7 +19,7 @@ struct ApiClient {
}
func getGatewayURL() async throws -> URL {
var req = URLRequest(url: ApiClient.apiUrl.appending(path: "/gateway/bot"))
var req = URLRequest(url: DiscordClient.apiUrl.appending(path: "/gateway/bot"))
req.httpMethod = "GET"
let (data, _) = try await authReq(req)
@@ -27,6 +27,26 @@ struct ApiClient {
let decoded = try json.decode(GetGatewayResponse.self, from: data)
return decoded.url
}
public func getOwnUser() async throws -> User {
var req = URLRequest(url: DiscordClient.apiUrl.appending(path: "/users/@me"))
req.httpMethod = "GET"
let (data, _) = try await authReq(req)
let json = JSONDecoder()
let decoded = try json.decode(User.self, from: data)
return decoded
}
public func createMessage(channelId: String, payload: CreateMessageReq) async throws {
var req = URLRequest(url: DiscordClient.apiUrl.appending(path: "/channels/\(channelId)/messages"))
req.httpMethod = "POST"
let json = JSONEncoder()
req.httpBody = try json.encode(payload)
let (data, res) = try await authReq(req)
}
}
public enum ApiError: Error {

View File

@@ -56,32 +56,44 @@ actor GatewayClient {
}
func getMessage() async throws -> GatewayMessage {
let wsMessage = try! await ws.receive()
print(wsMessage)
guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage }
var strBuffer = ""
var gwMessage: GatewayMessage? = nil
let json = JSONDecoder()
let gwMessage = try json.decode(GatewayMessage.self, from: Data(str.utf8))
while gwMessage == nil {
let wsMessage = try await ws.receive()
guard case .string(let str) = wsMessage else { throw GatewayError.invalidMessage }
strBuffer.append(str)
do {
gwMessage = try json.decode(GatewayMessage.self, from: Data(strBuffer.utf8))
} catch DecodingError.dataCorrupted {
continue
}
}
guard let gwMessage = gwMessage else { throw GatewayError.invalidMessage }
sequenceNum = gwMessage.s ?? sequenceNum
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<GatewayMessage> {
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
var event: GatewayMessage? = nil
while event == nil {
do {
while await !open {
try await Task.sleep(for: .seconds(1))
}
event = try await getMessage()
if event!.op == 1 { try await sendHeartbeat() }
} catch {
print("Error listening to gateway: \(error)")
}
}
return event!
}
}

View File

@@ -35,27 +35,28 @@ public struct Intents: OptionSet, Sendable {
}
public struct GetGatewayResponse: Codable, Sendable {
let url: URL
let shards: Int
let session_start_limit: SessionStartLimit
public let url: URL
public let shards: Int
public let session_start_limit: SessionStartLimit
}
public struct SessionStartLimit: Codable, Sendable {
let total: Int
let remaining: Int
let reset_after: Int
let max_concurrency: Int
public let total: Int
public let remaining: Int
public let reset_after: Int
public let max_concurrency: Int
}
public enum GatewayPayload: Decodable, Sendable {
case hello(HelloPayload)
case hello(GatewayHello)
case messageCreate(MessageCreate)
}
public struct GatewayMessage: Decodable, Sendable {
let op: Int
let d: GatewayPayload?
let s: Int?
let t: String?
public let op: Int
public let d: GatewayPayload?
public let s: Int?
public let t: String?
enum CodingKeys: String, CodingKey {
case t
@@ -71,8 +72,11 @@ public struct GatewayMessage: Decodable, Sendable {
t = try container.decode(String?.self, forKey: .t)
switch op {
case 10:
let hello = try container.decode(HelloPayload.self, forKey: .d)
let hello = try container.decode(GatewayHello.self, forKey: .d)
d = .hello(hello)
case 0 where t == "MESSAGE_CREATE":
let messageCreate = try container.decode(MessageCreate.self, forKey: .d)
d = .messageCreate(messageCreate)
default:
d = nil
break
@@ -87,7 +91,46 @@ public struct GatewayMessage: Decodable, Sendable {
}
}
public struct HelloPayload: Codable, Sendable {
public struct GatewayHello: Codable, Sendable {
let heartbeat_interval: Int
}
public struct CreateMessageReq: Codable, Sendable {
public init(content: String? = nil, message_reference: MessageRefrence? = nil) {
self.content = content
self.message_reference = message_reference
}
public let content: String?
public let message_reference: MessageRefrence?
}
public struct MessageRefrence: Codable, Sendable {
public init(type: Int? = nil, message_id: String? = nil, channel_id: String? = nil, guild_id: String? = nil) {
self.type = type
self.message_id = message_id
self.channel_id = channel_id
self.guild_id = guild_id
}
let type: Int?
let message_id: String?
let channel_id: String?
let guild_id: String?
}
public struct MessageCreate: Codable, Sendable {
public let id: String
public let channel_id: String
public let guild_id: String?
public let author: User?
public let content: String
public let mentions: [User]
}
public struct User: Codable, Sendable {
public let id: String?
public let bot: Bool?
}