From b0802b247cb835879dfd01781348506d4cc9fe79 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Wed, 13 Apr 2022 16:14:18 +0800 Subject: [PATCH 01/10] In the process of porting Gateway to use URLSession for connections Basic Gateway handling works, but without any reconnection logic --- .../DiscordAPI/Gateway/DiscordGateway.swift | 91 +++++++++++++------ .../DiscordAPI/Gateway/GatewayHeartbeat.swift | 3 +- .../DiscordAPI/Gateway/GatewaySend.swift | 8 +- .../DiscordAPI/Objects/Gateway/Gateway.swift | 4 +- 4 files changed, 75 insertions(+), 31 deletions(-) diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index 9b09ee20..4dd1498d 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -8,7 +8,7 @@ import Foundation import Starscream -class DiscordGateway: WebSocketDelegate, ObservableObject { +class DiscordGateway: NSObject, ObservableObject, URLSessionWebSocketDelegate { // Events let onStateChange = EventDispatch<(Bool, Bool, GatewayCloseCode?)>() let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() @@ -19,7 +19,9 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { let connTimeout: Double // WebSocket object - private(set) var socket: WebSocket! + // private(set) var socket: WebSocket! + private(set) var session: URLSession! + private(set) var socket: URLSessionWebSocketTask! // State @Published private(set) var isConnected = false @@ -41,22 +43,28 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { // Logger let log = Logger(tag: "DiscordGateway") - // Dispatch queue + // Queues let queue: DispatchQueue + let opQueue: OperationQueue func incMissedACK() { missedACK += 1 } func initWSConn() { authFailed = false - var request = URLRequest(url: URL(string: apiConfig.gateway)!) + /*var request = URLRequest(url: URL(string: apiConfig.gateway)!) request.timeoutInterval = connTimeout socket = WebSocket(request: request) socket.delegate = self - socket.callbackQueue = queue + socket.callbackQueue = queue*/ + + socket = session.webSocketTask(with: URL(string: apiConfig.gateway)!) + socket.maximumMessageSize = 5243000 // (5MiB) Raise max incoming message size to avoid errors log.i("Attempting connection to Gateway: \(apiConfig.gateway)") - socket.connect() + socket.resume() + + addReceiveListener() // If connection isn't connected after timeout, try again let curConnCnt = connTimes @@ -69,6 +77,22 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { } } + func addReceiveListener() { + socket.receive { [weak self] (result) in + switch result { + case .success(let response): + switch response { + case .data(let data): self?.didReceive(event: .binary(data)) + case .string(let message): self?.didReceive(event: .text(message)) + + @unknown default: break + } + case .failure(let error): self?.didReceive(event: .error(error)) + } + self?.addReceiveListener() + } + } + // Attempt reconnection with resume after 1-5s as per spec func attemptReconnect(resume: Bool = true, overrideViability: Bool = false) { log.d("Resume called") @@ -77,7 +101,7 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { return } // Kill connection if connection is still active - if isConnected { self.socket.forceDisconnect() } + /*if isConnected { self.socket.forceDisconnect() } guard viability || overrideViability, !isReconnecting else { return } isReconnecting = true if !resume { doNotResume = true } @@ -90,14 +114,17 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { self.log.d("Attempting reconnection now") self.log.d("Can resume: \(!self.doNotResume)") self.initWSConn() // Recreate WS object because sometimes it gets stuck in a "not gonna reconnect" state - } + }*/ + + } // Log out the user - delete token from keychain and disconnect connection func logOut() { log.d("Logging out...") let _ = Keychain.remove(key: "token") - socket.disconnect(closeCode: 1000) + // socket.disconnect(closeCode: 1000) + socket.cancel(with: .normalClosure, reason: nil) authFailed = true } @@ -105,17 +132,26 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { missedACKTolerance = maxMissedACK connTimeout = connectionTimeout queue = DispatchQueue(label: "com.swiftcord.gatewayQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: .global(qos: .background)) + opQueue = OperationQueue() + opQueue.qualityOfService = .utility + opQueue.underlyingQueue = queue + super.init() + session = URLSession(configuration: .default, delegate: self, delegateQueue: opQueue) + initWSConn() } + // MARK: Low level receive handler - func didReceive(event: WebSocketEvent, client: WebSocket) { + func didReceive(event: WebSocketEvent) { switch (event) { case .connected(_): log.i("Gateway Connected") - isReconnecting = false - isConnected = true - connTimes += 1 + DispatchQueue.main.async { [weak self] in + self?.isReconnecting = false + self?.isConnected = true + self?.connTimes += 1 + } onStateChange.notify(event: (isConnected, isReconnecting, nil)) case .disconnected(_, let c): isConnected = false @@ -133,7 +169,7 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { onStateChange.notify(event: (isConnected, isReconnecting, code)) case .text(let string): self.handleIncoming(received: string) case .error(let error): - isConnected = false + DispatchQueue.main.async { self.isConnected = false } attemptReconnect() onStateChange.notify(event: (isConnected, isReconnecting, nil)) log.e("Connection error: \(String(describing: error))") @@ -142,18 +178,7 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { onStateChange.notify(event: (isConnected, isReconnecting, nil)) log.d("Connection cancelled") case .binary(_): break // Won't receive binary - case .ping(_): break // Don't care - case .pong(_): break // Don't care - case .viabilityChanged(let viability): - // If viability is false, reconnection will most likely fail - log.d("Viability changed: \(viability)") - if viability && !self.viability { - // We should reconnect since connection is now viable - attemptReconnect(resume: true, overrideViability: true) - } - self.viability = viability - case .reconnectSuggested(_): - log.d("Reconnect suggested!") + default: break } } @@ -192,7 +217,7 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { guard let identify = getIdentify() else { log.d("Token not in keychain") authFailed = true - socket.disconnect(closeCode: 1000) + // socket.disconnect(closeCode: 1000) return } sendToGateway(op: .identify, d: identify) @@ -211,6 +236,7 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { self.cache.user = d.user } log.i("Gateway ready") + //onEvent.notify(event: (type, data)) default: log.i("Dispatched event <\(type)>") } onEvent.notify(event: (type, data)) @@ -221,4 +247,15 @@ class DiscordGateway: WebSocketDelegate, ObservableObject { default: log.w("Unimplemented opcode: \(decoded.op)") } } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + // didOpenConnection?() + didReceive(event: .connected([:])) + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + // didCloseConnection?() + log.w("Session Gateway disconnected") + didReceive(event: .disconnected("", UInt16(closeCode.rawValue))) + } } diff --git a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift b/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift index 9b937aa1..14b09ee9 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift @@ -17,7 +17,8 @@ extension DiscordGateway { // Connection is dead ☠️ if (missedACK > 1) { - socket.forceDisconnect() + // socket.forceDisconnect() + socket.cancel() attemptReconnect() } } diff --git a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift b/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift index 944f8d47..dd23838c 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift @@ -16,6 +16,12 @@ extension DiscordGateway { else { return } log.d("Outgoing Payload: <\(op)>", sendPayload.d != nil ? String(describing: sendPayload.d!) : "[No data]", "Seq:", String(describing: sendPayload.s)) - socket.write(string: String(data: encoded, encoding: .utf8)!) + // socket.write(string: String(data: encoded, encoding: .utf8)!) + socket.send(.data(encoded)) { err in + self.log.i("Socket send completed") + if let err = err { + self.log.e("Socket send error:", err.localizedDescription) + } + } } } diff --git a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift index 3fef5b67..754b5b19 100644 --- a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift +++ b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift @@ -78,7 +78,7 @@ struct GatewayIncoming: Decodable { // Cue the long switch case to parse every single event switch t { case .ready: d = try values.decode(ReadyEvt.self, forKey: .d) - case .resumed: d = nil + /*case .resumed: d = nil case .channelCreate: fallthrough case .channelUpdate: fallthrough case .channelDelete: fallthrough @@ -114,7 +114,7 @@ struct GatewayIncoming: Decodable { case .messageDelete: d = try values.decode(MessageDelete.self, forKey: .d) case .messageDeleteBulk: d = try values.decode(MessageDeleteBulk.self, forKey: .d) case .presenceUpdate: d = try values.decode(PartialPresenceUpdate .self, forKey: .d) - // TODO: Add the remaining like 100 events + // TODO: Add the remaining like 100 events*/ default: break } default: From de747bd22a8dab23cef268810d2a13584fbfbc27 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 16:44:09 +0800 Subject: [PATCH 02/10] Completely rewritten low-level WebSocket connection wrapper, now mostly stable Please open an issue if you encounter any bugs, and include logs as well --- Swiftcord.xcodeproj/project.pbxproj | 4 + Swiftcord/ContentView.swift | 2 +- .../DiscordAPI/Gateway/DiscordGateway.swift | 54 ++- .../DiscordAPI/Gateway/GatewayHeartbeat.swift | 3 +- .../DiscordAPI/Gateway/GatewayIdentify.swift | 2 +- .../DiscordAPI/Gateway/GatewaySend.swift | 4 +- .../DiscordAPI/Gateway/RobustWebSocket.swift | 332 ++++++++++++++++++ .../DiscordAPI/Objects/Gateway/Gateway.swift | 4 +- .../Views/Settings/UserSettingsView.swift | 2 +- 9 files changed, 391 insertions(+), 16 deletions(-) create mode 100644 Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 462a5fff..2117ee59 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ DA520AE727D8ECA6009FD740 /* DecodableThrowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA520AE627D8ECA6009FD740 /* DecodableThrowable.swift */; }; DA57F44428056718001DC46E /* ChannelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57F44328056718001DC46E /* ChannelList.swift */; }; DA57F44628065209001DC46E /* ChannelButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57F44528065209001DC46E /* ChannelButton.swift */; }; + DA57F4482806C4F5001DC46E /* RobustWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA57F4472806C4F5001DC46E /* RobustWebSocket.swift */; }; DA585C9927E1F6AC00FA4EE0 /* CursorOnViewHover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA585C9827E1F6AC00FA4EE0 /* CursorOnViewHover.swift */; }; /* End PBXBuildFile section */ @@ -238,6 +239,7 @@ DA520AE627D8ECA6009FD740 /* DecodableThrowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableThrowable.swift; sourceTree = ""; }; DA57F44328056718001DC46E /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; DA57F44528065209001DC46E /* ChannelButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelButton.swift; sourceTree = ""; }; + DA57F4472806C4F5001DC46E /* RobustWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RobustWebSocket.swift; sourceTree = ""; }; DA585C9827E1F6AC00FA4EE0 /* CursorOnViewHover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorOnViewHover.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -426,6 +428,7 @@ DA4A890D27C4876900720909 /* GatewayHeartbeat.swift */, DA4A890F27C48A7B00720909 /* GatewaySend.swift */, DA4A891527C4B06500720909 /* GatewayCachedState.swift */, + DA57F4472806C4F5001DC46E /* RobustWebSocket.swift */, ); path = Gateway; sourceTree = ""; @@ -680,6 +683,7 @@ DA4A889C27C0B23C00720909 /* WebView.swift in Sources */, DA4A88E527C3482A00720909 /* LoginView.swift in Sources */, DA32EF5927CB5F4F00A9ED72 /* UIStateEnv.swift in Sources */, + DA57F4482806C4F5001DC46E /* RobustWebSocket.swift in Sources */, DA4A890527C383DD00720909 /* MessageDelete.swift in Sources */, DA32EF2627C62E6900A9ED72 /* MessageView.swift in Sources */, DA4A888A27C0AF3000720909 /* ContentView.swift in Sources */, diff --git a/Swiftcord/ContentView.swift b/Swiftcord/ContentView.swift index dcdaba7b..8bcc72cb 100644 --- a/Swiftcord/ContentView.swift +++ b/Swiftcord/ContentView.swift @@ -121,7 +121,7 @@ struct ContentView: View { if tk != nil { state.attemptLogin = false let _ = Keychain.save(key: "token", data: tk!) - gateway.initWSConn() // Reconnect to the socket + gateway.connect() // Reconnect to the socket } }) .onAppear { diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index 4dd1498d..0cb1e965 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -8,20 +8,21 @@ import Foundation import Starscream -class DiscordGateway: NSObject, ObservableObject, URLSessionWebSocketDelegate { +class DiscordGateway: ObservableObject { // Events let onStateChange = EventDispatch<(Bool, Bool, GatewayCloseCode?)>() let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() let onAuthFailure = EventDispatch() // Config - let missedACKTolerance: Int - let connTimeout: Double + // let missedACKTolerance: Int + // let connTimeout: Double // WebSocket object // private(set) var socket: WebSocket! - private(set) var session: URLSession! - private(set) var socket: URLSessionWebSocketTask! + // private(set) var session: URLSession! + // private(set) var socket: URLSessionWebSocketTask! + private var socket: RobustWebSocket! // State @Published private(set) var isConnected = false @@ -31,20 +32,22 @@ class DiscordGateway: NSObject, ObservableObject, URLSessionWebSocketDelegate { @Published private(set) var seq: Int? = nil // Sequence int of latest received payload @Published private(set) var viability = true @Published private(set) var connTimes = 0 - private(set) var authFailed = false { + /*private(set) var authFailed = false { didSet { if authFailed { onAuthFailure.notify() } cache = CachedState() // Clear the cache } - } + }*/ @Published private(set) var sessionID: String? = nil @Published var cache: CachedState = CachedState() + private var evtListenerID: EventDispatch.HandlerIdentifier? = nil + // Logger let log = Logger(tag: "DiscordGateway") // Queues - let queue: DispatchQueue + /*let queue: DispatchQueue let opQueue: OperationQueue func incMissedACK() { missedACK += 1 } @@ -257,5 +260,40 @@ class DiscordGateway: NSObject, ObservableObject, URLSessionWebSocketDelegate { // didCloseConnection?() log.w("Session Gateway disconnected") didReceive(event: .disconnected("", UInt16(closeCode.rawValue))) + }*/ + + public func logout() { + log.d("Logging out on request") + let _ = Keychain.remove(key: "token") + // socket.disconnect(closeCode: 1000) + socket.close(code: .normalClosure) + // authFailed = true + } + + public func connect() { + socket.open() + } + + private func handleEvt(type: GatewayEvent, data: GatewayData) { + switch (type) { + case .ready: + guard let d = data as? ReadyEvt else { return } + //self.doNotResume = false + //self.sessionID = d.session_id + self.cache.guilds = d.guilds + self.cache.user = d.user + log.i("Gateway ready") + //onEvent.notify(event: (type, data)) + default: break + } + onEvent.notify(event: (type, data)) + log.i("Dispatched event <\(type)>") + } + + init(connectionTimeout: Double = 5, maxMissedACK: Int = 3) { + socket = RobustWebSocket() + evtListenerID = socket.onEvent.addHandler { [weak self] (t, d) in + self?.handleEvt(type: t, data: d) + } } } diff --git a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift b/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift index 14b09ee9..5dada6d7 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift @@ -7,7 +7,7 @@ import Foundation -extension DiscordGateway { +/*extension DiscordGateway { func initHeartbeat(interval: Int) { let initialConnTimes = connTimes func sendHeartbeat() { @@ -41,3 +41,4 @@ extension DiscordGateway { } } } +*/ diff --git a/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift b/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift index 0c499535..95c05a86 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift @@ -7,7 +7,7 @@ import Foundation -extension DiscordGateway { +extension RobustWebSocket { func getIdentify() -> GatewayIdentify? { // Keychain.save(key: "token", data: "token goes here") // Keychain.remove(key: "token") // For testing diff --git a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift b/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift index dd23838c..e6407fe6 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift @@ -7,7 +7,7 @@ import Foundation -extension DiscordGateway { +/*extension DiscordGateway { func sendToGateway(op: GatewayOutgoingOpcodes, d: T?) { guard isConnected else { return } @@ -24,4 +24,4 @@ extension DiscordGateway { } } } -} +}*/ diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift new file mode 100644 index 00000000..f4d92fa2 --- /dev/null +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -0,0 +1,332 @@ +// +// RobustWebSocket.swift +// Swiftcord +// +// Created by Vincent on 4/13/22. +// + +import Foundation +import Reachability + +/// A robust WebSocket that handles resuming, reconnection and heartbeats +/// with the Discord Gateway, inspired by robust-websocket + +class RobustWebSocket: NSObject { + public let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() + + private var session: URLSession!, socket: URLSessionWebSocketTask! + private let reachability = try! Reachability(), log = Logger(tag: "RobustWebSocket") + + private let queue: OperationQueue + + private let timeout: TimeInterval, maxMsgSize: Int, + reconnectInterval: (URLSessionWebSocketTask.CloseCode?, Int) -> TimeInterval? + private var attempts = 0, reconnects = -1, connected = false, awaitingHb: Int = 0, + reachable = false, reconnectWhenOnlineAgain = false, explicitlyClosed = false, + seq: Int? = nil, canResume = false, sessionID: String? = nil, + pendingReconnect: Timer? = nil, connTimeout: Timer? = nil, hbTimer: Timer? = nil + + private func clearPendingReconnectIfNeeded() { + if let reconnectTimer = pendingReconnect { + reconnectTimer.invalidate() + pendingReconnect = nil + } + } + + // MARK: - (Re)Connection + private func reconnect(code: URLSessionWebSocketTask.CloseCode?) { + guard !explicitlyClosed else { + log.w("Not reconnecting: connection was explicitly closed") + attempts = 0 + return + } + guard reachable else { + log.w("Not reconnecting: connection is unreachable") + reconnectWhenOnlineAgain = true + return + } + guard connTimeout == nil else { + log.w("Not reconnecting: already attempting a connection") + return + } + + let delay = reconnectInterval(code, attempts) + if let delay = delay { + log.i("Reconnecting after \(delay)s...") + DispatchQueue.main.async { [weak self] in + self?.pendingReconnect = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in + guard self?.connected != true else { + self?.log.w("Looks like we're already connected, no need to reconnect") + return + } + guard self?.connTimeout == nil else { + self?.log.w("Not reconnecting: already attempting a connection") + return + } + self?.log.d("Attempting reconnection now") + self?.connect() + } + } + } + } + + private func attachSockReceiveListener() { + socket.receive { [weak self] result in + switch result { + case .success(let message): + switch (message) { + case .data(_): break + case .string(let str): self?.handleMessage(message: str) + @unknown default: self?.log.w("Unknown sock message case!") + } + // self?.onMessage.notify(event: message) + self?.attachSockReceiveListener() + case .failure(let error): + // If an error is encountered here, the connection is probably broken + self?.log.e("Error when receiving: \(error)") + self?.forceClose() + return + } + } + } + private func connect() { + pendingReconnect = nil + awaitingHb = 0 + stopHeartbeating() + + socket = session.webSocketTask(with: URL(string: apiConfig.gateway)!) + socket.maximumMessageSize = maxMsgSize + + DispatchQueue.main.async { [weak self] in + self?.connTimeout = Timer.scheduledTimer(withTimeInterval: self!.timeout, repeats: false) { [weak self] _ in + self?.connTimeout = nil + // reachability.stopNotifier() + self?.log.w("Connection timed out after \(self!.timeout)s") + self?.forceClose() + } + } + + attempts += 1 + socket.resume() + + setupReachability() + attachSockReceiveListener() + } + + // MARK: - Handlers + private func handleMessage(message: String) { + guard let decoded = try? JSONDecoder().decode(GatewayIncoming.self, from: message.data(using: .utf8)!) + else { return } + + if let sequence = decoded.s { seq = sequence } + + switch(decoded.op) { + case .heartbeat: + log.d("Sending expedited heartbeat as requested") + send(op: .heartbeat, data: GatewayHeartbeat()) + case .heartbeatAck: awaitingHb -= 1 + case .hello: + // Start heartbeating and send identify + guard let d = decoded.d as? GatewayHello else { return } + log.d("Hello payload is:", String(describing: d)) + startHeartbeating(interval: Double(d.heartbeat_interval) / 1000.0) + + // Check if we're attempting to and can resume + if canResume, let sessionID = sessionID, let seq = seq { + log.i("Attempting resume") + guard let resume = getResume(seq: seq, sessionID: sessionID) + else { return } + send(op: .resume, data: resume) + return + } + log.d("Sending identify") + // Send identify + seq = nil // Clear sequence # + // isReconnecting = false // Resuming failed/not attempted + guard let identify = getIdentify() else { + log.d("Token not in keychain") + // authFailed = true + // socket.disconnect(closeCode: 1000) + return + } + send(op: .identify, data: identify) + case .invalidSession: + // Check if the session can be resumed + let shouldResume = (decoded.primitiveData as? Bool) ?? false + if !shouldResume { canResume = false } + log.w("Session is invalid, reconnecting without resuming") + forceClose(code: .normalClosure) + // attemptReconnect(resume: shouldResume) + case .dispatchEvent: + guard let type = decoded.t else { return } + guard let data = decoded.d else { return } + switch type { + case .ready: + guard let d = data as? ReadyEvt else { return } + sessionID = d.session_id + canResume = true + default: break + } + onEvent.notify(event: (type, data)) + case .reconnect: + log.w("Gateway-requested reconnect: disconnecting and reconnecting immediately") + + } + } + + + // MARK: - Initializers + init(timeout: TimeInterval, maxMessageSize: Int, reconnectIntClosure: @escaping (URLSessionWebSocketTask.CloseCode?, Int) -> TimeInterval?) { + self.timeout = timeout + queue = OperationQueue() + queue.qualityOfService = .utility + reconnectInterval = reconnectIntClosure + maxMsgSize = maxMessageSize + super.init() + session = URLSession(configuration: .default, delegate: self, delegateQueue: queue) + connect() + } + + override convenience init() { + self.init(timeout: TimeInterval(4), maxMessageSize: 1024*1024*10) { code, times in + if code == .policyViolation || code == .internalServerError { return nil } + return [2, 5, 10][times] + } + } +} + + +// MARK: - WebSocketTask delegate functions +extension RobustWebSocket: URLSessionWebSocketDelegate { + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + // didOpenConnection?() + if let timer = connTimeout { + timer.invalidate() + connTimeout = nil + } + reconnectWhenOnlineAgain = true + attempts = 0 + connected = true + log.i("Socket connection opened") + } + + func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + reconnect(code: closeCode) + connected = false + stopHeartbeating() + // didCloseConnection?() + // didReceive(event: .disconnected("", UInt16(closeCode.rawValue))) + reconnectWhenOnlineAgain = false + log.i("Socket connection closed") + } +} + + +// MARK: - Reachability +extension RobustWebSocket { + private func setupReachability() { + reachability.whenReachable = { [weak self] _ in + self?.reachable = true + self?.log.i("Connection reachable") + //if let reconnect = self?.reconnectWhenOnlineAgain, reconnect { + // Temporarily ignore reconnectWhenOnlineAgain since that was causing issues + self?.clearPendingReconnectIfNeeded() + self?.reconnect(code: nil) + //} + } + reachability.whenUnreachable = { [weak self] _ in + self?.reachable = false + self?.log.i("Connection unreachable") + self?.forceClose() + } + do { try reachability.startNotifier() } + catch { log.e("Starting reachability notifier failed!") } + } +} + + +// MARK: - Heartbeating +extension RobustWebSocket { + @objc private func sendHeartbeat() { + log.d("Sending heartbeat, awaiting \(awaitingHb) ACKs") + if awaitingHb > 1 { + log.e("Too many pending heartbeats, closing socket") + forceClose() + } + send(op: .heartbeat, data: GatewayHeartbeat()) + awaitingHb += 1 + } + + private func startHeartbeating(interval: TimeInterval) { + if hbTimer != nil { stopHeartbeating() } + log.d("Sending heartbeats every \(interval)s") + awaitingHb = 0 + + DispatchQueue.main.asyncAfter( + deadline: .now() + interval * Double.random(in: 0...1), + qos: .utility, + flags: .enforceQoS + ) { + self.sendHeartbeat() + self.hbTimer = Timer(timeInterval: interval, target: self, selector: #selector(self.sendHeartbeat), userInfo: nil, repeats: true) + RunLoop.current.add(self.hbTimer!, forMode: .common) + } + } + private func stopHeartbeating() { + if let heartbeatTimer = hbTimer { + log.d("Stopping heartbeat timer") + heartbeatTimer.invalidate() + hbTimer = nil + } + } +} + + +// MARK: - Extension with public exposed methods +extension RobustWebSocket { + public func forceClose(code: URLSessionWebSocketTask.CloseCode = .abnormalClosure) { + log.w("Forcibly closing connection") + stopHeartbeating() + self.socket.cancel(with: code, reason: nil) + connected = false + self.reconnect(code: nil) + } + public func close(code: URLSessionWebSocketTask.CloseCode) { + clearPendingReconnectIfNeeded() + reconnectWhenOnlineAgain = false + explicitlyClosed = true + connected = false + sessionID = nil + reachability.stopNotifier() + + socket.cancel(with: code, reason: nil) + stopHeartbeating() + } + + public func open() { + guard socket.state != .running else { return } + clearPendingReconnectIfNeeded() + reconnectWhenOnlineAgain = false + explicitlyClosed = false + + connect() + } + + public func send( + op: GatewayOutgoingOpcodes, + data: T, + completionHandler: ((Error?) -> Void)? = nil + ) { + guard connected else { return } + + let sendPayload = GatewayOutgoing(op: op, d: data, s: seq) + guard let encoded = try? JSONEncoder().encode(sendPayload) + else { return } + + log.d("Outgoing Payload: <\(op)>", sendPayload.d != nil ? String(describing: sendPayload.d!) : "[No data]", "Seq:", String(describing: sendPayload.s)) + // socket.write(string: String(data: encoded, encoding: .utf8)!) + socket.send(.data(encoded), completionHandler: completionHandler ?? { [weak self] err in + if let err = err { self?.log.e("Socket send error:", err.localizedDescription) } + }) + } +} diff --git a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift index 754b5b19..3fef5b67 100644 --- a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift +++ b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift @@ -78,7 +78,7 @@ struct GatewayIncoming: Decodable { // Cue the long switch case to parse every single event switch t { case .ready: d = try values.decode(ReadyEvt.self, forKey: .d) - /*case .resumed: d = nil + case .resumed: d = nil case .channelCreate: fallthrough case .channelUpdate: fallthrough case .channelDelete: fallthrough @@ -114,7 +114,7 @@ struct GatewayIncoming: Decodable { case .messageDelete: d = try values.decode(MessageDelete.self, forKey: .d) case .messageDeleteBulk: d = try values.decode(MessageDeleteBulk.self, forKey: .d) case .presenceUpdate: d = try values.decode(PartialPresenceUpdate .self, forKey: .d) - // TODO: Add the remaining like 100 events*/ + // TODO: Add the remaining like 100 events default: break } default: diff --git a/Swiftcord/Views/Settings/UserSettingsView.swift b/Swiftcord/Views/Settings/UserSettingsView.swift index c299b321..4ca172fb 100644 --- a/Swiftcord/Views/Settings/UserSettingsView.swift +++ b/Swiftcord/Views/Settings/UserSettingsView.swift @@ -187,7 +187,7 @@ struct UserSettingsView: View { Text("Log Out").font(.title) Text("Are you sure you want to log out?") Button(role: .destructive) { - gateway.logOut() + gateway.logout() } label: { Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right") } From 0dfac3758ae840646b98401586ac1675360a91df Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 17:39:20 +0800 Subject: [PATCH 03/10] Bump version: 0.2.1(2) -> 0.2.2(3) for GitHub release --- Swiftcord.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 2117ee59..72df3c01 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -884,7 +884,7 @@ CODE_SIGN_ENTITLEMENTS = Swiftcord/Swiftcord.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"Swiftcord/Preview Content\""; DEVELOPMENT_TEAM = Q382QLKDG3; ENABLE_HARDENED_RUNTIME = YES; @@ -896,7 +896,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2.1; + MARKETING_VERSION = 0.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.cryptoalgo.swiftcord; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -912,7 +912,7 @@ CODE_SIGN_ENTITLEMENTS = Swiftcord/Swiftcord.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_ASSET_PATHS = "\"Swiftcord/Preview Content\""; DEVELOPMENT_TEAM = Q382QLKDG3; ENABLE_HARDENED_RUNTIME = YES; @@ -924,7 +924,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.2.1; + MARKETING_VERSION = 0.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.cryptoalgo.swiftcord; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; From b14a2c3bcaa7a353cdf2002c564f218b69aea957 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 17:53:24 +0800 Subject: [PATCH 04/10] Add tolerance to heartbeat timer for better resource friendliness --- Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift index f4d92fa2..6bd7a0be 100644 --- a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -262,6 +262,8 @@ extension RobustWebSocket { log.d("Sending heartbeats every \(interval)s") awaitingHb = 0 + // First heartbeat after interval * jitter where jitter is a value from 0-1 + // ~ Discord API docs DispatchQueue.main.asyncAfter( deadline: .now() + interval * Double.random(in: 0...1), qos: .utility, @@ -269,6 +271,7 @@ extension RobustWebSocket { ) { self.sendHeartbeat() self.hbTimer = Timer(timeInterval: interval, target: self, selector: #selector(self.sendHeartbeat), userInfo: nil, repeats: true) + self.hbTimer!.tolerance = 2 // 2s of tolerance RunLoop.current.add(self.hbTimer!, forMode: .common) } } From 4e3fe9fab827dbe02af599a679a8a57a73d29633 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 17:55:43 +0800 Subject: [PATCH 05/10] Completely remove Starscream dependency --- Swiftcord.xcodeproj/project.pbxproj | 17 ----------------- .../xcshareddata/swiftpm/Package.resolved | 13 ++----------- .../DiscordAPI/Gateway/DiscordGateway.swift | 1 - 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 72df3c01..9e9c6efb 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -64,7 +64,6 @@ DA4A88C427C1442300720909 /* Sticker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88C327C1442300720909 /* Sticker.swift */; }; DA4A88C627C1E77000720909 /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88C527C1E77000720909 /* Permission.swift */; }; DA4A88C827C1EB8F00720909 /* DiscordGateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88C727C1EB8F00720909 /* DiscordGateway.swift */; }; - DA4A88CB27C1EC5500720909 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = DA4A88CA27C1EC5500720909 /* Starscream */; }; DA4A88CD27C1FAEE00720909 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88CC27C1FAEE00720909 /* Config.swift */; }; DA4A88D027C2068F00720909 /* Gateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88CF27C2068F00720909 /* Gateway.swift */; }; DA4A88D227C20FC300720909 /* Activity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A88D127C20FC300720909 /* Activity.swift */; }; @@ -250,7 +249,6 @@ files = ( DA2384C227CCFAEC009E15E0 /* CachedAsyncImage in Frameworks */, DA520AE027D75147009FD740 /* Reachability in Frameworks */, - DA4A88CB27C1EC5500720909 /* Starscream in Frameworks */, DA32EF2D27C65E2700A9ED72 /* Lottie in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -582,7 +580,6 @@ ); name = Swiftcord; packageProductDependencies = ( - DA4A88CA27C1EC5500720909 /* Starscream */, DA32EF2C27C65E2700A9ED72 /* Lottie */, DA2384C127CCFAEC009E15E0 /* CachedAsyncImage */, DA520ADF27D75147009FD740 /* Reachability */, @@ -616,7 +613,6 @@ ); mainGroup = DA4A887B27C0AF3000720909; packageReferences = ( - DA4A88C927C1EC5500720909 /* XCRemoteSwiftPackageReference "Starscream" */, DA32EF2B27C65E2700A9ED72 /* XCRemoteSwiftPackageReference "lottie-ios" */, DA2384C027CCFAEC009E15E0 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */, DA520ADE27D75147009FD740 /* XCRemoteSwiftPackageReference "Reachability" */, @@ -972,14 +968,6 @@ minimumVersion = 3.3.0; }; }; - DA4A88C927C1EC5500720909 /* XCRemoteSwiftPackageReference "Starscream" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/daltoniam/Starscream.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 4.0.0; - }; - }; DA520ADE27D75147009FD740 /* XCRemoteSwiftPackageReference "Reachability" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ashleymills/Reachability.swift"; @@ -1001,11 +989,6 @@ package = DA32EF2B27C65E2700A9ED72 /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; - DA4A88CA27C1EC5500720909 /* Starscream */ = { - isa = XCSwiftPackageProductDependency; - package = DA4A88C927C1EC5500720909 /* XCRemoteSwiftPackageReference "Starscream" */; - productName = Starscream; - }; DA520ADF27D75147009FD740 /* Reachability */ = { isa = XCSwiftPackageProductDependency; package = DA520ADE27D75147009FD740 /* XCRemoteSwiftPackageReference "Reachability" */; diff --git a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 714250b8..e89fb72e 100644 --- a/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftcord.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,22 +19,13 @@ "version": null } }, - { - "package": "Starscream", - "repositoryURL": "https://github.com/daltoniam/Starscream.git", - "state": { - "branch": null, - "revision": "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version": "4.0.4" - } - }, { "package": "swiftui-cached-async-image", "repositoryURL": "https://github.com/lorenzofiamingo/swiftui-cached-async-image", "state": { "branch": null, - "revision": "6c3847d5a94538213c0ce2c6a42e36a3c5d99042", - "version": "2.0.0" + "revision": "eeb1565d780d1b75d045e21b5ca2a1e3650b0fc2", + "version": "2.1.0" } } ] diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index 0cb1e965..fbc55cde 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -6,7 +6,6 @@ // import Foundation -import Starscream class DiscordGateway: ObservableObject { // Events From 28248fd50df4bd012e7dc4a3d9a6a48658b957f4 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 19:46:15 +0800 Subject: [PATCH 06/10] Quit app when all windows have been closed to fix some bugs (read comment in delegate) Add delay before reconnecting when the session cannot be resumed --- AppDelegate.swift | 8 +++++ Swiftcord.xcodeproj/project.pbxproj | 10 ++++-- Swiftcord/AppDelegate.swift | 18 +++++++++++ .../DiscordAPI/Gateway/RobustWebSocket.swift | 32 +++++++++++++++---- Swiftcord/SwiftcordApp.swift | 1 + Swiftcord/{ => Utils}/WebView.swift | 0 6 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 AppDelegate.swift create mode 100644 Swiftcord/AppDelegate.swift rename Swiftcord/{ => Utils}/WebView.swift (100%) diff --git a/AppDelegate.swift b/AppDelegate.swift new file mode 100644 index 00000000..f6c680e5 --- /dev/null +++ b/AppDelegate.swift @@ -0,0 +1,8 @@ +// +// AppDelegate.swift +// Swiftcord +// +// Created by Vincent on 4/14/22. +// + +import Foundation diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 9e9c6efb..ee1bdb7c 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ DA2384C427CCFEF3009E15E0 /* MessageReadAck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */; }; DA2384C627CD1921009E15E0 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C527CD1921009E15E0 /* LoadingView.swift */; }; DA2384C827CD9418009E15E0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C727CD9418009E15E0 /* Logger.swift */; }; + DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2802782808337B00B14E5C /* AppDelegate.swift */; }; DA32EF2427C6249000A9ED72 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2327C6249000A9ED72 /* MessagesView.swift */; }; DA32EF2627C62E6900A9ED72 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2527C62E6900A9ED72 /* MessageView.swift */; }; DA32EF2827C633FE00A9ED72 /* UserAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */; }; @@ -131,6 +132,8 @@ DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReadAck.swift; sourceTree = ""; }; DA2384C527CD1921009E15E0 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; DA2384C727CD9418009E15E0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + DA2802772808337800B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = "../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/AppDelegate.swift"; sourceTree = ""; }; + DA2802782808337B00B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DA32EF2327C6249000A9ED72 /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; DA32EF2527C62E6900A9ED72 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatarView.swift; sourceTree = ""; }; @@ -328,6 +331,7 @@ DA4A887B27C0AF3000720909 = { isa = PBXGroup; children = ( + DA2802772808337800B14E5C /* AppDelegate.swift */, DA4A891727C4B0DF00720909 /* README.md */, DA4A888627C0AF3000720909 /* Swiftcord */, DA4A888527C0AF3000720909 /* Products */, @@ -350,10 +354,10 @@ DA4A88DC27C3272500720909 /* Utils */, DA4A889D27C1133300720909 /* DiscordAPI */, DA32EF6327CB770E00A9ED72 /* DataModels */, - DA4A888727C0AF3000720909 /* SwiftcordApp.swift */, DA4A888927C0AF3000720909 /* ContentView.swift */, + DA4A888727C0AF3000720909 /* SwiftcordApp.swift */, + DA2802782808337B00B14E5C /* AppDelegate.swift */, DA4A889027C0AF3200720909 /* Persistence.swift */, - DA4A889B27C0B23C00720909 /* WebView.swift */, DA4A889527C0AF3200720909 /* Swiftcord.entitlements */, DA23843827CB934D009E15E0 /* Info.plist */, DA4A888B27C0AF3200720909 /* Assets.xcassets */, @@ -461,6 +465,7 @@ children = ( DA520AE327D76BF8009FD740 /* Extensions */, DA520AC127D37863009FD740 /* MergeStructs */, + DA4A889B27C0B23C00720909 /* WebView.swift */, DA4A88DD27C3273C00720909 /* Keychain.swift */, DA4A88E027C3341400720909 /* EventDispatch.swift */, DA2384C727CD9418009E15E0 /* Logger.swift */, @@ -690,6 +695,7 @@ DA32EF6627CB772300A9ED72 /* CacheModel.xcdatamodeld in Sources */, DA2384C827CD9418009E15E0 /* Logger.swift in Sources */, DA520AE227D76BEB009FD740 /* BoolToString.swift in Sources */, + DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */, DA4A88DE27C3273C00720909 /* Keychain.swift in Sources */, DA4A88E727C34BB200720909 /* ThreadListSync.swift in Sources */, DA4A891427C49B1100720909 /* ServerButton.swift in Sources */, diff --git a/Swiftcord/AppDelegate.swift b/Swiftcord/AppDelegate.swift new file mode 100644 index 00000000..5e6892c3 --- /dev/null +++ b/Swiftcord/AppDelegate.swift @@ -0,0 +1,18 @@ +// +// AppDelegate.swift +// Swiftcord +// +// Created by Vincent on 4/14/22. +// + +import Foundation +import AppKit + +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + /// Close the app when there are no more open windows + /// This is mostly to fix bugs occuring when windows are + /// reopened after all windows are closed + return true + } +} diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift index 6bd7a0be..de25bc51 100644 --- a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -153,9 +153,19 @@ class RobustWebSocket: NSObject { case .invalidSession: // Check if the session can be resumed let shouldResume = (decoded.primitiveData as? Bool) ?? false - if !shouldResume { canResume = false } - log.w("Session is invalid, reconnecting without resuming") - forceClose(code: .normalClosure) + if !shouldResume { + log.w("Session is invalid, reconnecting without resuming") + canResume = false + } + /// Close the connection immediately and reconnect after 1-5s, as per Discord docs + /// Unfortunately Discord seems to reject the new identify no matter how long I + /// wait before sending it, so there will always be at least 2 identify attempts before + /// the Gateway session is reestablished + close(code: .normalClosure) + DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 1...5)) { [weak self] in + self?.log.d("Attempting to reconnect now") + self?.open() + } // attemptReconnect(resume: shouldResume) case .dispatchEvent: guard let type = decoded.t else { return } @@ -170,7 +180,7 @@ class RobustWebSocket: NSObject { onEvent.notify(event: (type, data)) case .reconnect: log.w("Gateway-requested reconnect: disconnecting and reconnecting immediately") - + close(code: .goingAway) } } @@ -248,6 +258,13 @@ extension RobustWebSocket { // MARK: - Heartbeating extension RobustWebSocket { @objc private func sendHeartbeat() { + guard connected else { + // Obviously, a dead connection will not respond to heartbeats + log.w("Socket is not connected, cancelling heartbeat timer") + hbTimer?.invalidate() + return + } + log.d("Sending heartbeat, awaiting \(awaitingHb) ACKs") if awaitingHb > 1 { log.e("Too many pending heartbeats, closing socket") @@ -287,12 +304,15 @@ extension RobustWebSocket { // MARK: - Extension with public exposed methods extension RobustWebSocket { - public func forceClose(code: URLSessionWebSocketTask.CloseCode = .abnormalClosure) { + public func forceClose( + code: URLSessionWebSocketTask.CloseCode = .abnormalClosure, + shouldReconnect: Bool = true + ) { log.w("Forcibly closing connection") stopHeartbeating() self.socket.cancel(with: code, reason: nil) connected = false - self.reconnect(code: nil) + if shouldReconnect { self.reconnect(code: nil) } } public func close(code: URLSessionWebSocketTask.CloseCode) { clearPendingReconnectIfNeeded() diff --git a/Swiftcord/SwiftcordApp.swift b/Swiftcord/SwiftcordApp.swift index faefeb28..9c4a3b03 100644 --- a/Swiftcord/SwiftcordApp.swift +++ b/Swiftcord/SwiftcordApp.swift @@ -23,6 +23,7 @@ extension NSTextField { @main struct SwiftcordApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let persistenceController = PersistenceController.shared @StateObject private var gateway = DiscordGateway() diff --git a/Swiftcord/WebView.swift b/Swiftcord/Utils/WebView.swift similarity index 100% rename from Swiftcord/WebView.swift rename to Swiftcord/Utils/WebView.swift From 7e7c28dd15276454fb91e36207b619d235e6d164 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Thu, 14 Apr 2022 19:58:22 +0800 Subject: [PATCH 07/10] Clean up left over comments and files from previous WebSocket wrapper --- Swiftcord.xcodeproj/project.pbxproj | 8 - .../DiscordAPI/Gateway/DiscordGateway.swift | 248 +----------------- .../DiscordAPI/Gateway/GatewayHeartbeat.swift | 44 ---- .../DiscordAPI/Gateway/GatewayIdentify.swift | 2 +- .../DiscordAPI/Gateway/GatewaySend.swift | 27 -- .../Views/Settings/MiscSettingsView.swift | 4 +- 6 files changed, 10 insertions(+), 323 deletions(-) delete mode 100644 Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift delete mode 100644 Swiftcord/DiscordAPI/Gateway/GatewaySend.swift diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index ee1bdb7c..a9ced9fa 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -92,8 +92,6 @@ DA4A890727C38E4300720909 /* DiscordAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A890627C38E4300720909 /* DiscordAPI.swift */; }; DA4A890927C3CFA700720909 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A890827C3CFA700720909 /* APIRequest.swift */; }; DA4A890C27C3D29C00720909 /* APIChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A890B27C3D29C00720909 /* APIChannel.swift */; }; - DA4A890E27C4876900720909 /* GatewayHeartbeat.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A890D27C4876900720909 /* GatewayHeartbeat.swift */; }; - DA4A891027C48A7B00720909 /* GatewaySend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A890F27C48A7B00720909 /* GatewaySend.swift */; }; DA4A891427C49B1100720909 /* ServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A891327C49B1000720909 /* ServerButton.swift */; }; DA4A891627C4B06500720909 /* GatewayCachedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A891527C4B06500720909 /* GatewayCachedState.swift */; }; DA4A891927C4E90900720909 /* APIUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4A891827C4E90900720909 /* APIUser.swift */; }; @@ -212,8 +210,6 @@ DA4A890827C3CFA700720909 /* APIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = ""; }; DA4A890A27C3D02A00720909 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; DA4A890B27C3D29C00720909 /* APIChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIChannel.swift; sourceTree = ""; }; - DA4A890D27C4876900720909 /* GatewayHeartbeat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewayHeartbeat.swift; sourceTree = ""; }; - DA4A890F27C48A7B00720909 /* GatewaySend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewaySend.swift; sourceTree = ""; }; DA4A891327C49B1000720909 /* ServerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerButton.swift; sourceTree = ""; }; DA4A891527C4B06500720909 /* GatewayCachedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GatewayCachedState.swift; sourceTree = ""; }; DA4A891727C4B0DF00720909 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -427,8 +423,6 @@ DA520ACD27D4C6AA009FD740 /* README.md */, DA4A88C727C1EB8F00720909 /* DiscordGateway.swift */, DA4A890227C3824800720909 /* GatewayIdentify.swift */, - DA4A890D27C4876900720909 /* GatewayHeartbeat.swift */, - DA4A890F27C48A7B00720909 /* GatewaySend.swift */, DA4A891527C4B06500720909 /* GatewayCachedState.swift */, DA57F4472806C4F5001DC46E /* RobustWebSocket.swift */, ); @@ -688,7 +682,6 @@ DA4A890527C383DD00720909 /* MessageDelete.swift in Sources */, DA32EF2627C62E6900A9ED72 /* MessageView.swift in Sources */, DA4A888A27C0AF3000720909 /* ContentView.swift in Sources */, - DA4A891027C48A7B00720909 /* GatewaySend.swift in Sources */, DA4A88F127C3543A00720909 /* Voice.swift in Sources */, DA4A88C827C1EB8F00720909 /* DiscordGateway.swift in Sources */, DA57F44628065209001DC46E /* ChannelButton.swift in Sources */, @@ -736,7 +729,6 @@ DA32EF3227C676FE00A9ED72 /* LottieLoopMode.swift in Sources */, DA4A88AE27C135F300720909 /* Member.swift in Sources */, DA4A88BA27C13BDF00720909 /* Emoji.swift in Sources */, - DA4A890E27C4876900720909 /* GatewayHeartbeat.swift in Sources */, DA32EF3427C6861800A9ED72 /* StickerView.swift in Sources */, DA4A891627C4B06500720909 /* GatewayCachedState.swift in Sources */, DA4A88C427C1442300720909 /* Sticker.swift in Sources */, diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index fbc55cde..1c4bfd9f 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -13,31 +13,10 @@ class DiscordGateway: ObservableObject { let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() let onAuthFailure = EventDispatch() - // Config - // let missedACKTolerance: Int - // let connTimeout: Double - // WebSocket object - // private(set) var socket: WebSocket! - // private(set) var session: URLSession! - // private(set) var socket: URLSessionWebSocketTask! private var socket: RobustWebSocket! - // State - @Published private(set) var isConnected = false - @Published private(set) var isReconnecting = false // Attempt to resume broken conn - @Published private(set) var doNotResume = false // Cannot resume - @Published private(set) var missedACK = 0 - @Published private(set) var seq: Int? = nil // Sequence int of latest received payload - @Published private(set) var viability = true - @Published private(set) var connTimes = 0 - /*private(set) var authFailed = false { - didSet { - if authFailed { onAuthFailure.notify() } - cache = CachedState() // Clear the cache - } - }*/ - @Published private(set) var sessionID: String? = nil + // State cache @Published var cache: CachedState = CachedState() private var evtListenerID: EventDispatch.HandlerIdentifier? = nil @@ -45,222 +24,6 @@ class DiscordGateway: ObservableObject { // Logger let log = Logger(tag: "DiscordGateway") - // Queues - /*let queue: DispatchQueue - let opQueue: OperationQueue - - func incMissedACK() { missedACK += 1 } - - func initWSConn() { - authFailed = false - - /*var request = URLRequest(url: URL(string: apiConfig.gateway)!) - request.timeoutInterval = connTimeout - socket = WebSocket(request: request) - socket.delegate = self - socket.callbackQueue = queue*/ - - socket = session.webSocketTask(with: URL(string: apiConfig.gateway)!) - socket.maximumMessageSize = 5243000 // (5MiB) Raise max incoming message size to avoid errors - - log.i("Attempting connection to Gateway: \(apiConfig.gateway)") - socket.resume() - - addReceiveListener() - - // If connection isn't connected after timeout, try again - let curConnCnt = connTimes - DispatchQueue.main.asyncAfter(deadline: .now() + connTimeout) { - if !self.isConnected && self.connTimes == curConnCnt { - self.log.w("Connection timed out, trying to reconnect") - self.isReconnecting = false - self.attemptReconnect() - } - } - } - - func addReceiveListener() { - socket.receive { [weak self] (result) in - switch result { - case .success(let response): - switch response { - case .data(let data): self?.didReceive(event: .binary(data)) - case .string(let message): self?.didReceive(event: .text(message)) - - @unknown default: break - } - case .failure(let error): self?.didReceive(event: .error(error)) - } - self?.addReceiveListener() - } - } - - // Attempt reconnection with resume after 1-5s as per spec - func attemptReconnect(resume: Bool = true, overrideViability: Bool = false) { - log.d("Resume called") - if authFailed { - log.e("Not reconnecting - auth failed") - return - } - // Kill connection if connection is still active - /*if isConnected { self.socket.forceDisconnect() } - guard viability || overrideViability, !isReconnecting else { return } - isReconnecting = true - if !resume { doNotResume = true } - let reconnectAfter = 1000 + Int(Double(4000) * Double.random(in: 0...1)) - log.i("Reconnecting in \(reconnectAfter)ms") - DispatchQueue.main.asyncAfter( - deadline: .now() + - .milliseconds(reconnectAfter) - ) { - self.log.d("Attempting reconnection now") - self.log.d("Can resume: \(!self.doNotResume)") - self.initWSConn() // Recreate WS object because sometimes it gets stuck in a "not gonna reconnect" state - }*/ - - - } - - // Log out the user - delete token from keychain and disconnect connection - func logOut() { - log.d("Logging out...") - let _ = Keychain.remove(key: "token") - // socket.disconnect(closeCode: 1000) - socket.cancel(with: .normalClosure, reason: nil) - authFailed = true - } - - init(connectionTimeout: Double = 5, maxMissedACK: Int = 3) { - missedACKTolerance = maxMissedACK - connTimeout = connectionTimeout - queue = DispatchQueue(label: "com.swiftcord.gatewayQueue", qos: .background, attributes: .concurrent, autoreleaseFrequency: .workItem, target: .global(qos: .background)) - opQueue = OperationQueue() - opQueue.qualityOfService = .utility - opQueue.underlyingQueue = queue - super.init() - session = URLSession(configuration: .default, delegate: self, delegateQueue: opQueue) - - initWSConn() - } - - - // MARK: Low level receive handler - func didReceive(event: WebSocketEvent) { - switch (event) { - case .connected(_): - log.i("Gateway Connected") - DispatchQueue.main.async { [weak self] in - self?.isReconnecting = false - self?.isConnected = true - self?.connTimes += 1 - } - onStateChange.notify(event: (isConnected, isReconnecting, nil)) - case .disconnected(_, let c): - isConnected = false - guard let code = GatewayCloseCode(rawValue: Int(c)) else { - log.e("Unknown close code: \(c)") - return - } - // Check if code isn't an unrecoverable code, then attempt resume - if code != .authenthicationFail { attemptReconnect() } - log.w("Gateway Disconnected: \(code)") - switch code { - case .authenthicationFail: authFailed = true - default: log.w("Unhandled gateway close code:", code) - } - onStateChange.notify(event: (isConnected, isReconnecting, code)) - case .text(let string): self.handleIncoming(received: string) - case .error(let error): - DispatchQueue.main.async { self.isConnected = false } - attemptReconnect() - onStateChange.notify(event: (isConnected, isReconnecting, nil)) - log.e("Connection error: \(String(describing: error))") - case .cancelled: - isConnected = false - onStateChange.notify(event: (isConnected, isReconnecting, nil)) - log.d("Connection cancelled") - case .binary(_): break // Won't receive binary - default: break - } - } - - func handleIncoming(received: String) { - guard let decoded = try? JSONDecoder().decode(GatewayIncoming.self, from: received.data(using: .utf8)!) - else { return } - - DispatchQueue.main.async { - if (decoded.s != nil) { self.seq = decoded.s } // Update sequence - } - - switch (decoded.op) { - case .heartbeat: - // Immediately send heartbeat as requested - log.d("Send heartbeat by server request") - sendToGateway(op: .heartbeat, d: GatewayHeartbeat()) - case .hello: - // Start heartbeating and send identify - guard let d = decoded.d as? GatewayHello else { return } - initHeartbeat(interval: d.heartbeat_interval) - - // Check if we're attempting to and can resume - if isReconnecting && !doNotResume && sessionID != nil && seq != nil { - log.i("Attempting resume") - guard let resume = getResume(seq: seq!, sessionID: sessionID!) - else { return } - sendToGateway(op: .resume, d: resume) - } - else { - log.d("Sending identify:", isConnected, !doNotResume, sessionID ?? "No sessionID", seq ?? -1) - // Send identify - DispatchQueue.main.async { - self.seq = nil // Clear sequence # - self.isReconnecting = false // Resuming failed/not attempted - } - guard let identify = getIdentify() else { - log.d("Token not in keychain") - authFailed = true - // socket.disconnect(closeCode: 1000) - return - } - sendToGateway(op: .identify, d: identify) - } - case .heartbeatAck: DispatchQueue.main.async { self.missedACK = 0 } - case .dispatchEvent: - guard let type = decoded.t else { return } - guard let data = decoded.d else { return } - switch (type) { - case .ready: - guard let d = data as? ReadyEvt else { return } - DispatchQueue.main.async { - self.doNotResume = false - self.sessionID = d.session_id - self.cache.guilds = d.guilds - self.cache.user = d.user - } - log.i("Gateway ready") - //onEvent.notify(event: (type, data)) - default: log.i("Dispatched event <\(type)>") - } - onEvent.notify(event: (type, data)) - case .invalidSession: - // Check if the session can be resumed - let shouldResume = (decoded.primitiveData as? Bool) ?? false - attemptReconnect(resume: shouldResume) - default: log.w("Unimplemented opcode: \(decoded.op)") - } - } - - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { - // didOpenConnection?() - didReceive(event: .connected([:])) - } - - func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { - // didCloseConnection?() - log.w("Session Gateway disconnected") - didReceive(event: .disconnected("", UInt16(closeCode.rawValue))) - }*/ - public func logout() { log.d("Logging out on request") let _ = Keychain.remove(key: "token") @@ -277,12 +40,9 @@ class DiscordGateway: ObservableObject { switch (type) { case .ready: guard let d = data as? ReadyEvt else { return } - //self.doNotResume = false - //self.sessionID = d.session_id self.cache.guilds = d.guilds self.cache.user = d.user log.i("Gateway ready") - //onEvent.notify(event: (type, data)) default: break } onEvent.notify(event: (type, data)) @@ -295,4 +55,10 @@ class DiscordGateway: ObservableObject { self?.handleEvt(type: t, data: d) } } + + deinit { + if let evtListenerID = evtListenerID { + let _ = socket.onEvent.removeHandler(handler: evtListenerID) + } + } } diff --git a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift b/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift deleted file mode 100644 index 5dada6d7..00000000 --- a/Swiftcord/DiscordAPI/Gateway/GatewayHeartbeat.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Heartbeat.swift -// Native Discord -// -// Created by Vincent Kwok on 22/2/22. -// - -import Foundation - -/*extension DiscordGateway { - func initHeartbeat(interval: Int) { - let initialConnTimes = connTimes - func sendHeartbeat() { - sendToGateway(op: .heartbeat, d: GatewayHeartbeat()) - log.d("Sent heartbeat, missed ACKs: \(missedACK)") - incMissedACK() - - // Connection is dead ☠️ - if (missedACK > 1) { - // socket.forceDisconnect() - socket.cancel() - attemptReconnect() - } - } - - // First heartbeat delayed by jitter interval as per Discord docs - let firstAfter = (Double(interval) * Double.random(in: 0...1)) / 1000 - log.i("Sending first heartbeat after \(firstAfter)s") - DispatchQueue.main.asyncAfter(deadline: .now() + firstAfter) { - sendHeartbeat() - - Timer.scheduledTimer(withTimeInterval: Double(interval) / Double(1000), repeats: true) { t in - // Do not continue sending heartbeats to a dead connection - // Also check that connection hasn't died between heartbeats - guard self.isConnected && self.connTimes == initialConnTimes else { - t.invalidate() - return - } - sendHeartbeat() - } - } - } -} -*/ diff --git a/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift b/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift index 95c05a86..ec2b95ab 100644 --- a/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift +++ b/Swiftcord/DiscordAPI/Gateway/GatewayIdentify.swift @@ -49,7 +49,7 @@ extension RobustWebSocket { large_threshold: nil, shard: nil, presence: nil, - capabilities: 253 + capabilities: 0b11111101 // TODO: Reverse engineer this ) } diff --git a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift b/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift deleted file mode 100644 index e6407fe6..00000000 --- a/Swiftcord/DiscordAPI/Gateway/GatewaySend.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// SendToGateway.swift -// Native Discord -// -// Created by Vincent Kwok on 22/2/22. -// - -import Foundation - -/*extension DiscordGateway { - func sendToGateway(op: GatewayOutgoingOpcodes, d: T?) { - guard isConnected else { return } - - let sendPayload = GatewayOutgoing(op: op, d: d, s: seq) - guard let encoded = try? JSONEncoder().encode(sendPayload) - else { return } - - log.d("Outgoing Payload: <\(op)>", sendPayload.d != nil ? String(describing: sendPayload.d!) : "[No data]", "Seq:", String(describing: sendPayload.s)) - // socket.write(string: String(data: encoded, encoding: .utf8)!) - socket.send(.data(encoded)) { err in - self.log.i("Socket send completed") - if let err = err { - self.log.e("Socket send error:", err.localizedDescription) - } - } - } -}*/ diff --git a/Swiftcord/Views/Settings/MiscSettingsView.swift b/Swiftcord/Views/Settings/MiscSettingsView.swift index ca80af43..7b9b859b 100644 --- a/Swiftcord/Views/Settings/MiscSettingsView.swift +++ b/Swiftcord/Views/Settings/MiscSettingsView.swift @@ -20,14 +20,14 @@ struct MiscSettingsView: View { var body: some View { let debugValues = [ - DebugTableItem(item: "Gateway connected", val: gateway.isConnected.toString()), + /*DebugTableItem(item: "Gateway connected", val: gateway.isConnected.toString()), DebugTableItem(item: "Gateway reconnecting", val: gateway.isReconnecting.toString()), DebugTableItem(item: "Gateway cannot resume", val: gateway.doNotResume.toString()), DebugTableItem(item: "Gateway sequence #", val: String(gateway.seq ?? 0)), // DebugTableItem(item: "Gateway viability", val: gateway.viability.toString()), DebugTableItem(item: "Gateway connection #", val: String(gateway.connTimes)), DebugTableItem(item: "Gateway session ID", val: gateway.sessionID ?? "nil"), - DebugTableItem(item: "Gateway missed heartbeat ACKs", val: String(gateway.missedACK)), + DebugTableItem(item: "Gateway missed heartbeat ACKs", val: String(gateway.missedACK)),*/ DebugTableItem(item: "Loading stage", val: String(describing: state.loadingState)), ] From b438e481be3f13277fea453000ee279d3c3930bf Mon Sep 17 00:00:00 2001 From: vinkwok Date: Fri, 15 Apr 2022 12:05:02 +0800 Subject: [PATCH 08/10] Fixed logging out while Gateway is connected Converted RobustWebSocket and Gateway to use OSLog for logging --- Swiftcord.xcodeproj/project.pbxproj | 12 ++- Swiftcord/ContentView.swift | 2 +- .../DiscordAPI/Gateway/DiscordGateway.swift | 19 +++-- .../DiscordAPI/Gateway/RobustWebSocket.swift | 74 ++++++++++--------- Swiftcord/DiscordAPI/REST/DiscordAPI.swift | 2 +- .../{Logger.swift => CustomLogger.swift} | 2 +- Swiftcord/Utils/LoggerInit.swift | 15 ++++ Swiftcord/Utils/WebView.swift | 2 +- 8 files changed, 80 insertions(+), 48 deletions(-) rename Swiftcord/Utils/{Logger.swift => CustomLogger.swift} (98%) create mode 100644 Swiftcord/Utils/LoggerInit.swift diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index a9ced9fa..3d0ab2cc 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -14,8 +14,9 @@ DA2384C227CCFAEC009E15E0 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = DA2384C127CCFAEC009E15E0 /* CachedAsyncImage */; }; DA2384C427CCFEF3009E15E0 /* MessageReadAck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */; }; DA2384C627CD1921009E15E0 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C527CD1921009E15E0 /* LoadingView.swift */; }; - DA2384C827CD9418009E15E0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C727CD9418009E15E0 /* Logger.swift */; }; + DA2384C827CD9418009E15E0 /* CustomLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C727CD9418009E15E0 /* CustomLogger.swift */; }; DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2802782808337B00B14E5C /* AppDelegate.swift */; }; + DA28027B280912BF00B14E5C /* LoggerInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA28027A280912BF00B14E5C /* LoggerInit.swift */; }; DA32EF2427C6249000A9ED72 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2327C6249000A9ED72 /* MessagesView.swift */; }; DA32EF2627C62E6900A9ED72 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2527C62E6900A9ED72 /* MessageView.swift */; }; DA32EF2827C633FE00A9ED72 /* UserAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */; }; @@ -129,9 +130,10 @@ DA2384BE27CCBB26009E15E0 /* EmbedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbedView.swift; sourceTree = ""; }; DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReadAck.swift; sourceTree = ""; }; DA2384C527CD1921009E15E0 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - DA2384C727CD9418009E15E0 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + DA2384C727CD9418009E15E0 /* CustomLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLogger.swift; sourceTree = ""; }; DA2802772808337800B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = "../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/AppDelegate.swift"; sourceTree = ""; }; DA2802782808337B00B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DA28027A280912BF00B14E5C /* LoggerInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoggerInit.swift; path = "../../../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/Swiftcord/Utils/LoggerInit.swift"; sourceTree = ""; }; DA32EF2327C6249000A9ED72 /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; DA32EF2527C62E6900A9ED72 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatarView.swift; sourceTree = ""; }; @@ -462,8 +464,9 @@ DA4A889B27C0B23C00720909 /* WebView.swift */, DA4A88DD27C3273C00720909 /* Keychain.swift */, DA4A88E027C3341400720909 /* EventDispatch.swift */, - DA2384C727CD9418009E15E0 /* Logger.swift */, + DA2384C727CD9418009E15E0 /* CustomLogger.swift */, DA520AE627D8ECA6009FD740 /* DecodableThrowable.swift */, + DA28027A280912BF00B14E5C /* LoggerInit.swift */, ); path = Utils; sourceTree = ""; @@ -675,6 +678,7 @@ DA4A88F927C35D5000720909 /* GuildMemberEvt.swift in Sources */, DA32EF3F27C7C1D000A9ED72 /* MessageInputView.swift in Sources */, DA520AD327D4D073009FD740 /* AppSettingsView.swift in Sources */, + DA28027B280912BF00B14E5C /* LoggerInit.swift in Sources */, DA4A889C27C0B23C00720909 /* WebView.swift in Sources */, DA4A88E527C3482A00720909 /* LoginView.swift in Sources */, DA32EF5927CB5F4F00A9ED72 /* UIStateEnv.swift in Sources */, @@ -686,7 +690,7 @@ DA4A88C827C1EB8F00720909 /* DiscordGateway.swift in Sources */, DA57F44628065209001DC46E /* ChannelButton.swift in Sources */, DA32EF6627CB772300A9ED72 /* CacheModel.xcdatamodeld in Sources */, - DA2384C827CD9418009E15E0 /* Logger.swift in Sources */, + DA2384C827CD9418009E15E0 /* CustomLogger.swift in Sources */, DA520AE227D76BEB009FD740 /* BoolToString.swift in Sources */, DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */, DA4A88DE27C3273C00720909 /* Keychain.swift in Sources */, diff --git a/Swiftcord/ContentView.swift b/Swiftcord/ContentView.swift index 8bcc72cb..f532694b 100644 --- a/Swiftcord/ContentView.swift +++ b/Swiftcord/ContentView.swift @@ -32,7 +32,7 @@ struct ContentView: View { @EnvironmentObject var gateway: DiscordGateway @EnvironmentObject var state: UIState - let log = Logger(tag: "ContentView") + let log = CustomLogger(tag: "ContentView") var body: some View { HStack(spacing: 0) { diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index 1c4bfd9f..50a94db4 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -6,6 +6,7 @@ // import Foundation +import os class DiscordGateway: ObservableObject { // Events @@ -19,17 +20,19 @@ class DiscordGateway: ObservableObject { // State cache @Published var cache: CachedState = CachedState() - private var evtListenerID: EventDispatch.HandlerIdentifier? = nil + private var evtListenerID: EventDispatch.HandlerIdentifier? = nil, + authFailureListenerID: EventDispatch.HandlerIdentifier? = nil // Logger - let log = Logger(tag: "DiscordGateway") + private let log = Logger(category: "DiscordGateway") public func logout() { - log.d("Logging out on request") + log.debug("Logging out on request") let _ = Keychain.remove(key: "token") // socket.disconnect(closeCode: 1000) socket.close(code: .normalClosure) // authFailed = true + onAuthFailure.notify() } public func connect() { @@ -42,11 +45,11 @@ class DiscordGateway: ObservableObject { guard let d = data as? ReadyEvt else { return } self.cache.guilds = d.guilds self.cache.user = d.user - log.i("Gateway ready") + log.info("Gateway ready") default: break } onEvent.notify(event: (type, data)) - log.i("Dispatched event <\(type)>") + log.info("Dispatched event <\(type.rawValue, privacy: .public)>") } init(connectionTimeout: Double = 5, maxMissedACK: Int = 3) { @@ -54,11 +57,17 @@ class DiscordGateway: ObservableObject { evtListenerID = socket.onEvent.addHandler { [weak self] (t, d) in self?.handleEvt(type: t, data: d) } + authFailureListenerID = socket.onAuthFailure.addHandler(handler: { [weak self] in + self?.onAuthFailure.notify() + }) } deinit { if let evtListenerID = evtListenerID { let _ = socket.onEvent.removeHandler(handler: evtListenerID) } + if let authFailureListenerID = authFailureListenerID { + let _ = socket.onAuthFailure.removeHandler(handler: authFailureListenerID) + } } } diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift index de25bc51..53c1793b 100644 --- a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -7,15 +7,17 @@ import Foundation import Reachability +import OSLog /// A robust WebSocket that handles resuming, reconnection and heartbeats /// with the Discord Gateway, inspired by robust-websocket class RobustWebSocket: NSObject { - public let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() + public let onEvent = EventDispatch<(GatewayEvent, GatewayData)>(), + onAuthFailure = EventDispatch() private var session: URLSession!, socket: URLSessionWebSocketTask! - private let reachability = try! Reachability(), log = Logger(tag: "RobustWebSocket") + private let reachability = try! Reachability(), log = Logger(category: "RobustWebSocket") private let queue: OperationQueue @@ -36,34 +38,34 @@ class RobustWebSocket: NSObject { // MARK: - (Re)Connection private func reconnect(code: URLSessionWebSocketTask.CloseCode?) { guard !explicitlyClosed else { - log.w("Not reconnecting: connection was explicitly closed") + log.warning("Not reconnecting: connection was explicitly closed") attempts = 0 return } guard reachable else { - log.w("Not reconnecting: connection is unreachable") + log.warning("Not reconnecting: connection is unreachable") reconnectWhenOnlineAgain = true return } guard connTimeout == nil else { - log.w("Not reconnecting: already attempting a connection") + log.warning("Not reconnecting: already attempting a connection") return } let delay = reconnectInterval(code, attempts) if let delay = delay { - log.i("Reconnecting after \(delay)s...") + log.info("Reconnecting after \(delay)s...") DispatchQueue.main.async { [weak self] in self?.pendingReconnect = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in guard self?.connected != true else { - self?.log.w("Looks like we're already connected, no need to reconnect") + self?.log.warning("Looks like we're already connected, no need to reconnect") return } guard self?.connTimeout == nil else { - self?.log.w("Not reconnecting: already attempting a connection") + self?.log.warning("Not reconnecting: already attempting a connection") return } - self?.log.d("Attempting reconnection now") + self?.log.debug("Attempting reconnection now") self?.connect() } } @@ -77,15 +79,14 @@ class RobustWebSocket: NSObject { switch (message) { case .data(_): break case .string(let str): self?.handleMessage(message: str) - @unknown default: self?.log.w("Unknown sock message case!") + @unknown default: self?.log.warning("Unknown sock message case!") } // self?.onMessage.notify(event: message) self?.attachSockReceiveListener() case .failure(let error): // If an error is encountered here, the connection is probably broken - self?.log.e("Error when receiving: \(error)") + self?.log.error("Error when receiving: \(error.localizedDescription, privacy: .public)") self?.forceClose() - return } } } @@ -101,7 +102,7 @@ class RobustWebSocket: NSObject { self?.connTimeout = Timer.scheduledTimer(withTimeInterval: self!.timeout, repeats: false) { [weak self] _ in self?.connTimeout = nil // reachability.stopNotifier() - self?.log.w("Connection timed out after \(self!.timeout)s") + self?.log.warning("Connection timed out after \(self!.timeout)s") self?.forceClose() } } @@ -122,31 +123,34 @@ class RobustWebSocket: NSObject { switch(decoded.op) { case .heartbeat: - log.d("Sending expedited heartbeat as requested") + log.debug("Sending expedited heartbeat as requested") send(op: .heartbeat, data: GatewayHeartbeat()) case .heartbeatAck: awaitingHb -= 1 case .hello: // Start heartbeating and send identify guard let d = decoded.d as? GatewayHello else { return } - log.d("Hello payload is:", String(describing: d)) + log.debug("Hello payload is: \(String(describing: d), privacy: .public)") startHeartbeating(interval: Double(d.heartbeat_interval) / 1000.0) // Check if we're attempting to and can resume if canResume, let sessionID = sessionID, let seq = seq { - log.i("Attempting resume") + log.info("Attempting resume") guard let resume = getResume(seq: seq, sessionID: sessionID) else { return } send(op: .resume, data: resume) return } - log.d("Sending identify") + log.debug("Identifying with gateway...") // Send identify seq = nil // Clear sequence # // isReconnecting = false // Resuming failed/not attempted guard let identify = getIdentify() else { - log.d("Token not in keychain") + log.debug("Token not in keychain") + Logger().debug("Hello there, \("safljslaf", privacy: .private(mask: .hash))") // authFailed = true // socket.disconnect(closeCode: 1000) + close(code: .normalClosure) + onAuthFailure.notify() return } send(op: .identify, data: identify) @@ -154,16 +158,16 @@ class RobustWebSocket: NSObject { // Check if the session can be resumed let shouldResume = (decoded.primitiveData as? Bool) ?? false if !shouldResume { - log.w("Session is invalid, reconnecting without resuming") + log.warning("Session is invalid, reconnecting without resuming") canResume = false } /// Close the connection immediately and reconnect after 1-5s, as per Discord docs /// Unfortunately Discord seems to reject the new identify no matter how long I - /// wait before sending it, so there will always be at least 2 identify attempts before + /// wait before sending it, so sometimes there will be 2 identify attempts before /// the Gateway session is reestablished close(code: .normalClosure) DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 1...5)) { [weak self] in - self?.log.d("Attempting to reconnect now") + self?.log.debug("Attempting to reconnect now") self?.open() } // attemptReconnect(resume: shouldResume) @@ -179,7 +183,7 @@ class RobustWebSocket: NSObject { } onEvent.notify(event: (type, data)) case .reconnect: - log.w("Gateway-requested reconnect: disconnecting and reconnecting immediately") + log.warning("Gateway-requested reconnect: disconnecting and reconnecting immediately") close(code: .goingAway) } } @@ -217,7 +221,7 @@ extension RobustWebSocket: URLSessionWebSocketDelegate { reconnectWhenOnlineAgain = true attempts = 0 connected = true - log.i("Socket connection opened") + log.info("Socket connection opened") } func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { @@ -227,7 +231,7 @@ extension RobustWebSocket: URLSessionWebSocketDelegate { // didCloseConnection?() // didReceive(event: .disconnected("", UInt16(closeCode.rawValue))) reconnectWhenOnlineAgain = false - log.i("Socket connection closed") + log.info("Socket connection closed") } } @@ -237,7 +241,7 @@ extension RobustWebSocket { private func setupReachability() { reachability.whenReachable = { [weak self] _ in self?.reachable = true - self?.log.i("Connection reachable") + self?.log.info("Connection reachable") //if let reconnect = self?.reconnectWhenOnlineAgain, reconnect { // Temporarily ignore reconnectWhenOnlineAgain since that was causing issues self?.clearPendingReconnectIfNeeded() @@ -246,11 +250,11 @@ extension RobustWebSocket { } reachability.whenUnreachable = { [weak self] _ in self?.reachable = false - self?.log.i("Connection unreachable") + self?.log.info("Connection unreachable") self?.forceClose() } do { try reachability.startNotifier() } - catch { log.e("Starting reachability notifier failed!") } + catch { log.error("Starting reachability notifier failed!") } } } @@ -260,14 +264,14 @@ extension RobustWebSocket { @objc private func sendHeartbeat() { guard connected else { // Obviously, a dead connection will not respond to heartbeats - log.w("Socket is not connected, cancelling heartbeat timer") + log.warning("Socket is not connected, cancelling heartbeat timer") hbTimer?.invalidate() return } - log.d("Sending heartbeat, awaiting \(awaitingHb) ACKs") + log.debug("Sending heartbeat, awaiting \(self.awaitingHb) ACKs") if awaitingHb > 1 { - log.e("Too many pending heartbeats, closing socket") + log.error("Too many pending heartbeats, closing socket") forceClose() } send(op: .heartbeat, data: GatewayHeartbeat()) @@ -276,7 +280,7 @@ extension RobustWebSocket { private func startHeartbeating(interval: TimeInterval) { if hbTimer != nil { stopHeartbeating() } - log.d("Sending heartbeats every \(interval)s") + log.debug("Sending heartbeats every \(interval)s") awaitingHb = 0 // First heartbeat after interval * jitter where jitter is a value from 0-1 @@ -294,7 +298,7 @@ extension RobustWebSocket { } private func stopHeartbeating() { if let heartbeatTimer = hbTimer { - log.d("Stopping heartbeat timer") + log.debug("Stopping heartbeat timer") heartbeatTimer.invalidate() hbTimer = nil } @@ -308,7 +312,7 @@ extension RobustWebSocket { code: URLSessionWebSocketTask.CloseCode = .abnormalClosure, shouldReconnect: Bool = true ) { - log.w("Forcibly closing connection") + log.warning("Forcibly closing connection") stopHeartbeating() self.socket.cancel(with: code, reason: nil) connected = false @@ -346,10 +350,10 @@ extension RobustWebSocket { guard let encoded = try? JSONEncoder().encode(sendPayload) else { return } - log.d("Outgoing Payload: <\(op)>", sendPayload.d != nil ? String(describing: sendPayload.d!) : "[No data]", "Seq:", String(describing: sendPayload.s)) + log.debug("Outgoing Payload: <\(op.rawValue, privacy: .public)> \(String(describing: data), privacy: .sensitive(mask: .hash)) [seq: \(String(describing: self.seq), privacy: .public)]") // socket.write(string: String(data: encoded, encoding: .utf8)!) socket.send(.data(encoded), completionHandler: completionHandler ?? { [weak self] err in - if let err = err { self?.log.e("Socket send error:", err.localizedDescription) } + if let err = err { self?.log.error("Socket send error: \(err.localizedDescription, privacy: .public)") } }) } } diff --git a/Swiftcord/DiscordAPI/REST/DiscordAPI.swift b/Swiftcord/DiscordAPI/REST/DiscordAPI.swift index 8f84d331..b411e70d 100644 --- a/Swiftcord/DiscordAPI/REST/DiscordAPI.swift +++ b/Swiftcord/DiscordAPI/REST/DiscordAPI.swift @@ -8,6 +8,6 @@ import Foundation struct DiscordAPI { - static let log = Logger(tag: "DiscordREST") + static let log = CustomLogger(tag: "DiscordREST") // How empty, everything is broken into smaller files (for now xD) } diff --git a/Swiftcord/Utils/Logger.swift b/Swiftcord/Utils/CustomLogger.swift similarity index 98% rename from Swiftcord/Utils/Logger.swift rename to Swiftcord/Utils/CustomLogger.swift index 2cfb0cd3..7b66da28 100644 --- a/Swiftcord/Utils/Logger.swift +++ b/Swiftcord/Utils/CustomLogger.swift @@ -16,7 +16,7 @@ enum LogLevel { // In increasing levels of importance case crit // Unrecoverable errors } -class Logger { +class CustomLogger { let tag: String init(tag: String) { diff --git a/Swiftcord/Utils/LoggerInit.swift b/Swiftcord/Utils/LoggerInit.swift new file mode 100644 index 00000000..720cfd68 --- /dev/null +++ b/Swiftcord/Utils/LoggerInit.swift @@ -0,0 +1,15 @@ +// +// GetLoggerInstance.swift +// Swiftcord +// +// Created by Vincent on 4/15/22. +// + +import Foundation +import os + +extension Logger { + init(category: String) { + self.init(subsystem: Bundle.main.bundleIdentifier ?? "", category: category) + } +} diff --git a/Swiftcord/Utils/WebView.swift b/Swiftcord/Utils/WebView.swift index 1364f4d1..a4ca0d15 100644 --- a/Swiftcord/Utils/WebView.swift +++ b/Swiftcord/Utils/WebView.swift @@ -125,7 +125,7 @@ struct WebView: NSViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { private var viewModel: WebViewModel - private let log = Logger(tag: "WebViewCoordinator") + private let log = CustomLogger(tag: "WebViewCoordinator") init(_ viewModel: WebViewModel) { // Initialise the WebViewModel From 60c91434d52f022690bf01d5d928a6114ec61631 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Fri, 15 Apr 2022 13:29:19 +0800 Subject: [PATCH 09/10] Completely remove custom logger, use OSLog instead --- Swiftcord.xcodeproj/project.pbxproj | 4 -- Swiftcord/ContentView.swift | 11 ++---- .../DiscordAPI/Gateway/DiscordGateway.swift | 1 - .../DiscordAPI/Gateway/RobustWebSocket.swift | 7 +++- .../DiscordAPI/Objects/Gateway/Gateway.swift | 29 ++++++-------- Swiftcord/DiscordAPI/REST/APIRequest.swift | 9 +++-- Swiftcord/DiscordAPI/REST/DiscordAPI.swift | 3 +- Swiftcord/Utils/CustomLogger.swift | 39 ------------------- Swiftcord/Utils/WebView.swift | 7 ++-- 9 files changed, 32 insertions(+), 78 deletions(-) delete mode 100644 Swiftcord/Utils/CustomLogger.swift diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 3d0ab2cc..863e4915 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ DA2384C227CCFAEC009E15E0 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = DA2384C127CCFAEC009E15E0 /* CachedAsyncImage */; }; DA2384C427CCFEF3009E15E0 /* MessageReadAck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */; }; DA2384C627CD1921009E15E0 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C527CD1921009E15E0 /* LoadingView.swift */; }; - DA2384C827CD9418009E15E0 /* CustomLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C727CD9418009E15E0 /* CustomLogger.swift */; }; DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2802782808337B00B14E5C /* AppDelegate.swift */; }; DA28027B280912BF00B14E5C /* LoggerInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA28027A280912BF00B14E5C /* LoggerInit.swift */; }; DA32EF2427C6249000A9ED72 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2327C6249000A9ED72 /* MessagesView.swift */; }; @@ -130,7 +129,6 @@ DA2384BE27CCBB26009E15E0 /* EmbedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbedView.swift; sourceTree = ""; }; DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReadAck.swift; sourceTree = ""; }; DA2384C527CD1921009E15E0 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - DA2384C727CD9418009E15E0 /* CustomLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLogger.swift; sourceTree = ""; }; DA2802772808337800B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = "../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/AppDelegate.swift"; sourceTree = ""; }; DA2802782808337B00B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DA28027A280912BF00B14E5C /* LoggerInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoggerInit.swift; path = "../../../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/Swiftcord/Utils/LoggerInit.swift"; sourceTree = ""; }; @@ -464,7 +462,6 @@ DA4A889B27C0B23C00720909 /* WebView.swift */, DA4A88DD27C3273C00720909 /* Keychain.swift */, DA4A88E027C3341400720909 /* EventDispatch.swift */, - DA2384C727CD9418009E15E0 /* CustomLogger.swift */, DA520AE627D8ECA6009FD740 /* DecodableThrowable.swift */, DA28027A280912BF00B14E5C /* LoggerInit.swift */, ); @@ -690,7 +687,6 @@ DA4A88C827C1EB8F00720909 /* DiscordGateway.swift in Sources */, DA57F44628065209001DC46E /* ChannelButton.swift in Sources */, DA32EF6627CB772300A9ED72 /* CacheModel.xcdatamodeld in Sources */, - DA2384C827CD9418009E15E0 /* CustomLogger.swift in Sources */, DA520AE227D76BEB009FD740 /* BoolToString.swift in Sources */, DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */, DA4A88DE27C3273C00720909 /* Keychain.swift in Sources */, diff --git a/Swiftcord/ContentView.swift b/Swiftcord/ContentView.swift index f532694b..7beb97ee 100644 --- a/Swiftcord/ContentView.swift +++ b/Swiftcord/ContentView.swift @@ -7,6 +7,7 @@ import SwiftUI import CoreData +import os struct CustomHorizontalDivider: View { var body: some View { @@ -32,7 +33,7 @@ struct ContentView: View { @EnvironmentObject var gateway: DiscordGateway @EnvironmentObject var state: UIState - let log = CustomLogger(tag: "ContentView") + private let log = Logger(category: "ContentView") var body: some View { HStack(spacing: 0) { @@ -125,18 +126,14 @@ struct ContentView: View { } }) .onAppear { - let _ = gateway.onStateChange.addHandler { (connected, resuming, error) in - log.d("Connection state change: \(connected), \(resuming)") - } let _ = gateway.onAuthFailure.addHandler { state.attemptLogin = true state.loadingState = .initial - log.d("User isn't logged in, attempting login") + log.debug("User isn't logged in, attempting login") } let _ = gateway.onEvent.addHandler { (evt, d) in switch evt { - case .ready: - state.loadingState = .gatewayConn + case .ready: state.loadingState = .gatewayConn default: break } } diff --git a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift index 50a94db4..1d054a84 100644 --- a/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift +++ b/Swiftcord/DiscordAPI/Gateway/DiscordGateway.swift @@ -10,7 +10,6 @@ import os class DiscordGateway: ObservableObject { // Events - let onStateChange = EventDispatch<(Bool, Bool, GatewayCloseCode?)>() let onEvent = EventDispatch<(GatewayEvent, GatewayData)>() let onAuthFailure = EventDispatch() diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift index 53c1793b..1823719c 100644 --- a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -173,7 +173,10 @@ class RobustWebSocket: NSObject { // attemptReconnect(resume: shouldResume) case .dispatchEvent: guard let type = decoded.t else { return } - guard let data = decoded.d else { return } + guard let data = decoded.d else { + log.warning("Event type <\(type.rawValue, privacy: .public)> has nil data") + return + } switch type { case .ready: guard let d = data as? ReadyEvt else { return } @@ -350,7 +353,7 @@ extension RobustWebSocket { guard let encoded = try? JSONEncoder().encode(sendPayload) else { return } - log.debug("Outgoing Payload: <\(op.rawValue, privacy: .public)> \(String(describing: data), privacy: .sensitive(mask: .hash)) [seq: \(String(describing: self.seq), privacy: .public)]") + log.debug("Outgoing Payload: <\(String(describing: op), privacy: .public)> \(String(describing: data), privacy: .sensitive(mask: .hash)) [seq: \(String(describing: self.seq), privacy: .public)]") // socket.write(string: String(data: encoded, encoding: .utf8)!) socket.send(.data(encoded), completionHandler: completionHandler ?? { [weak self] err in if let err = err { self?.log.error("Socket send error: \(err.localizedDescription, privacy: .public)") } diff --git a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift index 3fef5b67..3ea20215 100644 --- a/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift +++ b/Swiftcord/DiscordAPI/Objects/Gateway/Gateway.swift @@ -79,21 +79,17 @@ struct GatewayIncoming: Decodable { switch t { case .ready: d = try values.decode(ReadyEvt.self, forKey: .d) case .resumed: d = nil - case .channelCreate: fallthrough - case .channelUpdate: fallthrough - case .channelDelete: fallthrough - case .threadCreate: fallthrough - case .threadUpdate: fallthrough - case .threadDelete: d = try values.decode(Channel.self, forKey: .d) + case .channelCreate, .channelUpdate, .channelDelete, .threadCreate, .threadUpdate, .threadDelete: + d = try values.decode(Channel.self, forKey: .d) + case .channelPinUpdate: d = try values.decode(ChannelPinsUpdate.self, forKey: .d) + case .threadListSync: d = try values.decode(ThreadListSync.self, forKey: .d) case .threadMemberUpdate: d = try values.decode(ThreadMember.self, forKey: .d) case .threadMembersUpdate: d = try values.decode(ThreadMembersUpdate.self, forKey: .d) - case .channelPinUpdate: d = try values.decode(ChannelPinsUpdate.self, forKey: .d) - case .guildCreate: fallthrough - case .guildUpdate: d = try values.decode(Guild.self, forKey: .d) + + case .guildUpdate, .guildCreate: d = try values.decode(Guild.self, forKey: .d) case .guildDelete: d = try values.decode(GuildUnavailable.self, forKey: .d) - case .guildBanAdd: fallthrough - case .guildBanRemove: d = try values.decode(GuildBan.self, forKey: .d) + case .guildBanAdd, .guildBanRemove: d = try values.decode(GuildBan.self, forKey: .d) case .guildEmojisUpdate: d = try values.decode(GuildEmojisUpdate.self, forKey: .d) case .guildStickersUpdate: d = try values.decode(GuildStickersUpdate.self, forKey: .d) case .guildIntegrationsUpdate: d = try values.decode(GuildIntegrationsUpdate.self, forKey: .d) @@ -103,18 +99,17 @@ struct GatewayIncoming: Decodable { case .guildRoleCreate: d = try values.decode(GuildRoleEvt.self, forKey: .d) case .guildRoleUpdate: d = try values.decode(GuildRoleEvt.self, forKey: .d) case .guildRoleDelete: d = try values.decode(GuildRoleDelete.self, forKey: .d) - case .guildSchEvtCreate: fallthrough - case .guildSchEvtUpdate: fallthrough - case .guildSchEvtDelete: d = try values.decode(GuildScheduledEvent.self, forKey: .d) - case .guildSchEvtUserAdd: fallthrough - case .guildSchEvtUserRemove: d = try values.decode(GuildSchEvtUserEvt.self, forKey: .d) + case .guildSchEvtCreate, .guildSchEvtUpdate, .guildSchEvtDelete: d = try values.decode(GuildScheduledEvent.self, forKey: .d) + case .guildSchEvtUserAdd, .guildSchEvtUserRemove: d = try values.decode(GuildSchEvtUserEvt.self, forKey: .d) + // TODO: More events go here case .messageCreate: d = try values.decode(Message.self, forKey: .d) case .messageUpdate: d = try values.decode(PartialMessage.self, forKey: .d) case .messageDelete: d = try values.decode(MessageDelete.self, forKey: .d) case .messageDeleteBulk: d = try values.decode(MessageDeleteBulk.self, forKey: .d) - case .presenceUpdate: d = try values.decode(PartialPresenceUpdate .self, forKey: .d) + case .presenceUpdate: d = try values.decode(PartialPresenceUpdate.self, forKey: .d) // TODO: Add the remaining like 100 events + default: break } default: diff --git a/Swiftcord/DiscordAPI/REST/APIRequest.swift b/Swiftcord/DiscordAPI/REST/APIRequest.swift index 4edb13b3..d307b33f 100644 --- a/Swiftcord/DiscordAPI/REST/APIRequest.swift +++ b/Swiftcord/DiscordAPI/REST/APIRequest.swift @@ -23,7 +23,7 @@ extension DiscordAPI { body: String? = nil, method: RequestMethod = .get ) async throws -> Data? { - DiscordAPI.log.d("\(method.rawValue): \(path)") + DiscordAPI.log.debug("\(method.rawValue): \(path)") guard let token = Keychain.load(key: "token") else { return nil } guard var apiURL = URL(string: apiConfig.restBase) else { return nil } @@ -48,8 +48,8 @@ extension DiscordAPI { let (data, response) = try await URLSession.shared.data(for: req) guard let httpResponse = response as? HTTPURLResponse else { return nil } guard httpResponse.statusCode / 100 == 2 else { // Check if status code is 2** - print("Status code is not 2xx: \(httpResponse.statusCode)") - print(String(decoding: data, as: UTF8.self)) + log.warning("Status code is not 2xx: \(httpResponse.statusCode, privacy: .public)") + log.warning("Response: \(String(decoding: data, as: UTF8.self), privacy: .public)") return nil } @@ -61,13 +61,14 @@ extension DiscordAPI { path: String, query: [URLQueryItem] = [] ) async -> T? { + // This helps debug JSON decoding errors do { guard let d = try? await makeRequest(path: path, query: query) else { return nil } return try JSONDecoder().decode(T.self, from: d) } catch let DecodingError.dataCorrupted(context) { - print(context) + print(context) } catch let DecodingError.keyNotFound(key, context) { print("Key '\(key)' not found:", context.debugDescription) print("codingPath:", context.codingPath) diff --git a/Swiftcord/DiscordAPI/REST/DiscordAPI.swift b/Swiftcord/DiscordAPI/REST/DiscordAPI.swift index b411e70d..f4b17ee4 100644 --- a/Swiftcord/DiscordAPI/REST/DiscordAPI.swift +++ b/Swiftcord/DiscordAPI/REST/DiscordAPI.swift @@ -6,8 +6,9 @@ // import Foundation +import os struct DiscordAPI { - static let log = CustomLogger(tag: "DiscordREST") + static let log = Logger(category: "DiscordREST") // How empty, everything is broken into smaller files (for now xD) } diff --git a/Swiftcord/Utils/CustomLogger.swift b/Swiftcord/Utils/CustomLogger.swift deleted file mode 100644 index 7b66da28..00000000 --- a/Swiftcord/Utils/CustomLogger.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Logger.swift -// Swiftcord -// -// Created by Vincent Kwok on 1/3/22. -// - -import Foundation - -enum LogLevel { // In increasing levels of importance - case silly // Very verbose debug messages - case debug // Stuff for when you're sorting out an issue - case info // Typical log messages - case warn // Not an error, but something that shouldn't happen - case error // Non-fatal errors - case crit // Unrecoverable errors -} - -class CustomLogger { - let tag: String - - init(tag: String) { - self.tag = tag - } - - private func log(level: LogLevel, _ items: [Any]) { -#if DEBUG - let s = items.map { String(describing: $0) }.joined(separator: " ") - print("<\(String(describing: level).first?.uppercased() ?? "D")> [\(tag)] \(s)") -#endif - } - - public func s(_ items: Any...) { log(level: .silly, items) } - public func d(_ items: Any...) { log(level: .debug, items) } - public func i(_ items: Any...) { log(level: .info, items) } - public func w(_ items: Any...) { log(level: .warn, items) } - public func e(_ items: Any...) { log(level: .error, items) } - public func c(_ items: Any...) { log(level: .crit, items) } -} diff --git a/Swiftcord/Utils/WebView.swift b/Swiftcord/Utils/WebView.swift index a4ca0d15..10cde0e2 100644 --- a/Swiftcord/Utils/WebView.swift +++ b/Swiftcord/Utils/WebView.swift @@ -7,6 +7,7 @@ import SwiftUI import WebKit +import os // MARK: Adapted from: https://stackoverflow.com/a/63055549/13409955 @@ -125,7 +126,7 @@ struct WebView: NSViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { private var viewModel: WebViewModel - private let log = CustomLogger(tag: "WebViewCoordinator") + private let log = Logger(category: "WebViewCoordinator") init(_ viewModel: WebViewModel) { // Initialise the WebViewModel @@ -141,7 +142,7 @@ struct WebView: NSViewRepresentable { self.viewModel.pageTitle = web.title! self.viewModel.link = web.url?.absoluteString ?? "" self.viewModel.didFinishLoading = true - log.i("didFinishNavigation") + log.info("didFinishNavigation") } public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { } @@ -149,7 +150,7 @@ struct WebView: NSViewRepresentable { public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { let decision: WKNavigationActionPolicy = navigationAction.request.url?.absoluteString == viewModel.link || navigationAction.request.url?.host == "newassets.hcaptcha.com" ? .allow : .cancel decisionHandler(decision) - log.d("Navigation to", String(describing: navigationAction.request.url?.absoluteString), decision == .allow ? "allowed" : "cancelled") + log.debug("Navigation to \(navigationAction.request.url?.absoluteString ?? "[unknown URL]", privacy: .public) \(decision == .allow ? "allowed" : "cancelled", privacy: .public)") } func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { } From 555849de4bac793e9854bb6afcd24739393d4244 Mon Sep 17 00:00:00 2001 From: vinkwok Date: Fri, 15 Apr 2022 16:48:39 +0800 Subject: [PATCH 10/10] Delete duplicate AppDelegate and move LoggerInit to the Extensions folder --- AppDelegate.swift | 8 -------- Swiftcord.xcodeproj/project.pbxproj | 14 ++++++++------ Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift | 1 + .../Objects/Gateway/Event/TypingStartEvt.swift | 8 ++++++++ Swiftcord/Utils/{ => Extensions}/LoggerInit.swift | 0 5 files changed, 17 insertions(+), 14 deletions(-) delete mode 100644 AppDelegate.swift create mode 100644 Swiftcord/DiscordAPI/Objects/Gateway/Event/TypingStartEvt.swift rename Swiftcord/Utils/{ => Extensions}/LoggerInit.swift (100%) diff --git a/AppDelegate.swift b/AppDelegate.swift deleted file mode 100644 index f6c680e5..00000000 --- a/AppDelegate.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// AppDelegate.swift -// Swiftcord -// -// Created by Vincent on 4/14/22. -// - -import Foundation diff --git a/Swiftcord.xcodeproj/project.pbxproj b/Swiftcord.xcodeproj/project.pbxproj index 863e4915..0c5f7ce9 100644 --- a/Swiftcord.xcodeproj/project.pbxproj +++ b/Swiftcord.xcodeproj/project.pbxproj @@ -15,7 +15,8 @@ DA2384C427CCFEF3009E15E0 /* MessageReadAck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */; }; DA2384C627CD1921009E15E0 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2384C527CD1921009E15E0 /* LoadingView.swift */; }; DA2802792808337B00B14E5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2802782808337B00B14E5C /* AppDelegate.swift */; }; - DA28027B280912BF00B14E5C /* LoggerInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA28027A280912BF00B14E5C /* LoggerInit.swift */; }; + DA28027D280957F200B14E5C /* TypingStartEvt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA28027C280957F200B14E5C /* TypingStartEvt.swift */; }; + DA28027F28095E3100B14E5C /* LoggerInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA28027E28095E3000B14E5C /* LoggerInit.swift */; }; DA32EF2427C6249000A9ED72 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2327C6249000A9ED72 /* MessagesView.swift */; }; DA32EF2627C62E6900A9ED72 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2527C62E6900A9ED72 /* MessageView.swift */; }; DA32EF2827C633FE00A9ED72 /* UserAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */; }; @@ -129,9 +130,9 @@ DA2384BE27CCBB26009E15E0 /* EmbedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbedView.swift; sourceTree = ""; }; DA2384C327CCFEF3009E15E0 /* MessageReadAck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReadAck.swift; sourceTree = ""; }; DA2384C527CD1921009E15E0 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - DA2802772808337800B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = "../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/AppDelegate.swift"; sourceTree = ""; }; DA2802782808337B00B14E5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - DA28027A280912BF00B14E5C /* LoggerInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoggerInit.swift; path = "../../../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/Swiftcord/Utils/LoggerInit.swift"; sourceTree = ""; }; + DA28027C280957F200B14E5C /* TypingStartEvt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TypingStartEvt.swift; path = "../../../../../../../../../../Macintosh HD/Users/vinkwok/XcodeProjects/Swiftcord/Swiftcord/DiscordAPI/Objects/Gateway/Event/TypingStartEvt.swift"; sourceTree = ""; }; + DA28027E28095E3000B14E5C /* LoggerInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerInit.swift; sourceTree = ""; }; DA32EF2327C6249000A9ED72 /* MessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; DA32EF2527C62E6900A9ED72 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; DA32EF2727C633FE00A9ED72 /* UserAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatarView.swift; sourceTree = ""; }; @@ -327,7 +328,6 @@ DA4A887B27C0AF3000720909 = { isa = PBXGroup; children = ( - DA2802772808337800B14E5C /* AppDelegate.swift */, DA4A891727C4B0DF00720909 /* README.md */, DA4A888627C0AF3000720909 /* Swiftcord */, DA4A888527C0AF3000720909 /* Products */, @@ -463,7 +463,6 @@ DA4A88DD27C3273C00720909 /* Keychain.swift */, DA4A88E027C3341400720909 /* EventDispatch.swift */, DA520AE627D8ECA6009FD740 /* DecodableThrowable.swift */, - DA28027A280912BF00B14E5C /* LoggerInit.swift */, ); path = Utils; sourceTree = ""; @@ -482,6 +481,7 @@ DA4A88FC27C35FC700720909 /* GuildSchEvtUserEvt.swift */, DA4A890027C360D300720909 /* ReadyEvt.swift */, DA4A890427C383DD00720909 /* MessageDelete.swift */, + DA28027C280957F200B14E5C /* TypingStartEvt.swift */, ); path = Event; sourceTree = ""; @@ -533,6 +533,7 @@ DA520AE327D76BF8009FD740 /* Extensions */ = { isa = PBXGroup; children = ( + DA28027E28095E3000B14E5C /* LoggerInit.swift */, DA520ACA27D4A23A009FD740 /* EmojiExtensions.swift */, DA520AE127D76BEB009FD740 /* BoolToString.swift */, DA520ADC27D643CE009FD740 /* DecodeIntFlags.swift */, @@ -675,7 +676,6 @@ DA4A88F927C35D5000720909 /* GuildMemberEvt.swift in Sources */, DA32EF3F27C7C1D000A9ED72 /* MessageInputView.swift in Sources */, DA520AD327D4D073009FD740 /* AppSettingsView.swift in Sources */, - DA28027B280912BF00B14E5C /* LoggerInit.swift in Sources */, DA4A889C27C0B23C00720909 /* WebView.swift in Sources */, DA4A88E527C3482A00720909 /* LoginView.swift in Sources */, DA32EF5927CB5F4F00A9ED72 /* UIStateEnv.swift in Sources */, @@ -728,9 +728,11 @@ DA520ADD27D643CE009FD740 /* DecodeIntFlags.swift in Sources */, DA32EF3227C676FE00A9ED72 /* LottieLoopMode.swift in Sources */, DA4A88AE27C135F300720909 /* Member.swift in Sources */, + DA28027F28095E3100B14E5C /* LoggerInit.swift in Sources */, DA4A88BA27C13BDF00720909 /* Emoji.swift in Sources */, DA32EF3427C6861800A9ED72 /* StickerView.swift in Sources */, DA4A891627C4B06500720909 /* GatewayCachedState.swift in Sources */, + DA28027D280957F200B14E5C /* TypingStartEvt.swift in Sources */, DA4A88C427C1442300720909 /* Sticker.swift in Sources */, DA4A88A227C1244200720909 /* Embed.swift in Sources */, DA4A88FF27C3609900720909 /* ApplicationObj.swift in Sources */, diff --git a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift index 1823719c..e8b31b79 100644 --- a/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift +++ b/Swiftcord/DiscordAPI/Gateway/RobustWebSocket.swift @@ -74,6 +74,7 @@ class RobustWebSocket: NSObject { private func attachSockReceiveListener() { socket.receive { [weak self] result in + // print(result) switch result { case .success(let message): switch (message) { diff --git a/Swiftcord/DiscordAPI/Objects/Gateway/Event/TypingStartEvt.swift b/Swiftcord/DiscordAPI/Objects/Gateway/Event/TypingStartEvt.swift new file mode 100644 index 00000000..b55dff98 --- /dev/null +++ b/Swiftcord/DiscordAPI/Objects/Gateway/Event/TypingStartEvt.swift @@ -0,0 +1,8 @@ +// +// TypingStartEvt.swift +// Swiftcord +// +// Created by Vincent on 4/15/22. +// + +import Foundation diff --git a/Swiftcord/Utils/LoggerInit.swift b/Swiftcord/Utils/Extensions/LoggerInit.swift similarity index 100% rename from Swiftcord/Utils/LoggerInit.swift rename to Swiftcord/Utils/Extensions/LoggerInit.swift