wip
This commit is contained in:
parent
5510dd3900
commit
0be3f68710
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
112
AnotherIM/View/TestScreen.swift
Normal file
112
AnotherIM/View/TestScreen.swift
Normal 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)")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue