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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|