206 lines
7.5 KiB
Swift
206 lines
7.5 KiB
Swift
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
|
||
// }
|
||
// }
|
||
}
|