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 .addRosterItem(let jidStr, let args): return update(jidStr: jidStr, args: args) case .updateRosterItem(let jidStr, let args): return update(jidStr: jidStr, args: args) case .deleteRosterItem(let jidStr): return delete(jidStr: jidStr) case .stanzaInbound(let stanza): if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" { if stanza.type == .iq(.set) { return processSet(state: state, stanza: stanza) } else if stanza.type == .iq(.result) { return processResult(state: state, stanza: stanza) } else { return nil } } else { return nil } default: return nil } } } private extension RosterModule { private func update(jidStr: String, args: [String: String]) -> Event? { print(jidStr, args) return nil } private func delete(jidStr: String) -> Event? { print(jidStr) return nil } private func processSet(state: ClientState, stanza: Stanza) -> Event? { // sanity check if stanza.wrapped.attributes["from"] != state.jid.bare { return nil } // process // 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) -> Event? { print(stanza) return nil } // // 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 { // 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, new.subsription != .none { // existItems.append(new) // guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil } // await storage?.setRoster(jid: state.jid, roster: data) // } else { // // do nothing, this item was removed // } // } // 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 // } // } }