spark-server/docs/clients/SparkImClient.swift
2026-09-18 14:50:41 +08:00

122 lines
4.0 KiB
Swift

// 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<String, Error> = .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) ?? "<gzip \(payload.count)B>"))
}
}
}
}