import Foundation // MARK: Events enum Event { case startClientLogin(jid: JID, credsId: UUID) case resolveDomain case domainResolved([SRVRecord]) case domainResolvingError(SRVResolverError) case tryConnect case socketConnected(SocketType) case socketDisconnected case socketError(Error) case socketReceived(Data) case allRecordsUnreachable case startStream case streamStarted(args: [String: String]) case streamEnded case parserError(Error) case xmlInbound(XMLElement) case xmlOutbound(XMLElement) case startTls case startTlsDone case startTlsFailed(Error) case gotAuthError(AuthorizationError) case startAuth(XMLElement) case challengeAuth(XMLElement) case authDone(sasl: SaslType, args: [String: String]) case stanzaInbound(Stanza) case stanzaOutbound(Stanza) case bindStream case bindStreamDone(String) case bindStreamError // stream established, RFC-6120 procedure done case streamReady case requestRoster case rosterUpdated case addRosterItem(jidStr: String) case deleteRosterItem(jidStr: String) } // MARK: State struct ClientState: Codable & Equatable { var jid: JID var credentialsId: UUID var userAgent: UserAgent var sessionState: SessionState var srvRecords: [SRVRecord] var srvRecordIndex: Int var socketType: SocketType var isSocketSecured: Bool var streamId: String // for allow self-signed or expired certificates // not secure, but sometimes needed var allowInsecure: Bool var allowPlainAuth: Bool var authorizationStep: AuthorizationStep var isStreamBound: Bool static var initial: ClientState { // swiftlint:disable:next force_try let initJid = try! JID("need@initiali.ze") return .init( jid: initJid, credentialsId: UUID(), userAgent: .init(uuid: "", software: "", device: ""), sessionState: .waitingSRVRecords, srvRecords: [], srvRecordIndex: -1, socketType: .startTls, isSocketSecured: false, streamId: "", allowInsecure: false, allowPlainAuth: false, authorizationStep: .notAuthorized, isStreamBound: false ) } } // MARK: Client final class XMPPClient { private var state = ClientState.initial private let logger = ClientLogger() private let storage: XMPPStorage private lazy var modules: [any XmppModule] = [ SRVResolverModule(), ConnectionModule(self.fire), ParserModule(self.fire), SessionModule(), AuthorizationModule(self.storage), StanzaModule(self.storage), DiscoveryModule(), RosterModule(self.storage) ] init(storage: any XMPPStorage, userAgent: UserAgent) { self.storage = storage state.userAgent = userAgent } } // MARK: Public part extension XMPPClient { func tryLogin(jid: JID, credentialsId: UUID) { logger.update(jid.full) Task { await fire(.startClientLogin(jid: jid, credsId: credentialsId)) } } func addContact(jidStr: String) { Task { await fire(.addRosterItem(jidStr: jidStr)) } } func deleteContact(jidStr: String) { Task { await fire(.deleteRosterItem(jidStr: jidStr)) } } } // MARK: Private part private extension XMPPClient { private func fire(_ event: Event) async { // log logger.logEvent(event) // apply reducing let newState = modules.reduce(state) { result, next in next.reduce(oldState: result, with: event) } logger.logState(state, newState) state = newState // apply side effects await withTaskGroup(of: Event?.self) { [state] group in for mod in modules { group.addTask { await mod.process(state: state, with: event) } } for await case let nextEvent? in group { await fire(nextEvent) } } } }