import Combine import Foundation import SocketIO /// Entspricht `SocketClient.kt`: gleiche Events, `setSessionId`, `login`, Emits. final class SocketClient: @unchecked Sendable { private let baseURL: String private var manager: SocketManager? private var socket: SocketIOClient? private var pendingExpressSessionId: String? private let eventSubject = PassthroughSubject() /// Ereignisse vom Server (analog `SharedFlow`). var eventPublisher: AnyPublisher { eventSubject.eraseToAnyPublisher() } var isConnected: Bool { socket?.status == .connected } init(baseURL: String) { self.baseURL = baseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) } func connect() { disconnect() guard let url = URL(string: baseURL) else { notify(.error("Ungültige Socket-Basis-URL")) return } var config: SocketIOClientConfiguration = [ .log(false), .compress, .reconnects(true), .reconnectAttempts(-1), .reconnectWait(1), .reconnectWaitMax(30), .version(.three), ] if let cookies = HTTPCookieStorage.shared.cookies(for: url), !cookies.isEmpty { config.insert(.cookies(cookies)) } let manager = SocketManager(socketURL: url, config: config) self.manager = manager let socket = manager.defaultSocket self.socket = socket socket.on(clientEvent: .connect) { [weak self] _, _ in self?.handleSocketConnect() } socket.on(clientEvent: .disconnect) { [weak self] data, _ in let reason = data.first.map { String(describing: $0) } self?.notify(.connectionChanged(connected: false, reason: reason)) } socket.on(clientEvent: .error) { [weak self] data, _ in let msg = data.first.map { String(describing: $0) } ?? "" self?.notify(.error("Socket-Verbindung fehlgeschlagen: \(msg)")) } registerServerEvents(socket) manager.connect() } func disconnect() { socket?.removeAllHandlers() manager?.disconnect() socket = nil manager = nil } func setSessionId(_ expressSessionId: String) { pendingExpressSessionId = expressSessionId guard isConnected else { return } socket?.emit("setSessionId", ["expressSessionId": expressSessionId]) } func login(userName: String, gender: String, age: Int, country: String, expressSessionId: String?) { var payload: [String: Any] = [ "userName": userName, "gender": gender, "age": age, "country": country, ] if let expressSessionId { payload["expressSessionId"] = expressSessionId } else { payload["expressSessionId"] = NSNull() } socket?.emit("login", payload) } func sendMessage(toUserName: String?, message: String, messageId: String = "\(Int(Date().timeIntervalSince1970 * 1000))") { var payload: [String: Any] = [ "message": message.trimmingCharacters(in: .whitespacesAndNewlines), "messageId": messageId, ] if let toUserName, !toUserName.isEmpty { payload["toUserName"] = toUserName } socket?.emit("message", payload) } func sendImage(toUserName: String, imageCode: String, imageUrl: String, messageId: String = "\(Int(Date().timeIntervalSince1970 * 1000))") { socket?.emit( "message", [ "toUserName": toUserName, "message": imageCode, "messageId": messageId, "isImage": true, "imageUrl": imageUrl, ] ) } func requestConversation(withUserName: String) { socket?.emit("requestConversation", ["withUserName": withUserName]) } func userSearch(nameIncludes: String?, minAge: Int?, maxAge: Int?, countries: [String], genders: [String]) { var payload: [String: Any] = [ "countries": countries, "genders": genders, ] payload["nameIncludes"] = nameIncludes ?? NSNull() payload["minAge"] = minAge.map { $0 as Any } ?? NSNull() payload["maxAge"] = maxAge.map { $0 as Any } ?? NSNull() socket?.emit("userSearch", payload) } func requestHistory() { socket?.emit("requestHistory") } func requestOpenConversations() { socket?.emit("requestOpenConversations") } func blockUser(userName: String) { socket?.emit("blockUser", ["userName": userName]) } func unblockUser(userName: String) { socket?.emit("unblockUser", ["userName": userName]) } // MARK: - Private private func handleSocketConnect() { if let sid = pendingExpressSessionId { socket?.emit("setSessionId", ["expressSessionId": sid]) } // Reihenfolge wie `SocketClient.kt` (EVENT_CONNECT): zuerst setSessionId, dann ConnectionChanged. notify(.connectionChanged(connected: true, reason: nil)) } private func notify(_ event: SocketEvent) { eventSubject.send(event) } private func registerServerEvents(_ socket: SocketIOClient) { socket.on("connected") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify( .connected( sessionId: json.stringOrNil("sessionId"), loggedIn: json.bool("loggedIn", default: false), user: json.userObject("user") ) ) } socket.on("loginSuccess") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.loginSuccess(sessionId: json.stringOrNil("sessionId"), user: json.userObject("user"))) } socket.on("userList") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data), let users = json["users"] as? [[String: Any]] else { return } self?.notify(.userList(users: users.map { UserDto(json: $0) })) } socket.on("message") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.incomingMessage(ChatMessageDto(json: json))) } socket.on("messageSent") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.messageSent(messageId: json.stringOrNil("messageId"), to: json.stringOrNil("to"))) } socket.on("conversation") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } let withName = json.string("with") let rawMessages = json["messages"] as? [[String: Any]] ?? [] let messages = rawMessages.map { ChatMessageDto(json: $0) } self?.notify(.conversation(withUserName: withName, messages: messages)) } socket.on("searchResults") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data), let results = json["results"] as? [[String: Any]] else { return } self?.notify(.searchResults(results.map { UserDto(json: $0) })) } socket.on("historyResults") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data), let results = json["results"] as? [[String: Any]] else { return } self?.notify(.historyResults(results.map { HistoryItemDto(json: $0) })) } socket.on("inboxResults") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data), let results = json["results"] as? [[String: Any]] else { return } self?.notify(.inboxResults(results.map { InboxItemDto(json: $0) })) } socket.on("unreadChats") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.unreadChats(count: json.int("count", default: 0))) } socket.on("userBlocked") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.userBlocked(json.string("userName"))) } socket.on("userUnblocked") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } self?.notify(.userUnblocked(json.string("userName"))) } socket.on("commandResult") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } let lines = (json["lines"] as? [Any])?.map { String(describing: $0) } ?? [] let kind = json.string("kind", default: "info") self?.notify(.commandResult(lines: lines, kind: kind)) } socket.on("commandTable") { [weak self] data, _ in guard let json = Self.firstDictionary(from: data) else { return } let title = json.string("title", default: "Ausgabe") let columns = (json["columns"] as? [Any])?.map { String(describing: $0) } ?? [] let rows = Self.nestedStringRows(json["rows"]) self?.notify(.commandTable(title: title, columns: columns, rows: rows)) } socket.on("error") { [weak self] data, _ in let json = Self.firstDictionary(from: data) let message = json?.stringOrNil("message") ?? data.first.map { String(describing: $0) } ?? "Unbekannter Socket-Fehler" self?.notify(.error(message)) } } private static func firstDictionary(from data: [Any]) -> [String: Any]? { guard let first = data.first else { return nil } if let d = first as? [String: Any] { return d } if let n = first as? NSDictionary { return n as? [String: Any] } return nil } private static func nestedStringRows(_ any: Any?) -> [[String]] { guard let outer = any as? [Any] else { return [] } return outer.map { row in (row as? [Any])?.map { String(describing: $0) } ?? [] } } } // MARK: - JSON helpers (JSONObject-Äquivalent) private extension Dictionary where Key == String, Value == Any { func stringOrNil(_ key: String) -> String? { guard let v = self[key], !(v is NSNull) else { return nil } if let s = v as? String { return s } return String(describing: v) } func string(_ key: String, default def: String = "") -> String { stringOrNil(key) ?? def } func bool(_ key: String, default def: Bool) -> Bool { guard let v = self[key], !(v is NSNull) else { return def } if let b = v as? Bool { return b } if let n = v as? NSNumber { return n.boolValue } return def } func int(_ key: String, default def: Int) -> Int { guard let v = self[key], !(v is NSNull) else { return def } if let i = v as? Int { return i } if let n = v as? NSNumber { return n.intValue } return def } func userObject(_ key: String) -> UserDto? { guard let nested = self[key] as? [String: Any] else { return nil } return UserDto(json: nested) } } private extension UserDto { init(json: [String: Any]) { self.init( sessionId: json.stringOrNil("sessionId"), userName: json.string("userName"), gender: json.string("gender"), age: json.int("age", default: 0), country: json.string("country"), isoCountryCode: json.string("isoCountryCode") ) } } private extension ChatMessageDto { init(json: [String: Any]) { self.init( from: json.string("from"), to: json.stringOrNil("to"), message: json.string("message"), messageId: json.stringOrNil("messageId"), timestamp: json.string("timestamp"), read: json.bool("read", default: false), isImage: json.bool("isImage", default: false), imageType: json.stringOrNil("imageType"), imageUrl: json.stringOrNil("imageUrl"), imageCode: json.stringOrNil("imageCode") ) } } private extension HistoryItemDto { init(json: [String: Any]) { let last: ChatMessageDto? = { guard let nested = json["lastMessage"] as? [String: Any] else { return nil } return ChatMessageDto(json: nested) }() self.init(userName: json.string("userName"), lastMessage: last) } } private extension InboxItemDto { init(json: [String: Any]) { self.init(userName: json.string("userName"), unreadCount: json.int("unreadCount", default: 0)) } }