173 lines
5.7 KiB
Plaintext
173 lines
5.7 KiB
Plaintext
|
import Combine
|
||
|
import Foundation
|
||
|
import GRDB
|
||
|
|
||
|
@MainActor
|
||
|
final class ClientsStore: ObservableObject {
|
||
|
static let shared = ClientsStore()
|
||
|
|
||
|
@Published private(set) var ready = false
|
||
|
@Published private(set) var clients: [Client] = []
|
||
|
@Published private(set) var actualRosters: [Roster] = []
|
||
|
@Published private(set) var actualChats: [Chat] = []
|
||
|
|
||
|
private var credentialsCancellable: AnyCancellable?
|
||
|
private var rostersCancellable: AnyCancellable?
|
||
|
private var chatsCancellable: AnyCancellable?
|
||
|
|
||
|
init() {
|
||
|
credentialsCancellable = ValueObservation
|
||
|
.tracking { db in
|
||
|
try Credentials.fetchAll(db)
|
||
|
}
|
||
|
.publisher(in: Database.shared.dbQueue)
|
||
|
.catch { _ in Just([]) }
|
||
|
.sink { [weak self] creds in
|
||
|
self?.processCredentials(creds)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private func processCredentials(_ credentials: [Credentials]) {
|
||
|
let existsJids = Set(clients.map { $0.credentials.bareJid })
|
||
|
let credentialsJids = Set(credentials.map { $0.bareJid })
|
||
|
|
||
|
let forAdd = credentials.filter { !existsJids.contains($0.bareJid) }
|
||
|
let newClients = forAdd.map { Client(credentials: $0) }
|
||
|
|
||
|
let forRemove = clients.filter { !credentialsJids.contains($0.credentials.bareJid) }
|
||
|
forRemove.forEach { $0.disconnect() }
|
||
|
|
||
|
var updatedClients = clients.filter { credentialsJids.contains($0.credentials.bareJid) }
|
||
|
updatedClients.append(contentsOf: newClients)
|
||
|
clients = updatedClients
|
||
|
|
||
|
if !ready {
|
||
|
ready = true
|
||
|
}
|
||
|
|
||
|
resubscribeRosters()
|
||
|
resubscribeChats()
|
||
|
reconnectAll()
|
||
|
}
|
||
|
|
||
|
private func client(for credentials: Credentials) -> Client? {
|
||
|
clients.first { $0.credentials == credentials }
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension ClientsStore {
|
||
|
func tryLogin(_ jidStr: String, _ pass: String) async throws {
|
||
|
// login with fake timeout
|
||
|
async let sleep: Void? = try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
|
||
|
async let request = try await Client.tryLogin(with: .init(bareJid: jidStr, pass: pass, isActive: true))
|
||
|
let client = try await(request, sleep).0
|
||
|
|
||
|
clients.append(client)
|
||
|
try? await client.credentials.save()
|
||
|
}
|
||
|
|
||
|
private func reconnectAll() {
|
||
|
Task {
|
||
|
await withTaskGroup(of: Void.self) { taskGroup in
|
||
|
for client in clients {
|
||
|
taskGroup.addTask {
|
||
|
await client.connect()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension ClientsStore {
|
||
|
private func resubscribeRosters() {
|
||
|
let clientsJids = clients
|
||
|
.filter { $0.state != .disabled }
|
||
|
.map { $0.credentials.bareJid }
|
||
|
|
||
|
rostersCancellable = ValueObservation.tracking { db in
|
||
|
try Roster
|
||
|
.filter(clientsJids.contains(Column("bareJid")))
|
||
|
.filter(Column("locallyDeleted") == false)
|
||
|
.fetchAll(db)
|
||
|
}
|
||
|
.publisher(in: Database.shared.dbQueue)
|
||
|
.catch { _ in Just([]) }
|
||
|
.sink { [weak self] rosters in
|
||
|
self?.actualRosters = rosters
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func addRoster(_ credentials: Credentials, contactJID: String, name: String?, groups: [String]) async throws {
|
||
|
// check that roster exist in db as locally deleted and undelete it
|
||
|
let deletedLocally = try await Roster.fetchDeletedLocally()
|
||
|
if var roster = deletedLocally.first(where: { $0.contactBareJid == contactJID }) {
|
||
|
try await roster.setLocallyDeleted(false)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// add new roster
|
||
|
guard let client = client(for: credentials) else {
|
||
|
throw AppError.clientNotFound
|
||
|
}
|
||
|
try await client.addRoster(contactJID, name: name, groups: groups)
|
||
|
}
|
||
|
|
||
|
func deleteRoster(_ roster: Roster) async throws {
|
||
|
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
|
||
|
throw AppError.clientNotFound
|
||
|
}
|
||
|
try await client.deleteRoster(roster)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension ClientsStore {
|
||
|
private func resubscribeChats() {
|
||
|
let clientsJids = clients
|
||
|
.filter { $0.state != .disabled }
|
||
|
.map { $0.credentials.bareJid }
|
||
|
|
||
|
chatsCancellable = ValueObservation.tracking { db in
|
||
|
try Chat
|
||
|
.filter(clientsJids.contains(Column("account")))
|
||
|
.fetchAll(db)
|
||
|
}
|
||
|
.publisher(in: Database.shared.dbQueue)
|
||
|
.catch { _ in Just([]) }
|
||
|
.sink { [weak self] chats in
|
||
|
self?.actualChats = chats
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
extension ClientsStore {
|
||
|
func conversationStores(for roster: Roster) async throws -> (ConversationStore, AttachmentsStore) {
|
||
|
while !ready {
|
||
|
await Task.yield()
|
||
|
}
|
||
|
|
||
|
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
|
||
|
throw AppError.clientNotFound
|
||
|
}
|
||
|
|
||
|
let conversationStore = ConversationStore(roster: roster, client: client)
|
||
|
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
|
||
|
return (conversationStore, attachmentsStore)
|
||
|
}
|
||
|
|
||
|
func conversationStores(for chat: Chat) async throws -> (ConversationStore, AttachmentsStore) {
|
||
|
while !ready {
|
||
|
await Task.yield()
|
||
|
}
|
||
|
|
||
|
guard let client = clients.first(where: { $0.credentials.bareJid == chat.account }) else {
|
||
|
throw ClientStoreError.clientNotFound
|
||
|
}
|
||
|
|
||
|
let roster = try await chat.fetchRoster()
|
||
|
let conversationStore = ConversationStore(roster: roster, client: client)
|
||
|
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
|
||
|
return (conversationStore, attachmentsStore)
|
||
|
}
|
||
|
}
|