349 lines
13 KiB
Swift
Executable File
349 lines
13 KiB
Swift
Executable File
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<SocketEvent, Never>()
|
|
|
|
/// Ereignisse vom Server (analog `SharedFlow<SocketEvent>`).
|
|
var eventPublisher: AnyPublisher<SocketEvent, Never> {
|
|
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))
|
|
}
|
|
}
|