122 lines
4.0 KiB
Swift
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>"))
|
|
}
|
|
}
|
|
}
|
|
}
|