import Combine import Foundation import GRDB import Martin enum ClientState: Equatable { enum ClientConnectionState { case connected case disconnected } case disabled case enabled(ClientConnectionState) } final class Client: ObservableObject { @Published private(set) var state: ClientState = .enabled(.disconnected) @Published private(set) var credentials: Credentials @Published private(set) var rosters: [Roster] = [] private var connection: XMPPClient private var connectionCancellable: AnyCancellable? private var rostersCancellable: AnyCancellable? private var rosterManager = ClientMartinRosterManager() private var chatsManager = ClientMartinChatsManager() private var messageManager: ClientMartinMessagesManager init(credentials: Credentials) { self.credentials = credentials state = credentials.isActive ? .enabled(.disconnected) : .disabled connection = Self.prepareConnection(credentials, rosterManager, chatsManager) messageManager = ClientMartinMessagesManager(connection) connectionCancellable = connection.$state .sink { [weak self] state in guard let self = self else { return } guard self.credentials.isActive else { self.state = .disabled return } rostersCancellable = ValueObservation .tracking { db in try Roster .filter(Column("bareJid") == self.credentials.bareJid) .filter(Column("locallyDeleted") == false) .fetchAll(db) } .publisher(in: Database.shared.dbQueue) .catch { _ in Just([]) } .sink { rosters in self.rosters = rosters } switch state { case .connected: self.state = .enabled(.connected) default: self.state = .enabled(.disconnected) } } } } extension Client { func addRoster(_ jid: String, name: String?, groups: [String]) async throws { _ = try await connection.module(.roster).addItem( jid: JID(jid), name: name, groups: groups ) } func deleteRoster(_ roster: Roster) async throws { _ = try await connection.module(.roster).removeItem(jid: JID(roster.contactBareJid)) } func connect() async { guard credentials.isActive, state == .enabled(.disconnected) else { return } try? await connection.loginAndWait() } func disconnect() { _ = connection.disconnect() } } extension Client { func sendMessage(_ message: Message) async { guard let to = message.to else { return } guard let chat = connection.module(MessageModule.self).chatManager.chat(for: connection.context, with: BareJID(to)) else { return } let msg = chat.createMessage(text: message.body ?? "??", id: message.id) do { try await chat.send(message: msg) } catch { print("Error sending message: \(error)") } } // func sendMessage(_ message: String, to roster: Roster) async { // guard let chat = chatsManager.chat(for: connection.context, with: BareJID(roster.contactBareJid)) else { // return // } // let message = chat.createMessage(text: message, id: UUID().uuidString) // try? await chat.send(message: message) // } } extension Client { static func tryLogin(with credentials: Credentials) async throws -> Client { let client = Client(credentials: credentials) try await client.connection.loginAndWait() return client } } private extension Client { static func prepareConnection(_ credentials: Credentials, _ roster: RosterManager, _ chat: ChatManager) -> XMPPClient { let client = XMPPClient() // register modules // core modules RFC 6120 client.modulesManager.register(StreamFeaturesModule()) client.modulesManager.register(SaslModule()) client.modulesManager.register(AuthModule()) client.modulesManager.register(SessionEstablishmentModule()) client.modulesManager.register(ResourceBinderModule()) client.modulesManager.register(DiscoveryModule(identity: .init(category: "client", type: "iOS", name: Const.appName))) // messaging modules RFC 6121 client.modulesManager.register(RosterModule(rosterManager: roster)) client.modulesManager.register(PresenceModule()) // client.modulesManager.register(PubSubModule()) client.modulesManager.register(MessageModule(chatManager: chat)) // client.modulesManager.register(MessageArchiveManagementModule()) // client.modulesManager.register(MessageCarbonsModule()) // file transfer modules // client.modulesManager.register(HttpFileUploadModule()) // extensions client.modulesManager.register(SoftwareVersionModule()) client.modulesManager.register(PingModule()) client.connectionConfiguration.userJid = .init(credentials.bareJid) client.connectionConfiguration.credentials = .password(password: credentials.pass) // group chats // client.modulesManager.register(MucModule(roomManager: manager)) // channels // client.modulesManager.register(MixModule(channelManager: manager)) // add client to clients return client } }