roster get

This commit is contained in:
fmodf 2024-03-03 20:07:16 +04:00
parent ed82109382
commit dfa9ce3ec5
24 changed files with 204 additions and 58 deletions

View file

@ -4,4 +4,5 @@ enum AppAction: Codable {
case changeFlow(_ flow: AppFlow)
case accountsAction(_ action: AccountsAction)
case registrationAction(_ action: RegistrationAction)
case contactsAction(_ action: ContactsAction)
}

View file

@ -0,0 +1,3 @@
enum ContactsAction: Codable {
case remoteContactsFetched
}

View file

@ -26,6 +26,9 @@ extension AppStore {
} else {
dispatch(.registrationAction(.clientStateChanged(.loginError)))
}
case .clientRosterFetched(let contacts):
print(contacts)
}
}
}

View file

@ -1,5 +1,5 @@
import Foundation
import Combine
import Foundation
let isConsoleLoggingEnabled = false
@ -13,11 +13,11 @@ let isConsoleLoggingEnabled = false
default:
let timeStr = dateFormatter.string(from: Date())
var actionStr = "\(action)"
actionStr = String(actionStr.prefix(200)) + " ..."
actionStr = String(actionStr.prefix(500)) + " ..."
var stateStr = "\(state)"
stateStr = String(stateStr.prefix(200)) + " ..."
stateStr = String(stateStr.prefix(500)) + " ..."
let str = "\(timeStr) \(actionStr)\n\(timeStr)\(stateStr)\n"
let str = "\(timeStr) \(actionStr)\n\(timeStr)\(stateStr)\n\n"
print(str)
if isConsoleLoggingEnabled {
NSLog(str)

View file

@ -17,6 +17,9 @@ extension AppState {
case let .registrationAction(action):
RegistrationState.reducer(state: &state.registrationState, action: action)
case let .contactsAction(action):
print("action")
}
}
}

View file

@ -1,7 +1,7 @@
import Foundation
import KeychainAccess
// CHANGE THIS PREFIX TO YOURS!!!
// TODO: CHANGE THIS PREFIX TO YOURS!!!
private let group: String = "U6CKGHL5VR.imt.narayana.snikket.ios"
private let path: String = "snikket.db.key"
private let service: String = "snikket"

View file

@ -0,0 +1,17 @@
import SwiftUI
struct ChatCell: View {
var body: some View {
Text("O")
}
}
#if DEBUG
struct ChatCell_Previews: PreviewProvider {
static var previews: some View {
showPreview(stateConfig: { state in
state.currentFlow = .chats
})
}
}
#endif

View file

@ -1,30 +1,48 @@
import SwiftUI
// A SWiftUI big view which represent chats in Telegram-like manner
struct ChatsMainScreen: View {
@EnvironmentObject var store: AppStore
var body: some View {
VStack {
SharedHeader(title: "Chats")
VStack {
Text("Under construction")
// list of chats
Text("s")
}
.frame(maxHeight: .infinity)
.frame(maxHeight: .infinity)
SharedTabBar()
.frame(height: 48)
}
.background(Color.Main.backgroundLight)
.background(Color.Main.backgroundLight)
}
}
#if DEBUG
struct ChatsMainScreen_Previews: PreviewProvider {
static var previews: some View {
showPreview(stateConfig: { state in
state.currentFlow = .chats
})
}
}
#endif
// struct ChatsMainScreen: View {
// @EnvironmentObject var store: AppStore
//
// var body: some View {
// VStack {
// SharedHeader(title: "Chats")
//
// VStack {
// Text("Under construction")
// }
// .frame(maxHeight: .infinity)
//
// SharedTabBar()
// .frame(height: 48)
// }
// .background(Color.Main.backgroundLight)
// }
// }
//
// #if DEBUG
// struct ChatsMainScreen_Previews: PreviewProvider {
// static var previews: some View {
// showPreview(stateConfig: { state in
// state.currentFlow = .chats
// })
// }
// }
// #endif

View file

@ -1,11 +1,8 @@
// swiftlint:disable all
/*
This file contains full state for xcode preview purposes.
Please, change this file only if you update state during development
(if you need something different in state - just change it in preview locally)
Will work only on debug builds
*/
// This file contains full state for xcode preview purposes.
// Please, change this file only if you update state during development
// (if you need something different in state - just change it in preview locally)
//
// Will work only on debug builds
#if DEBUG
import SwiftUI

View file

@ -7,6 +7,7 @@ enum ClientAction: Codable {
case streamAction(_ action: StreamAction)
case authAction(_ action: AuthAction)
case bindResourceAction(_ action: BindResourceAction)
case rosterAction(_ action: RosterAction)
case externalServiceAction(_ action: ExternalServicesAction)
case callAction(_ action: CallAction)
}

View file

@ -0,0 +1,5 @@
import Foundation
enum RosterAction: Codable {
case getRoster
}

View file

@ -13,4 +13,5 @@ extension String {
let data = Data(base64Encoded: self)!
return String(data: data, encoding: .utf8)!
}
// swiftlint:enable force_unwrapping
}

View file

@ -1,11 +1,11 @@
import Foundation
import Combine
import Foundation
import Network
final class BindResourceMiddleware {
private var reqId = ""
func middleware(state: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
func middleware(state _: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
switch action {
case .bindResourceAction(.bindResource):
let resourceName = "iPhone-" + XMLElement.randomId
@ -28,11 +28,13 @@ final class BindResourceMiddleware {
case .parserAction(.elementParsed(let element)):
if element.name == "iq", element.attributes["id"] == reqId, element.attributes["type"] == "result" {
if let jidStr = element
.nodes.first(where: { $0.name == "bind" })?
.nodes.first(where: { $0.name == "jid" })?
.content,
let newJid = try? JID(jidStr) {
if
let jidStr = element
.nodes.first(where: { $0.name == "bind" })?
.nodes.first(where: { $0.name == "jid" })?
.content,
let newJid = try? JID(jidStr)
{
return Just(ClientAction.bindResourceAction(.updateJid(newJid)))
.eraseToAnyPublisher()
} else {
@ -47,6 +49,10 @@ final class BindResourceMiddleware {
return Just(ClientAction.bindResourceAction(.resourceBound))
.eraseToAnyPublisher()
case .bindResourceAction(.resourceBound):
return Just(ClientAction.rosterAction(.getRoster))
.eraseToAnyPublisher()
default:
return Empty().eraseToAnyPublisher()
}

View file

@ -1,5 +1,5 @@
import Foundation
import Combine
import Foundation
let isConsoleLoggingEnabled = false
@ -12,10 +12,11 @@ final class LoggerMiddleware {
default:
let timeStr = dateFormatter.string(from: Date())
let len = lineLength(action)
var actionStr = "\(action)"
let ends = actionStr.count > 800 ? "..." : ""
actionStr = String(actionStr.prefix(800)) + ends
let str = "\(timeStr) xmpp-client \(state.jid): \(lineSymbol(action))\(actionStr)"
let ends = actionStr.count > len ? "..." : ""
actionStr = String(actionStr.prefix(len)) + ends
let str = "\(timeStr) 🔁 xmpp-client \(state.jid): \(lineSymbol(action))\(actionStr)"
print(str)
if isConsoleLoggingEnabled {
NSLog(str)
@ -51,4 +52,14 @@ final class LoggerMiddleware {
return ""
}
}
private func lineLength(_ action: ClientAction) -> Int {
switch action {
case .rosterAction:
return 1000
default:
return 100
}
}
}

View file

@ -0,0 +1,45 @@
import Combine
import Foundation
final class RosterMiddleware {
private var reqId = ""
func middleware(state: ClientState, action: ClientAction) -> AnyPublisher<ClientAction, Never> {
switch action {
case .rosterAction(.getRoster):
reqId = XMLElement.randomId
let req = XMLElement(
name: "iq",
xmlns: nil,
attributes: [
"id": reqId,
"from": state.jid.description,
"type": "get"
],
content: nil,
nodes: [
.init(
name: "query",
xmlns: "jabber:iq:roster",
attributes: [:],
content: nil,
nodes: []
)
]
)
return Just(ClientAction.streamAction(.streamSend(req)))
.eraseToAnyPublisher()
case .parserAction(.elementParsed(let element)):
if element.name == "iq", element.attributes["id"] == reqId, element.attributes["type"] == "result" {
print(element)
return Empty().eraseToAnyPublisher()
} else {
return Empty().eraseToAnyPublisher()
}
default:
return Empty().eraseToAnyPublisher()
}
}
}

View file

@ -1,5 +1,5 @@
import Foundation
import Combine
import Foundation
import Network
final class StreamMiddleware {
@ -72,7 +72,7 @@ final class StreamMiddleware {
break
}
}
let text = element.nodes.first(where: { $0.name == "text"})?.content
let text = element.nodes.first(where: { $0.name == "text" })?.content
let error = StreamError(type: errType, text: text)
return Just(ClientAction.streamAction(.streamError(error)))
.eraseToAnyPublisher()
@ -134,20 +134,23 @@ private extension StreamMiddleware {
for node in element.nodes {
switch node.name {
case "mechanisms":
let mechanisms = node.nodes.map({
let mechanisms = node.nodes.map {
StreamFeature(
$0.name,
node.nodes.map({ $0.name }).contains("required"),
node.nodes.map { $0.name }.contains("required"),
_additionalInfo: $0.content
)
})
}
features.append(contentsOf: mechanisms)
case "register", "auth":
features.append(.init(node.name, false, _additionalInfo: node.xmlns))
case "bind":
features.append(.init(node.name, true))
default:
features.append(StreamFeature(node.name, node.nodes.map({ $0.name }).contains("required")))
features.append(StreamFeature(node.name, node.nodes.map { $0.name }.contains("required")))
}
}
return features

View file

@ -55,3 +55,4 @@ private extension JID {
}
}
}
// swiftlint:enable large_tuple

View file

@ -0,0 +1,16 @@
import Foundation
public enum ContactSubscription: String {
case none
case to
case from
case both
case remove
}
public struct RosterContact {
let jid: JID
let name: String
let subscription: ContactSubscription
let groups: [String]
}

View file

@ -171,7 +171,10 @@ final class Connection {
}
#if DEBUG
print("\(direction)\(src): \(String(bytes: data, encoding: .ascii) ?? "?")")
let flg = false
if flg {
print("\(direction)\(src): \(String(bytes: data, encoding: .ascii) ?? "?")")
}
#endif
}
}

View file

@ -29,6 +29,9 @@ extension ClientState {
default:
break
}
case .rosterAction:
break
case .externalServiceAction(let action):
ExternalServicesState.reducer(state: &state.externalServicesState, action: action)

View file

@ -1,5 +1,6 @@
import Foundation
// swiftlint:disable large_tuple
private typealias Replace = (orig: String, asName: String, asCode: String)
private let replaces = [
Replace("&", "&amp;", "&#38;"),
@ -8,6 +9,7 @@ private let replaces = [
Replace("\"", "&quot;", "&#34;"),
Replace("'", "&apos;", "&#39;")
]
// swiftlint:enable large_tuple
enum XMLEscaping {
static func unescape(_ value: String) -> String {

View file

@ -28,6 +28,7 @@ final class XMPPClient {
StreamMiddleware().middleware,
AuthMiddleware().middleware,
BindResourceMiddleware().middleware,
RosterMiddleware().middleware,
ExternalServiceMiddleware().middleware,
callMiddleware.middleware
]

View file

@ -4,28 +4,33 @@ import Foundation
// MARK: Private action processing
extension XMPPService {
func _doAction(_ action: Action) {
switch action.type {
case .initClient:
// actions with no client JID
if case .deinitClient = action.type {
// TODO: check that client disappears from memory
clients.removeValue(forKey: action.jid)
}
if case .initClient = action.type {
let client = XMPPClient(jid: action.jid, updateCallback: clientUpdated)
clients[action.jid] = client
feed.send(.init(jid: action.jid, eventType: .clientInitialised))
}
// actions with client JID
guard let client = clients[action.jid] else {
feed.send(.init(jid: action.jid, eventType: .serviceError(.unknownClient)))
return
}
switch action.type {
case .initClient:
break
case .deinitClient:
clients.removeValue(forKey: action.jid)
// TODO: check that client disappears from memory
break
case .connectToServer:
guard let client = clients[action.jid] else {
feed.send(.init(jid: action.jid, eventType: .serviceError(.unknownClient)))
return
}
client.reconnect()
case .login(let password):
guard let client = clients[action.jid] else {
feed.send(.init(jid: action.jid, eventType: .serviceError(.unknownClient)))
return
}
client.loginWith(password)
}
}

View file

@ -42,6 +42,7 @@ public extension XMPPService {
case clientInitialised
case serverConnectionEstablished
case clientAuthResult(authorized: Bool)
case clientRosterFetched(contacts: [RosterContact])
}
struct Event {