another.im-ios/AnotherIM/xmpp/XMPPClient.swift
2024-12-16 09:04:14 +01:00

138 lines
3.6 KiB
Swift

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
case streamReady
}
// 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
)
}
}
final class XMPPClient {
private var state = ClientState.initial
private let logger = ClientLogger()
private let storage: XMPPClientStorageProtocol
private lazy var modules: [any XmppModule] = [
SRVResolverModule(),
ConnectionModule(self.fire),
ParserModule(self.fire),
SessionModule(),
AuthorizationModule(self.storage),
StanzaModule(self.storage),
DiscoveryModule()
]
init(storage: any XMPPClientStorageProtocol, userAgent: UserAgent) {
self.storage = storage
state.userAgent = userAgent
}
func tryLogin(jid: JID, credentialsId: UUID) {
logger.update(jid.description)
Task {
await fire(.startClientLogin(jid: jid, credsId: credentialsId))
}
}
}
// 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)
}
}
}
}