Files
singlechat/ios/YpChat/Data/SocketClient.swift
Torsten Schulz (local) bfe28a25fa
All checks were successful
Deploy SingleChat / deploy (push) Successful in 40s
android app, extension of stats
2026-08-10 14:03:34 +02:00

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))
}
}