another.im-ios/AnotherXMPP/XMPPClient.swift

162 lines
4.1 KiB
Swift
Raw Permalink Normal View History

2024-06-19 15:06:39 +00:00
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
2024-12-16 12:51:12 +00:00
// stream established, RFC-6120 procedure done
2024-06-19 15:06:39 +00:00
case streamReady
2024-12-16 12:51:12 +00:00
case requestRoster
2024-12-17 15:28:30 +00:00
case rosterUpdated
case addRosterItem(jidStr: String)
case deleteRosterItem(jidStr: String)
2024-06-19 15:06:39 +00:00
}
// MARK: State
2024-12-18 04:08:11 +00:00
public struct ClientState: Codable & Equatable {
2024-06-19 15:06:39 +00:00
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
)
}
}
2024-12-16 12:51:12 +00:00
// MARK: Client
2024-12-18 04:08:11 +00:00
public final class XMPPClient {
2024-06-19 15:06:39 +00:00
private var state = ClientState.initial
private let logger = ClientLogger()
2024-12-17 08:34:44 +00:00
private let storage: XMPPStorage
2024-06-19 15:06:39 +00:00
private lazy var modules: [any XmppModule] = [
SRVResolverModule(),
ConnectionModule(self.fire),
ParserModule(self.fire),
SessionModule(),
AuthorizationModule(self.storage),
2024-12-16 08:04:14 +00:00
StanzaModule(self.storage),
2024-12-16 12:51:12 +00:00
DiscoveryModule(),
2024-12-17 10:00:51 +00:00
RosterModule(self.storage)
2024-06-19 15:06:39 +00:00
]
2024-12-18 04:08:11 +00:00
public init(storage: any XMPPStorage, userAgent: UserAgent) {
2024-06-19 15:06:39 +00:00
self.storage = storage
state.userAgent = userAgent
}
2024-12-17 10:00:51 +00:00
}
2024-06-19 15:06:39 +00:00
2024-12-17 10:00:51 +00:00
// MARK: Public part
2024-12-18 04:08:11 +00:00
public extension XMPPClient {
2024-06-19 15:06:39 +00:00
func tryLogin(jid: JID, credentialsId: UUID) {
2024-12-16 12:51:12 +00:00
logger.update(jid.full)
2024-06-19 15:06:39 +00:00
Task {
await fire(.startClientLogin(jid: jid, credsId: credentialsId))
}
}
2024-12-17 15:28:30 +00:00
func addContact(jidStr: String) {
Task {
await fire(.addRosterItem(jidStr: jidStr))
}
}
2024-12-17 16:07:13 +00:00
func deleteContact(jidStr: String) {
2024-12-17 15:28:30 +00:00
Task {
await fire(.deleteRosterItem(jidStr: jidStr))
}
}
2024-06-19 15:06:39 +00:00
}
// 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)
}
}
}
}