import Foundation // TODO: add versioning (XEP-0237) // TODO: implement error catching final class RosterModule: XmppModule { let id = "Roseter module" private weak var storage: (any XMPPStorage)? private var fullReqId = "" init(_ storage: any XMPPStorage) { self.storage = storage } func reduce(oldState: ClientState, with _: Event) -> ClientState { oldState } func process(state: ClientState, with event: Event) async -> Event? { switch event { case .streamReady: return .requestRoster case .requestRoster: let req = Stanza.iqGet( from: state.jid.full, payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: []) ) if let req { fullReqId = req.id ?? "???" return .stanzaOutbound(req) } else { return nil } case .addRosterItem(let jidStr, let args): return await update(state: state, jidStr: jidStr, args: args) case .updateRosterItem(let jidStr, let args): return await update(state: state, jidStr: jidStr, args: args) case .deleteRosterItem(let jidStr): return await delete(state: state, jidStr: jidStr) case .stanzaInbound(let stanza): if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" { switch stanza.type { case .iq(.set): return await processSet(state: state, stanza: stanza) case .iq(.result): return await processResult(state: state, stanza: stanza) case .iq(.error): // handle errors here return nil default: return nil } } else { return nil } default: return nil } } } private extension RosterModule { private func update(state: ClientState, jidStr: String, args: [String: String]) async -> Event? { print(state, jidStr, args) return nil } private func delete(state: ClientState, jidStr: String) async -> Event? { var existItems: [RosterItem] = [] guard let data = await storage?.getRoster(jid: state.jid), let decoded = try? JSONDecoder().decode([XMLElement].self, from: data), let jid = try? JID(jidStr) else { return nil } existItems = decoded.compactMap { RosterItem(wrap: $0, owner: state.jid) } existItems = existItems.filter { $0.jid != jid } return .rosterUpdated } private func processSet(state: ClientState, stanza: Stanza) async -> Event? { // sanity check if stanza.wrapped.attributes["from"] != state.jid.bare { return nil } // get exists roster items var existItems: [RosterItem] = [] if let data = await storage?.getRoster(jid: state.jid), let decoded = try? JSONDecoder().decode([XMLElement].self, from: data) { existItems = decoded.compactMap { RosterItem(wrap: $0, owner: state.jid) } } // process let items = stanza.wrapped .nodes .first(where: { $0.name == "query" })? .nodes .filter { $0.name == "item" } ?? [] for item in items { guard let itemJidStr = item.attributes["jid"], let itemJid = try? JID(itemJidStr) else { continue } let subscription = item.attributes["subscription"] switch subscription { // TODO: scheck subscription type for removed contacts // on different servers case "remove": existItems = existItems.filter { $0.jid == itemJid } // by default just update roster (or add it if its new) default: if let rosterItem = RosterItem(wrap: item, owner: state.jid) { existItems = existItems.filter { $0.jid == itemJid } existItems.append(rosterItem) } else { continue } } } // save roster guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil } await storage?.setRoster(jid: state.jid, roster: data) // according to RFC-6121 a set from server (push) // shouyld be answered with result guard let res = Stanza(wrap: XMLElement( name: "iq", xmlns: nil, attributes: [ "from": state.jid.full, "id": stanza.id ?? "???", "type": "result" ], content: nil, nodes: [] )) else { return nil } return .stanzaOutbound(res) } private func processResult(state _: ClientState, stanza: Stanza) async -> Event? { print("--WE HERE 2!") print(stanza) return nil } }