ios: push notifications (#482)

* ios: get device token for push notifications

* ios: receive messages when background notification is received

* add notifications API, update simplexmq

* chat API to register and verify notification token

* update AppDelegate to recognize different notification types, update simplexmq

* core: api to enable periodic background notifications

* update simplexmq

* chat API to delete device notification token

* use base64url encoding in verification code

* update simplexmq for notifications
This commit is contained in:
Evgeny Poberezkin 2022-04-21 20:04:22 +01:00 committed by GitHub
parent 03b8cdea8d
commit f594774579
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 191 additions and 69 deletions

2
apps/ios/.gitignore vendored
View File

@ -67,3 +67,5 @@ iOSInjectionProject/
Libraries/ Libraries/
Shared/MyPlayground.playground/* Shared/MyPlayground.playground/*
testpush.sh

View File

@ -0,0 +1,59 @@
//
// AppDelegate.swift
// SimpleX
//
// Created by Evgeny on 30/03/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import Foundation
import UIKit
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
ChatModel.shared.deviceToken = token
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
logger.error("AppDelegate: didFailToRegisterForRemoteNotificationsWithError \(error.localizedDescription)")
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
logger.debug("AppDelegate: didReceiveRemoteNotification")
print(userInfo)
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any] {
if let verification = ntfData["verification"] as? String {
logger.debug("AppDelegate: didReceiveRemoteNotification: verification, confirming \(verification)")
// TODO send to chat
completionHandler(.newData)
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
// TODO check if app in background
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
receiveMessages(completionHandler)
} else if let smpQueue = ntfData["checkMessage"] as? String {
// TODO check if app in background
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessage \(smpQueue)")
receiveMessages(completionHandler)
}
}
}
private func receiveMessages(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let complete = BGManager.shared.completionHandler {
logger.debug("AppDelegate: completed BGManager.receiveMessages")
completionHandler(.newData)
}
BGManager.shared.receiveMessages(complete)
}
}

View File

@ -20,7 +20,7 @@ class BGManager {
static let shared = BGManager() static let shared = BGManager()
var chatReceiver: ChatReceiver? var chatReceiver: ChatReceiver?
var bgTimer: Timer? var bgTimer: Timer?
var completed = false var completed = true
func register() { func register() {
logger.debug("BGManager.register") logger.debug("BGManager.register")
@ -43,36 +43,48 @@ class BGManager {
private func handleRefresh(_ task: BGAppRefreshTask) { private func handleRefresh(_ task: BGAppRefreshTask) {
logger.debug("BGManager.handleRefresh") logger.debug("BGManager.handleRefresh")
schedule() schedule()
self.completed = false let completeRefresh = completionHandler {
task.setTaskCompleted(success: true)
}
task.expirationHandler = { completeRefresh("expirationHandler") }
receiveMessages(completeRefresh)
}
let completeTask: (String) -> Void = { reason in func completionHandler(_ complete: @escaping () -> Void) -> ((String) -> Void) {
logger.debug("BGManager.handleRefresh completeTask: \(reason)") { reason in
logger.debug("BGManager.completionHandler: \(reason)")
if !self.completed { if !self.completed {
self.completed = true self.completed = true
self.chatReceiver?.stop() self.chatReceiver?.stop()
self.chatReceiver = nil self.chatReceiver = nil
self.bgTimer?.invalidate() self.bgTimer?.invalidate()
self.bgTimer = nil self.bgTimer = nil
task.setTaskCompleted(success: true) complete()
}
} }
} }
task.expirationHandler = { completeTask("expirationHandler") } func receiveMessages(_ completeReceiving: @escaping (String) -> Void) {
if (!self.completed) {
logger.debug("BGManager.receiveMessages: in progress, exiting")
return
}
self.completed = false
DispatchQueue.main.async { DispatchQueue.main.async {
initializeChat() initializeChat()
if ChatModel.shared.currentUser == nil { if ChatModel.shared.currentUser == nil {
completeTask("no current user") completeReceiving("no current user")
return return
} }
logger.debug("BGManager.handleRefresh: starting chat") logger.debug("BGManager.receiveMessages: starting chat")
let cr = ChatReceiver() let cr = ChatReceiver()
self.chatReceiver = cr self.chatReceiver = cr
cr.start() cr.start()
RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in
logger.debug("BGManager.handleRefresh: timer") logger.debug("BGManager.receiveMessages: timer")
self.bgTimer = timer self.bgTimer = timer
if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages { if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages {
completeTask("timer (no messages after \(waitForMessages) seconds)") completeReceiving("timer (no messages after \(waitForMessages) seconds)")
} }
}, forMode: .default) }, forMode: .default)
} }

View File

@ -23,6 +23,7 @@ final class ChatModel: ObservableObject {
@Published var userAddress: String? @Published var userAddress: String?
@Published var userSMPServers: [String]? @Published var userSMPServers: [String]?
@Published var appOpenUrl: URL? @Published var appOpenUrl: URL?
@Published var deviceToken: String?
var messageDelivery: Dictionary<Int64, () -> Void> = [:] var messageDelivery: Dictionary<Int64, () -> Void> = [:]

View File

@ -25,6 +25,8 @@ enum ChatCommand {
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiRegisterToken(token: String)
case apiVerifyToken(token: String, code: String)
case getUserSMPServers case getUserSMPServers
case setUserSMPServers(smpServers: [String]) case setUserSMPServers(smpServers: [String])
case addContact case addContact
@ -59,6 +61,8 @@ enum ChatCommand {
} }
case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)" case let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiRegisterToken(token): return "/_ntf register apn \(token)"
case let .apiVerifyToken(token, code): return "/_ntf verify apn \(token) \(code)"
case .getUserSMPServers: return "/smp_servers" case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case .addContact: return "/connect" case .addContact: return "/connect"
@ -90,6 +94,8 @@ enum ChatCommand {
case .apiSendMessage: return "apiSendMessage" case .apiSendMessage: return "apiSendMessage"
case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem"
case .apiRegisterToken: return "apiRegisterToken"
case .apiVerifyToken: return "apiRegisterToken"
case .getUserSMPServers: return "getUserSMPServers" case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers" case .setUserSMPServers: return "setUserSMPServers"
case .addContact: return "addContact" case .addContact: return "addContact"
@ -444,6 +450,18 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM
throw r throw r
} }
func apiRegisterToken(token: String) async throws {
let r = await chatSendCmd(.apiRegisterToken(token: token))
if case .cmdOk = r { return }
throw r
}
func apiVerifyToken(token: String, code: String) async throws {
let r = await chatSendCmd(.apiVerifyToken(token: token, code: code))
if case .cmdOk = r { return }
throw r
}
func getUserSMPServers() throws -> [String] { func getUserSMPServers() throws -> [String] {
let r = chatSendCmdSync(.getUserSMPServers) let r = chatSendCmdSync(.getUserSMPServers)
if case let .userSMPServers(smpServers) = r { return smpServers } if case let .userSMPServers(smpServers) = r { return smpServers }

View File

@ -12,6 +12,7 @@ let logger = Logger()
@main @main
struct SimpleXApp: App { struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared @StateObject private var chatModel = ChatModel.shared
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:simplex.chat</string> <string>applinks:simplex.chat</string>

View File

@ -24,6 +24,7 @@
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -18,6 +18,9 @@
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E261127A30FEA00F70299 /* TerminalView.swift */; }; 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E261127A30FEA00F70299 /* TerminalView.swift */; };
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; }; 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; };
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; };
5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; };
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
5C36027427F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; }; 5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; }; 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
@ -94,6 +97,7 @@
5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; }; 5C2E261127A30FEA00F70299 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = "<group>"; };
5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; }; 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = "<group>"; };
5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; }; 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = "<group>"; };
5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; }; 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; }; 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
@ -274,6 +278,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */,
5C36027227F47AD5009F19D9 /* AppDelegate.swift */,
5CA059C4279559F40002BEB4 /* ContentView.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */,
64DAE1502809D9F5000DA960 /* FileUtils.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */,
5C764E87279CBC8E000C6508 /* Model */, 5C764E87279CBC8E000C6508 /* Model */,
@ -475,6 +480,7 @@
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,

View File

@ -3,7 +3,7 @@ packages: .
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: d38303d5f1d55a90dd95d951ba8d798e17699269 tag: 7774fc327173ac86f5b94bc16742296f1d1fb85f
source-repository-package source-repository-package
type: git type: git

View File

@ -30,7 +30,7 @@ dependencies:
- optparse-applicative >= 0.15 && < 0.17 - optparse-applicative >= 0.15 && < 0.17
- process == 1.6.* - process == 1.6.*
- simple-logger == 0.1.* - simple-logger == 0.1.*
- simplexmq >= 1.0 && < 1.1 - simplexmq >= 1.1 && < 3.0
- sqlite-simple == 0.4.* - sqlite-simple == 0.4.*
- stm == 2.5.* - stm == 2.5.*
- terminal == 0.2.* - terminal == 0.2.*

View File

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."d38303d5f1d55a90dd95d951ba8d798e17699269" = "0gx9wlk0v8ml6aid9hzm1nqy357plk8sbnhfcnchna12dgm8v0sd"; "https://github.com/simplex-chat/simplexmq.git"."7774fc327173ac86f5b94bc16742296f1d1fb85f" = "0cj4hywxmsiljgfv20syyn8cbhp56pxr5s0hfmik37l4ii8pj8rs";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";

View File

@ -72,7 +72,7 @@ library
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.* , process ==1.6.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplexmq ==1.0.* , simplexmq >=1.1 && <3.0
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
, stm ==2.5.* , stm ==2.5.*
, terminal ==0.2.* , terminal ==0.2.*
@ -109,7 +109,7 @@ executable simplex-bot
, process ==1.6.* , process ==1.6.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplex-chat , simplex-chat
, simplexmq ==1.0.* , simplexmq >=1.1 && <3.0
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
, stm ==2.5.* , stm ==2.5.*
, terminal ==0.2.* , terminal ==0.2.*
@ -146,7 +146,7 @@ executable simplex-bot-advanced
, process ==1.6.* , process ==1.6.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplex-chat , simplex-chat
, simplexmq ==1.0.* , simplexmq >=1.1 && <3.0
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
, stm ==2.5.* , stm ==2.5.*
, terminal ==0.2.* , terminal ==0.2.*
@ -183,7 +183,7 @@ executable simplex-chat
, process ==1.6.* , process ==1.6.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplex-chat , simplex-chat
, simplexmq ==1.0.* , simplexmq >=1.1 && <3.0
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
, stm ==2.5.* , stm ==2.5.*
, terminal ==0.2.* , terminal ==0.2.*
@ -229,7 +229,7 @@ test-suite simplex-chat-test
, process ==1.6.* , process ==1.6.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
, simplex-chat , simplex-chat
, simplexmq ==1.0.* , simplexmq >=1.1 && <3.0
, sqlite-simple ==0.4.* , sqlite-simple ==0.4.*
, stm ==2.5.* , stm ==2.5.*
, terminal ==0.2.* , terminal ==0.2.*

View File

@ -49,11 +49,12 @@ import Simplex.Chat.Store
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM) import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM)
import Simplex.Messaging.Agent import Simplex.Messaging.Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..))
import Simplex.Messaging.Parsers (base64P, parseAll) import Simplex.Messaging.Parsers (base64P, parseAll)
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
@ -75,8 +76,7 @@ defaultChatConfig =
{ agentConfig = { agentConfig =
defaultAgentConfig defaultAgentConfig
{ tcpPort = undefined, -- agent does not listen to TCP { tcpPort = undefined, -- agent does not listen to TCP
initialSMPServers = undefined, -- filled in newChatController dbFile = "simplex_v1",
dbFile = undefined, -- filled in newChatController
dbPoolSize = 1, dbPoolSize = 1,
yesToMigrations = False yesToMigrations = False
}, },
@ -109,7 +109,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize} Ch
firstTime <- not <$> doesFileExist f firstTime <- not <$> doesFileExist f
currentUser <- newTVarIO user currentUser <- newTVarIO user
initialSMPServers <- resolveServers initialSMPServers <- resolveServers
smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db", initialSMPServers} let servers = InitialAgentServers {smp = initialSMPServers, ntf = ["smp://smAc80rtvJKA02nysCCmiDzMUmcGnYA3gujwKU1NT30=@127.0.0.1:443"]}
smpAgent <- getSMPAgentClient aCfg {dbFile = dbFilePrefix <> "_agent.db"} servers
agentAsync <- newTVarIO Nothing agentAsync <- newTVarIO Nothing
idsDrg <- newTVarIO =<< drgNew idsDrg <- newTVarIO =<< drgNew
inputQ <- newTBQueueIO tbqSize inputQ <- newTBQueueIO tbqSize
@ -369,6 +370,10 @@ processChatCommand = \case
pure $ CRContactRequestRejected cReq pure $ CRContactRequestRejected cReq
APIUpdateProfile profile -> withUser (`updateProfile` profile) APIUpdateProfile profile -> withUser (`updateProfile` profile)
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
APIRegisterToken token -> withUser $ \_ -> withAgent (`registerNtfToken` token) $> CRCmdOk
APIVerifyToken token code nonce -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token code nonce) $> CRCmdOk
APIIntervalNofication token interval -> withUser $ \_ -> withAgent (\a -> enableNtfCron a token interval) $> CRCmdOk
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) $> CRCmdOk
GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore (`getSMPServers` user)) GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore (`getSMPServers` user))
SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do
withStore $ \st -> overwriteSMPServers st user smpServers withStore $ \st -> overwriteSMPServers st user smpServers
@ -1893,6 +1898,10 @@ chatCommandP =
<|> "/_reject " *> (APIRejectContact <$> A.decimal) <|> "/_reject " *> (APIRejectContact <$> A.decimal)
<|> "/_profile " *> (APIUpdateProfile <$> jsonP) <|> "/_profile " *> (APIUpdateProfile <$> jsonP)
<|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString) <|> "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString)
<|> "/_ntf register " *> (APIRegisterToken <$> tokenP)
<|> "/_ntf verify " *> (APIVerifyToken <$> tokenP <* A.space <*> strP <* A.space <*> strP)
<|> "/_ntf interval " *> (APIIntervalNofication <$> tokenP <* A.space <*> A.decimal)
<|> "/_ntf delete " *> (APIDeleteToken <$> tokenP)
<|> "/smp_servers default" $> SetUserSMPServers [] <|> "/smp_servers default" $> SetUserSMPServers []
<|> "/smp_servers " *> (SetUserSMPServers <$> smpServersP) <|> "/smp_servers " *> (SetUserSMPServers <$> smpServersP)
<|> "/smp_servers" $> GetUserSMPServers <|> "/smp_servers" $> GetUserSMPServers
@ -1958,6 +1967,10 @@ chatCommandP =
"text " *> (MCText . safeDecodeUtf8 <$> A.takeByteString) "text " *> (MCText . safeDecodeUtf8 <$> A.takeByteString)
<|> "json " *> jsonP <|> "json " *> jsonP
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
tokenP = "apns " *> (DeviceToken PPApns <$> hexStringP)
hexStringP =
A.takeWhile (\c -> (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) >>= \s ->
if even (B.length s) then pure s else fail "odd number of hex characters"
displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ')) displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' '))
sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString
quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space

View File

@ -22,6 +22,7 @@ import Data.Map.Strict (Map)
import Data.Text (Text) import Data.Text (Text)
import Data.Time (ZonedTime) import Data.Time (ZonedTime)
import Data.Version (showVersion) import Data.Version (showVersion)
import Data.Word (Word16)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Numeric.Natural import Numeric.Natural
import qualified Paths_simplex_chat as SC import qualified Paths_simplex_chat as SC
@ -34,6 +35,8 @@ import Simplex.Messaging.Agent (AgentClient)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..))
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (CorrId) import Simplex.Messaging.Protocol (CorrId)
import System.IO (Handle) import System.IO (Handle)
@ -106,6 +109,10 @@ data ChatCommand
| APIRejectContact Int64 | APIRejectContact Int64
| APIUpdateProfile Profile | APIUpdateProfile Profile
| APIParseMarkdown Text | APIParseMarkdown Text
| APIRegisterToken DeviceToken
| APIVerifyToken DeviceToken ByteString C.CbNonce
| APIIntervalNofication DeviceToken Word16
| APIDeleteToken DeviceToken
| GetUserSMPServers | GetUserSMPServers
| SetUserSMPServers [SMPServer] | SetUserSMPServers [SMPServer]
| ChatHelp HelpSection | ChatHelp HelpSection

View File

@ -33,9 +33,8 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgMeta (..)) import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgMeta (..))
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON)
import Simplex.Messaging.Protocol (MsgBody) import Simplex.Messaging.Protocol (MsgBody)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))

View File

@ -13,7 +13,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Options.Applicative import Options.Applicative
import Simplex.Chat.Controller (updateStr, versionStr) import Simplex.Chat.Controller (updateStr, versionStr)
import Simplex.Messaging.Agent.Protocol (SMPServer (..)) import Simplex.Messaging.Agent.Protocol (SMPServer)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Parsers (parseAll)
import System.FilePath (combine) import System.FilePath (combine)

View File

@ -31,8 +31,8 @@ import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (fromTextField_)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
data ConnectionEntity data ConnectionEntity

View File

@ -8,6 +8,7 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
@ -195,12 +196,13 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Util (eitherToMaybe) import Simplex.Chat.Util (eitherToMaybe)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..), SMPServer (..)) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, InvitationId, MsgMeta (..))
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore, firstRow, withTransaction)
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Encoding.String (StrEncoding (strEncode))
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer)
import Simplex.Messaging.Util (liftIOEither, (<$$>)) import Simplex.Messaging.Util (liftIOEither, (<$$>))
import System.FilePath (takeFileName) import System.FilePath (takeFileName)
import UnliftIO.STM import UnliftIO.STM
@ -3552,7 +3554,7 @@ overwriteSMPServers st User {userId} smpServers = do
liftIOEither . checkConstraint SEUniqueID . withTransaction st $ \db -> do liftIOEither . checkConstraint SEUniqueID . withTransaction st $ \db -> do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId) DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId)
forM_ smpServers $ \SMPServer {host, port, keyHash} -> forM_ smpServers $ \ProtocolServer {host, port, keyHash} ->
DB.execute DB.execute
db db
[sql| [sql|

View File

@ -32,9 +32,8 @@ import Database.SQLite.Simple.Ok (Ok (Ok))
import Database.SQLite.Simple.ToField (ToField (..)) import Database.SQLite.Simple.ToField (ToField (..))
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Simplex.Messaging.Agent.Protocol (ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId) import Simplex.Messaging.Agent.Protocol (ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
import Simplex.Messaging.Agent.Store.SQLite (fromTextField_)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON)
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
class IsContact a where class IsContact a where
@ -398,25 +397,25 @@ data GroupMemberCategory
| GCPostMember -- member who joined after the user to whom the user was introduced (user receives x.grp.mem.new announcing these members and then x.grp.mem.fwd with invitation from these members) | GCPostMember -- member who joined after the user to whom the user was introduced (user receives x.grp.mem.new announcing these members and then x.grp.mem.fwd with invitation from these members)
deriving (Eq, Show) deriving (Eq, Show)
instance FromField GroupMemberCategory where fromField = fromTextField_ decodeText instance FromField GroupMemberCategory where fromField = fromTextField_ textDecode
instance ToField GroupMemberCategory where toField = toField . encodeText instance ToField GroupMemberCategory where toField = toField . textEncode
instance FromJSON GroupMemberCategory where parseJSON = textParseJSON "GroupMemberCategory" instance FromJSON GroupMemberCategory where parseJSON = textParseJSON "GroupMemberCategory"
instance ToJSON GroupMemberCategory where instance ToJSON GroupMemberCategory where
toJSON = J.String . encodeText toJSON = J.String . textEncode
toEncoding = JE.text . encodeText toEncoding = JE.text . textEncode
instance TextEncoding GroupMemberCategory where instance TextEncoding GroupMemberCategory where
decodeText = \case textDecode = \case
"user" -> Just GCUserMember "user" -> Just GCUserMember
"invitee" -> Just GCInviteeMember "invitee" -> Just GCInviteeMember
"host" -> Just GCHostMember "host" -> Just GCHostMember
"pre" -> Just GCPreMember "pre" -> Just GCPreMember
"post" -> Just GCPostMember "post" -> Just GCPostMember
_ -> Nothing _ -> Nothing
encodeText = \case textEncode = \case
GCUserMember -> "user" GCUserMember -> "user"
GCInviteeMember -> "invitee" GCInviteeMember -> "invitee"
GCHostMember -> "host" GCHostMember -> "host"
@ -437,15 +436,15 @@ data GroupMemberStatus
| GSMemCreator -- user member that created the group (only GCUserMember) | GSMemCreator -- user member that created the group (only GCUserMember)
deriving (Eq, Show, Ord) deriving (Eq, Show, Ord)
instance FromField GroupMemberStatus where fromField = fromTextField_ decodeText instance FromField GroupMemberStatus where fromField = fromTextField_ textDecode
instance ToField GroupMemberStatus where toField = toField . encodeText instance ToField GroupMemberStatus where toField = toField . textEncode
instance FromJSON GroupMemberStatus where parseJSON = textParseJSON "GroupMemberStatus" instance FromJSON GroupMemberStatus where parseJSON = textParseJSON "GroupMemberStatus"
instance ToJSON GroupMemberStatus where instance ToJSON GroupMemberStatus where
toJSON = J.String . encodeText toJSON = J.String . textEncode
toEncoding = JE.text . encodeText toEncoding = JE.text . textEncode
memberActive :: GroupMember -> Bool memberActive :: GroupMember -> Bool
memberActive m = case memberStatus m of memberActive m = case memberStatus m of
@ -476,7 +475,7 @@ memberCurrent m = case memberStatus m of
GSMemCreator -> True GSMemCreator -> True
instance TextEncoding GroupMemberStatus where instance TextEncoding GroupMemberStatus where
decodeText = \case textDecode = \case
"removed" -> Just GSMemRemoved "removed" -> Just GSMemRemoved
"left" -> Just GSMemLeft "left" -> Just GSMemLeft
"deleted" -> Just GSMemGroupDeleted "deleted" -> Just GSMemGroupDeleted
@ -489,7 +488,7 @@ instance TextEncoding GroupMemberStatus where
"complete" -> Just GSMemComplete "complete" -> Just GSMemComplete
"creator" -> Just GSMemCreator "creator" -> Just GSMemCreator
_ -> Nothing _ -> Nothing
encodeText = \case textEncode = \case
GSMemRemoved -> "removed" GSMemRemoved -> "removed"
GSMemLeft -> "left" GSMemLeft -> "left"
GSMemGroupDeleted -> "deleted" GSMemGroupDeleted -> "deleted"
@ -633,25 +632,25 @@ fileTransferCancelled (FTRcv RcvFileTransfer {cancelled}) = cancelled
data FileStatus = FSNew | FSAccepted | FSConnected | FSComplete | FSCancelled deriving (Eq, Ord, Show) data FileStatus = FSNew | FSAccepted | FSConnected | FSComplete | FSCancelled deriving (Eq, Ord, Show)
instance FromField FileStatus where fromField = fromTextField_ decodeText instance FromField FileStatus where fromField = fromTextField_ textDecode
instance ToField FileStatus where toField = toField . encodeText instance ToField FileStatus where toField = toField . textEncode
instance FromJSON FileStatus where parseJSON = textParseJSON "FileStatus" instance FromJSON FileStatus where parseJSON = textParseJSON "FileStatus"
instance ToJSON FileStatus where instance ToJSON FileStatus where
toJSON = J.String . encodeText toJSON = J.String . textEncode
toEncoding = JE.text . encodeText toEncoding = JE.text . textEncode
instance TextEncoding FileStatus where instance TextEncoding FileStatus where
decodeText = \case textDecode = \case
"new" -> Just FSNew "new" -> Just FSNew
"accepted" -> Just FSAccepted "accepted" -> Just FSAccepted
"connected" -> Just FSConnected "connected" -> Just FSConnected
"complete" -> Just FSComplete "complete" -> Just FSComplete
"cancelled" -> Just FSCancelled "cancelled" -> Just FSCancelled
_ -> Nothing _ -> Nothing
encodeText = \case textEncode = \case
FSNew -> "new" FSNew -> "new"
FSAccepted -> "accepted" FSAccepted -> "accepted"
FSConnected -> "connected" FSConnected -> "connected"
@ -701,18 +700,18 @@ data ConnStatus
ConnDeleted ConnDeleted
deriving (Eq, Show) deriving (Eq, Show)
instance FromField ConnStatus where fromField = fromTextField_ decodeText instance FromField ConnStatus where fromField = fromTextField_ textDecode
instance ToField ConnStatus where toField = toField . encodeText instance ToField ConnStatus where toField = toField . textEncode
instance FromJSON ConnStatus where parseJSON = textParseJSON "ConnStatus" instance FromJSON ConnStatus where parseJSON = textParseJSON "ConnStatus"
instance ToJSON ConnStatus where instance ToJSON ConnStatus where
toJSON = J.String . encodeText toJSON = J.String . textEncode
toEncoding = JE.text . encodeText toEncoding = JE.text . textEncode
instance TextEncoding ConnStatus where instance TextEncoding ConnStatus where
decodeText = \case textDecode = \case
"new" -> Just ConnNew "new" -> Just ConnNew
"joined" -> Just ConnJoined "joined" -> Just ConnJoined
"requested" -> Just ConnRequested "requested" -> Just ConnRequested
@ -721,7 +720,7 @@ instance TextEncoding ConnStatus where
"ready" -> Just ConnReady "ready" -> Just ConnReady
"deleted" -> Just ConnDeleted "deleted" -> Just ConnDeleted
_ -> Nothing _ -> Nothing
encodeText = \case textEncode = \case
ConnNew -> "new" ConnNew -> "new"
ConnJoined -> "joined" ConnJoined -> "joined"
ConnRequested -> "requested" ConnRequested -> "requested"
@ -733,25 +732,25 @@ instance TextEncoding ConnStatus where
data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile | ConnUserContact data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile | ConnUserContact
deriving (Eq, Show) deriving (Eq, Show)
instance FromField ConnType where fromField = fromTextField_ decodeText instance FromField ConnType where fromField = fromTextField_ textDecode
instance ToField ConnType where toField = toField . encodeText instance ToField ConnType where toField = toField . textEncode
instance FromJSON ConnType where parseJSON = textParseJSON "ConnType" instance FromJSON ConnType where parseJSON = textParseJSON "ConnType"
instance ToJSON ConnType where instance ToJSON ConnType where
toJSON = J.String . encodeText toJSON = J.String . textEncode
toEncoding = JE.text . encodeText toEncoding = JE.text . textEncode
instance TextEncoding ConnType where instance TextEncoding ConnType where
decodeText = \case textDecode = \case
"contact" -> Just ConnContact "contact" -> Just ConnContact
"member" -> Just ConnMember "member" -> Just ConnMember
"snd_file" -> Just ConnSndFile "snd_file" -> Just ConnSndFile
"rcv_file" -> Just ConnRcvFile "rcv_file" -> Just ConnRcvFile
"user_contact" -> Just ConnUserContact "user_contact" -> Just ConnUserContact
_ -> Nothing _ -> Nothing
encodeText = \case textEncode = \case
ConnContact -> "contact" ConnContact -> "contact"
ConnMember -> "member" ConnMember -> "member"
ConnSndFile -> "snd_file" ConnSndFile -> "snd_file"
@ -812,9 +811,5 @@ data Notification = Notification {title :: Text, text :: Text}
type JSONString = String type JSONString = String
class TextEncoding a where
encodeText :: a -> Text
decodeText :: Text -> Maybe a
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . decodeText textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode

View File

@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq # - ../simplexmq
- github: simplex-chat/simplexmq - github: simplex-chat/simplexmq
commit: d38303d5f1d55a90dd95d951ba8d798e17699269 commit: 7774fc327173ac86f5b94bc16742296f1d1fb85f
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
- github: simplex-chat/aeson - github: simplex-chat/aeson
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7

View File

@ -1,6 +1,7 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
@ -182,7 +183,10 @@ serverCfg =
msgQueueQuota = 4, msgQueueQuota = 4,
queueIdBytes = 12, queueIdBytes = 12,
msgIdBytes = 6, msgIdBytes = 6,
storeLog = Nothing, storeLogFile = Nothing,
allowNewQueues = True,
messageTTL = Just $ 7 * 86400, -- 7 days
expireMessagesInterval = Just 21600_000000, -- microseconds, 6 hours
caCertificateFile = "tests/fixtures/tls/ca.crt", caCertificateFile = "tests/fixtures/tls/ca.crt",
privateKeyFile = "tests/fixtures/tls/server.key", privateKeyFile = "tests/fixtures/tls/server.key",
certificateFile = "tests/fixtures/tls/server.crt" certificateFile = "tests/fixtures/tls/server.crt"

View File

@ -15,7 +15,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Crypto.Ratchet
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (smpClientVRange) import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange)
import Test.Hspec import Test.Hspec
protocolTests :: Spec protocolTests :: Spec
@ -23,7 +23,7 @@ protocolTests = decodeChatMessageTest
srv :: SMPServer srv :: SMPServer
srv = srv =
SMPServer ProtocolServer
{ host = "smp.simplex.im", { host = "smp.simplex.im",
port = "5223", port = "5223",
keyHash = C.KeyHash "\215m\248\251" keyHash = C.KeyHash "\215m\248\251"