// RFC 6121 // XEP-0237 import Foundation final class RosterModule: XmppModule { let id = "Roseter module" private weak var storage: (any XMPPStorage)? private var isVerSupported = false 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 .xmlInbound(let xml): if xml.name == "stream:features" { if let ver = xml.nodes.first(where: { $0.name == "ver" }), ver.xmlns == "urn:xmpp:features:rosterver" { isVerSupported = true } } return nil case .requestRoster: var attributes: [String: String] = [:] if isVerSupported { let ver = await storage?.getRosterVer(jid: state.jid) attributes["ver"] = ver ?? "" } let req = Stanza.iqGet( from: state.jid.full, payload: XMLElement( name: "query", xmlns: "jabber:iq:roster", attributes: attributes, content: nil, nodes: [] ) ) if let req { return .stanzaOutbound(req) } else { return nil } case .addRosterItem(let jidStr, let args), .updateRosterItem(let jidStr, let args): var attr = ["jid": jidStr] if let name = args["name"] { attr["name"] = name } let req = Stanza.iqSet( from: state.jid.full, payload: XMLElement( name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [ XMLElement( name: "item", xmlns: nil, attributes: attr, content: nil, nodes: [] ) ] ) ) if let req { return .stanzaOutbound(req) } else { return nil } case .deleteRosterItem(let jidStr): let req = Stanza.iqSet( from: state.jid.full, payload: XMLElement( name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [ XMLElement( name: "item", xmlns: nil, attributes: ["jid": jidStr, "subscription": "remove"], content: nil, nodes: [] ) ] ) ) if let req { 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" { // update version if needed if let ver = stanza.wrapped.nodes.first(where: { $0.name == "query" })?.attributes["ver"] { await storage?.setRosterVer(jid: state.jid, version: ver) } // get items from stanza var items: [XMLElement] = [] switch stanza.type { case .iq(.set): break // return await processSet(state: state, stanza: stanza) case .iq(.result): break // return await processResult(state: state, stanza: stanza) case .iq(.error): // handle errors here // TODO: implement error catching break default: break } // process items // result 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 // } // }