diff --git a/apps/ios/.gitignore b/apps/ios/.gitignore index 99ef9f7530..3d152a0610 100644 --- a/apps/ios/.gitignore +++ b/apps/ios/.gitignore @@ -67,3 +67,5 @@ iOSInjectionProject/ Libraries/ Shared/MyPlayground.playground/* + +testpush.sh diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift new file mode 100644 index 0000000000..09782c406c --- /dev/null +++ b/apps/ios/Shared/AppDelegate.swift @@ -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) + } +} diff --git a/apps/ios/Shared/Model/BGManager.swift b/apps/ios/Shared/Model/BGManager.swift index 8b66fb6e67..b76097bc79 100644 --- a/apps/ios/Shared/Model/BGManager.swift +++ b/apps/ios/Shared/Model/BGManager.swift @@ -20,7 +20,7 @@ class BGManager { static let shared = BGManager() var chatReceiver: ChatReceiver? var bgTimer: Timer? - var completed = false + var completed = true func register() { logger.debug("BGManager.register") @@ -43,36 +43,48 @@ class BGManager { private func handleRefresh(_ task: BGAppRefreshTask) { logger.debug("BGManager.handleRefresh") schedule() - self.completed = false + let completeRefresh = completionHandler { + task.setTaskCompleted(success: true) + } + task.expirationHandler = { completeRefresh("expirationHandler") } + receiveMessages(completeRefresh) + } - let completeTask: (String) -> Void = { reason in - logger.debug("BGManager.handleRefresh completeTask: \(reason)") + func completionHandler(_ complete: @escaping () -> Void) -> ((String) -> Void) { + { reason in + logger.debug("BGManager.completionHandler: \(reason)") if !self.completed { self.completed = true self.chatReceiver?.stop() self.chatReceiver = nil self.bgTimer?.invalidate() 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 { initializeChat() if ChatModel.shared.currentUser == nil { - completeTask("no current user") + completeReceiving("no current user") return } - logger.debug("BGManager.handleRefresh: starting chat") + logger.debug("BGManager.receiveMessages: starting chat") let cr = ChatReceiver() self.chatReceiver = cr cr.start() RunLoop.current.add(Timer(timeInterval: 2, repeats: true) { timer in - logger.debug("BGManager.handleRefresh: timer") + logger.debug("BGManager.receiveMessages: timer") self.bgTimer = timer if cr.lastMsgTime.distance(to: Date.now) >= waitForMessages { - completeTask("timer (no messages after \(waitForMessages) seconds)") + completeReceiving("timer (no messages after \(waitForMessages) seconds)") } }, forMode: .default) } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 4d63b485a6..776692b913 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -23,6 +23,7 @@ final class ChatModel: ObservableObject { @Published var userAddress: String? @Published var userSMPServers: [String]? @Published var appOpenUrl: URL? + @Published var deviceToken: String? var messageDelivery: Dictionary Void> = [:] diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 14a52c7e02..3cace7634d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -25,6 +25,8 @@ enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) + case apiRegisterToken(token: String) + case apiVerifyToken(token: String, code: String) case getUserSMPServers case setUserSMPServers(smpServers: [String]) 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 .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 let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case .addContact: return "/connect" @@ -90,6 +94,8 @@ enum ChatCommand { case .apiSendMessage: return "apiSendMessage" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" + case .apiRegisterToken: return "apiRegisterToken" + case .apiVerifyToken: return "apiRegisterToken" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .addContact: return "addContact" @@ -444,6 +450,18 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM 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] { let r = chatSendCmdSync(.getUserSMPServers) if case let .userSMPServers(smpServers) = r { return smpServers } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 392857d7be..d99a0d197c 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -12,6 +12,7 @@ let logger = Logger() @main struct SimpleXApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject private var chatModel = ChatModel.shared @Environment(\.scenePhase) var scenePhase diff --git a/apps/ios/SimpleX (iOS).entitlements b/apps/ios/SimpleX (iOS).entitlements index 3a6f1244b0..7e024ae355 100644 --- a/apps/ios/SimpleX (iOS).entitlements +++ b/apps/ios/SimpleX (iOS).entitlements @@ -2,6 +2,8 @@ + aps-environment + development com.apple.developer.associated-domains applinks:simplex.chat diff --git a/apps/ios/SimpleX--iOS--Info.plist b/apps/ios/SimpleX--iOS--Info.plist index f996e93668..1287772e17 100644 --- a/apps/ios/SimpleX--iOS--Info.plist +++ b/apps/ios/SimpleX--iOS--Info.plist @@ -24,6 +24,7 @@ UIBackgroundModes fetch + remote-notification diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 073cfe88aa..bdd35577d8 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E261127A30FEA00F70299 /* TerminalView.swift */; }; 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.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 */; }; 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.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 = ""; }; 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = ""; }; 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = ""; }; + 5C36027227F47AD5009F19D9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = ""; }; 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; @@ -274,6 +278,7 @@ isa = PBXGroup; children = ( 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, + 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, 5C764E87279CBC8E000C6508 /* Model */, @@ -475,6 +480,7 @@ 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, + 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */, diff --git a/cabal.project b/cabal.project index a7af3451fd..448de5ce6c 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: d38303d5f1d55a90dd95d951ba8d798e17699269 + tag: 7774fc327173ac86f5b94bc16742296f1d1fb85f source-repository-package type: git diff --git a/package.yaml b/package.yaml index 835bf19a9f..72ef61505d 100644 --- a/package.yaml +++ b/package.yaml @@ -30,7 +30,7 @@ dependencies: - optparse-applicative >= 0.15 && < 0.17 - process == 1.6.* - simple-logger == 0.1.* - - simplexmq >= 1.0 && < 1.1 + - simplexmq >= 1.1 && < 3.0 - sqlite-simple == 0.4.* - stm == 2.5.* - terminal == 0.2.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 63e6c1a0bb..94ae61487b 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 65c7ea92e2..92baa1eb24 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -72,7 +72,7 @@ library , optparse-applicative >=0.15 && <0.17 , process ==1.6.* , simple-logger ==0.1.* - , simplexmq ==1.0.* + , simplexmq >=1.1 && <3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -109,7 +109,7 @@ executable simplex-bot , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq ==1.0.* + , simplexmq >=1.1 && <3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -146,7 +146,7 @@ executable simplex-bot-advanced , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq ==1.0.* + , simplexmq >=1.1 && <3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -183,7 +183,7 @@ executable simplex-chat , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq ==1.0.* + , simplexmq >=1.1 && <3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* @@ -229,7 +229,7 @@ test-suite simplex-chat-test , process ==1.6.* , simple-logger ==0.1.* , simplex-chat - , simplexmq ==1.0.* + , simplexmq >=1.1 && <3.0 , sqlite-simple ==0.4.* , stm ==2.5.* , terminal ==0.2.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 49c915aeb6..06f99671da 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -49,11 +49,12 @@ import Simplex.Chat.Store import Simplex.Chat.Types import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM) 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 qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..)) import Simplex.Messaging.Parsers (base64P, parseAll) import Simplex.Messaging.Protocol (ErrorType (..), MsgBody) import qualified Simplex.Messaging.Protocol as SMP @@ -75,8 +76,7 @@ defaultChatConfig = { agentConfig = defaultAgentConfig { tcpPort = undefined, -- agent does not listen to TCP - initialSMPServers = undefined, -- filled in newChatController - dbFile = undefined, -- filled in newChatController + dbFile = "simplex_v1", dbPoolSize = 1, yesToMigrations = False }, @@ -109,7 +109,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize} Ch firstTime <- not <$> doesFileExist f currentUser <- newTVarIO user 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 idsDrg <- newTVarIO =<< drgNew inputQ <- newTBQueueIO tbqSize @@ -369,6 +370,10 @@ processChatCommand = \case pure $ CRContactRequestRejected cReq APIUpdateProfile profile -> withUser (`updateProfile` profile) 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)) SetUserSMPServers smpServers -> withUser $ \user -> withChatLock $ do withStore $ \st -> overwriteSMPServers st user smpServers @@ -1893,6 +1898,10 @@ chatCommandP = <|> "/_reject " *> (APIRejectContact <$> A.decimal) <|> "/_profile " *> (APIUpdateProfile <$> jsonP) <|> "/_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 " *> (SetUserSMPServers <$> smpServersP) <|> "/smp_servers" $> GetUserSMPServers @@ -1958,6 +1967,10 @@ chatCommandP = "text " *> (MCText . safeDecodeUtf8 <$> A.takeByteString) <|> "json " *> jsonP 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 (== ' ')) sendMsgQuote msgDir = SendMessageQuote <$> displayName <* A.space <*> pure msgDir <*> quotedMsg <*> A.takeByteString quotedMsg = A.char '(' *> A.takeTill (== ')') <* A.char ')' <* optional A.space diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index dcd334c610..96c5941f58 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -22,6 +22,7 @@ import Data.Map.Strict (Map) import Data.Text (Text) import Data.Time (ZonedTime) import Data.Version (showVersion) +import Data.Word (Word16) import GHC.Generics (Generic) import Numeric.Natural 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.Protocol 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.Protocol (CorrId) import System.IO (Handle) @@ -106,6 +109,10 @@ data ChatCommand | APIRejectContact Int64 | APIUpdateProfile Profile | APIParseMarkdown Text + | APIRegisterToken DeviceToken + | APIVerifyToken DeviceToken ByteString C.CbNonce + | APIIntervalNofication DeviceToken Word16 + | APIDeleteToken DeviceToken | GetUserSMPServers | SetUserSMPServers [SMPServer] | ChatHelp HelpSection diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index b2bc8d2218..a735566aa0 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -33,9 +33,8 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgMeta (..)) -import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) 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.Util ((<$?>)) diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 09cc1e9f85..9c55d1b614 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -13,7 +13,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import Options.Applicative 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.Parsers (parseAll) import System.FilePath (combine) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index b52786fe86..d7cd674041 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -31,8 +31,8 @@ import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Chat.Types import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) -import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (fromTextField_) import Simplex.Messaging.Util ((<$?>)) data ConnectionEntity diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index bb10843521..569368ed98 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -8,6 +8,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -195,12 +196,13 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields import Simplex.Chat.Protocol import Simplex.Chat.Types 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.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (StrEncoding (strEncode)) import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Protocol (ProtocolServer (..), SMPServer, pattern SMPServer) import Simplex.Messaging.Util (liftIOEither, (<$$>)) import System.FilePath (takeFileName) import UnliftIO.STM @@ -3552,7 +3554,7 @@ overwriteSMPServers st User {userId} smpServers = do liftIOEither . checkConstraint SEUniqueID . withTransaction st $ \db -> do currentTs <- getCurrentTime 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 [sql| diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 03eda5f6f5..edcd3940e3 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -32,9 +32,8 @@ import Database.SQLite.Simple.Ok (Ok (Ok)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Messaging.Agent.Protocol (ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId) -import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON) import Simplex.Messaging.Util ((<$?>)) 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) 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 ToJSON GroupMemberCategory where - toJSON = J.String . encodeText - toEncoding = JE.text . encodeText + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode instance TextEncoding GroupMemberCategory where - decodeText = \case + textDecode = \case "user" -> Just GCUserMember "invitee" -> Just GCInviteeMember "host" -> Just GCHostMember "pre" -> Just GCPreMember "post" -> Just GCPostMember _ -> Nothing - encodeText = \case + textEncode = \case GCUserMember -> "user" GCInviteeMember -> "invitee" GCHostMember -> "host" @@ -437,15 +436,15 @@ data GroupMemberStatus | GSMemCreator -- user member that created the group (only GCUserMember) 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 ToJSON GroupMemberStatus where - toJSON = J.String . encodeText - toEncoding = JE.text . encodeText + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode memberActive :: GroupMember -> Bool memberActive m = case memberStatus m of @@ -476,7 +475,7 @@ memberCurrent m = case memberStatus m of GSMemCreator -> True instance TextEncoding GroupMemberStatus where - decodeText = \case + textDecode = \case "removed" -> Just GSMemRemoved "left" -> Just GSMemLeft "deleted" -> Just GSMemGroupDeleted @@ -489,7 +488,7 @@ instance TextEncoding GroupMemberStatus where "complete" -> Just GSMemComplete "creator" -> Just GSMemCreator _ -> Nothing - encodeText = \case + textEncode = \case GSMemRemoved -> "removed" GSMemLeft -> "left" GSMemGroupDeleted -> "deleted" @@ -633,25 +632,25 @@ fileTransferCancelled (FTRcv RcvFileTransfer {cancelled}) = cancelled 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 ToJSON FileStatus where - toJSON = J.String . encodeText - toEncoding = JE.text . encodeText + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode instance TextEncoding FileStatus where - decodeText = \case + textDecode = \case "new" -> Just FSNew "accepted" -> Just FSAccepted "connected" -> Just FSConnected "complete" -> Just FSComplete "cancelled" -> Just FSCancelled _ -> Nothing - encodeText = \case + textEncode = \case FSNew -> "new" FSAccepted -> "accepted" FSConnected -> "connected" @@ -701,18 +700,18 @@ data ConnStatus ConnDeleted 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 ToJSON ConnStatus where - toJSON = J.String . encodeText - toEncoding = JE.text . encodeText + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode instance TextEncoding ConnStatus where - decodeText = \case + textDecode = \case "new" -> Just ConnNew "joined" -> Just ConnJoined "requested" -> Just ConnRequested @@ -721,7 +720,7 @@ instance TextEncoding ConnStatus where "ready" -> Just ConnReady "deleted" -> Just ConnDeleted _ -> Nothing - encodeText = \case + textEncode = \case ConnNew -> "new" ConnJoined -> "joined" ConnRequested -> "requested" @@ -733,25 +732,25 @@ instance TextEncoding ConnStatus where data ConnType = ConnContact | ConnMember | ConnSndFile | ConnRcvFile | ConnUserContact 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 ToJSON ConnType where - toJSON = J.String . encodeText - toEncoding = JE.text . encodeText + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode instance TextEncoding ConnType where - decodeText = \case + textDecode = \case "contact" -> Just ConnContact "member" -> Just ConnMember "snd_file" -> Just ConnSndFile "rcv_file" -> Just ConnRcvFile "user_contact" -> Just ConnUserContact _ -> Nothing - encodeText = \case + textEncode = \case ConnContact -> "contact" ConnMember -> "member" ConnSndFile -> "snd_file" @@ -812,9 +811,5 @@ data Notification = Notification {title :: Text, text :: Text} type JSONString = String -class TextEncoding a where - encodeText :: a -> Text - decodeText :: Text -> Maybe 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 diff --git a/stack.yaml b/stack.yaml index bd9c1fda85..18a2440b9c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: d38303d5f1d55a90dd95d951ba8d798e17699269 + commit: 7774fc327173ac86f5b94bc16742296f1d1fb85f # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 57143e7d8f..cc4ab3f7ab 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} @@ -182,7 +183,10 @@ serverCfg = msgQueueQuota = 4, queueIdBytes = 12, 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", privateKeyFile = "tests/fixtures/tls/server.key", certificateFile = "tests/fixtures/tls/server.crt" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 9b37be14a9..d90a416acc 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -15,7 +15,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (smpClientVRange) +import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange) import Test.Hspec protocolTests :: Spec @@ -23,7 +23,7 @@ protocolTests = decodeChatMessageTest srv :: SMPServer srv = - SMPServer + ProtocolServer { host = "smp.simplex.im", port = "5223", keyHash = C.KeyHash "\215m\248\251"