diff --git a/AnotherIM/AnotherIMApp.swift b/AnotherIM/AnotherIMApp.swift index 412ee7f..c405f8b 100644 --- a/AnotherIM/AnotherIMApp.swift +++ b/AnotherIM/AnotherIMApp.swift @@ -5,7 +5,8 @@ import SwiftUI struct AnotherIMApp: App { var body: some Scene { WindowGroup { - StartScreen() + // StartScreen() + TestScreen() } } } diff --git a/AnotherIM/View/StartScreen.swift b/AnotherIM/View/StartScreen.swift index 1c0028f..031adbc 100644 --- a/AnotherIM/View/StartScreen.swift +++ b/AnotherIM/View/StartScreen.swift @@ -1,14 +1,5 @@ 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 { var body: some View { ZStack { @@ -19,79 +10,5 @@ struct StartScreen: View { .frame(width: 200, height: 200) } .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] } } diff --git a/AnotherIM/View/TestScreen.swift b/AnotherIM/View/TestScreen.swift new file mode 100644 index 0000000..e916a0e --- /dev/null +++ b/AnotherIM/View/TestScreen.swift @@ -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)") + // } + // } +} diff --git a/AnotherIM/xmpp/XMPPClient.swift b/AnotherIM/xmpp/XMPPClient.swift index 157a835..0d6b183 100644 --- a/AnotherIM/xmpp/XMPPClient.swift +++ b/AnotherIM/xmpp/XMPPClient.swift @@ -101,14 +101,17 @@ final class XMPPClient { AuthorizationModule(self.storage), StanzaModule(self.storage), DiscoveryModule(), - RosterModule() + RosterModule(self.storage) ] init(storage: any XMPPStorage, userAgent: UserAgent) { self.storage = storage state.userAgent = userAgent } +} +// MARK: Public part +extension XMPPClient { func tryLogin(jid: JID, credentialsId: UUID) { logger.update(jid.full) Task { diff --git a/AnotherIM/xmpp/XMPPStorage.swift b/AnotherIM/xmpp/XMPPStorage.swift index e3bca87..11b8186 100644 --- a/AnotherIM/xmpp/XMPPStorage.swift +++ b/AnotherIM/xmpp/XMPPStorage.swift @@ -7,8 +7,11 @@ protocol XMPPStorage: AnyObject { func getCredentialsByUUID(_ uuid: UUID) async -> Credentials? // roster - func getRosterVersion(jid: JID) async -> String? - func setRosterVersion(jid: JID, ver: String) async + func deleteRoster(jid: JID) 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 diff --git a/AnotherIM/xmpp/models/Roster.swift b/AnotherIM/xmpp/models/Roster.swift index 5997d1a..796d8b2 100644 --- a/AnotherIM/xmpp/models/Roster.swift +++ b/AnotherIM/xmpp/models/Roster.swift @@ -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! diff --git a/AnotherIM/xmpp/modules/roster/RosterModule.swift b/AnotherIM/xmpp/modules/roster/RosterModule.swift index 6df5a43..e0488b9 100644 --- a/AnotherIM/xmpp/modules/roster/RosterModule.swift +++ b/AnotherIM/xmpp/modules/roster/RosterModule.swift @@ -1,8 +1,15 @@ import Foundation +// TODO: add versioning (XEP-0237) if needed final class RosterModule: XmppModule { let id = "Roseter module" + private weak var storage: (any XMPPStorage)? + + init(_ storage: any XMPPStorage) { + self.storage = storage + } + func reduce(oldState: ClientState, with _: Event) -> ClientState { oldState } @@ -13,16 +20,57 @@ final class RosterModule: XmppModule { return .requestRoster case .requestRoster: - // TODO: check version! - let req = Stanza.iqGet(from: state.jid.full, payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: [])) + let req = Stanza.iqGet( + from: state.jid.full, + payload: XMLElement(name: "query", xmlns: "jabber:iq:roster", attributes: [:], content: nil, nodes: []) + ) if let req { return .stanzaOutbound(req) } else { 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: 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 + } +} + +// +// +// +// +// +//