mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-03-14 09:45:42 +00:00
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:
parent
03b8cdea8d
commit
f594774579
23 changed files with 191 additions and 69 deletions
2
apps/ios/.gitignore
vendored
2
apps/ios/.gitignore
vendored
|
@ -67,3 +67,5 @@ iOSInjectionProject/
|
|||
Libraries/
|
||||
|
||||
Shared/MyPlayground.playground/*
|
||||
|
||||
testpush.sh
|
||||
|
|
59
apps/ios/Shared/AppDelegate.swift
Normal file
59
apps/ios/Shared/AppDelegate.swift
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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> = [:]
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.*
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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.*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ((<$?>))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue