another.im-ios/AnotherXMPP/modules/roster/RosterModule.swift
2024-12-26 14:12:19 +01:00

206 lines
7.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 await 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) 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 {
case "remove":
existItems = existItems.filter { $0.jid == itemJid }
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) -> Event? {
print(stanza)
return nil
}
// <iq from=juliet@example.com/balcony
// id=a78b4q6ha463
// type=result/>
// 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
// }
// }
}