Compare commits

..

35 Commits

Author SHA1 Message Date
Evgeny Poberezkin f856bb5faf Merge branch 'master' into ab/async-subs 2024-06-24 16:52:36 +01:00
Evgeny Poberezkin 41e6e6b54e revert some changes 2024-06-24 15:35:57 +01:00
Alexander Bondarenko 137656f13f Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-06-12 22:56:01 +03:00
Alexander Bondarenko 5c44f0e17e Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-06-03 15:46:11 +03:00
Alexander Bondarenko baced8a6da use connections from getConnectionEntity for the subscribing user 2024-06-03 15:21:16 +03:00
IC Rainbow 6548245883 request empty network statuses explicitly 2024-05-31 13:07:09 +03:00
Alexander Bondarenko 63026acf46 remove reference queries 2024-05-30 21:36:10 +03:00
Alexander Bondarenko 900aad60e9 update nix 2024-05-30 21:21:03 +03:00
IC Rainbow e5e231fe9e add user index on user contact links 2024-05-30 21:16:24 +03:00
IC Rainbow eb11e6c409 request names only when enabled 2024-05-30 20:56:03 +03:00
Alexander Bondarenko e045aa214d request names only for errored contacts 2024-05-30 20:45:43 +03:00
Alexander Bondarenko 6311a581cf clenup 2024-05-30 20:27:07 +03:00
Alexander Bondarenko 370936aaa1 hide up/down details in CLI unless requested with -c 2024-05-30 20:26:15 +03:00
Alexander Bondarenko 7b108536fd send only errors contact results 2024-05-30 20:25:41 +03:00
Alexander Bondarenko 13d779f41d stripped contactLinkSubs 2024-05-30 20:22:44 +03:00
Alexander Bondarenko ed12f34330 Merge remote-tracking branch 'origin/master' into ab/async-subs 2024-05-30 19:08:51 +03:00
Alexander Bondarenko ddd9c6f16f more races 2024-05-29 23:28:42 +03:00
Alexander Bondarenko 561c923cdb fix network status 2024-05-29 22:37:45 +03:00
Alexander Bondarenko a126215c8c fix bg conns 2024-05-29 22:25:55 +03:00
Alexander Bondarenko f21b5af568 more races 2024-05-29 22:00:47 +03:00
Alexander Bondarenko 73bd003984 fix race in test 2024-05-29 21:41:21 +03:00
Alexander Bondarenko 4d2452b03f test that light query results match the original 2024-05-29 20:29:14 +03:00
Alexander Bondarenko 4ceb0dc564 remove pendingContact details 2024-05-29 16:45:43 +03:00
Alexander Bondarenko 551a34dd1a log forked subscriber errors 2024-05-29 16:35:35 +03:00
Alexander Bondarenko 30f00c2f2e lightweight queries 2024-05-29 16:18:33 +03:00
Alexander Bondarenko b94ced6b39 restore rare entities prefetch 2024-05-29 15:12:52 +03:00
Alexander Bondarenko a1216d86fd process user links 2024-05-29 12:38:46 +03:00
Alexander Bondarenko b6cf81e680 fix ok sizes in summary 2024-05-28 22:11:27 +03:00
Alexander Bondarenko 998907f107 process group results 2024-05-28 21:54:13 +03:00
Alexander Bondarenko a975ffe82a add rfc 2024-05-28 16:42:51 +03:00
Alexander Bondarenko a1d2f4cda9 give active user a minor priority 2024-05-27 22:22:37 +03:00
Alexander Bondarenko cd3992fd0f WIP 2024-05-27 22:12:39 +03:00
Alexander Bondarenko 1bc47c6910 inline calls 2024-05-25 11:04:01 +03:00
Alexander Bondarenko ab07096235 remove unused code 2024-05-25 11:03:28 +03:00
Alexander Bondarenko 1295e538ed WIP: remove sync connection replies 2024-05-25 11:03:28 +03:00
42 changed files with 417 additions and 1940 deletions
-28
View File
@@ -487,11 +487,6 @@ func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg])
try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers))
}
func addKnownProtoServer(server: String) async throws {
let userId = try currentUserId("addKnownProtoServer")
try await sendCommandOkResp(.apiAddKnownProtoServer(userId: userId, server: server))
}
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
let userId = try currentUserId("testProtoServer")
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
@@ -546,11 +541,6 @@ func reconnectAllServers() async throws {
try await sendCommandOkResp(.reconnectAllServers)
}
func reconnectServer(smpServer: String) async throws {
let userId = try currentUserId("reconnectServer")
try await sendCommandOkResp(.reconnectServer(userId: userId, smpServer: smpServer))
}
func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws {
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
}
@@ -1344,24 +1334,6 @@ func apiGetVersion() throws -> CoreVersionInfo {
throw r
}
func getAgentServersSummary() throws -> PresentedServersSummary {
let userId = try currentUserId("getAgentServersSummary")
let r = chatSendCmdSync(.getAgentServersSummary(userId: userId))
if case let .agentServersSummary(_, serversSummary) = r { return serversSummary }
throw r
}
func resetAgentServersStats() async throws {
try await sendCommandOkResp(.resetAgentServersStats)
}
func getAgentSubsSummary() throws -> SMPServerSubs {
let userId = try currentUserId("getAgentSubsSummary")
let r = chatSendCmdSync(.getAgentSubsSummary(userId: userId))
if case let .agentSubsSummary(_, subsSummary) = r { return subsSummary }
throw r
}
private func currentUserId(_ funcName: String) throws -> Int64 {
if let userId = ChatModel.shared.currentUser?.userId {
return userId
@@ -115,7 +115,9 @@ struct ChatListView: View {
HStack(spacing: 4) {
Text("Chats")
.font(.headline)
SubsStatusIndicator()
if chatModel.chats.count > 0 {
toggleFilterButton()
}
}
.frame(maxWidth: .infinity, alignment: .center)
}
@@ -129,6 +131,15 @@ struct ChatListView: View {
}
}
private func toggleFilterButton() -> some View {
Button {
showUnreadAndFavorites = !showUnreadAndFavorites
} label: {
Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : ""))
.foregroundColor(.accentColor)
}
}
@ViewBuilder private var chatList: some View {
let cs = filteredChats()
ZStack {
@@ -263,66 +274,6 @@ struct ChatListView: View {
}
}
struct SubsStatusIndicator: View {
@State private var subs: SMPServerSubs = SMPServerSubs(ssActive: 0, ssPending: 0)
@State private var timer: Timer? = nil
@State private var timerCounter = 0
@State private var showServersSummary = false
// Constants for the intervals
let initialInterval: TimeInterval = 1.0
let regularInterval: TimeInterval = 5.0
let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds
var body: some View {
Button {
showServersSummary = true
} label: {
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending, variableValueAsPercentage: true)
}
.onAppear {
startInitialTimer()
}
.onDisappear {
stopTimer()
}
.sheet(isPresented: $showServersSummary) {
ServersSummaryView()
}
}
private func startInitialTimer() {
timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in
getSubsSummary()
timerCounter += 1
// Switch to the regular timer after the initial phase
if timerCounter * Int(initialInterval) >= Int(initialPhaseDuration) {
switchToRegularTimer()
}
}
}
func switchToRegularTimer() {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: regularInterval, repeats: true) { _ in
getSubsSummary()
}
}
func stopTimer() {
timer?.invalidate()
timer = nil
}
private func getSubsSummary() {
do {
subs = try getAgentSubsSummary()
} catch let error {
logger.error("getAgentSubsSummary error: \(responseError(error))")
}
}
}
struct ChatListSearchBar: View {
@EnvironmentObject var m: ChatModel
@Binding var searchMode: Bool
@@ -331,9 +282,9 @@ struct ChatListSearchBar: View {
@Binding var searchShowingSimplexLink: Bool
@Binding var searchChatFilteredBySimplexLink: String?
@State private var ignoreSearchTextChange = false
@State private var showScanCodeSheet = false
@State private var alert: PlanAndConnectAlert?
@State private var sheet: PlanAndConnectActionSheet?
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
VStack(spacing: 12) {
@@ -350,6 +301,26 @@ struct ChatListSearchBar: View {
.onTapGesture {
searchText = ""
}
} else if !searchFocussed {
HStack(spacing: 24) {
if m.pasteboardHasStrings {
Image(systemName: "doc")
.onTapGesture {
if let str = UIPasteboard.general.string {
searchText = str
}
}
}
Image(systemName: "qrcode")
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.onTapGesture {
showScanCodeSheet = true
}
}
.padding(.trailing, 2)
}
}
.padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7))
@@ -364,12 +335,14 @@ struct ChatListSearchBar: View {
searchText = ""
searchFocussed = false
}
} else if m.chats.count > 0 {
toggleFilterButton()
}
}
Divider()
}
.sheet(isPresented: $showScanCodeSheet) {
NewChatView(selection: .connect, showQRCodeScanner: true)
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) // fixes .refreshable in ChatListView affecting nested view
}
.onChange(of: searchFocussed) { sf in
withAnimation { searchMode = sf }
}
@@ -403,21 +376,6 @@ struct ChatListSearchBar: View {
}
}
private func toggleFilterButton() -> some View {
ZStack {
Color.clear
.frame(width: 22, height: 22)
Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease")
.resizable()
.scaledToFit()
.foregroundColor(showUnreadAndFavorites ? .accentColor : .secondary)
.frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16)
.onTapGesture {
showUnreadAndFavorites = !showUnreadAndFavorites
}
}
}
private func connect(_ link: String) {
planAndConnect(
link,
@@ -1,629 +0,0 @@
//
// ServersSummaryView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 25.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ServersSummaryView: View {
@State private var serversSummary: PresentedServersSummary? = nil
@State private var selectedUserCategory: PresentedUserCategory = .allUsers
@State private var selectedServerType: PresentedServerType = .smp
@State private var selectedSMPServer: String? = nil
@State private var selectedXFTPServer: String? = nil
@State private var alert: SomeAlert?
enum PresentedUserCategory {
case currentUser
case allUsers
}
enum PresentedServerType {
case smp
case xftp
}
var body: some View {
NavigationView {
viewBody()
.navigationTitle("Servers info")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
reloadButton()
}
ToolbarItem(placement: .navigationBarTrailing) {
shareButton()
}
}
}
.onAppear {
getServersSummary()
}
.alert(item: $alert) { $0.alert }
}
private func shareButton() -> some View {
Button {
if let serversSummary = serversSummary {
showShareSheet(items: [encodePrettyPrinted(serversSummary)])
}
} label: {
Image(systemName: "square.and.arrow.up")
}
.disabled(serversSummary == nil)
}
public func encodePrettyPrinted<T: Encodable>(_ value: T) -> String {
let encoder = jsonEncoder
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(value)
return String(decoding: data, as: UTF8.self)
}
private func reloadButton() -> some View {
Button {
getServersSummary()
} label: {
Image(systemName: "arrow.counterclockwise")
}
}
@ViewBuilder private func viewBody() -> some View {
if let summ = serversSummary {
List {
Group {
Picker("User selection", selection: $selectedUserCategory) {
Text("All users").tag(PresentedUserCategory.allUsers)
Text("Current user").tag(PresentedUserCategory.currentUser)
}
.pickerStyle(.segmented)
Picker("Server type", selection: $selectedServerType) {
Text("SMP").tag(PresentedServerType.smp)
Text("XFTP").tag(PresentedServerType.xftp)
}
.pickerStyle(.segmented)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
switch (selectedUserCategory, selectedServerType) {
case (.allUsers, .smp):
if summ.allUsedSMP.count > 0 || summ.allPrevSMP.count > 0 || summ.allProxSMP.count > 0 {
if summ.allUsedSMP.count > 0 {
smpServersListView(summ.allUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Current session")
}
if summ.allPrevSMP.count > 0 {
smpServersListView(summ.allPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used")
}
if summ.allProxSMP.count > 0 {
smpServersListView(summ.allProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.currentUser, .smp):
if summ.userUsedSMP.count > 0 || summ.userPrevSMP.count > 0 || summ.userProxSMP.count > 0 {
if summ.userUsedSMP.count > 0 {
smpServersListView(summ.userUsedSMP, showReconnectButton: true, summ.statsStartedAt, "Current session")
}
if summ.userPrevSMP.count > 0 {
smpServersListView(summ.userPrevSMP, showReconnectButton: false, summ.statsStartedAt, "Previously used")
}
if summ.userProxSMP.count > 0 {
smpServersListView(summ.userProxSMP, showReconnectButton: false, summ.statsStartedAt, "Proxied", "You are not connected to these servers directly.")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.allUsers, .xftp):
if summ.allUsedXFTP.count > 0 || summ.allPrevXFTP.count > 0 {
if summ.allUsedXFTP.count > 0 {
xftpServersListView(summ.allUsedXFTP, summ.statsStartedAt, "Current session")
}
if summ.allPrevXFTP.count > 0 {
xftpServersListView(summ.allPrevXFTP, summ.statsStartedAt, "Previously used")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
case (.currentUser, .xftp):
if summ.userUsedXFTP.count > 0 || summ.userPrevXFTP.count > 0 {
if summ.userUsedXFTP.count > 0 {
xftpServersListView(summ.userUsedXFTP, summ.statsStartedAt, "Current session")
}
if summ.userPrevXFTP.count > 0 {
xftpServersListView(summ.userPrevXFTP, summ.statsStartedAt, "Previously used")
}
resetStatsButtonSection()
} else {
noCategoryInfoText()
}
}
}
} else {
Text("No info, try to reload")
}
}
@ViewBuilder private func smpServersListView(
_ servers: [SMPServerSummary],
showReconnectButton: Bool,
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
_ footer: LocalizedStringKey? = nil
) -> some View {
let sortedServers = servers.sorted { serverAddress($0.smpServer).compare(serverAddress($1.smpServer)) == .orderedAscending }
Section {
ForEach(sortedServers) { server in
smpServerView(server, showReconnectButton, statsStartedAt)
}
} header: {
if let header = header {
Text(header)
}
} footer: {
if let footer = footer {
Text(footer)
}
}
}
private func smpServerView(_ server: SMPServerSummary, _ showReconnectButton: Bool, _ statsStartedAt: Date) -> some View {
NavigationLink(tag: server.id, selection: $selectedSMPServer) {
SMPServerSummaryView(
summary: server,
showReconnectButton: showReconnectButton,
statsStartedAt: statsStartedAt
)
.navigationBarTitle("SMP server")
.navigationBarTitleDisplayMode(.large)
} label: {
HStack {
if let subs = server.subs {
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending)
.frame(width: 16, alignment: .center)
.padding(.trailing, 4)
}
Text(serverAddress(server.smpServer))
.lineLimit(1)
}
}
}
private func serverAddress(_ server: String) -> String {
parseServerAddress(server)?.hostnames.first ?? server
}
@ViewBuilder private func xftpServersListView(
_ servers: [XFTPServerSummary],
_ statsStartedAt: Date,
_ header: LocalizedStringKey? = nil,
_ footer: LocalizedStringKey? = nil
) -> some View {
let sortedServers = servers.sorted { serverAddress($0.xftpServer).compare(serverAddress($1.xftpServer)) == .orderedAscending }
Section {
ForEach(sortedServers) { server in
xftpServerView(server, statsStartedAt)
}
} header: {
if let header = header {
Text(header)
}
} footer: {
if let footer = footer {
Text(footer)
}
}
}
private func xftpServerView(_ server: XFTPServerSummary, _ statsStartedAt: Date) -> some View {
NavigationLink(tag: server.id, selection: $selectedXFTPServer) {
XFTPServerSummaryView(
summary: server,
statsStartedAt: statsStartedAt
)
.navigationBarTitle("XFTP server")
.navigationBarTitleDisplayMode(.large)
} label: {
Text(serverAddress(server.xftpServer))
.lineLimit(1)
}
}
private func noCategoryInfoText() -> some View {
ZStack {
Rectangle()
.aspectRatio(contentMode: .fill)
.foregroundColor(Color.clear)
Text("No info")
.foregroundColor(.secondary)
}
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
}
private func resetStatsButtonSection() -> some View {
Section {
Button {
alert = SomeAlert(
alert: Alert(
title: Text("Reset servers statistics?"),
message: Text("Servers statistics will be reset - this cannot be undone!"),
primaryButton: .destructive(Text("Reset")) {
Task {
do {
try await resetAgentServersStats()
getServersSummary()
} catch let error {
alert = SomeAlert(
alert: mkAlert(
title: "Error resetting statistics",
message: "\(responseError(error))"
),
id: "error resetting statistics"
)
}
}
},
secondaryButton: .cancel()
),
id: "reset statistics question"
)
} label: {
Text("Reset statistics")
}
}
}
private func getServersSummary() {
do {
serversSummary = try getAgentServersSummary()
} catch let error {
logger.error("getAgentServersSummary error: \(responseError(error))")
}
}
}
struct SubscriptionStatusView: View {
@EnvironmentObject var m: ChatModel
var activeSubs: Int
var pendingSubs: Int
var variableValueAsPercentage: Bool = false
var body: some View {
let netInfo = m.networkInfo
if netInfo.online {
let (image, color, variableValue, opacity) = networkOnlineImage(netInfo.networkType)
if #available(iOS 16.0, *) {
Image(systemName: image, variableValue: variableValue)
.foregroundColor(color)
} else {
Image(systemName: image)
.foregroundColor(color.opacity(opacity))
}
} else {
Image(systemName: "wifi.slash")
.foregroundColor(.secondary)
}
}
func networkOnlineImage(_ networkType: UserNetworkType) -> (String, Color, Double, Double) {
switch networkType {
case .cellular:
let (color, variableValue, opacity) = cellularbarsColor
return ("cellularbars", color, variableValue, opacity)
default:
let (color, variableValue, opacity) = wifiColor
return ("wifi", color, variableValue, opacity)
}
}
// We manipulate variableValue so all "wifi" sections are filled only with 100% active subs,
// unless variableValueAsPercentage is true; same for cellularbarsColor
var wifiColor: (Color, Double, Double) {
if activeSubs > 0 {
let wifiVariableValue = (
variableValueAsPercentage ? activeSubsPercentage
: ( // "wifi" has 3 sections
activeSubsPercentage >= 1 ? 1
: (activeSubsPercentage >= 0.5 && activeSubsPercentage < 1) ? 0.6
: (activeSubsPercentage > 0 && activeSubsPercentage < 0.5) ? 0.3
: 0
)
)
return (.accentColor, wifiVariableValue, activeSubsPercentage)
} else {
return (.secondary, 1, 1)
}
}
var cellularbarsColor: (Color, Double, Double) {
if activeSubs > 0 {
let wifiVariableValue = (
variableValueAsPercentage ? activeSubsPercentage
: ( // "cellularbars" has 4 sections
activeSubsPercentage >= 1 ? 1
: (activeSubsPercentage >= 0.67 && activeSubsPercentage < 1) ? 0.7
: (activeSubsPercentage >= 0.33 && activeSubsPercentage < 0.67) ? 0.45
: (activeSubsPercentage > 0 && activeSubsPercentage < 0.33) ? 0.2
: 0
)
)
return (.accentColor, wifiVariableValue, activeSubsPercentage)
} else {
return (.secondary, 1, 1)
}
}
var activeSubsPercentage: Double {
let total = activeSubs + pendingSubs
guard total != 0 else { return 0.0 }
return Double(activeSubs) / Double(total)
}
}
struct SMPServerSummaryView: View {
var summary: SMPServerSummary
var showReconnectButton: Bool
var statsStartedAt: Date
@State private var alert: SomeAlert?
var body: some View {
List {
Section {
Text(summary.smpServer)
.textSelection(.enabled)
if let known = summary.known, !known {
Button {
addKnownServer()
} label: {
Text("Add as known")
}
}
} header: {
Text("Server address")
} footer: {
if let known = summary.known, known {
Text("Server is configured in **Settings** → **Network & servers**.")
}
}
if showReconnectButton {
reconnectButtonSection()
}
if let subs = summary.subs {
subsSection(subs)
}
if let sess = summary.sessions {
sessionsSection(sess)
}
if let stats = summary.stats {
statsSection(stats)
}
}
.alert(item: $alert) { $0.alert }
}
private func reconnectButtonSection() -> some View {
Section {
Button {
alert = SomeAlert(
alert: Alert(
title: Text("Reconnect server?"),
message: Text("Reconnect server to force message delivery. It uses additional traffic."),
primaryButton: .default(Text("Ok")) {
Task {
do {
try await reconnectServer(smpServer: summary.smpServer)
} catch let error {
alert = SomeAlert(
alert: mkAlert(
title: "Error reconnecting server",
message: "\(responseError(error))"
),
id: "error reconnecting server"
)
}
}
},
secondaryButton: .cancel()
),
id: "reconnect server question"
)
} label: {
Text("Reconnect")
}
}
}
private func subsSection(_ subs: SMPServerSubs) -> some View {
Section {
infoRow("Active", "\(subs.ssActive)")
infoRow("Pending", "\(subs.ssPending)")
} header: {
HStack {
Text("Subscriptions")
SubscriptionStatusView(activeSubs: subs.ssActive, pendingSubs: subs.ssPending)
}
}
}
private func sessionsSection(_ sess: ServerSessions) -> some View {
Section("Sessions") {
infoRow("Connected", "\(sess.ssConnected)")
infoRow("Errors", "\(sess.ssErrors)")
infoRow("Connecting", "\(sess.ssConnecting)")
}
}
private func statsSection(_ stats: AgentSMPServerStatsData) -> some View {
Section("Statistics") {
infoRow("Starting from", localTimestamp(statsStartedAt))
infoRow("Messages sent directly", "\(stats._sentDirect)")
indentedInfoRow("attempts", "\(stats._sentDirectAttempts)")
infoRow("Messages sent via proxy", "\(stats._sentViaProxy)")
indentedInfoRow("attempts", "\(stats._sentViaProxyAttempts)")
infoRow("Messages sent to proxy", "\(stats._sentProxied)")
indentedInfoRow("attempts", "\(stats._sentProxiedAttempts)")
infoRow("Sending AUTH errors", "\(stats._sentAuthErrs)")
indentedInfoRow("QUOTA errors", "\(stats._sentQuotaErrs)")
indentedInfoRow("expired", "\(stats._sentExpiredErrs)")
indentedInfoRow("other errors", "\(stats._sentOtherErrs)")
infoRow("Messages received", "\(stats._recvMsgs)")
indentedInfoRow("duplicates", "\(stats._recvDuplicates)")
indentedInfoRow("decryption errors", "\(stats._recvCryptoErrs)")
indentedInfoRow("other errors", "\(stats._recvErrs)")
infoRow("Messages acknowledged", "\(stats._ackMsgs)")
indentedInfoRow("attempts", "\(stats._ackAttempts)")
infoRow("Connections created", "\(stats._connCreated)")
indentedInfoRow("secured", "\(stats._connSecured)")
indentedInfoRow("completed", "\(stats._connCompleted)")
infoRow("Connections deleted", "\(stats._connDeleted)")
infoRow("Connections subscribed", "\(stats._connSubscribed)")
indentedInfoRow("attempts", "\(stats._connSubAttempts)")
indentedInfoRow("errors", "\(stats._connSubErrs)")
}
}
func addKnownServer() {
Task {
do {
try await addKnownProtoServer(server: summary.smpServer)
await MainActor.run {
// TODO disable button, etc.
}
} catch let error {
await MainActor.run {
alert = errorAddingServerAlert(error)
}
}
}
}
}
private func indentedInfoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
HStack {
Text(title)
.padding(.leading, 24)
Spacer()
Text(value)
.foregroundStyle(.secondary)
}
}
private func errorAddingServerAlert(_ error: Error) -> SomeAlert {
SomeAlert(
alert: mkAlert(
title: "Error adding server",
message: "Make sure server address is in correct format (\(responseError(error)))."
),
id: "error saving server"
)
}
struct XFTPServerSummaryView: View {
var summary: XFTPServerSummary
var statsStartedAt: Date
@State private var alert: SomeAlert?
var body: some View {
List {
Section {
Text(summary.xftpServer)
.textSelection(.enabled)
if let known = summary.known, !known {
Button {
addKnownServer()
} label: {
Text("Add as known")
}
}
} header: {
Text("Server address")
} footer: {
if let known = summary.known, known {
Text("Server is configured in **Settings** → **Network & servers**.")
}
}
if let sess = summary.sessions {
sessionsSection(sess)
}
inProgressSection()
if let stats = summary.stats {
statsSection(stats)
}
}
.alert(item: $alert) { $0.alert }
}
private func sessionsSection(_ sess: ServerSessions) -> some View {
Section("Sessions") {
infoRow("Connected", "\(sess.ssConnected)")
infoRow("Errors", "\(sess.ssErrors)")
infoRow("Connecting", "\(sess.ssConnecting)")
}
}
private func inProgressSection() -> some View {
Section("In progress") {
localizedInfoRow("Download", boolYesNo(summary.rcvInProgress))
localizedInfoRow("Upload", boolYesNo(summary.sndInProgress))
localizedInfoRow("Deletion", boolYesNo(summary.delInProgress))
}
}
private func boolYesNo(_ b: Bool) -> LocalizedStringKey {
b ? "yes" : "no"
}
private func statsSection(_ stats: AgentXFTPServerStatsData) -> some View {
Section("Statistics") {
infoRow("Starting from", localTimestamp(statsStartedAt))
infoRow("Chunks uploaded", "\(stats._uploads)")
indentedInfoRow("attempts", "\(stats._uploadAttempts)")
indentedInfoRow("errors", "\(stats._uploadErrs)")
infoRow("Chunks downloaded", "\(stats._downloads)")
indentedInfoRow("attempts", "\(stats._downloadAttempts)")
indentedInfoRow("AUTH errors", "\(stats._downloadAuthErrs)")
indentedInfoRow("other errors", "\(stats._downloadErrs)")
infoRow("Chunks deleted", "\(stats._deletions)")
indentedInfoRow("attempts", "\(stats._deleteAttempts)")
indentedInfoRow("errors", "\(stats._deleteErrs)")
}
}
func addKnownServer() {
Task {
do {
try await addKnownProtoServer(server: summary.xftpServer)
await MainActor.run {
// TODO disable button, etc.
}
} catch let error {
await MainActor.run {
alert = errorAddingServerAlert(error)
}
}
}
}
}
#Preview {
ServersSummaryView()
}
@@ -10,7 +10,6 @@ import SwiftUI
enum NewChatMenuOption: Identifiable {
case newContact
case scanPaste
case newGroup
var id: Self { self }
@@ -26,11 +25,6 @@ struct NewChatMenuButton: View {
} label: {
Text("Add contact")
}
Button {
newChatMenuOption = .scanPaste
} label: {
Text("Scan / Paste link")
}
Button {
newChatMenuOption = .newGroup
} label: {
@@ -45,7 +39,6 @@ struct NewChatMenuButton: View {
.sheet(item: $newChatMenuOption) { opt in
switch opt {
case .newContact: NewChatView(selection: .invite)
case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true)
case .newGroup: AddGroupView()
}
}
@@ -14,7 +14,6 @@ struct ProtocolServerView: View {
let serverProtocol: ServerProtocol
@Binding var server: ServerCfg
@State var serverToEdit: ServerCfg
@State var serverEnabled: Bool
@State private var showTestFailure = false
@State private var testing = false
@State private var testFailure: ProtocolTestFailure?
@@ -111,10 +110,7 @@ struct ProtocolServerView: View {
Spacer()
showTestStatus(server: serverToEdit)
}
Toggle("Use for new connections", isOn: $serverEnabled)
.onChange(of: serverEnabled) { enabled in
serverToEdit.enabled = enabled ? .enabled : .disabled
}
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
}
}
}
@@ -183,8 +179,7 @@ struct ProtocolServerView_Previews: PreviewProvider {
ProtocolServerView(
serverProtocol: .smp,
server: Binding.constant(ServerCfg.sampleData.custom),
serverToEdit: ServerCfg.sampleData.custom,
serverEnabled: true
serverToEdit: ServerCfg.sampleData.custom
)
}
}
@@ -159,7 +159,7 @@ struct ProtocolServersView: View {
}
private var allServersDisabled: Bool {
servers.allSatisfy { $0.enabled != .enabled }
servers.allSatisfy { !$0.enabled }
}
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
@@ -168,8 +168,7 @@ struct ProtocolServersView: View {
ProtocolServerView(
serverProtocol: serverProtocol,
server: server,
serverToEdit: srv,
serverEnabled: srv.enabled == .enabled
serverToEdit: srv
)
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
.navigationBarTitleDisplayMode(.large)
@@ -182,7 +181,7 @@ struct ProtocolServersView: View {
invalidServer()
} else if !uniqueAddress(srv, address) {
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
} else if srv.enabled != .enabled {
} else if !srv.enabled {
Image(systemName: "slash.circle").foregroundColor(.secondary)
} else {
showTestStatus(server: srv)
@@ -195,7 +194,7 @@ struct ProtocolServersView: View {
.padding(.trailing, 4)
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
if srv.enabled == .enabled {
if srv.enabled {
v
} else {
v.foregroundColor(.secondary)
@@ -236,7 +235,7 @@ struct ProtocolServersView: View {
private func addAllPresets() {
for srv in presetServers {
if !hasPreset(srv) {
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled))
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true))
}
}
}
@@ -261,7 +260,7 @@ struct ProtocolServersView: View {
private func resetTestStatus() {
for i in 0..<servers.count {
if servers[i].enabled == .enabled {
if servers[i].enabled {
servers[i].tested = nil
}
}
@@ -270,7 +269,7 @@ struct ProtocolServersView: View {
private func runServersTest() async -> [String: ProtocolTestFailure] {
var fs: [String: ProtocolTestFailure] = [:]
for i in 0..<servers.count {
if servers[i].enabled == .enabled {
if servers[i].enabled {
if let f = await testServerConnection(server: $servers[i]) {
fs[serverHostname(servers[i].server)] = f
}
@@ -40,7 +40,7 @@ struct ScanProtocolServer: View {
switch resp {
case let .success(r):
if parseServerAddress(r.string) != nil {
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: .enabled))
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: true))
dismiss()
} else {
showAddressError = true
+32 -36
View File
@@ -148,12 +148,6 @@
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; };
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
641753572C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; };
641753582C2AC158005415B4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753532C2AC158005415B4 /* libgmp.a */; };
641753592C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; };
6417535A2C2AC158005415B4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753552C2AC158005415B4 /* libgmpxx.a */; };
6417535B2C2AC158005415B4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 641753562C2AC158005415B4 /* libffi.a */; };
6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */; };
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
@@ -198,6 +192,11 @@
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; };
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3B2C22D78C00CBA347 /* libffi.a */; };
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; };
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3D2C22D78C00CBA347 /* libgmp.a */; };
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -444,12 +443,6 @@
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; };
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; path = "Libraries/libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = "<group>"; };
641753532C2AC158005415B4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmp.a; path = Libraries/libgmp.a; sourceTree = "<group>"; };
641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; path = "Libraries/libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = "<group>"; };
641753552C2AC158005415B4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgmpxx.a; path = Libraries/libgmpxx.a; sourceTree = "<group>"; };
641753562C2AC158005415B4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libffi.a; path = Libraries/libffi.a; sourceTree = "<group>"; };
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
@@ -494,6 +487,11 @@
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = "<group>"; };
E5D68D3B2C22D78C00CBA347 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = "<group>"; };
E5D68D3D2C22D78C00CBA347 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -531,13 +529,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6417535A2C2AC158005415B4 /* libgmpxx.a in Frameworks */,
641753592C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */,
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
641753572C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
6417535B2C2AC158005415B4 /* libffi.a in Frameworks */,
641753582C2AC158005415B4 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */,
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */,
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */,
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -603,6 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5D68D3B2C22D78C00CBA347 /* libffi.a */,
E5D68D3D2C22D78C00CBA347 /* libgmp.a */,
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */,
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -669,11 +672,6 @@
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
5C764E5C279C70B7000C6508 /* Libraries */,
641753562C2AC158005415B4 /* libffi.a */,
641753532C2AC158005415B4 /* libgmp.a */,
641753552C2AC158005415B4 /* libgmpxx.a */,
641753522C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
641753542C2AC158005415B4 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */,
5CA059C2279559F40002BEB4 /* Shared */,
5CDCAD462818589900503DA2 /* SimpleX NSE */,
5CA059DA279559F40002BEB4 /* Tests iOS */,
@@ -804,7 +802,6 @@
5C13730A28156D2700F43030 /* ContactConnectionView.swift */,
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */,
18415835CBD939A9ABDC108A /* UserPicker.swift */,
6417535C2C2ACD77005415B4 /* ServersSummaryView.swift */,
);
path = ChatList;
sourceTree = "<group>";
@@ -1259,7 +1256,6 @@
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
6417535D2C2ACD77005415B4 /* ServersSummaryView.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
@@ -1556,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1581,7 +1577,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1605,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1630,7 +1626,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1691,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1706,7 +1702,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1728,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1743,7 +1739,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1765,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1791,7 +1787,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1816,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 224;
CURRENT_PROJECT_VERSION = 225;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1842,7 +1838,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8;
MARKETING_VERSION = 5.8.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
+7 -140
View File
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI
public let jsonDecoder = getJSONDecoder()
public let jsonEncoder = getJSONEncoder()
let jsonEncoder = getJSONEncoder()
public enum ChatCommand {
case showActiveUser
@@ -71,7 +71,6 @@ public enum ChatCommand {
case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent)
case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol)
case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg])
case apiAddKnownProtoServer(userId: Int64, server: String)
case apiTestProtoServer(userId: Int64, server: String)
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
case apiGetChatItemTTL(userId: Int64)
@@ -79,7 +78,6 @@ public enum ChatCommand {
case apiGetNetworkConfig
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
case reconnectAllServers
case reconnectServer(userId: Int64, smpServer: String)
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
case apiContactInfo(contactId: Int64)
@@ -124,7 +122,6 @@ public enum ChatCommand {
case apiEndCall(contact: Contact)
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
// WebRTC calls /
case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
@@ -145,9 +142,6 @@ public enum ChatCommand {
case apiStandaloneFileInfo(url: String)
// misc
case showVersion
case getAgentServersSummary(userId: Int64)
case resetAgentServersStats
case getAgentSubsSummary(userId: Int64)
case string(String)
public var cmdString: String {
@@ -225,7 +219,6 @@ public enum ChatCommand {
case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)"
case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)"
case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))"
case let .apiAddKnownProtoServer(userId, server): return "/_known server \(userId) \(server)"
case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)"
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
@@ -233,7 +226,6 @@ public enum ChatCommand {
case .apiGetNetworkConfig: return "/network"
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
case .reconnectAllServers: return "/reconnect"
case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)"
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
@@ -309,9 +301,6 @@ public enum ChatCommand {
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
case .showVersion: return "/version"
case let .getAgentServersSummary(userId): return "/get servers summary \(userId)"
case .resetAgentServersStats: return "/reset servers stats"
case let .getAgentSubsSummary(userId): return "/get subs summary \(userId)"
case let .string(str): return str
}
}
@@ -379,7 +368,6 @@ public enum ChatCommand {
case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation"
case .apiGetUserProtoServers: return "apiGetUserProtoServers"
case .apiSetUserProtoServers: return "apiSetUserProtoServers"
case .apiAddKnownProtoServer: return "apiAddKnownProtoServer"
case .apiTestProtoServer: return "apiTestProtoServer"
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
@@ -387,7 +375,6 @@ public enum ChatCommand {
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
case .reconnectAllServers: return "reconnectAllServers"
case .reconnectServer: return "reconnectServer"
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiSetMemberSettings: return "apiSetMemberSettings"
case .apiContactInfo: return "apiContactInfo"
@@ -448,9 +435,6 @@ public enum ChatCommand {
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
case .showVersion: return "showVersion"
case .getAgentServersSummary: return "getAgentServersSummary"
case .resetAgentServersStats: return "resetAgentServersStats"
case .getAgentSubsSummary: return "getAgentSubsSummary"
case .string: return "console command"
}
}
@@ -679,8 +663,6 @@ public enum ChatResponse: Decodable, Error {
// misc
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
case cmdOk(user: UserRef?)
case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary)
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
case chatCmdError(user_: UserRef?, chatError: ChatError)
case chatError(user_: UserRef?, chatError: ChatError)
case archiveImported(archiveErrors: [ArchiveError])
@@ -839,8 +821,6 @@ public enum ChatResponse: Decodable, Error {
case .contactPQEnabled: return "contactPQEnabled"
case .versionInfo: return "versionInfo"
case .cmdOk: return "cmdOk"
case .agentServersSummary: return "agentServersSummary"
case .agentSubsSummary: return "agentSubsSummary"
case .chatCmdError: return "chatCmdError"
case .chatError: return "chatError"
case .archiveImported: return "archiveImported"
@@ -1004,8 +984,6 @@ public enum ChatResponse: Decodable, Error {
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
case .cmdOk: return noDetails
case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary))
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary))
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
@@ -1131,13 +1109,13 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var server: String
public var preset: Bool
public var tested: Bool?
public var enabled: ServerEnabled
public var enabled: Bool
var createdAt = Date()
// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive?
// Even if we don't see the use case, it's probably better to allow it in the model
// In any case, "trusted/known" servers are out of scope of this change
public init(server: String, preset: Bool, tested: Bool?, enabled: ServerEnabled) {
public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) {
self.server = server
self.preset = preset
self.tested = tested
@@ -1150,7 +1128,7 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
public var id: String { "\(server) \(createdAt)" }
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: .enabled)
public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: true)
public var isEmpty: Bool {
server.trimmingCharacters(in: .whitespaces) == ""
@@ -1167,19 +1145,19 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
server: "smp://abcd@smp8.simplex.im",
preset: true,
tested: true,
enabled: .enabled
enabled: true
),
custom: ServerCfg(
server: "smp://abcd@smp9.simplex.im",
preset: false,
tested: false,
enabled: .enabled
enabled: false
),
untested: ServerCfg(
server: "smp://abcd@smp10.simplex.im",
preset: false,
tested: nil,
enabled: .enabled
enabled: true
)
)
@@ -1191,12 +1169,6 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
}
}
public enum ServerEnabled: String, Codable {
case disabled
case enabled
case known
}
public enum ProtocolTestStep: String, Decodable, Equatable {
case connect
case disconnect
@@ -2252,108 +2224,3 @@ public enum MsgType: String, Codable {
case message
case quota
}
public struct PresentedServersSummary: Codable {
public var statsStartedAt: Date
public var currentUserServers: ServersSummary
public var allUsersServers: ServersSummary
public var allUsedSMP: [SMPServerSummary] { self.allUsersServers.currentlyUsedSMPServers }
public var allPrevSMP: [SMPServerSummary] { self.allUsersServers.previouslyUsedSMPServers }
public var allProxSMP: [SMPServerSummary] { self.allUsersServers.onlyProxiedSMPServers }
public var userUsedSMP: [SMPServerSummary] { self.currentUserServers.currentlyUsedSMPServers }
public var userPrevSMP: [SMPServerSummary] { self.currentUserServers.previouslyUsedSMPServers }
public var userProxSMP: [SMPServerSummary] { self.currentUserServers.onlyProxiedSMPServers }
public var allUsedXFTP: [XFTPServerSummary] { self.allUsersServers.currentlyUsedXFTPServers }
public var allPrevXFTP: [XFTPServerSummary] { self.allUsersServers.previouslyUsedXFTPServers }
public var userUsedXFTP: [XFTPServerSummary] { self.currentUserServers.currentlyUsedXFTPServers }
public var userPrevXFTP: [XFTPServerSummary] { self.currentUserServers.previouslyUsedXFTPServers }
}
public struct ServersSummary: Codable {
public var currentlyUsedSMPServers: [SMPServerSummary]
public var previouslyUsedSMPServers: [SMPServerSummary]
public var onlyProxiedSMPServers: [SMPServerSummary]
public var currentlyUsedXFTPServers: [XFTPServerSummary]
public var previouslyUsedXFTPServers: [XFTPServerSummary]
}
public struct SMPServerSummary: Codable, Identifiable {
public var smpServer: String
public var known: Bool?
public var sessions: ServerSessions?
public var subs: SMPServerSubs?
public var stats: AgentSMPServerStatsData?
public var id: String { smpServer }
}
public struct ServerSessions: Codable {
public var ssConnected: Int
public var ssErrors: Int
public var ssConnecting: Int
}
public struct SMPServerSubs: Codable {
public var ssActive: Int
public var ssPending: Int
public init(ssActive: Int, ssPending: Int) {
self.ssActive = ssActive
self.ssPending = ssPending
}
}
public struct AgentSMPServerStatsData: Codable {
public var _sentDirect: Int
public var _sentViaProxy: Int
public var _sentProxied: Int
public var _sentDirectAttempts: Int
public var _sentViaProxyAttempts: Int
public var _sentProxiedAttempts: Int
public var _sentAuthErrs: Int
public var _sentQuotaErrs: Int
public var _sentExpiredErrs: Int
public var _sentOtherErrs: Int
public var _recvMsgs: Int
public var _recvDuplicates: Int
public var _recvCryptoErrs: Int
public var _recvErrs: Int
public var _ackMsgs: Int
public var _ackAttempts: Int
public var _connCreated: Int
public var _connSecured: Int
public var _connCompleted: Int
public var _connDeleted: Int
public var _connSubscribed: Int
public var _connSubAttempts: Int
public var _connSubErrs: Int
}
public struct XFTPServerSummary: Codable, Identifiable {
public var xftpServer: String
public var known: Bool?
public var sessions: ServerSessions?
public var stats: AgentXFTPServerStatsData?
public var rcvInProgress: Bool
public var sndInProgress: Bool
public var delInProgress: Bool
public var id: String { xftpServer }
}
public struct AgentXFTPServerStatsData: Codable {
public var _uploads: Int
public var _uploadAttempts: Int
public var _uploadErrs: Int
public var _downloads: Int
public var _downloadAttempts: Int
public var _downloadAuthErrs: Int
public var _downloadErrs: Int
public var _deletions: Int
public var _deleteAttempts: Int
public var _deleteErrs: Int
}
-2
View File
@@ -11,7 +11,6 @@ import SwiftUI
public struct User: Identifiable, Decodable, UserLike, NamedChat {
public var userId: Int64
public var agentUserId: String
var userContactId: Int64
var localDisplayName: ContactName
public var profile: LocalProfile
@@ -42,7 +41,6 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat {
public static let sampleData = User(
userId: 1,
agentUserId: "abc",
userContactId: 1,
localDisplayName: "alice",
profile: LocalProfile.sampleData,
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: c7886926870e97fa592d51fa36a2cdec49296388
tag: 8a3b72458f917e9867f4e3640dda0fa1827ff6cf
source-repository-package
type: git
+4 -22
View File
@@ -4,16 +4,9 @@ sequenceDiagram
participant B as Bob
participant C as Existing<br>contact
alt invite contact
note over A, B: 1a. send and accept group invitation
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
else join via group link
note over A, B: 1b. join via group link and accept request
B ->> A: join via group link<br>SimpleX contact address
A ->> B: x.grp.link.inv in SMP confirmation<br>accept joining member request,<br>sending group profile, etc.<br>establish group member connection
A ->> B: x.grp.link.mem<br>send inviting member profile
end
note over A, B: 1. send and accept group invitation
A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection)
B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection
note over M, B: 2. introduce new member Bob to all existing members
A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections)
@@ -27,25 +20,14 @@ sequenceDiagram
end
A ->> M: x.grp.mem.fwd<br>forward "invitations" and<br>Bob's chat protocol version<br>to all members<br>(via member connections)
note over M, B: group message forwarding<br>(while connections between members are being established)
M -->> B: messages between members and Bob are forwarded by Alice
B -->> M:
note over M, B: 3. establish direct and group member connections
M ->> B: establish group member connection
opt chat protocol compatible version < 2
M ->> B: establish direct connection
note over M, C: 3*. deduplicate new contact
note over M, C: 4. deduplicate new contact
B ->> M: x.info.probe<br>"probe" is sent to all new members
B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match
C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user
note over B: merge existing and new contacts if received and sent probe hashes match
end
note over M, B: 4. notify inviting member that connection is established
M ->> A: x.grp.mem.con
B ->> A: x.grp.mem.con
note over A: stops forwarding messages
M -->> B: messages are sent via group connection without forwarding
B -->> M:
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 34 KiB

+7 -70
View File
@@ -2,7 +2,7 @@
title: SimpleX Chat Protocol
revision: 08.08.2022
---
Revision 2, 2024-06-24
DRAFT Revision 0.1, 2022-08-08
Evgeny Poberezkin
@@ -157,7 +157,7 @@ This message is sent by both sides of the connection during the connection hands
### Probing for duplicate contacts
As there are no globally unique user identifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts.
As there are no globally unique user identitifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts.
Sending clients MAY disable this functionality, and receiving clients MAY ignore probe messages.
@@ -210,23 +210,23 @@ File attachment can optionally include connection address to receive the file -
### Decentralized design for chat groups
SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation or joins via group link, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections.
SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections.
There is a possibility of the attack here: as the introducing member forwards the addresses, they can substitute them with other addresses, performing MITM attack on the communication between existing and introduced members - this is similar to the communication operator being able to perform MITM on any connection between the users. To mitigate this attack this group sub-protocol will be extended to allow validating security of the connection by sending connection verification out-of-band.
Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via another user.
Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via annother user.
Each member in the group is identified by a group-wide unique identifier used by all members in the group. This is to allow referencing members in the messages and to allow group message integrity validation.
The diagram below shows the sequence of messages sent between the users' clients to add the new member to the group.
While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message.
![Adding member to the group](./diagrams/group.svg)
While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message.
### Member roles
Currently members can have one of three roles - `owner`, `admin`, `member` and `observer`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. `Observer` members only receive messages and aren't allowed to send messages.
Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role.
### Messages to manage groups and add members
@@ -279,66 +279,3 @@ These message are used for WebRTC calls:
3. `x.call.answer`: to continue with call connection the initiating clients must reply with `x.call.answer` message. This message contains WebRTC answer and collected ICE candidates. Additional ICE candidates can be sent in `x.call.extra` message.
4. `x.call.end` message is sent to notify the other party that the call is terminated.
## Threat model
This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models:
- [SimpleX Messaging Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#threat-model);
- [SimpleX File Transfer Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xftp.md#threat-model);
- [Push notifications threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/push-notifications.md#threat-model);
- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model).
#### A user's contact
*can:*
- send messages prohibited by user's preferences or otherwise act non-compliantly with user's preferences (for example, if message with updated preferences was lost or failed to be processed, or with modified client), in which case user client should treat such messages and actions as prohibited.
- by exchanging special messages with user's client, match user's contact with existing group members and/or contacts that have identical user profile (see [Probing for duplicate contacts](#probing-for-duplicate-contacts)).
- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses.
*cannot:*
- match user's contact with existing group members and/or contacts with different or with incognito profiles.
- match user's contact without communicating with the user's client.
#### A group member
*can:*
- send messages prohibited by group's preferences and member restrictions or otherwise act non-compliantly with preferences and restrictions (for example, if decentralized group state diverged, or with modified client), in which case user client should treat such messages and actions as prohibited.
- create a direct contact with a user if group permissions allow it.
- by exchanging special messages with user's client, match user's group member record with the existing group members and/or contacts that have identical user profile.
- undetectably send different messages to different group members, or selectively send messages to some members and not send to others.
- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses.
- join the same group several times, from the same or from different user profile, and pretend to be different members.
*cannot:*
- match user's contact with existing group members and/or contacts with different or with incognito profiles.
- match user's group member record with existing group members and/or contacts without communication of user's client.
- determine whether two group members with different or with incognito profiles are the same user.
#### A group admin
*can:*
- carry out MITM attack between user and other group member(s) when forwarding invitations for group connections (user can detect such attack by verifying connection security codes out-of-band).
- undetectably forward different messages to different group members, selectively adding, modifying, and dropping forwarded messages.
- disrupt decentralized group state by sending different messages that change group state (such as adding or removing members, member role changes, etc.) to different group members, or sending such messages selectively.
*cannot:*
- prove that two group members with incognito profiles is the same user.
+12 -269
View File
@@ -8,7 +8,7 @@
"displayName": {
"type": "string",
"metadata": {
"format": "non-empty string, the first character must not be # or @"
"format": "non-empty string without spaces, the first character must not be # or @"
}
},
"fullName": {"type": "string"}
@@ -19,39 +19,6 @@
"metadata": {
"format": "data URI format for base64 encoded image"
}
},
"contactLink": {"ref": "connReqUri"},
"preferences": {
"type": "string",
"metadata": {
"format": "JSON encoded user preferences"
}
}
},
"additionalProperties": true
},
"groupProfile": {
"properties": {
"displayName": {
"type": "string",
"metadata": {
"format": "non-empty string, the first character must not be # or @"
}
},
"fullName": {"type": "string"}
},
"optionalProperties": {
"image": {
"type": "string",
"metadata": {
"format": "data URI format for base64 encoded image"
}
},
"groupPreferences": {
"type": "string",
"metadata": {
"format": "JSON encoded user preferences"
}
}
},
"additionalProperties": true
@@ -62,8 +29,6 @@
},
"optionalProperties": {
"file": {"ref": "fileInvitation"},
"ttl": {"type": "integer"},
"live": {"type": "boolean"},
"quote": {
"properties": {
"msgRef": {"ref": "msgRef"},
@@ -91,47 +56,17 @@
}
},
"image": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"}
}
},
"video": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"},
"duration": {"type": "integer"}
}
},
"voice": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"duration": {"type": "integer"}
}
"text": {"type": "string", "metadata": {"comment": "can be empty"}},
"image": {"ref": "base64url"}
},
"file": {
"properties": {
"text": {"type": "string", "metadata": {"comment": "can be empty"}}
}
"text": {"type": "string", "metadata": {"comment": "can be empty"}}
}
},
"metadata": {
"comment": "it is RECOMMENDED that the clients support other values in `type` properties showing them as text messages in case `text` property is present"
}
},
"msgReaction" : {
"discriminator": "type",
"mapping": {
"emoji": {
"properties": {
"emoji": {
"type": "string",
"metadata": {"comment": "emoji character"}
}
}
}
}
},
"msgRef": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -156,31 +91,7 @@
"fileSize": {"type": "uint32"}
},
"optionalProperties": {
"fileDigest": {"ref": "base64url"},
"fileConnReq": {"ref": "connReqUri"},
"fileDescr": {"ref": "fileDescription"}
}
},
"fileDescription": {
"properties": {
"fileDescrText": {
"type": "string",
"metadata": {
"format": "XFTP file description part text"
}
},
"fileDescrPartNo": {
"type": "integer",
"metadata": {
"format": "XFTP file description part number"
}
},
"fileDescrComplete": {
"type": "boolean",
"metadata": {
"format": "XFTP file description completion marker"
}
}
"fileConnReq": {"ref": "connReqUri"}
}
},
"linkPreview": {
@@ -189,21 +100,6 @@
"title": {"type": "string"},
"description": {"type": "string"},
"image": {"ref": "base64url"}
},
"optionalProperties": {
"content": {"ref": "linkContent"}
}
},
"linkContent": {
"discriminator": "type",
"mapping": {
"page": {},
"image": {},
"video": {
"optionalProperties": {
"duration": {"type": "integer"}
}
}
}
},
"groupInvitation": {
@@ -211,27 +107,15 @@
"fromMember": {"ref": "memberIdRole"},
"invitedMember": {"ref": "memberIdRole"},
"connRequest": {"ref": "connReqUri"},
"groupProfile": {"ref": "groupProfile"}
"groupProfile": {"ref": "profile"}
},
"optionalProperties": {
"groupLinkId": {"ref": "base64url"},
"groupSize": {"type": "integer"},
"metadata": {
"comment": "groupLinkId is used to identify invitation via group link"
"comment": "used to identify invitation via group link"
}
}
},
"groupLinkInvitation": {
"properties": {
"fromMember": {"ref": "memberIdRole"},
"fromMemberName": {"type": "string"},
"invitedMember": {"ref": "memberIdRole"},
"groupProfile": {"ref": "groupProfile"}
},
"optionalProperties": {
"groupSize": {"type": "integer"}
}
},
"memberIdRole": {
"properties": {
"memberId": {"ref": "base64url"},
@@ -243,35 +127,16 @@
"memberId": {"ref": "base64url"},
"memberRole": {"ref": "groupMemberRole"},
"profile": {"ref": "profile"}
},
"optionalProperties": {
"v": {"ref": "chatVersionRange"}
}
},
"memberRestrictions": {
"properties": {
"restriction": {"ref": "memberRestrictionStatus"}
}
},
"memberRestrictionStatus": {
"enum": ["blocked", "unrestricted"]
},
"chatVersionRange": {
"type": "string",
"metadata": {
"format": "chat version range string encoded as `<min>-<max>`, or as `<number>` if min = max"
}
},
"introInvitation": {
"properties": {
"groupConnReq": {"ref": "connReqUri"}
},
"optionalProperties": {
"groupConnReq": {"ref": "connReqUri"},
"directConnReq": {"ref": "connReqUri"}
}
},
"groupMemberRole": {
"enum": ["observer", "author", "member", "admin", "owner"]
"enum": ["author", "member", "admin", "owner"]
},
"callInvitation": {
"properties": {
@@ -392,17 +257,6 @@
"params": {"ref": "msgContainer"}
}
},
"x.msg.file.descr": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileDescr": {"ref": "fileDescription"}
}
}
}
},
"x.msg.update": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -410,10 +264,6 @@
"properties": {
"msgId": {"ref": "base64url"},
"content": {"ref": "msgContent"}
},
"optionalProperties": {
"ttl": {"type": "integer"},
"live": {"type": "boolean"}
}
}
}
@@ -424,24 +274,6 @@
"params": {
"properties": {
"msgId": {"ref": "base64url"}
},
"optionalProperties": {
"memberId": {"ref": "base64url"}
}
}
}
},
"x.msg.react": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"reaction": {"ref": "msgReaction"},
"add": {"type": "boolean"}
},
"optionalProperties": {
"memberId": {"ref": "base64url"}
}
}
}
@@ -462,10 +294,8 @@
"params": {
"properties": {
"msgId": {"ref": "base64url"},
"fileConnReq": {"ref": "connReqUri"},
"fileName": {"type": "string"}
},
"optionalProperties": {
"fileConnReq": {"ref": "connReqUri"}
}
}
}
@@ -480,14 +310,6 @@
}
}
},
"x.direct.del": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {}
}
}
},
"x.grp.inv": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -508,26 +330,6 @@
}
}
},
"x.grp.link.inv": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"groupLinkInvitation": {"ref": "groupLinkInvitation"}
}
}
}
},
"x.grp.link.mem": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"profile": {"ref": "profile"}
}
}
}
},
"x.grp.mem.new": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -544,9 +346,6 @@
"params": {
"properties": {
"memberInfo": {"ref": "memberInfo"}
},
"optionalProperties": {
"memberRestrictions": {"ref": "memberRestrictions"}
}
}
}
@@ -595,27 +394,6 @@
}
}
},
"x.grp.mem.restrict": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"},
"memberRestrictions": {"ref": "memberRestrictions"}
}
}
}
},
"x.grp.mem.con": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"}
}
}
}
},
"x.grp.mem.del": {
"properties": {
"msgId": {"ref": "base64url"},
@@ -647,42 +425,7 @@
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"groupProfile": {"ref": "groupProfile"}
}
}
}
},
"x.grp.direct.inv": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"connReq": {"ref": "connReqUri"}
},
"optionalProperties": {
"content": {"ref": "msgContent"}
}
}
}
},
"x.grp.msg.forward": {
"properties": {
"msgId": {"ref": "base64url"},
"params": {
"properties": {
"memberId": {"ref": "base64url"},
"msg": {
"type": "string",
"metadata": {
"format": "JSON encoded chat message"
}
},
"msgTs": {
"type": "string",
"metadata": {
"format": "ISO8601 UTC time of the message"
}
}
"groupProfile": {"ref": "profile"}
}
}
}
@@ -693,7 +436,7 @@
"params": {
"properties": {
"callId": {"ref": "base64url"},
"invitation": {"ref": "callInvitation"}
"invitation": {}
}
}
}
@@ -0,0 +1,43 @@
# Optimized subscription
## Problem
The `subscribeUserConnections` function has a few problems that affect UX on app start:
1. It loads entity data that isn't used until result processing. This produces a memory spike and takes CPU time to parse all the data.
2. Subscription results are processed synchronously after the agent finishes all the batches for user. The app wouldn't see connections as active until the slowest server responds or timeouts.
3. User subscriptions are processed sequentially. A currently active user is given a first round of subs, but the remaining are blocked. If a user profile is switched right away, the new user may start receiving updates with even more lag.
## Solution
Functions that fetch connections and entities are reduced to return only connection IDs. The filters should be moved from Haskell into specialized SQL queries that only return `[ConnId]`.
With the connection list on hands the agent subscriber thread forks off to do its thing and process batch results. This allows outer loop to start collecting connections for the remaining users.
Successful results are communicated with a new `UP srv conns` message emitted from agent when a batch finishes its processing. The `conns` payload would be a list of connections that actually have just switched subs from "pending" to "active". The UP handling machinery processes status updates just as it would in a server reconnect event. This would update connection state in chat apps as soon as the server responds, keeping app and core in tighter sync.
`reconnectSMPClient` should stop sending UPs to prevent double processing of the same result. The `okConns` membership test it currently uses is the same "did not belong to an active connection" that the batch result would use.
Sending results with UP allows to reduce summary responses to a bunch of counters so no entity data would be needed for CLI here:
```haskell
| CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
```
Subscription errors are reported to API as `CRNetworkStatuses` as ususal, but the active subs are removed from the list as they are already handled by `UP`.
Subscription errors for CLI (when connection error reporting is enabled) are reported with the types reduced to a textual name:
```haskell
| CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubError {user :: User, groupName :: GroupName, contactName :: ContactName, chatError :: ChatError}
| CRSndFileSubError {user :: User, sndFileTransfer :: Text, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: Text, chatError :: ChatError}
```
> A generic constructor could be used instead, but then it would contain extra fields to form a message in View if matching the current output is desired.
The textual names for connections can be requested for the subset of all connections that needs them with the same procedure that's used in `UP` handling.
@@ -1,87 +0,0 @@
# Agent stats persistence
## Problem
State/state tracked in agent are lost on app restart, which makes it difficult to debug user bugs.
## Solution
Persist stats between sessions.
App terminal signals may vary per platform / be absent (?) -> persist stats periodically.
Stats would have `<userId, server>` key, so we don't want to store them in a plaintext file to not leak used servers locally -> persist in encrypted db.
There's couple of orthogonal design decision to be made:
- persist in chat or in agent db
- pros for chat:
- possibly less contention for db than agent
- pros for agent:
- no unnecessary back and forth, especially if agent starts accumulating from past sessions and has to be parameterized with past stats (see below)
- agent to start accumulating from past sessions stats, or keep past separately and only accumulate for current session from zeros
- pros for accumulating from past sessions:
- easier to maintain stats - e.g. user deletion has to remove keys, which is more convoluted if past stats are not stored in memory
- simpler UI - overall stats, no differentiation for past/current session (or less logic in backend preparing presentation data)
- pros for accumulating from zeros:
- simpler start logic - no need to restore stats from agent db / pass initial stats from chat db
- can differentiate between past sessions and current session stats in UI
### Option 1 - Persist in chat db, agent to track only current session
- Chat stores stats in such table:
```sql
CREATE TABLE agent_stats(
agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record
past_stats TEXT, -- accumulated from previous sessions
session_stats TEXT, -- current session
past_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset
session_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of current session
session_updated_at TEXT NOT NULL DEFAULT(datetime('now')) -- last update of current session stats (periodic, frequent updates)
);
```
- Chat periodically calls getAgentServersStats api and updates `session_stats`.
- interval? should be short to not lose too much data, 5-30 seconds?
- On start `session_stats` are accumulated into `past_stats` and set to null.
- On user deletion, agent updates current session stats in memory (removes keys), chat has to do same for both stats fields in db.
- other cases where stats have to be manipulated in similar way?
### Option 2 - Persist in chat db, agent to accumulate stats from past sessions
- Table is only used for persistence of overall stats:
```sql
CREATE TABLE agent_stats(
agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record
agent_stats TEXT, -- overall stats - past and session
started_tracking_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
```
- Chat to parameterize creation of agent client with initial stats.
### Option 3 - Persist in agent db, agent to differentiate past stats and session stats
- Table in agent db similar to option 1.
- Agent is responsible for periodic updates in session, as well as accumulating into "past" and resetting session stats on start.
- Agent only communicates stats to chat on request.
- On user deletion agent is fully responsible for maintaining both in-memory session stats, and updating db records.
### Option 4 - Persist in agent db, agent to accumulate stats from past sessions
- Table in agent db similar to option 2.
- On start agent restores initial stats into memory by itself.
- Since all stats are in memory, on user deletion it's enough to update in memory without updating db.
- there is a race possible where agent crashes after updating stats (removing user keys) in memory before database stats have been overwritten by a periodic update, so it may be better to immediately overwrite and not wait for periodic update.
- still at least there's at least no additional logic to update past stats.
### Other considerations
Why is it important to timely remove user keys from past stats?
- stats not being saved for past users:
- important both privacy-wise and to not cause confusion when showing "All" stats (e.g. user summing up across users stats would have smaller total than total stats).
- to avoid accidentally mixing up with newer users.
- though we do have an AUTOINCREMENT user_id in agent so probably it wouldn't be a problem.
- on the other hand maybe we don't want to "forget" stats on user deletion so that stats would reflect networking more accurately?
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."c7886926870e97fa592d51fa36a2cdec49296388" = "1r3nibcgw3whl0q3ssyr1606x4ilqphhzqyihi3aw4nw5fmz226h";
"https://github.com/simplex-chat/simplexmq.git"."8a3b72458f917e9867f4e3640dda0fa1827ff6cf" = "1mmxdaj563kjmlkacxdnq62n6mzw9khampzaqghnk6iiwzdig0qy";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -1
View File
@@ -146,6 +146,7 @@ library
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Migrations.M20240528_quota_err_counter
Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
@@ -160,7 +161,6 @@ library
Simplex.Chat.Remote.RevHTTP
Simplex.Chat.Remote.Transport
Simplex.Chat.Remote.Types
Simplex.Chat.Stats
Simplex.Chat.Store
Simplex.Chat.Store.AppSettings
Simplex.Chat.Store.Connections
+97 -140
View File
@@ -20,7 +20,6 @@ import Control.Applicative (optional, (<|>))
import Control.Concurrent.STM (retry)
import Control.Logger.Simple
import Control.Monad
import Simplex.Chat.Stats
import Control.Monad.Except
import Control.Monad.IO.Unlift
import Control.Monad.Reader
@@ -85,6 +84,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
import qualified Simplex.Chat.Util as U
import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard)
@@ -113,7 +113,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol, ProtocolServer)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
@@ -369,7 +369,7 @@ activeAgentServers ChatConfig {defaultServers} p =
fromMaybe (cfgServers p defaultServers)
. nonEmpty
. map (\ServerCfg {server} -> server)
. filter (\ServerCfg {enabled} -> enabled == SEEnabled)
. filter (\ServerCfg {enabled} -> enabled)
cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p))
cfgServers p DefaultAgentServers {smp, xftp} = case p of
@@ -1315,7 +1315,7 @@ processChatCommand' vr = \case
servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers
pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers
where
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = SEEnabled}
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True}
GetUserProtoServers aProtocol -> withUser $ \User {userId} ->
processChatCommand $ APIGetUserProtoServers userId aProtocol
APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do
@@ -2253,21 +2253,6 @@ processChatCommand' vr = \case
CLUserContact ucId -> "UserContact " <> show ucId
CLFile fId -> "File " <> show fId
DebugEvent event -> toView event >> ok_
GetAgentServersSummary userId -> withUserId userId $ \user -> do
agentServersSummary <- lift $ withAgent' getAgentServersSummary
users <- withStore' getUsers
smpServers <- getUserServers user SPSMP
xftpServers <- getUserServers user SPXFTP
let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers
pure $ CRAgentServersSummary user presentedServersSummary
where
getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => User -> SProtocolType p -> CM [ProtocolServer p]
getUserServers users protocol = do
ChatConfig {defaultServers} <- asks config
let defServers = cfgServers protocol defaultServers
servers <- map (\ServerCfg {server} -> server) <$> withStore' (`getProtocolServers` users)
let srvs = if null servers then L.toList defServers else servers
pure $ map protoServer srvs
GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary
GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails
GetAgentStats -> lift $ CRAgentStats . map stat <$> withAgent' getAgentStats
@@ -3415,71 +3400,55 @@ subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> U
subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
-- get user connections
ce <- asks $ subscriptionEvents . config
(conns, cts, ucs, gs, ms, sfts, rfts, pcs) <-
(conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns) <-
if onlyNeeded
then do
(conns, entities) <- withStore' (`getConnectionsToSubscribe` vr)
let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities
(conns, entities) <- withStore' $ \db -> getConnectionsToSubscribe db vr user
let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity ([], [], [], M.empty, M.empty, []) entities
pure (conns, cts, ucs, [], ms, sfts, rfts, pcs)
else do
withStore' unsetConnectionToSubscribe
(ctConns, cts) <- getContactConns
withStore' (`unsetConnectionToSubscribe` user)
ctConns <- getContactConns
(ucConns, ucs) <- getUserContactLinkConns
(gs, mConns, ms) <- getGroupMemberConns
(gs, mConns) <- getGroupMemberConns
(sftConns, sfts) <- getSndFileTransferConns
(rftConns, rfts) <- getRcvFileTransferConns
(pcConns, pcs) <- getPendingContactConns
pcConns <- getPendingContactConns
let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs)
-- subscribe using batched commands
rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view
contactSubsToView rs cts ce
-- TODO possibly, we could either disable these events or replace with less noisy for API
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
rcvFileSubsToView rs rfts
pendingConnSubsToView rs pcs
pure (conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns)
-- detach subscription and result processing
void . lift . forkIO . runSubscriber $ do
-- subscribe using batched commands
rs <- withAgent $ \a -> agentBatchSubscribe a conns
let (errs, _oks) = M.mapEither id rs
refs <- if ce then withStore' $ \db -> getConnectionsContacts db (M.keys errs) else pure []
let connRefs = M.fromList $ map (\ContactRef {agentConnId = AgentConnId acId, localDisplayName} -> (acId, localDisplayName)) refs
contactSubsToView errs ctConns connRefs ce
contactLinkSubsToView errs ucs
groupSubsToView errs gs mConns connRefs ce
sndFileSubsToView errs sfts
rcvFileSubsToView errs rfts
pendingConnSubsToView errs pcConns
where
runSubscriber :: CM () -> CM' ()
runSubscriber action = tryAllErrors' mkChatError action >>= either (logError . tshow) pure
addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case
RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs)
addConn :: Connection -> a -> Map ConnId a -> Map ConnId a
addConn = M.insert . aConnId
toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} =
PendingContactConnection
{ pccConnId = connId,
pccAgentConnId = agentConnId,
pccConnStatus = connStatus,
viaContactUri = False,
viaUserContactLink,
groupLinkId,
customUserProfileId,
connReqInv = Nothing,
localAlias,
createdAt,
updatedAt = createdAt
}
getContactConns :: CM ([ConnId], Map ConnId Contact)
getContactConns = do
cts <- withStore_ (`getUserContacts` vr)
let cts' = mapMaybe (\ct -> (,ct) <$> contactConnId ct) $ filter contactActive cts
pure (map fst cts', M.fromList cts')
getUserContactLinkConns :: CM ([ConnId], Map ConnId UserContact)
RcvDirectMsgConnection c (Just _ct) -> let cts' = aConnId c : cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = aConnId c : pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g _m -> let ms' = aConnId c : ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = M.insert (aConnId c) sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = M.insert (aConnId c) rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = (aConnId c, isNothing $ userContactGroupId uc) : ucs in (cts, ucs', ms, sfts, rfts, pcs)
getContactConns :: CM [ConnId]
getContactConns = withStore_ getUserContactConnIds
getUserContactLinkConns :: CM ([ConnId], [(ConnId, Bool)])
getUserContactLinkConns = do
(cs, ucs) <- unzip <$> withStore_ (`getUserContactLinks` vr)
let connIds = map aConnId cs
pure (connIds, M.fromList $ zip connIds ucs)
getGroupMemberConns :: CM ([Group], [ConnId], Map ConnId GroupMember)
ucs <- withStore_ getUserContactLinks
pure (map fst ucs, ucs)
getGroupMemberConns :: CM ([(GroupInfo, [ConnId])], [ConnId])
getGroupMemberConns = do
gs <- withStore_ (`getUserGroups` vr)
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs
pure (gs, map fst mPairs, M.fromList mPairs)
gs <- withStore_ (`getUserGroupMemberConnIds` vr)
pure (gs, concatMap snd gs)
getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer)
getSndFileTransferConns = do
sfts <- withStore_ getLiveSndFileTransfers
@@ -3490,92 +3459,81 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
rfts <- withStore_ getLiveRcvFileTransfers
let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts
pure (map fst rftPairs, M.fromList rftPairs)
getPendingContactConns :: CM ([ConnId], Map ConnId PendingContactConnection)
getPendingContactConns = do
pcs <- withStore_ getPendingContactConnections
let connIds = map aConnId' pcs
pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM ()
contactSubsToView rs cts ce = do
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI
getPendingContactConns :: CM [ConnId]
getPendingContactConns = withStore_ getPendingContactConnections
contactSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
contactSubsToView errs cts names ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
where
conns = S.fromList cts
errConns = M.restrictKeys errs conns
notifyCLI = do
let cRs = resultsFor rs cts
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus)
statuses = M.foldrWithKey' addStatus [] cts
toView CRContactSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
when ce $ forM_ (M.assocs errConns) $ \(acId, err) ->
forM_ (M.lookup acId names) $ \contactName ->
toView CRContactSubError {user, contactName, chatError = ChatErrorAgent err Nothing}
notifyAPI = unless (M.null errConns) $ toView $ CRNetworkStatuses (Just user) $ map status (M.assocs errConns)
where
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
addStatus _ Contact {activeConn = Nothing} nss = nss
addStatus connId Contact {activeConn = Just Connection {agentConnId}} nss =
let ns = (agentConnId, netStatus $ resultErr connId rs)
in ns : nss
netStatus :: Maybe ChatError -> NetworkStatus
netStatus = maybe NSConnected $ NSError . errorNetworkStatus
errorNetworkStatus :: ChatError -> String
status (connId, err) = ConnNetworkStatus (AgentConnId connId) $ NSError (errorNetworkStatus err)
errorNetworkStatus :: AgentErrorType -> String
errorNetworkStatus = \case
ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted"
BROKER _ NETWORK -> "network"
SMP _ SMP.AUTH -> "contact deleted"
e -> show e
-- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM ()
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> CM ()
groupSubsToView rs gs ms ce = do
mapM_ groupSub $
sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs
toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs
contactLinkSubsToView :: Map ConnId AgentErrorType -> [(ConnId, Bool)] -> CM ()
contactLinkSubsToView errs ucs = do
let (addresses, groupLinks) = partition snd ucs
forM_ addresses $ \(acId, _uc) -> toView $ CRUserAddrSubStatus {user, userContactError = (`ChatErrorAgent` Nothing) <$> M.lookup acId errs}
let groups = S.fromList $ map fst groupLinks
errGroups = M.restrictKeys errs groups
unless (S.null groups) $ toView CRUserGroupLinksSubSummary
{ user,
okSubs = S.size groups - M.size errGroups,
errSubs = M.size errGroups
}
groupSubsToView :: Map ConnId AgentErrorType -> [(GroupInfo, [ConnId])] -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
groupSubsToView errs gs allMembers names ce = do
mapM_ (uncurry groupSub) gs
toView CRMemberSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
where
mRs = resultsFor rs ms
groupSub :: Group -> CM ()
groupSub (Group g@GroupInfo {membership, groupId = gId} members) = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors
conns = S.fromList allMembers
errConns = M.restrictKeys errs conns
groupSub :: GroupInfo -> [ConnId] -> CM ()
groupSub g@GroupInfo {membership} groupMembers = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g) ) mErrors
toView groupEvent
where
mErrors :: [(GroupMember, ChatError)]
mErrors =
sortOn (\(GroupMember {localDisplayName = n}, _) -> n)
. filterErrors
$ filter (\(GroupMember {groupId}, _) -> groupId == gId) mRs
mErrors :: [(ContactName, ChatError)]
mErrors = sortOn fst $ mapMaybe mError groupMembers
mError :: ConnId -> Maybe (ContactName, ChatError)
mError mConnId = do
mErr <- M.lookup mConnId errConns
name <- M.lookup mConnId names
Just (name, ChatErrorAgent mErr Nothing)
groupEvent :: ChatResponse
groupEvent
| memberStatus membership == GSMemInvited = CRGroupInvitation user g
| all (\GroupMember {activeConn} -> isNothing activeConn) members =
| null groupMembers =
if memberActive membership
then CRGroupEmpty user g
else CRGroupRemoved user g
| otherwise = CRGroupSubscribed user g
sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView rs sfts = do
let sftRs = resultsFor rs sfts
forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do
forM_ err_ $ toView . CRSndFileSubError user ft
sndFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView errs sfts =
forM_ (M.assocs sfts) $ \(acId, ft@SndFileTransfer {fileId, fileStatus}) -> do
forM_ (M.lookup acId errs) $ toView . CRSndFileSubError user ft . (`ChatErrorAgent` Nothing)
void . forkIO $ do
threadDelay 1000000
when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $
sendFileChunk user ft
rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM ()
rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM ()
pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs
rcvFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId RcvFileTransfer -> CM ()
rcvFileSubsToView errs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . M.mapMaybeWithKey (\acId rft -> (\e -> (rft, ChatErrorAgent e Nothing)) <$> M.lookup acId errs)
pendingConnSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> CM () -- XXX: ignored by View
pendingConnSubsToView errs pcs = toView CRPendingSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
where
conns = S.fromList pcs
errConns = M.restrictKeys errs conns
withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a]
withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> []
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
resultsFor rs = M.foldrWithKey' addResult []
where
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
addResult connId = (:) . (,resultErr connId rs)
resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
resultErr connId rs = case M.lookup connId rs of
Just (Left e) -> Just $ ChatErrorAgent e Nothing
Just _ -> Nothing
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
cleanupManager :: CM ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
@@ -3761,7 +3719,7 @@ processAgentMessageNoConn = \case
where
connIds = map AgentConnId conns
notifyAPI = toView . CRNetworkStatus nsStatus
notifyCLI = do
notifyCLI = whenM (asks $ subscriptionEvents . config) $ do
cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs
@@ -7626,7 +7584,6 @@ chatCommandP =
("/version" <|> "/v") $> ShowVersion,
"/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP),
"/get servers summary " *> (GetAgentServersSummary <$> A.decimal),
"/get stats" $> GetAgentStats,
"/reset stats" $> ResetAgentStats,
"/get subs" $> GetAgentSubs,
@@ -7771,7 +7728,7 @@ chatCommandP =
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
(pure Nothing)
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = SEEnabled}
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P))
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char
+1 -1
View File
@@ -21,10 +21,10 @@ import Data.Time.Clock (UTCTime)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types (Contact, ContactId, User)
import Simplex.Chat.Types.Util (decodeJSON, encodeJSON)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
data Call = Call
{ contactId :: ContactId,
+8 -12
View File
@@ -60,7 +60,6 @@ import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol
import Simplex.Chat.Remote.AppVersion
import Simplex.Chat.Remote.Types
import Simplex.Chat.Stats (PresentedServersSummary)
import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -76,7 +75,7 @@ import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..))
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -506,7 +505,6 @@ data ChatCommand
| ShowVersion
| DebugLocks
| DebugEvent ChatResponse
| GetAgentServersSummary UserId
| GetAgentStats
| ResetAgentStats
| GetAgentSubs
@@ -682,9 +680,10 @@ data ChatResponse
| CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity}
| CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
| CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRUserAddrSubStatus {user :: User, userContactError :: Maybe ChatError}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
@@ -721,10 +720,10 @@ data ChatResponse
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
| CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]}
| CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
| CRCallInvitation {callInvitation :: RcvCallInvitation}
@@ -733,8 +732,6 @@ data ChatResponse
| CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo}
| CRCallEnded {user :: User, contact :: Contact}
| CRCallInvitations {callInvitations :: [RcvCallInvitation]}
| CRUserContactLinkSubscribed -- TODO delete
| CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete
| CRNtfTokenStatus {status :: NtfTknStatus}
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer}
| CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
@@ -758,7 +755,6 @@ data ChatResponse
| CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks}
| CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary}
| CRAgentStats {agentStats :: [[String]]}
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
+3 -2
View File
@@ -29,12 +29,13 @@ import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Types
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..))
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8)
import Simplex.Messaging.Util (safeDecodeUtf8)
import System.Console.ANSI.Types
import qualified Text.Email.Validate as Email
@@ -145,7 +146,7 @@ parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInpu
isSimplexLink :: Format -> Bool
isSimplexLink = \case
SimplexLink {} -> True
SimplexLink {} -> True;
_ -> False
markdownP :: Parser Markdown
+3 -2
View File
@@ -29,11 +29,12 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOff, pattern PQEncOn)
import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOn, pattern PQEncOff)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Util (encodeJSON, safeDecodeUtf8, tshow, (<$?>))
import Simplex.Messaging.Util (safeDecodeUtf8, tshow, (<$?>))
data MsgDirection = MDRcv | MDSnd
deriving (Eq, Show)
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240530_user_contact_links_user_id where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240530_user_contact_links_user_id :: Query
m20240530_user_contact_links_user_id =
[sql|
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
|]
down_m20240530_user_contact_links_user_id :: Query
down_m20240530_user_contact_links_user_id =
[sql|
DROP INDEX idx_user_contact_links_user_id;
|]
@@ -882,3 +882,4 @@ CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id);
CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(
fwd_from_chat_item_id
);
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
+2 -1
View File
@@ -46,13 +46,14 @@ import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion)
import Simplex.Messaging.Compression (Compressed, compress1, decompress1)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util (decodeJSON, eitherToMaybe, encodeJSON, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Version hiding (version)
-- Chat version history:
-268
View File
@@ -1,268 +0,0 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
module Simplex.Chat.Stats where
import Control.Applicative ((<|>))
import qualified Data.Aeson.TH as J
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (fromMaybe, isJust)
import Data.Time.Clock (UTCTime)
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Client
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Agent.Stats
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol
data PresentedServersSummary = PresentedServersSummary
{ statsStartedAt :: UTCTime,
currentUserServers :: ServersSummary,
allUsersServers :: ServersSummary
}
deriving (Show)
-- Presentation of servers will be split into separate categories,
-- so users can differentiate currently used (connected) servers,
-- previously connected servers that were in use in previous sessions,
-- and servers that are only proxied (not connected directly).
data ServersSummary = ServersSummary
{ -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary;
-- all other servers would fall either into previously used or only proxied servers category
currentlyUsedSMPServers :: [SMPServerSummary],
-- previously used SMP servers are those with Nothing in sessions and subs,
-- and have any of sentDirect, sentProxied, recvMsgs, etc. > 0 in server stats (see toPresentedServersSummary);
-- remaining servers would fall into only proxied servers category
previouslyUsedSMPServers :: [SMPServerSummary],
-- only proxied SMP servers are those that aren't (according to current state - sessions and subs)
-- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs,
-- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats
onlyProxiedSMPServers :: [SMPServerSummary],
-- currently used XFTP servers are those with Just in sessions in XFTPServerSummary,
-- and/or have upload/download/deletion in progress;
-- all other servers would fall into previously used servers category
currentlyUsedXFTPServers :: [XFTPServerSummary],
-- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress
previouslyUsedXFTPServers :: [XFTPServerSummary]
}
deriving (Show)
data SMPServerSummary = SMPServerSummary
{ smpServer :: SMPServer,
-- known:
-- for simplicity always Nothing in totalServersSummary - allows us to load configured servers only for current user,
-- and also unnecessary unless we want to add navigation to other users servers settings;
-- always Just in currentUserServers - True if server is in list of user servers, otherwise False;
-- True - allows to navigate to server settings, False - allows to add server to configured as known (SEKnown)
known :: Maybe Bool,
sessions :: Maybe ServerSessions,
subs :: Maybe SMPServerSubs,
-- stats:
-- even if sessions and subs are Nothing, stats can be Just - server could be used earlier in session,
-- or in previous sessions and stats for it were restored; server would fall into a category of
-- previously used or only proxied servers - see ServersSummary above
stats :: Maybe AgentSMPServerStatsData
}
deriving (Show)
data XFTPServerSummary = XFTPServerSummary
{ xftpServer :: XFTPServer,
known :: Maybe Bool, -- same as for SMPServerSummary
sessions :: Maybe ServerSessions,
stats :: Maybe AgentXFTPServerStatsData,
rcvInProgress :: Bool,
sndInProgress :: Bool,
delInProgress :: Bool
}
deriving (Show)
-- Maps AgentServersSummary to PresentedServersSummary:
-- - currentUserServers is for currentUser;
-- - users are passed to exclude hidden users from totalServersSummary;
-- - if currentUser is hidden, it should be accounted in totalServersSummary;
-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs
toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary
toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do
let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries
(userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms
(allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms
(userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries
(userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms
(allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms
PresentedServersSummary
{ statsStartedAt,
currentUserServers =
ServersSummary
{ currentlyUsedSMPServers = userSMPCurr,
previouslyUsedSMPServers = userSMPPrev,
onlyProxiedSMPServers = userSMPProx,
currentlyUsedXFTPServers = userXFTPCurr,
previouslyUsedXFTPServers = userXFTPPrev
},
allUsersServers =
ServersSummary
{ currentlyUsedSMPServers = allSMPCurr,
previouslyUsedSMPServers = allSMPPrev,
onlyProxiedSMPServers = allSMPProx,
currentlyUsedXFTPServers = allXFTPCurr,
previouslyUsedXFTPServers = allXFTPPrev
}
}
where
AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = agentSummary
countUserInAll auId = auId == aUserId currentUser || auId `notElem` hiddenUserIds
hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users
smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary])
smpSummsIntoCategories = foldr partitionSummary ([], [], [])
where
partitionSummary srvSumm (curr, prev, prox)
| isCurrentlyUsed srvSumm = (srvSumm : curr, prev, prox)
| isPreviouslyUsed srvSumm = (curr, srvSumm : prev, prox)
| otherwise = (curr, prev, srvSumm : prox)
isCurrentlyUsed SMPServerSummary {sessions, subs} = isJust sessions || isJust subs
isPreviouslyUsed SMPServerSummary {stats} = case stats of
Nothing -> False
-- add connCompleted, connDeleted?
-- check: should connCompleted be counted for proxy? is it?
Just AgentSMPServerStatsData {_sentDirect, _sentProxied, _sentDirectAttempts, _sentProxiedAttempts, _recvMsgs, _connCreated, _connSecured, _connSubscribed, _connSubAttempts} ->
_sentDirect > 0 || _sentProxied > 0 || _sentDirectAttempts > 0 || _sentProxiedAttempts > 0 || _recvMsgs > 0 || _connCreated > 0 || _connSecured > 0 || _connSubscribed > 0 || _connSubAttempts > 0
xftpSummsIntoCategories :: Map XFTPServer XFTPServerSummary -> ([XFTPServerSummary], [XFTPServerSummary])
xftpSummsIntoCategories = foldr partitionSummary ([], [])
where
partitionSummary srvSumm (curr, prev)
| isCurrentlyUsed srvSumm = (srvSumm : curr, prev)
| otherwise = (curr, srvSumm : prev)
isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} =
isJust sessions || rcvInProgress || sndInProgress || delInProgress
accSMPSrvsSummaries :: (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary)
accSMPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs2 smpServersStats
where
summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) smpServersSessions
summs2 = M.foldrWithKey' (addServerData addSubs) summs1 smpServersSubs
addServerData ::
(a -> SMPServerSummary -> SMPServerSummary) ->
(UserId, SMPServer) ->
a ->
(Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) ->
(Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary)
addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms')
where
userSumms'
| userId == aUserId currentUser = alterSumms newUserSummary userSumms
| otherwise = userSumms
allUsersSumms'
| countUserInAll userId = alterSumms newSummary allUsersSumms
| otherwise = allUsersSumms
alterSumms n = M.alter (Just . addData d . fromMaybe n) srv
newUserSummary = (newSummary :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs}
newSummary =
SMPServerSummary
{ smpServer = srv,
known = Nothing,
sessions = Nothing,
subs = Nothing,
stats = Nothing
}
addSessions :: ServerSessions -> SMPServerSummary -> SMPServerSummary
addSessions s summ@SMPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary
addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs}
addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary
addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStats`) stats}
accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats
where
summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) xftpServersSessions
addServerData ::
(a -> XFTPServerSummary -> XFTPServerSummary) ->
(UserId, XFTPServer) ->
a ->
(Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) ->
(Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary)
addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms')
where
userSumms'
| userId == aUserId currentUser = alterSumms newUserSummary userSumms
| otherwise = userSumms
allUsersSumms'
| countUserInAll userId = alterSumms newSummary allUsersSumms
| otherwise = allUsersSumms
alterSumms n = M.alter (Just . addData d . fromMaybe n) srv
newUserSummary = (newSummary :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs}
newSummary =
XFTPServerSummary
{ xftpServer = srv,
known = Nothing,
sessions = Nothing,
stats = Nothing,
rcvInProgress = srv `elem` xftpRcvInProgress,
sndInProgress = srv `elem` xftpSndInProgress,
delInProgress = srv `elem` xftpDelInProgress
}
addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary
addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions}
addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary
addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStats`) stats}
addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions
addServerSessions ss1 ss2 =
ServerSessions
{ ssConnected = ssConnected ss1 + ssConnected ss2,
ssErrors = ssErrors ss1 + ssErrors ss2,
ssConnecting = ssConnecting ss1 + ssConnecting ss2
}
addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs
addSMPSubs ss1 ss2 =
SMPServerSubs
{ ssActive = ssActive ss1 + ssActive ss2,
ssPending = ssPending ss1 + ssPending ss2
}
addSMPStats :: AgentSMPServerStatsData -> AgentSMPServerStatsData -> AgentSMPServerStatsData
addSMPStats sd1 sd2 =
AgentSMPServerStatsData
{ _sentDirect = _sentDirect sd1 + _sentDirect sd2,
_sentViaProxy = _sentViaProxy sd1 + _sentViaProxy sd2,
_sentProxied = _sentProxied sd1 + _sentProxied sd2,
_sentDirectAttempts = _sentDirectAttempts sd1 + _sentDirectAttempts sd2,
_sentViaProxyAttempts = _sentViaProxyAttempts sd1 + _sentViaProxyAttempts sd2,
_sentProxiedAttempts = _sentProxiedAttempts sd1 + _sentProxiedAttempts sd2,
_sentAuthErrs = _sentAuthErrs sd1 + _sentAuthErrs sd2,
_sentQuotaErrs = _sentQuotaErrs sd1 + _sentQuotaErrs sd2,
_sentExpiredErrs = _sentExpiredErrs sd1 + _sentExpiredErrs sd2,
_sentOtherErrs = _sentOtherErrs sd1 + _sentOtherErrs sd2,
_recvMsgs = _recvMsgs sd1 + _recvMsgs sd2,
_recvDuplicates = _recvDuplicates sd1 + _recvDuplicates sd2,
_recvCryptoErrs = _recvCryptoErrs sd1 + _recvCryptoErrs sd2,
_recvErrs = _recvErrs sd1 + _recvErrs sd2,
_connCreated = _connCreated sd1 + _connCreated sd2,
_connSecured = _connSecured sd1 + _connSecured sd2,
_connCompleted = _connCompleted sd1 + _connCompleted sd2,
_connDeleted = _connDeleted sd1 + _connDeleted sd2,
_connSubscribed = _connSubscribed sd1 + _connSubscribed sd2,
_connSubAttempts = _connSubAttempts sd1 + _connSubAttempts sd2,
_connSubErrs = _connSubErrs sd1 + _connSubErrs sd2
}
addXFTPStats :: AgentXFTPServerStatsData -> AgentXFTPServerStatsData -> AgentXFTPServerStatsData
addXFTPStats sd1 sd2 =
AgentXFTPServerStatsData
{ _uploads = _uploads sd1 + _uploads sd2,
_uploadAttempts = _uploadAttempts sd1 + _uploadAttempts sd2,
_uploadErrs = _uploadErrs sd1 + _uploadErrs sd2,
_downloads = _downloads sd1 + _downloads sd2,
_downloadAttempts = _downloadAttempts sd1 + _downloadAttempts sd2,
_downloadAuthErrs = _downloadAuthErrs sd1 + _downloadAuthErrs sd2,
_downloadErrs = _downloadErrs sd1 + _downloadErrs sd2,
_deletions = _deletions sd1 + _deletions sd2,
_deleteAttempts = _deleteAttempts sd1 + _deleteAttempts sd2,
_deleteErrs = _deleteErrs sd1 + _deleteErrs sd2
}
$(J.deriveJSON defaultJSON ''SMPServerSummary)
$(J.deriveJSON defaultJSON ''XFTPServerSummary)
$(J.deriveJSON defaultJSON ''ServersSummary)
$(J.deriveJSON defaultJSON ''PresentedServersSummary)
+9 -12
View File
@@ -19,7 +19,7 @@ module Simplex.Chat.Store.Connections
where
import Control.Applicative ((<|>))
import Control.Monad
import Control.Monad (forM)
import Control.Monad.Except
import Data.Int (Int64)
import Data.Maybe (catMaybes, fromMaybe)
@@ -28,7 +28,6 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol
import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ConnId)
@@ -213,19 +212,17 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2
(userId, cReqHash1, cReqHash2, ConnDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db vr = do
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
entities <- forM aConnIds $ \acId -> do
getUserByAConnId db acId >>= \case
Just user -> eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)
Nothing -> pure Nothing
unsetConnectionToSubscribe db
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> User -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db vr user@User {userId} = do
aConnIds <- map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE to_subscribe = 1 AND user_id = ?" (Only userId)
entities <- forM aConnIds $ \acId ->
eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)
unsetConnectionToSubscribe db user
let connIds = map (\(AgentConnId connId) -> connId) aConnIds
pure (connIds, catMaybes entities)
unsetConnectionToSubscribe :: DB.Connection -> IO ()
unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1"
unsetConnectionToSubscribe :: DB.Connection -> User -> IO ()
unsetConnectionToSubscribe db User {userId} = DB.execute db "UPDATE connections SET to_subscribe = 0 WHERE user_id = ? AND to_subscribe = 1" (Only userId)
deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO ()
deleteConnectionRecord db User {userId} cId = do
+31 -13
View File
@@ -56,6 +56,7 @@ module Simplex.Chat.Store.Direct
incQuotaErrCounter,
setQuotaErrCounter,
getUserContacts,
getUserContactConnIds,
createOrUpdateContactRequest,
getContactRequest',
getContactRequest,
@@ -839,19 +840,9 @@ getUserByContactRequestId db contactRequestId =
ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $
DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId)
getPendingContactConnections :: DB.Connection -> User -> IO [PendingContactConnection]
getPendingContactConnections db User {userId} = do
map toPendingContactConnection
<$> DB.queryNamed
db
[sql|
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_type
AND contact_id IS NULL
|]
[":user_id" := userId, ":conn_type" := ConnContact]
getPendingContactConnections :: DB.Connection -> User -> IO [ConnId]
getPendingContactConnections db User {userId} =
map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL" (userId, ConnContact)
getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection]
getContactConnections db vr userId Contact {contactId} =
@@ -873,6 +864,33 @@ getContactConnections db vr userId Contact {contactId} =
connections [] = pure []
connections rows = pure $ map (toConnection vr) rows
getUserContactConnIds :: DB.Connection -> User -> IO [ConnId]
getUserContactConnIds db User {userId} =
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM contacts ct
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ?
AND ct.contact_status = ?
AND ct.deleted = 0
AND
c.connection_id = (
SELECT cc_connection_id FROM (
SELECT
cc.connection_id AS cc_connection_id,
cc.created_at AS cc_created_at
FROM connections cc
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
ORDER BY cc_created_at DESC
LIMIT 1
)
)
|]
(userId, CSActive)
getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection
getConnectionById db vr User {userId} connId = ExceptT $ do
firstRow (toConnection vr) (SEConnectionNotFoundById connId) $
+29
View File
@@ -52,6 +52,7 @@ module Simplex.Chat.Store.Groups
deleteGroupItemsAndMembers,
deleteGroup,
getUserGroups,
getUserGroupMemberConnIds,
getUserGroupDetails,
getUserGroupsWithSummary,
getGroupSummary,
@@ -628,6 +629,14 @@ getUserGroups db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroup db vr user) groupIds
getUserGroupMemberConnIds :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, [ConnId])]
getUserGroupMemberConnIds db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ? ORDER BY local_display_name" (Only userId)
fmap rights . forM groupIds $ \groupId -> runExceptT $ do
gInfo <- getGroupInfo db vr user groupId
members <- liftIO $ getGroupMemberConnIds db user gInfo
pure (gInfo, members)
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
map (toGroupInfo vr userContactId)
@@ -748,6 +757,26 @@ getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
(userId, groupId, userId, userContactId)
getGroupMemberConnIds :: DB.Connection -> User -> GroupInfo -> IO [ConnId]
getGroupMemberConnIds db User {userId, userContactId} GroupInfo {groupId} = do
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM group_members m
JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id
)
WHERE m.group_id = ?
AND m.user_id = ?
AND (m.contact_id IS NULL OR m.contact_id != ?)
AND m.member_status NOT IN (?, ?, ?, ?)
|]
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do
map (toContactMember vr user)
+3 -1
View File
@@ -110,6 +110,7 @@ import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
import Simplex.Chat.Migrations.M20240528_quota_err_counter
import Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -219,7 +220,8 @@ schemaMigrations =
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays),
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter)
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter),
("20240530_user_contact_links_user_id", m20240530_user_contact_links_user_id, Just down_m20240530_user_contact_links_user_id)
]
-- | The list of migrations in ascending order by date
+14 -23
View File
@@ -350,25 +350,17 @@ getUserAddressConnections db vr User {userId} = do
|]
(userId, userId)
getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)]
getUserContactLinks db vr User {userId} =
map toUserContactConnection
<$> DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ?
|]
(userId, userId)
where
toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact)
toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection vr connRow, UserContact {userContactLinkId, connReqContact, groupId})
getUserContactLinks :: DB.Connection -> User -> IO [(ConnId, Bool)]
getUserContactLinks db User {userId} =
DB.query
db
[sql|
SELECT c.agent_conn_id, uc.group_id IS NULL
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ?
|]
(userId, userId)
deleteUserAddress :: DB.Connection -> User -> IO ()
deleteUserAddress db user@User {userId} = do
@@ -523,10 +515,9 @@ getProtocolServers db User {userId} =
(userId, decodeLatin1 $ strEncode protocol)
where
protocol = protocolTypeI @p
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Int) -> ServerCfg p
toServerCfg (host, port, keyHash, auth_, preset, tested, enabledInt) =
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p
toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) =
let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
enabled = toServerEnabled enabledInt
in ServerCfg {server, preset, tested, enabled}
overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO ()
@@ -543,7 +534,7 @@ overwriteProtocolServers db User {userId} servers =
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|]
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, fromServerEnabled enabled, userId, currentTs, currentTs))
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs))
pure $ Right ()
where
protocol = decodeLatin1 $ strEncode $ protocolTypeI @p
+1 -35
View File
@@ -1632,42 +1632,10 @@ data ServerCfg p = ServerCfg
{ server :: ProtoServerWithAuth p,
preset :: Bool,
tested :: Maybe Bool,
enabled :: ServerEnabled
enabled :: Bool
}
deriving (Show)
data ServerEnabled
= SEDisabled
| SEEnabled
| -- server is marked as known, but it's not in the list of configured servers;
-- e.g., it may be added via an unknown server dialogue and user didn't manually configure it,
-- meaning server wasn't tested (or at least such option wasn't presented in UI)
-- and it may be inoperable for user due to server password
SEKnown
deriving (Eq, Show)
pattern DBSEDisabled :: Int
pattern DBSEDisabled = 0
pattern DBSEEnabled :: Int
pattern DBSEEnabled = 1
pattern DBSEKnown :: Int
pattern DBSEKnown = 2
toServerEnabled :: Int -> ServerEnabled
toServerEnabled = \case
DBSEDisabled -> SEDisabled
DBSEEnabled -> SEEnabled
DBSEKnown -> SEKnown
_ -> SEDisabled
fromServerEnabled :: ServerEnabled -> Int
fromServerEnabled = \case
SEDisabled -> DBSEDisabled
SEEnabled -> DBSEEnabled
SEKnown -> DBSEKnown
data ChatVersion
instance VersionScope ChatVersion
@@ -1796,8 +1764,6 @@ $(JQ.deriveJSON defaultJSON ''ContactRef)
$(JQ.deriveJSON defaultJSON ''NoteFolder)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SE") ''ServerEnabled)
instance ProtocolTypeI p => ToJSON (ServerCfg p) where
toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg)
toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg)
+1 -1
View File
@@ -36,7 +36,7 @@ import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.Util
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
data ChatFeature
= CFTimedMessages
-1
View File
@@ -18,7 +18,6 @@ import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types.Util
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_)
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
data UITheme = UITheme
{ themeId :: Text,
+11
View File
@@ -2,15 +2,26 @@
module Simplex.Chat.Types.Util where
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as J
import qualified Data.Aeson.Types as JT
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Typeable
import Database.SQLite.Simple (ResultError (..), SQLData (..))
import Database.SQLite.Simple.FromField (FieldParser, returnError)
import Database.SQLite.Simple.Internal (Field (..))
import Database.SQLite.Simple.Ok (Ok (Ok))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util (safeDecodeUtf8)
encodeJSON :: ToJSON a => a -> Text
encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode
decodeJSON :: FromJSON a => Text -> Maybe a
decodeJSON = J.decode . LB.fromStrict . encodeUtf8
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
+14 -22
View File
@@ -18,7 +18,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace, toUpper)
import Data.Function (on)
import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
import Data.List (groupBy, intercalate, intersperse, sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
@@ -54,6 +54,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..))
import qualified Simplex.Messaging.Crypto as C
@@ -237,19 +238,13 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
in ttyUser u [sShow connId <> ": END"]
CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e]
CRContactSubSummary u summary ->
ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors"
where
(errors, subscribed) = partition (isJust . contactError) summary
CRUserContactSubSummary u summary ->
ttyUser u $
map addressSS addresses
<> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors")
where
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
CRContactSubError u c e -> ttyUser u [ttyContact c <> ": contact error " <> sShow e]
CRContactSubSummary {user, okSubs, errSubs} -> ttyUser user $ [sShow okSubs <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | okSubs > 0] <> viewErrorsSummary errSubs " contact errors"
CRUserAddrSubStatus {user, userContactError} -> ttyUser user [maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError]
CRUserGroupLinksSubSummary {user, okSubs, errSubs} ->
ttyUser user $
[sShow okSubs <> " group links active" | okSubs > 0]
<> viewErrorsSummary errSubs " group link errors"
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
@@ -283,10 +278,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"]
CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct'
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyContact m <> " error: " <> sShow e]
CRMemberSubSummary {user, errSubs} -> ttyUser user $ viewErrorsSummary errSubs " group member errors"
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
CRPendingSubSummary u _ -> ttyUser u []
CRPendingSubSummary {user} -> ttyUser user [] -- XXX: ???
CRSndFileSubError u SndFileTransfer {fileId, fileName} e ->
ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
@@ -297,8 +292,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact]
CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"]
CRCallInvitations _ -> []
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv]
@@ -365,7 +358,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
"chat entity locks: " <> viewJSON chatEntityLocks,
"agent locks: " <> viewJSON agentLocks
]
CRAgentServersSummary u serversSummary -> ttyUser u ["agent servers summary: " <> viewJSON serversSummary]
CRAgentStats stats -> map (plain . intercalate ",") stats
CRAgentSubs {activeSubs, pendingSubs, removedSubs} ->
[plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)]
@@ -458,8 +450,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ =
let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_)
in itemText <> deleted_
viewErrorsSummary :: [a] -> StyledString -> [StyledString]
viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)]
viewErrorsSummary :: Int -> StyledString -> [StyledString]
viewErrorsSummary numErrors s = [ttyError (tshow numErrors) <> s <> " (run with -c option to show each error)" | numErrors > 0]
contactList :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs
unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
+9 -10
View File
@@ -1448,6 +1448,7 @@ testUsersSubscribeAfterRestart :: HasCallStack => FilePath -> IO ()
testUsersSubscribeAfterRestart tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
connectUsers alice bob
alice <##> bob
@@ -1458,8 +1459,7 @@ testUsersSubscribeAfterRestart tmp = do
withTestChat tmp "alice" $ \alice -> do
-- second user is active
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
-- second user receives message
alice <##> bob
@@ -1793,8 +1793,7 @@ testUsersRestartCIExpiration tmp = do
showActiveUser alice "alice (Alice)"
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alisa] 1 contacts connected (use /cs for the list)"
alice <### ["1 contacts connected (use /cs for the list)", "[user: alisa] 1 contacts connected (use /cs for the list)"]
-- first user messages
alice ##> "/user alice"
@@ -1892,8 +1891,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
-- messages are not deleted for second user after restart
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
@@ -1948,8 +1946,7 @@ testDisableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, [])
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
-- second user still has ttl configured after restart
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
@@ -2055,8 +2052,10 @@ testUsersTimedMessages tmp = do
alice <# "bob> alisa 4"
withTestChat tmp "alice" $ \alice -> do
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
alice
<### [ "1 contacts connected (use /cs for the list)",
"[user: alice] 1 contacts connected (use /cs for the list)"
]
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
+2
View File
@@ -2590,6 +2590,7 @@ testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkConnecting tmp = do
-- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
@@ -3200,6 +3201,7 @@ testPlanGroupLinkNoContactKnown =
testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkNoContactConnecting tmp = do
gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
+1 -1
View File
@@ -17,7 +17,7 @@ import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Util (encodeJSON)
import Simplex.Chat.Types.Util (encodeJSON)
import Simplex.Messaging.Encoding.String (StrEncoding (..))
import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it)
+3 -4
View File
@@ -136,10 +136,10 @@ networkStatuses =
#endif
networkStatusesSwift :: LB.ByteString
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"networkStatuses\":[]}}}"
networkStatusesTagged :: LB.ByteString
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"networkStatuses\":[]}}"
memberSubSummary :: LB.ByteString
memberSubSummary =
@@ -222,8 +222,7 @@ testChatApi tmp = do
chatSendCmd cc "/u" `shouldReturn` activeUser
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary
chatSendCmd cc "/_network_statuses" `shouldReturn` networkStatuses
chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown