2024-12-16 12:51:12 +00:00
|
|
|
import Foundation
|
|
|
|
|
2024-12-31 12:28:03 +00:00
|
|
|
// TODO: add versioning (XEP-0237)
|
2024-12-17 15:28:30 +00:00
|
|
|
// TODO: implement error catching
|
2024-12-16 12:51:12 +00:00
|
|
|
final class RosterModule: XmppModule {
|
|
|
|
let id = "Roseter module"
|
|
|
|
|
2024-12-17 10:00:51 +00:00
|
|
|
private weak var storage: (any XMPPStorage)?
|
2024-12-17 16:07:13 +00:00
|
|
|
private var fullReqId = ""
|
2024-12-17 10:00:51 +00:00
|
|
|
|
|
|
|
init(_ storage: any XMPPStorage) {
|
|
|
|
self.storage = storage
|
|
|
|
}
|
|
|
|
|
2024-12-16 12:51:12 +00:00
|
|
|
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:
|
2024-12-17 10:00:51 +00:00
|
|
|
let req = Stanza.iqGet(
|
|
|
|
from: state.jid.full,
|
|
|
|
payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [])
|
|
|
|
)
|
2024-12-16 12:51:12 +00:00
|
|
|
if let req {
|
2024-12-17 16:07:13 +00:00
|
|
|
fullReqId = req.id ?? "???"
|
2024-12-16 12:51:12 +00:00
|
|
|
return .stanzaOutbound(req)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-12-24 10:43:15 +00:00
|
|
|
case .addRosterItem(let jidStr, let args):
|
2024-12-31 12:28:03 +00:00
|
|
|
return await update(state: state, jidStr: jidStr, args: args)
|
2024-12-24 10:43:15 +00:00
|
|
|
|
|
|
|
case .updateRosterItem(let jidStr, let args):
|
2024-12-31 12:28:03 +00:00
|
|
|
return await update(state: state, jidStr: jidStr, args: args)
|
2024-12-24 10:43:15 +00:00
|
|
|
|
|
|
|
case .deleteRosterItem(let jidStr):
|
2024-12-31 12:28:03 +00:00
|
|
|
return await delete(state: state, jidStr: jidStr)
|
2024-12-24 10:43:15 +00:00
|
|
|
|
2024-12-17 10:00:51 +00:00
|
|
|
case .stanzaInbound(let stanza):
|
2024-12-17 15:28:30 +00:00
|
|
|
if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" {
|
2024-12-31 12:28:03 +00:00
|
|
|
switch stanza.type {
|
|
|
|
case .iq(.set):
|
2024-12-24 12:24:05 +00:00
|
|
|
return await processSet(state: state, stanza: stanza)
|
2024-12-31 12:28:03 +00:00
|
|
|
|
|
|
|
case .iq(.result):
|
|
|
|
return await processResult(state: state, stanza: stanza)
|
|
|
|
|
|
|
|
case .iq(.error):
|
|
|
|
// handle errors here
|
|
|
|
return nil
|
|
|
|
|
|
|
|
default:
|
2024-12-24 10:43:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
2024-12-17 15:28:30 +00:00
|
|
|
} else {
|
|
|
|
return nil
|
2024-12-17 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
2024-12-16 12:51:12 +00:00
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-12-17 10:00:51 +00:00
|
|
|
|
|
|
|
private extension RosterModule {
|
2024-12-31 12:28:03 +00:00
|
|
|
private func update(state: ClientState, jidStr: String, args: [String: String]) async -> Event? {
|
|
|
|
print(state, jidStr, args)
|
2024-12-24 10:43:15 +00:00
|
|
|
return nil
|
|
|
|
}
|
2024-12-17 10:00:51 +00:00
|
|
|
|
2024-12-31 12:28:03 +00:00
|
|
|
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
|
2024-12-17 10:00:51 +00:00
|
|
|
}
|
|
|
|
|
2024-12-24 12:24:05 +00:00
|
|
|
private func processSet(state: ClientState, stanza: Stanza) async -> Event? {
|
2024-12-24 10:43:15 +00:00
|
|
|
// sanity check
|
|
|
|
if stanza.wrapped.attributes["from"] != state.jid.bare {
|
2024-12-17 16:07:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
2024-12-24 10:43:15 +00:00
|
|
|
|
2024-12-24 12:24:05 +00:00
|
|
|
// 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) }
|
|
|
|
}
|
|
|
|
|
2024-12-24 10:43:15 +00:00
|
|
|
// process
|
2024-12-24 12:24:05 +00:00
|
|
|
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 {
|
2024-12-31 12:28:03 +00:00
|
|
|
// TODO: scheck subscription type for removed contacts
|
|
|
|
// on different servers
|
2024-12-24 12:24:05 +00:00
|
|
|
case "remove":
|
|
|
|
existItems = existItems.filter { $0.jid == itemJid }
|
|
|
|
|
2024-12-31 12:28:03 +00:00
|
|
|
// by default just update roster (or add it if its new)
|
2024-12-24 12:24:05 +00:00
|
|
|
default:
|
2024-12-26 13:12:19 +00:00
|
|
|
if let rosterItem = RosterItem(wrap: item, owner: state.jid) {
|
|
|
|
existItems = existItems.filter { $0.jid == itemJid }
|
|
|
|
existItems.append(rosterItem)
|
|
|
|
} else {
|
|
|
|
continue
|
|
|
|
}
|
2024-12-24 12:24:05 +00:00
|
|
|
}
|
|
|
|
}
|
2024-12-24 10:43:15 +00:00
|
|
|
|
2024-12-26 13:12:19 +00:00
|
|
|
// save roster
|
|
|
|
guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil }
|
|
|
|
await storage?.setRoster(jid: state.jid, roster: data)
|
|
|
|
|
2024-12-24 10:43:15 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2024-12-31 12:28:03 +00:00
|
|
|
private func processResult(state _: ClientState, stanza: Stanza) async -> Event? {
|
|
|
|
print("--WE HERE 2!")
|
2024-12-24 10:43:15 +00:00
|
|
|
print(stanza)
|
|
|
|
return nil
|
2024-12-17 16:07:13 +00:00
|
|
|
}
|
|
|
|
}
|