another.im-ios/AnotherXMPP/modules/roster/RosterModule.swift

308 lines
11 KiB
Swift
Raw Normal View History

2025-01-09 15:17:02 +00:00
// RFC 6121
// XEP-0237
2024-12-16 12:51:12 +00:00
import Foundation
2025-01-13 03:11:24 +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)?
2025-01-09 15:17:02 +00:00
private var isVerSupported = false
2025-01-13 03:11:24 +00:00
private let registry = StanzaRegistry()
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
2025-01-09 15:17:02 +00:00
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
2024-12-16 12:51:12 +00:00
case .requestRoster:
2025-01-09 15:17:02 +00:00
var attributes: [String: String] = [:]
if isVerSupported {
let ver = await storage?.getRosterVer(jid: state.jid)
attributes["ver"] = ver ?? ""
}
2024-12-17 10:00:51 +00:00
let req = Stanza.iqGet(
from: state.jid.full,
2025-01-09 15:17:02 +00:00
payload: XMLElement(
name: "query",
xmlns: "jabber:iq:roster",
attributes: attributes,
content: nil,
nodes: []
)
2024-12-17 10:00:51 +00:00
)
2024-12-16 12:51:12 +00:00
if let req {
2025-01-13 03:11:24 +00:00
await registry.enqueue(req)
2024-12-16 12:51:12 +00:00
return .stanzaOutbound(req)
} else {
return nil
}
2025-01-09 16:04:09 +00:00
case .addRosterItem(let jidStr, let args), .updateRosterItem(let jidStr, let args):
var attr = ["jid": jidStr]
if let name = args["name"] {
attr["name"] = name
}
let req = Stanza.iqSet(
from: state.jid.full,
payload: XMLElement(
name: "query",
xmlns: "jabber:iq:roster",
attributes: [:],
content: nil,
nodes: [
XMLElement(
name: "item",
xmlns: nil,
attributes: attr,
content: nil,
nodes: []
)
]
)
)
if let req {
2025-01-13 03:11:24 +00:00
await registry.enqueue(req)
2025-01-09 16:04:09 +00:00
return .stanzaOutbound(req)
} else {
return nil
}
2024-12-24 10:43:15 +00:00
case .deleteRosterItem(let jidStr):
2025-01-09 17:07:39 +00:00
let req = Stanza.iqSet(
from: state.jid.full,
payload: XMLElement(
name: "query",
xmlns: "jabber:iq:roster",
attributes: [:],
content: nil,
nodes: [
XMLElement(
name: "item",
xmlns: nil,
attributes: ["jid": jidStr, "subscription": "remove"],
content: nil,
nodes: []
)
]
)
)
if let req {
2025-01-13 03:11:24 +00:00
await registry.enqueue(req)
2025-01-09 17:07:39 +00:00
return .stanzaOutbound(req)
} else {
return nil
}
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" {
2025-01-09 17:47:42 +00:00
// update version if needed
if let ver = stanza.wrapped.nodes.first(where: { $0.name == "query" })?.attributes["ver"] {
await storage?.setRosterVer(jid: state.jid, version: ver)
}
2025-01-13 03:11:24 +00:00
// process stanza
2025-01-13 03:42:08 +00:00
return await processInbound(state: state, stanza: stanza)
2025-01-13 03:11:24 +00:00
} else {
return nil
}
2024-12-31 12:28:03 +00:00
2025-01-13 03:11:24 +00:00
default:
return nil
}
}
}
2024-12-31 12:28:03 +00:00
2025-01-13 03:11:24 +00:00
private extension RosterModule {
2025-01-13 03:42:08 +00:00
func processInbound(state: ClientState, stanza: Stanza) async -> Event? {
2025-01-13 03:11:24 +00:00
switch stanza.type {
2025-01-13 03:42:08 +00:00
// "set" type stanza from server is just "push", so change roster accordingly
2025-01-13 03:11:24 +00:00
case .iq(.set):
2025-01-13 03:42:08 +00:00
// sanity check
if stanza.wrapped.attributes["to"] != state.jid.bare {
return nil
}
2025-01-09 17:47:42 +00:00
2025-01-13 03:42:08 +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) }
}
// get item from stanza
let xmlItem = stanza.wrapped
.nodes
.first(where: { $0.name == "query" })?
.nodes
.first(where: { $0.name == "item" })
guard let xmlItem, let item = RosterItem(wrap: xmlItem, owner: state.jid) else { return nil }
// filter out exists items
existItems = existItems.filter { $0.id != item.id }
// append updated/new item (if it not "removed")
if item.subsription != .remove {
existItems.append(item)
}
2025-01-09 17:47:42 +00:00
2025-01-13 03:42:08 +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)
return .rosterUpdated
// the "result" is an answer from one of our "set", or initial roster request
2025-01-13 03:11:24 +00:00
case .iq(.result):
2025-01-13 03:42:08 +00:00
// get request stanza from registry
guard let respId = stanza.id, let req = await registry.deuque(for: respId) 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) }
}
// perform changes based on stanza request type
switch req.type {
// for simple roster request
case .iq(.get):
// get items from response
let stanzaItems = stanza.wrapped
.nodes
.first(where: { $0.name == "query" })?
.nodes
.filter { $0.name == "item" }
.compactMap { RosterItem(wrap: $0, owner: state.jid) } ?? []
let stanzaItemsIds = stanzaItems.map { $0.id }
// filter out exists items and append updated
existItems = existItems.filter { !stanzaItemsIds.contains($0.id) }
existItems.append(contentsOf: stanzaItems)
// save roster
guard let data = try? JSONEncoder().encode(existItems.map { $0.wrapped }) else { return nil }
await storage?.setRoster(jid: state.jid, roster: data)
return .rosterUpdated
// for result of one of our request
case .iq(.set):
break
default:
break
}
// append items from requested roster
return nil
// TODO: add error handling here
case .iq(.error):
2025-01-13 03:11:24 +00:00
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
2025-01-09 15:17:02 +00:00
// 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
// }
// }