This commit is contained in:
fmodf 2024-12-17 11:00:51 +01:00
parent 5510dd3900
commit 0be3f68710
7 changed files with 214 additions and 89 deletions

View file

@ -5,7 +5,8 @@ import SwiftUI
struct AnotherIMApp: App { struct AnotherIMApp: App {
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
StartScreen() // StartScreen()
TestScreen()
} }
} }
} }

View file

@ -1,14 +1,5 @@
import SwiftUI import SwiftUI
// let login = "kudahtk@conversations.im"
// let pass = "derevo77!"
// let login = "testmon4@test.anal.company"
// let pass = "12345"
let login = "testmon3@test.anal.company"
let pass = "12345"
struct StartScreen: View { struct StartScreen: View {
var body: some View { var body: some View {
ZStack { ZStack {
@ -19,79 +10,5 @@ struct StartScreen: View {
.frame(width: 200, height: 200) .frame(width: 200, height: 200)
} }
.ignoresSafeArea() .ignoresSafeArea()
.onAppear {
// doTestA()
doMainTest()
}
}
func doMainTest() {
let userAgent = UserAgent(
uuid: "aaa65fa7-5555-4749-d1a5-740edbf81764",
software: "another.im",
device: "iOS"
)
let cls = XMPPClient(storage: TestStorage(), userAgent: userAgent)
// swiftlint:disable:next force_try
let jid = try! JID(login)
cls.tryLogin(jid: jid, credentialsId: UUID())
}
func doTestA() {
let xml = XMLElement(
name: "test-me",
xmlns: "urn:test:me",
attributes: ["some1": "some-val-1", "type": "test-req"],
content: "asdffweqfqefq34234t2tergfsagewr",
nodes: [
XMLElement(
name: "sub-test-me",
xmlns: "urn:test:me",
attributes: ["some1": "some-val-1"],
content: "asdffweqfqefq34234t2tergfsagewr",
nodes: [
XMLElement(
name: "sub2-test-me",
xmlns: "urn:test:me",
attributes: [:],
content: nil,
nodes: []
)
]
)
]
)
print("before")
print(xml, "\n")
print("after:")
let encoded = try? JSONEncoder().encode(xml)
if let encoded {
print(String(decoding: encoded, as: UTF8.self))
let xml2 = try? JSONDecoder().decode(XMLElement.self, from: encoded)
if let xml2 {
print("\n\n\n")
print(xml2)
}
print("after all\n")
print("\(encoded as NSData)")
}
}
}
final class TestStorage: XMPPStorage {
private var rosterVer: [String: String] = [:]
func getRosterVersion(jid: JID) async -> String? {
rosterVer[jid.bare]
}
func setRosterVersion(jid: JID, ver: String) async {
rosterVer[jid.bare] = ver
}
func getCredentialsByUUID(_ uuid: UUID) async -> Credentials? {
print(uuid)
return ["password": pass]
} }
} }

View file

@ -0,0 +1,112 @@
import SwiftUI
// let login = "kudahtk@conversations.im"
// let pass = "derevo77!"
// let login = "testmon4@test.anal.company"
// let pass = "12345"
let login = "testmon3@test.anal.company"
let pass = "12345"
//
let userAgent = UserAgent(
uuid: "aaa65fa7-5555-4749-d1a5-740edbf81764",
software: "another.im",
device: "iOS"
)
let cls = XMPPClient(storage: TestStorage(), userAgent: userAgent)
struct TestScreen: View {
var body: some View {
ZStack {
Color.Material.Background.light
VStack {
Button {
doConnect()
} label: {
Text("Connect")
.padding()
.background { Color.blue.opacity(0.4) }
}
Button {
// cls.requestRoster()
} label: {
Text("Request roster")
.padding()
.background { Color.blue.opacity(0.4) }
}
}
}
.ignoresSafeArea()
}
func doConnect() {
// swiftlint:disable:next force_try
let jid = try! JID(login)
cls.tryLogin(jid: jid, credentialsId: UUID())
}
}
final class TestStorage: XMPPStorage {
private var roster: [String: Data] = [:]
func getCredentialsByUUID(_ uuid: UUID) async -> Credentials? {
print(uuid)
return ["password": pass]
}
func deleteRoster(jid: JID) async {
roster.removeValue(forKey: jid.bare)
}
func getRoster(jid: JID) async -> Data? {
roster[jid.bare]
}
func setRoster(jid: JID, roster: Data) async {
self.roster[jid.bare] = roster
}
}
extension TestScreen {
// func doTestA() {
// let xml = XMLElement(
// name: "test-me",
// xmlns: "urn:test:me",
// attributes: ["some1": "some-val-1", "type": "test-req"],
// content: "asdffweqfqefq34234t2tergfsagewr",
// nodes: [
// XMLElement(
// name: "sub-test-me",
// xmlns: "urn:test:me",
// attributes: ["some1": "some-val-1"],
// content: "asdffweqfqefq34234t2tergfsagewr",
// nodes: [
// XMLElement(
// name: "sub2-test-me",
// xmlns: "urn:test:me",
// attributes: [:],
// content: nil,
// nodes: []
// )
// ]
// )
// ]
// )
// print("before")
// print(xml, "\n")
// print("after:")
// let encoded = try? JSONEncoder().encode(xml)
// if let encoded {
// print(String(decoding: encoded, as: UTF8.self))
// let xml2 = try? JSONDecoder().decode(XMLElement.self, from: encoded)
// if let xml2 {
// print("\n\n\n")
// print(xml2)
// }
// print("after all\n")
// print("\(encoded as NSData)")
// }
// }
}

View file

@ -101,14 +101,17 @@ final class XMPPClient {
AuthorizationModule(self.storage), AuthorizationModule(self.storage),
StanzaModule(self.storage), StanzaModule(self.storage),
DiscoveryModule(), DiscoveryModule(),
RosterModule() RosterModule(self.storage)
] ]
init(storage: any XMPPStorage, userAgent: UserAgent) { init(storage: any XMPPStorage, userAgent: UserAgent) {
self.storage = storage self.storage = storage
state.userAgent = userAgent state.userAgent = userAgent
} }
}
// MARK: Public part
extension XMPPClient {
func tryLogin(jid: JID, credentialsId: UUID) { func tryLogin(jid: JID, credentialsId: UUID) {
logger.update(jid.full) logger.update(jid.full)
Task { Task {

View file

@ -7,8 +7,11 @@ protocol XMPPStorage: AnyObject {
func getCredentialsByUUID(_ uuid: UUID) async -> Credentials? func getCredentialsByUUID(_ uuid: UUID) async -> Credentials?
// roster // roster
func getRosterVersion(jid: JID) async -> String? func deleteRoster(jid: JID) async
func setRosterVersion(jid: JID, ver: String) async // where Data is byte representation of array of roster items
// i.e. [XMLElement] -> Data
func getRoster(jid: JID) async -> Data?
func setRoster(jid: JID, roster: Data) async
// messages // messages

View file

@ -27,3 +27,44 @@ enum RosterSubsriptionType: String {
} }
} }
} }
// Roster is a "transparent" structure
// which is just wrap xml item around
struct Roster: Identifiable, Equatable {
let owner: String
let wrapped: XMLElement
init?(wrap: XMLElement, owner: JID) {
guard
wrap.name == "item",
wrap.attributes.keys.contains("jid"),
wrap.xmlns == "jabber:iq:roster"
else { return nil }
self.owner = owner.bare
wrapped = wrap
}
var id: String {
"\(owner)|\(jid?.bare ?? "???")"
}
var jid: JID? {
guard let jidStr = wrapped.attributes["jid"] else { return nil }
return try? JID(jidStr)
}
var name: String? {
wrapped.name
}
var subsription: RosterSubsriptionType {
let str = wrapped.attributes["subscription"] ?? "none"
return RosterSubsriptionType(rawValue: str) ?? .none
}
static func == (_ rhs: Roster, _ lhs: Roster) -> Bool {
rhs.id == lhs.id
}
}
// TODO: Add groups and annotattions!

View file

@ -1,8 +1,15 @@
import Foundation import Foundation
// TODO: add versioning (XEP-0237) if needed
final class RosterModule: XmppModule { final class RosterModule: XmppModule {
let id = "Roseter module" let id = "Roseter module"
private weak var storage: (any XMPPStorage)?
init(_ storage: any XMPPStorage) {
self.storage = storage
}
func reduce(oldState: ClientState, with _: Event) -> ClientState { func reduce(oldState: ClientState, with _: Event) -> ClientState {
oldState oldState
} }
@ -13,16 +20,57 @@ final class RosterModule: XmppModule {
return .requestRoster return .requestRoster
case .requestRoster: case .requestRoster:
// TODO: check version! let req = Stanza.iqGet(
let req = Stanza.iqGet(from: state.jid.full, payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [])) from: state.jid.full,
payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [])
)
if let req { if let req {
return .stanzaOutbound(req) return .stanzaOutbound(req)
} else { } else {
return nil return nil
} }
case .stanzaInbound(let stanza):
if stanza.type == .iq(.result) {
if let query = stanza.wrapped.nodes.first(where: { $0.name == "query" }), query.xmlns == "jabber:iq:roster" {
return await processRoster(state: state, xml: query)
}
}
return nil
default: default:
return nil return nil
} }
} }
} }
private extension RosterModule {
func processRoster(state: ClientState, xml: XMLElement) async -> Event? {
// get exists roster items
var existItems: [Roster] = []
if let data = await storage?.getRoster(jid: state.jid), let decoded = try? JSONDecoder().decode([XMLElement].self, from: data) {
existItems = decoded.compactMap { Roster(wrap: $0, owner: state.jid) }
}
// extract items from incoming xml
var newItems = xml.nodes
.compactMap { Roster(wrap: $0, owner: state.jid) }
// manage it ?????
var roster: [XMLElement] = newItems.map { $0.wrapped }
// save updated roster
if let data = try? JSONEncoder().encode(roster) {
await storage?.setRoster(jid: state.jid, roster: data)
}
return nil
}
}
// <iq to='testmon3@test.anal.company/TwtWkVOZ3liz' type='result' id='7l899q9r'>
// <query ver='27' xmlns='jabber:iq:roster'>
// <item jid='fmodf@conversations.im' subscription='both' xmlns='jabber:iq:roster'/>
// <item subscription='to' jid='testmon4@test.anal.company' xmlns='jabber:iq:roster'/>
// </query>
// </iq>