another.im-ios/AnotherXMPP/XMPPClient.swift
2024-12-18 05:08:11 +01:00

162 lines
4.1 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
// stream established, RFC-6120 procedure done
case streamReady
case requestRoster
case rosterUpdated
case addRosterItem(jidStr: String)
case deleteRosterItem(jidStr: String)
}
// MARK: State
public 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
public 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)
]
public init(storage: any XMPPStorage, userAgent: UserAgent) {
self.storage = storage
state.userAgent = userAgent
}
}
// MARK: Public part
public 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)
}
}
}
}