conversations-classic-ios/ConversationsClassic/AppData/Client/Client.swift

208 lines
7.7 KiB
Swift
Raw Normal View History

2024-08-11 00:28:01 +00:00
import Combine
import Foundation
2024-08-11 20:33:04 +00:00
import GRDB
2024-08-11 00:28:01 +00:00
import Martin
2024-08-27 13:00:39 +00:00
import MartinOMEMO
2024-08-11 00:28:01 +00:00
enum ClientState: Equatable {
enum ClientConnectionState {
case connected
case disconnected
}
case disabled
case enabled(ClientConnectionState)
}
final class Client: ObservableObject {
@Published private(set) var state: ClientState = .enabled(.disconnected)
@Published private(set) var credentials: Credentials
2024-08-11 22:30:01 +00:00
@Published private(set) var rosters: [Roster] = []
2024-08-11 20:33:04 +00:00
2024-08-11 00:28:01 +00:00
private var connection: XMPPClient
private var connectionCancellable: AnyCancellable?
2024-08-11 22:30:01 +00:00
private var rostersCancellable: AnyCancellable?
2024-08-11 00:28:01 +00:00
2024-08-11 11:39:17 +00:00
private var rosterManager = ClientMartinRosterManager()
2024-08-11 23:52:45 +00:00
private var chatsManager = ClientMartinChatsManager()
2024-08-18 14:43:13 +00:00
private var discoManager: ClientMartinDiscoManager
2024-08-18 17:20:55 +00:00
private var messageManager: ClientMartinMessagesManager
private var carbonsManager: ClientMartinCarbonsManager
private var mamManager: ClientMartinMAM
2024-08-11 00:28:01 +00:00
init(credentials: Credentials) {
self.credentials = credentials
state = credentials.isActive ? .enabled(.disconnected) : .disabled
2024-08-11 23:52:45 +00:00
connection = Self.prepareConnection(credentials, rosterManager, chatsManager)
2024-08-18 14:43:13 +00:00
discoManager = ClientMartinDiscoManager(connection)
2024-08-18 17:20:55 +00:00
messageManager = ClientMartinMessagesManager(connection)
carbonsManager = ClientMartinCarbonsManager(connection)
mamManager = ClientMartinMAM(connection)
2024-08-11 00:28:01 +00:00
connectionCancellable = connection.$state
.sink { [weak self] state in
guard let self = self else { return }
guard self.credentials.isActive else {
self.state = .disabled
return
}
2024-08-11 22:30:01 +00:00
rostersCancellable = ValueObservation
.tracking { db in
try Roster
.filter(Column("bareJid") == self.credentials.bareJid)
.filter(Column("locallyDeleted") == false)
.fetchAll(db)
}
.publisher(in: Database.shared.dbQueue)
.catch { _ in Just([]) }
.sink { rosters in
self.rosters = rosters
}
2024-08-11 00:28:01 +00:00
switch state {
case .connected:
self.state = .enabled(.connected)
default:
self.state = .enabled(.disconnected)
}
}
}
2024-08-11 19:01:48 +00:00
}
2024-08-11 00:28:01 +00:00
2024-08-11 19:01:48 +00:00
extension Client {
2024-08-11 17:07:02 +00:00
func addRoster(_ jid: String, name: String?, groups: [String]) async throws {
2024-08-11 00:28:01 +00:00
_ = try await connection.module(.roster).addItem(
2024-08-11 17:07:02 +00:00
jid: JID(jid),
name: name,
groups: groups
2024-08-11 00:28:01 +00:00
)
}
func deleteRoster(_ roster: Roster) async throws {
_ = try await connection.module(.roster).removeItem(jid: JID(roster.contactBareJid))
}
2024-08-11 19:01:48 +00:00
func connect() async {
guard credentials.isActive, state == .enabled(.disconnected) else {
return
}
try? await connection.loginAndWait()
}
func disconnect() {
_ = connection.disconnect()
}
2024-08-11 00:28:01 +00:00
}
2024-08-15 15:15:49 +00:00
extension Client {
2024-08-17 08:06:28 +00:00
func sendMessage(_ message: Message) async throws {
2024-08-15 15:15:49 +00:00
guard let to = message.to else {
return
}
2024-08-17 22:05:18 +00:00
guard let chat = connection.module(MessageModule.self).chatManager.createChat(for: connection.context, with: BareJID(to)) else {
2024-08-15 15:15:49 +00:00
return
}
let msg = chat.createMessage(text: message.body ?? "??", id: message.id)
2024-08-18 11:26:05 +00:00
msg.oob = message.oobUrl
2024-08-17 08:06:28 +00:00
try await chat.send(message: msg)
2024-08-15 15:15:49 +00:00
}
2024-08-17 21:12:39 +00:00
2024-08-17 22:05:18 +00:00
func uploadFile(_ localURL: URL) async throws -> String {
guard let data = try? Data(contentsOf: localURL) else {
2024-08-18 15:56:47 +00:00
throw AppError.noData
2024-08-17 21:12:39 +00:00
}
let httpModule = connection.module(HttpFileUploadModule.self)
let components = try await httpModule.findHttpUploadComponents()
guard let component = components.first(where: { $0.maxSize > data.count }) else {
2024-08-18 15:56:47 +00:00
throw AppError.fileTooBig
2024-08-17 21:12:39 +00:00
}
let slot = try await httpModule.requestUploadSlot(
componentJid: component.jid,
2024-08-17 22:05:18 +00:00
filename: localURL.lastPathComponent,
2024-08-17 21:12:39 +00:00
size: data.count,
2024-08-17 22:05:18 +00:00
contentType: localURL.mimeType
2024-08-17 21:12:39 +00:00
)
var request = URLRequest(url: slot.putUri)
for (key, value) in slot.putHeaders {
request.addValue(value, forHTTPHeaderField: key)
}
request.httpMethod = "PUT"
request.httpBody = data
request.addValue(String(data.count), forHTTPHeaderField: "Content-Length")
2024-08-17 22:05:18 +00:00
request.addValue(localURL.mimeType, forHTTPHeaderField: "Content-Type")
2024-08-17 21:12:39 +00:00
let (_, response) = try await URLSession.shared.data(for: request)
switch response {
2024-08-17 22:05:18 +00:00
case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201:
2024-08-17 21:12:39 +00:00
return slot.getUri.absoluteString
2024-08-18 11:26:05 +00:00
2024-08-17 21:12:39 +00:00
default:
throw URLError(.badServerResponse)
}
}
2024-08-19 01:25:27 +00:00
func fetchArchiveMessages(for roster: Roster, query: RSM.Query) async throws -> Martin.MessageArchiveManagementModule.QueryResult {
if !discoManager.features.map({ $0.xep }).contains("XEP-0313") {
throw AppError.featureNotSupported
}
let module = connection.module(MessageArchiveManagementModule.self)
return try await module.queryItems(componentJid: JID(roster.bareJid), with: JID(roster.contactBareJid), queryId: UUID().uuidString, rsm: query)
}
2024-08-15 15:15:49 +00:00
}
2024-08-11 00:28:01 +00:00
extension Client {
2024-08-11 20:33:04 +00:00
static func tryLogin(with credentials: Credentials) async throws -> Client {
2024-08-11 00:28:01 +00:00
let client = Client(credentials: credentials)
2024-08-11 20:33:04 +00:00
try await client.connection.loginAndWait()
return client
2024-08-11 00:28:01 +00:00
}
}
private extension Client {
2024-08-11 23:52:45 +00:00
static func prepareConnection(_ credentials: Credentials, _ roster: RosterManager, _ chat: ChatManager) -> XMPPClient {
2024-08-11 00:28:01 +00:00
let client = XMPPClient()
// register modules
// core modules RFC 6120
client.modulesManager.register(StreamFeaturesModule())
client.modulesManager.register(SaslModule())
client.modulesManager.register(AuthModule())
client.modulesManager.register(SessionEstablishmentModule())
client.modulesManager.register(ResourceBinderModule())
client.modulesManager.register(DiscoveryModule(identity: .init(category: "client", type: "iOS", name: Const.appName)))
// messaging modules RFC 6121
client.modulesManager.register(RosterModule(rosterManager: roster))
client.modulesManager.register(PresenceModule())
2024-08-17 21:12:39 +00:00
client.modulesManager.register(PubSubModule())
2024-08-11 23:52:45 +00:00
client.modulesManager.register(MessageModule(chatManager: chat))
2024-08-18 14:43:13 +00:00
client.modulesManager.register(MessageArchiveManagementModule())
2024-08-11 00:28:01 +00:00
2024-08-18 11:26:05 +00:00
client.modulesManager.register(MessageCarbonsModule())
2024-08-11 00:28:01 +00:00
// file transfer modules
2024-08-17 21:12:39 +00:00
client.modulesManager.register(HttpFileUploadModule())
2024-08-11 00:28:01 +00:00
// extensions
client.modulesManager.register(SoftwareVersionModule())
client.modulesManager.register(PingModule())
client.connectionConfiguration.userJid = .init(credentials.bareJid)
client.connectionConfiguration.credentials = .password(password: credentials.pass)
2024-08-27 13:00:39 +00:00
// OMEMO
let omemoManager = ClientMartinOMEMO(credentials)
let (signalStorage, signalContext) = omemoManager.signal
client.modulesManager.register(OMEMOModule(aesGCMEngine: AESGSMEngine.shared, signalContext: signalContext, signalStorage: signalStorage))
2024-08-11 00:28:01 +00:00
// group chats
// client.modulesManager.register(MucModule(roomManager: manager))
// channels
// client.modulesManager.register(MixModule(channelManager: manager))
return client
}
}