// Spark IM HTTP + TCP demo against production (iOS 13+ / macOS). // Drop into a Swift command-line target or iOS app and call SparkImClient.run(). import Foundation import Network enum SparkImClient { static let api = URL(string: "https://sparkapi.wyrs365.com")! static let imHost = NWEndpoint.Host("sparkim.wyrs365.com") static let imPort = NWEndpoint.Port(443)! static let deviceID: UInt64 = 7 struct LoginEnv: Decodable { struct Data: Decodable { let access_token: String } let code: String let data: Data } static func run() { do { let token = try login() connect(token: token) RunLoop.main.run(until: Date().addingTimeInterval(15)) } catch { fputs("login failed: \(error)\n", stderr) } } static func login() throws -> String { var req = URLRequest(url: api.appendingPathComponent("/v1/auth/login")) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("zh-CN", forHTTPHeaderField: "X-Language") req.setValue("1.0.0", forHTTPHeaderField: "X-App-Version") req.httpBody = """ {"email":"test01@spark.com","verify_code":"000000","device_id":\(deviceID),"device_type":"ios","device_name":"demo"} """.data(using: .utf8) let sem = DispatchSemaphore(value: 0) var result: Result = .failure(URLError(.unknown)) URLSession.shared.dataTask(with: req) { data, _, err in defer { sem.signal() } if let err { result = .failure(err) return } guard let data else { result = .failure(URLError(.badServerResponse)) return } do { let env = try JSONDecoder().decode(LoginEnv.self, from: data) guard env.code == "0" else { result = .failure(URLError(.userAuthenticationRequired)) return } result = .success(env.data.access_token) } catch { result = .failure(error) } }.resume() sem.wait() return try result.get() } static func connect(token: String) { let conn = NWConnection(host: imHost, port: imPort, using: .tls) conn.stateUpdateHandler = { state in guard case .ready = state else { return } let auth = #"{"token":"\#(token)","device_id":\#(deviceID),"app_version":"1.0.0","language":"zh-CN"}"# send(conn, cmd: 1, seq: 1, body: Data(auth.utf8)) { recv(conn) { authResp in print("AUTH", authResp) send(conn, cmd: 2, seq: 2, body: Data()) { recv(conn) { hb in print("HB", hb) let sendBody = #"{"peer_id":1,"client_msg_id":"demo-1","content":{"t":1,"text":{"body":"hi"}}}"# send(conn, cmd: 4, seq: 3, body: Data(sendBody.utf8)) { recv(conn) { ack in print("SEND", ack) } } } } } } } conn.start(queue: .main) } static func frame(cmd: UInt16, seq: UInt32, body: Data) -> Data { var d = Data(capacity: 23 + body.count) func u16(_ v: UInt16) { d.append(UInt8(v >> 8)); d.append(UInt8(v & 0xff)) } func u32(_ v: UInt32) { d.append(UInt8((v >> 24) & 0xff)); d.append(UInt8((v >> 16) & 0xff)) d.append(UInt8((v >> 8) & 0xff)); d.append(UInt8(v & 0xff)) } u16(0x5350); d.append(1); u16(cmd); u16(0) d.append(contentsOf: [UInt8](repeating: 0, count: 8)) u32(seq); u32(UInt32(body.count)); d.append(body) return d } static func send(_ conn: NWConnection, cmd: UInt16, seq: UInt32, body: Data, done: @escaping () -> Void) { conn.send(content: frame(cmd: cmd, seq: seq, body: body), completion: .contentProcessed { _ in done() }) } static func recv(_ conn: NWConnection, done: @escaping (String) -> Void) { conn.receive(minimumIncompleteLength: 23, maximumLength: 23) { header, _, _, err in guard let header, header.count == 23, err == nil else { return } let n = Int(header[19]) << 24 | Int(header[20]) << 16 | Int(header[21]) << 8 | Int(header[22]) let cmd = Int(header[3]) << 8 | Int(header[4]) if n == 0 { done("cmd=\(cmd) {}") return } conn.receive(minimumIncompleteLength: n, maximumLength: max(n, 1)) { body, _, _, _ in let payload = body ?? Data() done("cmd=\(cmd) " + (String(data: payload, encoding: .utf8) ?? "")) } } } }