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/
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()
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)
}

View file

@ -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<Int64, () -> Void> = [:]

View file

@ -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 }

View file

@ -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

View file

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

View file

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

View file

@ -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 = "<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>"; };
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>"; };
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>"; };
@ -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 */,

View file

@ -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

View file

@ -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.*

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/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
"https://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97";

View file

@ -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.*

View file

@ -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

View file

@ -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

View file

@ -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 ((<$?>))

View file

@ -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)

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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"