import Foundation // TODO: add versioning (XEP-0237) if needed // 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 .stanzaInbound(let stanza): if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" { return await processRoster(state: state, stanza: stanza) } else { return nil } case .addRosterItem(let jidStr): return updRoster(state: state, target: jidStr, remove: false) case .deleteRosterItem(let jidStr): return updRoster(state: state, target: jidStr, remove: true) default: return nil } } } private extension RosterModule { private func processRoster(state: ClientState, stanza: Stanza) async -> Event? { // get inner query guard let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }) else { 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 push (.set from server) if stanza.type == .iq(.set) { guard let item = query.nodes.first(where: { $0.name == "item" }), let new = RosterItem(wrap: item, owner: state.jid) else { return nil } existItems = existItems.filter { $0.jid != new.jid } existItems.append(new) guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil } await storage?.setRoster(jid: state.jid, roster: data) return .rosterUpdated // process .result from server } else if stanza.type == .iq(.result) { // process full list if stanza.id == fullReqId { let items = query.nodes.filter { $0.name == "item" } guard let data = try? JSONEncoder().encode(items) else { return nil } await storage?.setRoster(jid: state.jid, roster: data) // process changed item } else { // TODO: Fix removing item! // guard // let item = query.nodes.first(where: { $0.name == "item" }), // let new = RosterItem(wrap: item, owner: state.jid) // else { return nil } // existItems = existItems.filter { $0.jid != new.jid } // if new.subsription != .remove { // existItems.append(new) // } else { // print("REMOVED!!!") // } // guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil } // await storage?.setRoster(jid: state.jid, roster: data) } return .rosterUpdated } else { return nil } } private func updRoster(state: ClientState, target: String, remove: Bool) -> Event? { var attributes = ["jid": target] if remove { attributes["subcription"] = "remove" } let item = XMLElement(name: "item", xmlns: "jabber:iq:roster", attributes: attributes, content: nil, nodes: []) let query = XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [item]) if let req = Stanza.iqSet(from: state.jid.full, payload: query) { return .stanzaOutbound(req) } else { return nil } } }