another.im-ios/AnotherXMPP/modules/roster/RosterModule.swift
2024-12-31 13:31:50 +01:00

156 lines
5.1 KiB
Swift

import Foundation
// TODO: add versioning (XEP-0237)
// 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 await update(state: state, jidStr: jidStr, args: args)
case .updateRosterItem(let jidStr, let args):
return await update(state: state, jidStr: jidStr, args: args)
case .deleteRosterItem(let jidStr):
return await delete(state: state, jidStr: jidStr)
case .stanzaInbound(let stanza):
if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" {
switch stanza.type {
case .iq(.set):
return await processSet(state: state, stanza: stanza)
case .iq(.result):
return await processResult(state: state, stanza: stanza)
case .iq(.error):
// handle errors here
return nil
default:
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
}
}