This commit is contained in:
fmodf 2025-01-09 16:17:02 +01:00
parent d733394196
commit 6d669f924d
2 changed files with 114 additions and 90 deletions

View file

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

View file

@ -81,6 +81,7 @@ targets:
- path: AnotherIM - path: AnotherIM
excludes: excludes:
- .nvim - .nvim
- xmls
settings: settings:
TARGETED_DEVICE_FAMILY: 1 TARGETED_DEVICE_FAMILY: 1
DEBUG_INFORMATION_FORMAT: dwarf-with-dsym DEBUG_INFORMATION_FORMAT: dwarf-with-dsym