Merge branch 'ios-notifications' into ep/ios-file-provider
This commit is contained in:
commit
c7b5d73512
68
README.md
68
README.md
@ -24,7 +24,24 @@
|
||||
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
|
||||
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
|
||||
|
||||
## Why privacy of communications matter
|
||||
## Contents
|
||||
|
||||
- [Why privacy matters](#why-privacy-matters)
|
||||
- [SimpleX approach to privacy and security](#simplex-approach-to-privacy-and-security)
|
||||
- [Complete privacy](#complete-privacy-of-your-identity-profile-contacts-and-metadata)
|
||||
- [Protection against spam and abuse](#the-best-protection-against-spam-and-abuse)
|
||||
- [Ownership and security of your data](#complete-ownership-control-and-security-of-your-data)
|
||||
- [Users own SimpleX network](#users-own-simplex-network)
|
||||
- [Frequently asked questions](#frequently-asked-questions)
|
||||
- [News and updates](#news-and-updates)
|
||||
- [Make a private connection](#make-a-private-connection)
|
||||
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
|
||||
- [SimpleX Platform design](#simplex-platform-design)
|
||||
- [For developers](#for-developers)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Disclaimer, License](#disclaimer)
|
||||
|
||||
## Why privacy matters
|
||||
|
||||
Everyone should care about privacy and security of their communications - innocuous conversations can put you in danger even if there is nothing to hide.
|
||||
|
||||
@ -32,9 +49,9 @@ One of the most shocking stories is the experience of [Mohamedou Ould Salahi](ht
|
||||
|
||||
It is not enough to use an end-to-end encrypted messenger, we all should use the messengers that protect the privacy of our personal networks - who we are connected with.
|
||||
|
||||
## SimpleX unique approach to privacy and security
|
||||
## SimpleX approach to privacy and security
|
||||
|
||||
### Full privacy of your identity, profile, contacts and metadata
|
||||
### Complete privacy of your identity, profile, contacts and metadata
|
||||
|
||||
**Unlike any other existing messaging platform, SimpleX has no identifiers assigned to the users** - not even random numbers. This protects the privacy of who are you communicating with, hiding it from SimpleX platform servers and from any observers. [Read more](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
|
||||
|
||||
@ -50,25 +67,24 @@ SimpleX stores all user data on client devices, the messages are only held tempo
|
||||
|
||||
You can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers. [Read more](./docs/SIMPLEX.md#users-own-simplex-network).
|
||||
|
||||
## For developers
|
||||
## Frequently asked questions
|
||||
|
||||
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
|
||||
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
|
||||
|
||||
You already can:
|
||||
2. _Why should I not just use Signal?_ This [post](https://github.com/dessalines/essays/blob/master/why_not_signal.md) shows why Signal cannot be considered a private messenger. Signal is a centralised platform that uses phone numbers to identify its users and their contacts.
|
||||
|
||||
- use SimpleX Chat library to integrate chat functionality into your apps.
|
||||
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages – it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
|
||||
|
||||
## News and updates
|
||||
|
||||
[Jun 4, 2022. v2.2: the new Privacy and Security settings](./blog/20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
[May 11, 2022. v2.0 released - sending images and files in mobile apps](./blog/20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
[Apr 04, 2022. Instant notifications for SimpleX Chat mobile apps](./blog/20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
[Mar 08, 2022 Mobile apps for iOS and Android released](./blog/20220308-simplex-chat-mobile-apps.md)
|
||||
|
||||
[Jan 12, 2022. SimpleX v1 released: the only messaging and application platform without user identities](./20220112-simplex-chat-v1-released.md)
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
## Make a private connection
|
||||
@ -103,6 +119,17 @@ Only the client devices have information about users, their contacts and groups.
|
||||
|
||||
See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information on platform objectives and technical design.
|
||||
|
||||
## For developers
|
||||
|
||||
We plan that the SimpleX platform will grow into the platform supporting any distributed Internet application. This will allow you to build any service that people can access via chat, with custom web-based UI widgets that anybody with basic HTML/CSS/JavaScript knowledge can create in a few hours.
|
||||
|
||||
You already can:
|
||||
|
||||
- use SimpleX Chat library to integrate chat functionality into your apps.
|
||||
- use SimpleX Chat bot templates in Haskell to build your own chat bot services (TypeScript SDK is coming soon).
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- ✅ Easy to deploy SimpleX server with in-memory message storage, without any dependencies.
|
||||
@ -113,12 +140,13 @@ See [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/p
|
||||
- ✅ Private instant notifications for Android using background service.
|
||||
- ✅ Haskell chat bot templates.
|
||||
- ✅ v2.0 - supporting images and files in mobile apps.
|
||||
- 🏗 End-to-end encrypted audio and video calls via the mobile apps.
|
||||
- 🏗 Automatic chat history deletion.
|
||||
- ✅ Manual chat history deletion.
|
||||
- 🚀 End-to-end encrypted audio and video calls via the mobile apps (enable via Experimental Features).
|
||||
- 🏗 Privacy preserving instant notifications for iOS using Apple Push Notification service (in progress).
|
||||
- 🏗 Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (in progress).
|
||||
- 🏗 Chat database portability and encryption.
|
||||
- Groups support for mobile apps.
|
||||
- Chat database portability and encryption.
|
||||
- Disappearing messages, with mutual agreement.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- SMP protocol improvements:
|
||||
- SMP queue redundancy and rotation.
|
||||
@ -139,3 +167,13 @@ You are likely to discover some bugs - we would really appreciate if you use it
|
||||
## License
|
||||
|
||||
[AGPL v3](./LICENSE)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
|
||||
|
||||
[](https://play.google.com/store/apps/details?id=chat.simplex.app)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/f_droid.svg" alt="F-Droid" height="41">](https://app.simplex.chat)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
|
||||
|
@ -12,21 +12,22 @@ struct ContentView: View {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Binding var doAuthenticate: Bool
|
||||
@Binding var enteredBackground: Double?
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var laFailed: Bool = false
|
||||
@Binding var userAuthorized: Bool?
|
||||
@State private var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if userAuthorized == true {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
} else {
|
||||
if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
let user = chatModel.currentUser {
|
||||
chatModel.currentUser != nil {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView(user: user)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(onDeny: {
|
||||
alertManager.showAlert(notificationAlert())
|
||||
@ -47,54 +48,39 @@ struct ContentView: View {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
}
|
||||
} else if prefPerformLA && laFailed {
|
||||
retryAuthView()
|
||||
}
|
||||
}
|
||||
.onChange(of: doAuthenticate) { doAuth in
|
||||
if doAuth, authenticationExpired() {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
.onAppear { if doAuthenticate { runAuthenticate() } }
|
||||
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
}
|
||||
|
||||
private func retryAuthView() -> some View {
|
||||
Button {
|
||||
laFailed = false
|
||||
runAuthenticate()
|
||||
} label: { Label("Retry", systemImage: "arrow.counterclockwise") }
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
if !prefPerformLA {
|
||||
userAuthorized = true
|
||||
} else {
|
||||
chatModel.showChatInfo = false
|
||||
DispatchQueue.main.async() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
laFailed = true
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
} else if showChatInfo {
|
||||
showChatInfo = false
|
||||
DispatchQueue.main.async {
|
||||
justAuthenticate()
|
||||
}
|
||||
} else {
|
||||
justAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
private func justAuthenticate() {
|
||||
userAuthorized = false
|
||||
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
case .failed:
|
||||
AlertManager.shared.showAlert(laFailedAlert())
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ import SimpleXChat
|
||||
final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var currentUser: User?
|
||||
@Published var showChatInfo: Bool = false // TODO comprehensively close modal views on authentication
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// current chat
|
||||
|
@ -55,8 +55,8 @@ struct SimpleXApp: App {
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var userAuthorized: Bool? = nil
|
||||
@State private var doAuthenticate: Bool = false
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var doAuthenticate = false
|
||||
@State private var enteredBackground: Double? = nil
|
||||
|
||||
init() {
|
||||
@ -74,7 +74,7 @@ struct SimpleXApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
return WindowGroup {
|
||||
ContentView(doAuthenticate: $doAuthenticate, enteredBackground: $enteredBackground)
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
@ -91,11 +91,13 @@ struct SimpleXApp: App {
|
||||
switch (phase) {
|
||||
case .background:
|
||||
BGManager.shared.schedule()
|
||||
if userAuthorized == true {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
machMessenger.stop()
|
||||
case .active:
|
||||
doAuthenticate = true
|
||||
doAuthenticate = authenticationExpired()
|
||||
machMessenger.start()
|
||||
default:
|
||||
break
|
||||
@ -103,4 +105,12 @@ struct SimpleXApp: App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func authenticationExpired() -> Bool {
|
||||
if let enteredBackground = enteredBackground {
|
||||
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ struct ChatInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State var alert: ChatInfoViewAlert? = nil
|
||||
@State var deletingContact: Contact?
|
||||
|
||||
@ -99,7 +100,7 @@ struct ChatInfoView: View {
|
||||
try await apiDeleteChat(type: .direct, id: contact.apiId)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.removeChat(contact.id)
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("ChatInfoView.deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
@ -118,7 +119,7 @@ struct ChatInfoView: View {
|
||||
Task {
|
||||
await clearChat(chat)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.showChatInfo = false
|
||||
showChatInfo = false
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -130,6 +131,6 @@ struct ChatInfoView: View {
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var showChatInfo = true
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
return ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ struct ChatView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@AppStorage(DEFAULT_EXPERIMENTAL_CALLS) private var enableCalls = false
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State private var composeState = ComposeState()
|
||||
@State private var deletingItem: ChatItem? = nil
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@ -98,12 +99,12 @@ struct ChatView: View {
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Button {
|
||||
chatModel.showChatInfo = true
|
||||
showChatInfo = true
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.sheet(isPresented: $chatModel.showChatInfo) {
|
||||
ChatInfoView(chat: chat)
|
||||
.sheet(isPresented: $showChatInfo) {
|
||||
ChatInfoView(chat: chat, showChatInfo: $showChatInfo)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
@ -270,7 +271,8 @@ struct ChatView_Previews: PreviewProvider {
|
||||
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"),
|
||||
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
]
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
@State var showChatInfo = false
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct ChatListNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State var chat: Chat
|
||||
@Binding var showChatInfo: Bool
|
||||
@State private var showContactRequestDialog = false
|
||||
|
||||
var body: some View {
|
||||
@ -28,7 +29,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
|
||||
private func chatView() -> some View {
|
||||
ChatView(chat: chat)
|
||||
ChatView(chat: chat, showChatInfo: $showChatInfo)
|
||||
.onAppear {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
@ -279,19 +280,20 @@ struct ChatListNavLink: View {
|
||||
struct ChatListNavLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var chatId: String? = "@1"
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.direct,
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")]
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
ChatListNavLink(chat: Chat(
|
||||
chatInfo: ChatInfo.sampleData.contactRequest,
|
||||
chatItems: []
|
||||
))
|
||||
), showChatInfo: $showChatInfo)
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 80))
|
||||
}
|
||||
|
@ -11,18 +11,17 @@ import SimpleXChat
|
||||
|
||||
struct ChatListView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var showChatInfo: Bool
|
||||
// not really used in this view
|
||||
@State private var showSettings = false
|
||||
@State private var searchText = ""
|
||||
@AppStorage(DEFAULT_PENDING_CONNECTIONS) private var pendingConnections = true
|
||||
|
||||
var user: User
|
||||
|
||||
var body: some View {
|
||||
let v = NavigationView {
|
||||
List {
|
||||
ForEach(filteredChats()) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
ChatListNavLink(chat: chat, showChatInfo: $showChatInfo)
|
||||
.padding(.trailing, -16)
|
||||
}
|
||||
}
|
||||
@ -93,10 +92,11 @@ struct ChatListView_Previews: PreviewProvider {
|
||||
)
|
||||
|
||||
]
|
||||
@State var showChatInfo = false
|
||||
return Group {
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(chatModel)
|
||||
ChatListView(user: User.sampleData)
|
||||
ChatListView(showChatInfo: $showChatInfo)
|
||||
.environmentObject(ChatModel())
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,10 @@
|
||||
<false/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>fetch</string>
|
||||
<string>remote-notification</string>
|
||||
<string>voip</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<string>YES</string>
|
||||
|
@ -56,6 +56,11 @@
|
||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
|
||||
5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3728522C9A00103588 /* libgmp.a */; };
|
||||
5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */; };
|
||||
5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */; };
|
||||
5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3828522C9A00103588 /* libffi.a */; };
|
||||
5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C6F2A3928522C9A00103588 /* libgmpxx.a */; };
|
||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||
@ -95,11 +100,6 @@
|
||||
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
|
||||
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */ = {isa = PBXBuildFile; fileRef = 5CE2BA78284530CC00EC33A6 /* SimpleXChat.docc */; };
|
||||
5CE2BA85284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */; };
|
||||
5CE2BA86284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */; };
|
||||
5CE2BA87284532AD00EC33A6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907D28376BB90076573F /* libffi.a */; };
|
||||
5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907F28376BB90076573F /* libgmp.a */; };
|
||||
5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64A6907E28376BB90076573F /* libgmpxx.a */; };
|
||||
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */; };
|
||||
5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD5228186F9500503DA2 /* AppGroup.swift */; };
|
||||
5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3C282447AB00B0488A /* CallTypes.swift */; };
|
||||
@ -263,11 +263,14 @@
|
||||
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
|
||||
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
|
||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||
5C6F2A3728522C9A00103588 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C6F2A3828522C9A00103588 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C6F2A3928522C9A00103588 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a"; sourceTree = "<group>"; };
|
||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||
5C764E7B279C71D4000C6508 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libiconv.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
5C764E7C279C71DB000C6508 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
|
||||
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
|
||||
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
|
||||
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
|
||||
@ -329,11 +332,6 @@
|
||||
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||
64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a"; sourceTree = "<group>"; };
|
||||
64A6907D28376BB90076573F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64A6907E28376BB90076573F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64A6907F28376BB90076573F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
|
||||
@ -392,13 +390,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C69D5B62852379F009B27A4 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CE2BA85284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a in Frameworks */,
|
||||
5C69D5B32852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a in Frameworks */,
|
||||
5C69D5B22852379F009B27A4 /* libgmp.a in Frameworks */,
|
||||
5C69D5B42852379F009B27A4 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CE2BA89284532AD00EC33A6 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA87284532AD00EC33A6 /* libffi.a in Frameworks */,
|
||||
5CE2BA88284532AD00EC33A6 /* libgmp.a in Frameworks */,
|
||||
5CE2BA86284532AD00EC33A6 /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a in Frameworks */,
|
||||
5C69D5B52852379F009B27A4 /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -490,11 +488,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64A6907D28376BB90076573F /* libffi.a */,
|
||||
64A6907F28376BB90076573F /* libgmp.a */,
|
||||
64A6907E28376BB90076573F /* libgmpxx.a */,
|
||||
64A6908028376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC-ghc8.10.7.a */,
|
||||
64A6907C28376BB90076573F /* libHSsimplex-chat-2.1.0-KOac7DFCSQz9HzISDnAYtC.a */,
|
||||
5C6F2A3828522C9A00103588 /* libffi.a */,
|
||||
5C6F2A3728522C9A00103588 /* libgmp.a */,
|
||||
5C6F2A3928522C9A00103588 /* libgmpxx.a */,
|
||||
5C6F2A3A28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy-ghc8.10.7.a */,
|
||||
5C6F2A3B28522C9A00103588 /* libHSsimplex-chat-2.2.0-3TOca6xkke4IR3YLgDepFy.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@ -503,11 +501,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */,
|
||||
5C1CAA162847C5C8009E5C72 /* UniformTypeIdentifiers.framework */,
|
||||
5CDCAD6028187D7900503DA2 /* libz.tbd */,
|
||||
5CDCAD5E28187D4A00503DA2 /* libiconv.tbd */,
|
||||
5C764E7C279C71DB000C6508 /* libz.tbd */,
|
||||
5C764E7B279C71D4000C6508 /* libiconv.tbd */,
|
||||
5C1CAA162847C5C8009E5C72 /* UniformTypeIdentifiers.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -1523,7 +1519,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1546,7 +1542,7 @@
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@ -1566,7 +1562,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1589,7 +1585,7 @@
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/Libraries/ios";
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/Libraries/sim";
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@ -1647,7 +1643,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1668,7 +1664,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1686,7 +1682,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 51;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1707,7 +1703,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
MARKETING_VERSION = 2.2;
|
||||
MARKETING_VERSION = 2.2.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@ -1741,6 +1737,10 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/ios",
|
||||
@ -1788,6 +1788,10 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Libraries/ios",
|
||||
|
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
46
blog/20220604-simplex-chat-new-privacy-security-settings.md
Normal file
@ -0,0 +1,46 @@
|
||||
# SimpleX Chat v2.2 - the first messaging platform without user identities - 100% private by design!
|
||||
|
||||
**Published:** June 4, 2022
|
||||
|
||||
See [v2 announcement](./20220511-simplex-chat-v2-images-files.md) for more information about SimpleX platform and how it protects your privacy by avoiding user identities of any kind in its design - SimpleX, unlike any other messaging platfom, has no identity keys or any numbers that identify its users.
|
||||
|
||||
## New Privacy and Security settings in version 2.2
|
||||
|
||||
<img src="./images/20220604-privacy-settings.png" width="480">
|
||||
|
||||
### Protect your chats
|
||||
|
||||
To protect your chats you can enable SimpleX Lock. Every time you open the chat after it was in the background for 30 second, you will need to pass biometric or pin code authentication to use the app (provided it is enabled for your device).
|
||||
|
||||
### Save data and avoid sharing you are online
|
||||
|
||||
In case you want to save your mobile data or to avoid showing to your contacts that you are online, you can disable automatic image downloads. For many users it is more convenient to have images downloaded automatically, so it is enabled by default.
|
||||
|
||||
Low resolution image previews would still be shown, the senders have no way to see if you received them or not.
|
||||
|
||||
### Avoid visiting websites of the links you send
|
||||
|
||||
When you receive the links that include link previews, it is fully private - these previews are generated by the sender, and they do not expose your IP address in any way.
|
||||
|
||||
When you send the links, the app automatically downloads the link description and the picture from the website of the link. While it is convenient, it exposes your IP address to the website. To avoid it you can disable sending link previews.
|
||||
|
||||
### Identify any lost messages in the chat
|
||||
|
||||
The app tracks the integrity of the messages you receive by cheching their sequential numbers and by validating that the hash of the previous message matches the hash included in the message – each conversation, effectively, is two blockchains that only you and your contact have access to.
|
||||
|
||||
In case some of the messages are lost, you would see it in the chat. It can happen because of one the following reasons:
|
||||
|
||||
- the messages have expired on the server after 30 days not being delivered.
|
||||
- the messages were removed when the server was restarted. We will add server redundancy later this year to avoid message loss in this case, for now if you see an indication that some messages were lost in the chat, you can check with your contact what it was.
|
||||
- some other app error. Please notify us via chat - we will investigate possible root causes.
|
||||
- the connection is compromised. This is very unlikely, but not an impossible scenario.
|
||||
|
||||
### There is more
|
||||
|
||||
You can discover additional features we are currently testing in Experimental Features - they will be announced later!
|
||||
|
||||
## More information
|
||||
|
||||
See [v1 announcement](./20220112-simplex-chat-v1-released.md) for information on how SimpleX protects the security of the messages.
|
||||
|
||||
Read about SimpleX design in [whitepaper](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md).
|
@ -1,8 +1,10 @@
|
||||
# Blog
|
||||
|
||||
May 24, 2022 [Clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
Jun 4, 2022 [v2.2: the new Privacy and Security settings](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
May 11, 2022 [Sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
May 24, 2022 [v2.1: clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
|
||||
May 11, 2022 [v2.0 released - sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
Apr 04, 2022 [Instant notifications for SimpleX Chat mobile apps](./20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
|
BIN
blog/images/20220604-privacy-settings.png
Normal file
BIN
blog/images/20220604-privacy-settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 389 KiB |
@ -3,7 +3,7 @@ packages: .
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 964daf5442e1069634762450bc28cfd69a2968a1
|
||||
tag: c1348aa54fba292d34339d6b111572cb1c74b546
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
25
docs/rfcs/2022-06-03-portable-archive.md
Normal file
25
docs/rfcs/2022-06-03-portable-archive.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Portable archive file format
|
||||
|
||||
## Problems
|
||||
|
||||
- database migration for notifications support
|
||||
- export and import of the database
|
||||
|
||||
The first problem could have been solved in an ad hoc way, but it may cause data loss, so the proposal is to have migration performed via export/import steps.
|
||||
|
||||
Out of scope of this doc - what will be the UX for database migration. It may be fully automatic, via code, with zero user interactions, or it could be via step by step wizard - irrespective of this choice it would include export and import steps.
|
||||
|
||||
# Proposal
|
||||
|
||||
Implement creating archive file and restoring from the archive in Haskell, application would only provide a source and target folders, respectively
|
||||
|
||||
Archive files structure:
|
||||
|
||||
- simplex_v1_chat.db
|
||||
- simplex_v1_agent.db
|
||||
- simplex_v1_files
|
||||
- ...
|
||||
|
||||
Archive file name (includes UTC time):
|
||||
|
||||
simplex-chat.YYYY-MM-DDTHH:MM:SSZ.zip
|
@ -77,7 +77,7 @@ fi
|
||||
|
||||
chmod +x $BIN_PATH
|
||||
|
||||
echo "$APP_NAME installed sucesfully!"
|
||||
echo "$APP_NAME installed successfully!"
|
||||
|
||||
if [ -z "$(command -v $APP_NAME)" ]; then
|
||||
if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then
|
||||
|
11
package.yaml
11
package.yaml
@ -39,6 +39,17 @@ dependencies:
|
||||
- time == 1.9.*
|
||||
- unliftio == 0.2.*
|
||||
- unliftio-core == 0.2.*
|
||||
- zip == 1.7.*
|
||||
|
||||
flags:
|
||||
disable-bzip2:
|
||||
description: removes dependency on bzip2 C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
disable-zstd:
|
||||
description: Removes dependency on zstd C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
library:
|
||||
source-dirs: src
|
||||
|
15
scripts/nix/README.md
Normal file
15
scripts/nix/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Updating nix package config
|
||||
|
||||
1. Install `nix`, `gawk` and `jq`.
|
||||
|
||||
2. Start nix-shell from repo root:
|
||||
|
||||
```sh
|
||||
nix-shell -p nix-prefetch-git
|
||||
```
|
||||
|
||||
3. Run in nix shell:
|
||||
|
||||
```sh
|
||||
gawk -f ./scripts/nix/update-sha256.awk cabal.project > ./scripts/nix/sha256map.nix
|
||||
```
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."964daf5442e1069634762450bc28cfd69a2968a1" = "1vsbiawqlvi6v48ws2rmg5cmp5qphnry3ymg6458p2w8wdm2gsng";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."c1348aa54fba292d34339d6b111572cb1c74b546" = "103hw1h1agy42krf11d98bv3c1w0q0wi2z7r2ll0gmp5xv1r4rf0";
|
||||
"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";
|
||||
|
@ -17,9 +17,20 @@ build-type: Simple
|
||||
extra-source-files:
|
||||
README.md
|
||||
|
||||
flag disable-bzip2
|
||||
description: removes dependency on bzip2 C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
flag disable-zstd
|
||||
description: Removes dependency on zstd C library (zip package)
|
||||
manual: True
|
||||
default: True
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.Chat
|
||||
Simplex.Chat.Archive
|
||||
Simplex.Chat.Bot
|
||||
Simplex.Chat.Call
|
||||
Simplex.Chat.Controller
|
||||
@ -83,6 +94,7 @@ library
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-bot
|
||||
@ -121,6 +133,7 @@ executable simplex-bot
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-bot-advanced
|
||||
@ -159,6 +172,7 @@ executable simplex-bot-advanced
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
executable simplex-chat
|
||||
@ -200,6 +214,7 @@ executable simplex-chat
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, websockets ==0.12.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite simplex-chat-test
|
||||
@ -247,4 +262,5 @@ test-suite simplex-chat-test
|
||||
, time ==1.9.*
|
||||
, unliftio ==0.2.*
|
||||
, unliftio-core ==0.2.*
|
||||
, zip ==1.7.*
|
||||
default-language: Haskell2010
|
||||
|
@ -41,6 +41,7 @@ import qualified Data.Text as T
|
||||
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.LocalTime (getCurrentTimeZone, getZonedTime)
|
||||
import Data.Word (Word32)
|
||||
import Simplex.Chat.Archive
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Markdown
|
||||
@ -49,7 +50,7 @@ import Simplex.Chat.Options (ChatOpts (..), smpServersP)
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (ifM, safeDecodeUtf8, unlessM, whenM)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), defaultAgentConfig)
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
@ -59,10 +60,10 @@ import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Client (NtfServer)
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), PushProvider (..))
|
||||
import Simplex.Messaging.Parsers (base64P, parseAll)
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody)
|
||||
import Simplex.Messaging.Protocol (ErrorType (..), MsgBody, MsgFlags (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Util (tryError, (<$?>))
|
||||
import Simplex.Messaging.Util (ifM, tryError, unlessM, whenM, (<$?>))
|
||||
import System.Exit (exitFailure, exitSuccess)
|
||||
import System.FilePath (combine, splitExtensions, takeFileName)
|
||||
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
|
||||
@ -134,7 +135,8 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
|
||||
rcvFiles <- newTVarIO M.empty
|
||||
currentCalls <- atomically TM.empty
|
||||
filesFolder <- newTVarIO Nothing
|
||||
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder}
|
||||
chatStoreChanged <- newTVarIO False
|
||||
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder}
|
||||
where
|
||||
resolveServers :: InitialAgentServers -> IO InitialAgentServers
|
||||
resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of
|
||||
@ -150,6 +152,7 @@ runChatController = race_ notificationSubscriber . agentSubscriber
|
||||
|
||||
startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m (Async ())
|
||||
startChatController user = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
s <- asks agentAsync
|
||||
readTVarIO s >>= maybe (start s) pure
|
||||
where
|
||||
@ -194,13 +197,23 @@ processChatCommand = \case
|
||||
StartChat -> withUser' $ \user ->
|
||||
asks agentAsync >>= readTVarIO >>= \case
|
||||
Just _ -> pure CRChatRunning
|
||||
_ -> startChatController user $> CRChatStarted
|
||||
_ ->
|
||||
ifM
|
||||
(asks chatStoreChanged >>= readTVarIO)
|
||||
(throwChatError CEChatStoreChanged)
|
||||
(startChatController user $> CRChatStarted)
|
||||
APIStopChat -> do
|
||||
ask >>= stopChatController
|
||||
pure CRChatStopped
|
||||
ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk
|
||||
SetFilesFolder filesFolder' -> withUser $ \_ -> do
|
||||
createDirectoryIfMissing True filesFolder'
|
||||
ff <- asks filesFolder
|
||||
atomically . writeTVar ff $ Just filesFolder'
|
||||
pure CRCmdOk
|
||||
APIExportArchive cfg -> checkChatStopped $ exportArchive cfg $> CRCmdOk
|
||||
APIImportArchive cfg -> checkChatStopped $ importArchive cfg >> setStoreChanged $> CRCmdOk
|
||||
APIDeleteStorage -> checkChatStopped $ deleteStorage >> setStoreChanged $> CRCmdOk
|
||||
APIGetChats withPCC -> CRApiChats <$> withUser (\user -> withStore $ \st -> getChatPreviews st user withPCC)
|
||||
APIGetChat (ChatRef cType cId) pagination -> withUser $ \user -> case cType of
|
||||
CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination)
|
||||
@ -770,6 +783,10 @@ processChatCommand = \case
|
||||
CTDirect -> withStore $ \st -> getContactIdByName st userId name
|
||||
CTGroup -> withStore $ \st -> getGroupIdByName st user name
|
||||
_ -> throwChatError $ CECommandError "not supported"
|
||||
checkChatStopped :: m ChatResponse -> m ChatResponse
|
||||
checkChatStopped a = asks agentAsync >>= readTVarIO >>= maybe a (const $ throwChatError CEChatNotStopped)
|
||||
setStoreChanged :: m ()
|
||||
setStoreChanged = asks chatStoreChanged >>= atomically . (`writeTVar` True)
|
||||
getSentChatItemIdByText :: User -> ChatRef -> ByteString -> m Int64
|
||||
getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of
|
||||
CTDirect -> withStore $ \st -> getDirectChatItemIdByText st userId cId SMDSnd (safeDecodeUtf8 msg)
|
||||
@ -1115,7 +1132,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
allowAgentConnection conn confId $ XInfo profile
|
||||
INFO connInfo ->
|
||||
saveConnInfo conn connInfo
|
||||
MSG meta msgBody -> do
|
||||
MSG meta _msgFlags msgBody -> do
|
||||
_ <- saveRcvMSG conn (ConnectionId connId) meta msgBody
|
||||
withAckMessage agentConnId meta $ pure ()
|
||||
ackMsgDeliveryEvent conn meta
|
||||
@ -1128,7 +1145,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
Just ct@Contact {localDisplayName = c, contactId} -> case agentMsg of
|
||||
MSG msgMeta msgBody -> do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
@ -1268,7 +1285,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
when (connStatus == ConnReady) $ do
|
||||
notifyMemberConnected gInfo m
|
||||
when (memberCategory m == GCPreMember) $ probeMatchingContacts ct
|
||||
MSG msgMeta msgBody -> do
|
||||
MSG msgMeta _msgFlags msgBody -> do
|
||||
msg@RcvMessage {chatMsgEvent} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody
|
||||
withAckMessage agentConnId msgMeta $
|
||||
case chatMsgEvent of
|
||||
@ -1327,7 +1344,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
ci <- withStore $ \st -> getChatItemByFileId st user fileId
|
||||
toView $ CRSndFileRcvCancelled ci ft
|
||||
_ -> throwChatError $ CEFileSend fileId err
|
||||
MSG meta _ ->
|
||||
MSG meta _ _ ->
|
||||
withAckMessage agentConnId meta $ pure ()
|
||||
-- TODO print errors
|
||||
ERR _ -> pure ()
|
||||
@ -1351,7 +1368,7 @@ processAgentMessage (Just user@User {userId, profile}) agentConnId agentMessage
|
||||
updateCIFileStatus st user fileId CIFSRcvTransfer
|
||||
getChatItemByFileId st user fileId
|
||||
toView $ CRRcvFileStart ci
|
||||
MSG meta@MsgMeta {recipient = (msgId, _), integrity} msgBody -> withAckMessage agentConnId meta $ do
|
||||
MSG meta@MsgMeta {recipient = (msgId, _), integrity} _ msgBody -> withAckMessage agentConnId meta $ do
|
||||
parseFileChunk msgBody >>= \case
|
||||
FileChunkCancel ->
|
||||
unless cancelled $ do
|
||||
@ -1894,7 +1911,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo
|
||||
sendFileChunkNo :: ChatMonad m => SndFileTransfer -> Integer -> m ()
|
||||
sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do
|
||||
chunkBytes <- readFileChunk ft chunkNo
|
||||
msgId <- withAgent $ \a -> sendMessage a acId $ smpEncode FileChunk {chunkNo, chunkBytes}
|
||||
msgId <- withAgent $ \a -> sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunk {chunkNo, chunkBytes}
|
||||
withStore $ \st -> updateSndFileChunkMsg st ft chunkNo msgId
|
||||
|
||||
readFileChunk :: ChatMonad m => SndFileTransfer -> Integer -> m ByteString
|
||||
@ -1988,7 +2005,7 @@ cancelSndFileTransfer ft@SndFileTransfer {agentConnId = AgentConnId acId, fileSt
|
||||
updateSndFileStatus st ft FSCancelled
|
||||
deleteSndFileChunks st ft
|
||||
withAgent $ \a -> do
|
||||
void (sendMessage a acId $ smpEncode FileChunkCancel) `catchError` \_ -> pure ()
|
||||
void (sendMessage a acId SMP.noMsgFlags $ smpEncode FileChunkCancel) `catchError` \_ -> pure ()
|
||||
deleteConnection a acId
|
||||
|
||||
closeFileHandle :: ChatMonad m => Int64 -> (ChatController -> TVar (Map Int64 Handle)) -> m ()
|
||||
@ -2016,7 +2033,7 @@ sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connS
|
||||
sendDirectMessage :: ChatMonad m => Connection -> ChatMsgEvent -> ConnOrGroupId -> m SndMessage
|
||||
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
||||
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
|
||||
deliverMessage conn msgBody msgId
|
||||
deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
||||
pure msg
|
||||
|
||||
createSndMessage :: ChatMonad m => ChatMsgEvent -> ConnOrGroupId -> m SndMessage
|
||||
@ -2029,9 +2046,10 @@ createSndMessage chatMsgEvent connOrGroupId = do
|
||||
directMessage :: ChatMsgEvent -> ByteString
|
||||
directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent}
|
||||
|
||||
deliverMessage :: ChatMonad m => Connection -> MsgBody -> MessageId -> m ()
|
||||
deliverMessage conn@Connection {connId} msgBody msgId = do
|
||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgBody
|
||||
deliverMessage :: ChatMonad m => Connection -> CMEventTag -> MsgBody -> MessageId -> m ()
|
||||
deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
|
||||
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
|
||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||
withStore $ \st -> createSndMsgDelivery st sndMsgDelivery msgId
|
||||
|
||||
@ -2051,10 +2069,12 @@ sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
|
||||
forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} ->
|
||||
case memberConn m of
|
||||
Nothing -> withStore $ \st -> createPendingGroupMessage st groupMemberId msgId introId_
|
||||
Just conn@Connection {connStatus} ->
|
||||
if not (connStatus == ConnSndReady || connStatus == ConnReady)
|
||||
then unless (connStatus == ConnDeleted) $ withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_)
|
||||
else (deliverMessage conn msgBody msgId >> postDeliver) `catchError` const (pure ())
|
||||
Just conn@Connection {connStatus}
|
||||
| connStatus == ConnSndReady || connStatus == ConnReady -> do
|
||||
let tag = toCMEventTag chatMsgEvent
|
||||
(deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ())
|
||||
| connStatus == ConnDeleted -> pure ()
|
||||
| otherwise -> withStore (\st -> createPendingGroupMessage st groupMemberId msgId introId_)
|
||||
pure msg
|
||||
|
||||
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
|
||||
@ -2062,7 +2082,7 @@ sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do
|
||||
pendingMessages <- withStore $ \st -> getPendingGroupMessages st groupMemberId
|
||||
-- TODO ensure order - pending messages interleave with user input messages
|
||||
forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} -> do
|
||||
deliverMessage conn msgBody msgId
|
||||
deliverMessage conn cmEventTag msgBody msgId
|
||||
withStore (\st -> deletePendingGroupMessage st groupMemberId msgId)
|
||||
when (cmEventTag == XGrpMemFwd_) $ case introId_ of
|
||||
Nothing -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
||||
@ -2212,8 +2232,12 @@ chatCommandP =
|
||||
("/user " <|> "/u ") *> (CreateActiveUser <$> userProfile)
|
||||
<|> ("/user" <|> "/u") $> ShowActiveUser
|
||||
<|> "/_start" $> StartChat
|
||||
<|> "/_stop" $> APIStopChat
|
||||
<|> "/_resubscribe all" $> ResubscribeAllConnections
|
||||
<|> "/_files_folder " *> (SetFilesFolder <$> filePath)
|
||||
<|> "/_db export " *> (APIExportArchive <$> jsonP)
|
||||
<|> "/_db import " *> (APIImportArchive <$> jsonP)
|
||||
<|> "/_db delete" $> APIDeleteStorage
|
||||
<|> "/_get chats" *> (APIGetChats <$> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False))
|
||||
<|> "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP)
|
||||
<|> "/_get items count=" *> (APIGetChatItems <$> A.decimal)
|
||||
|
81
src/Simplex/Chat/Archive.hs
Normal file
81
src/Simplex/Chat/Archive.hs
Normal file
@ -0,0 +1,81 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module Simplex.Chat.Archive where
|
||||
|
||||
import qualified Codec.Archive.Zip as Z
|
||||
import Control.Monad.Reader
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Messaging.Agent.Client (agentDbPath)
|
||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..))
|
||||
import Simplex.Messaging.Util (whenM)
|
||||
import System.FilePath
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.STM
|
||||
import UnliftIO.Temporary
|
||||
|
||||
archiveAgentDbFile :: String
|
||||
archiveAgentDbFile = "simplex_v1_agent.db"
|
||||
|
||||
archiveChatDbFile :: String
|
||||
archiveChatDbFile = "simplex_v1_chat.db"
|
||||
|
||||
archiveFilesFolder :: String
|
||||
archiveFilesFolder = "simplex_v1_files"
|
||||
|
||||
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||
exportArchive ArchiveConfig {archivePath, disableCompression} =
|
||||
withSystemTempDirectory "simplex-chat." $ \dir -> do
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
copyFile chatDb $ dir </> archiveChatDbFile
|
||||
copyFile agentDb $ dir </> archiveAgentDbFile
|
||||
forM_ filesPath $ \fp ->
|
||||
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
||||
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
||||
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
|
||||
|
||||
importArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||
importArchive ArchiveConfig {archivePath} =
|
||||
withSystemTempDirectory "simplex-chat." $ \dir -> do
|
||||
Z.withArchive archivePath $ Z.unpackInto dir
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
backup chatDb
|
||||
backup agentDb
|
||||
copyFile (dir </> archiveChatDbFile) chatDb
|
||||
copyFile (dir </> archiveAgentDbFile) agentDb
|
||||
let filesDir = dir </> archiveFilesFolder
|
||||
forM_ filesPath $ \fp ->
|
||||
whenM (doesDirectoryExist filesDir) $
|
||||
copyDirectoryFiles filesDir fp
|
||||
where
|
||||
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
|
||||
|
||||
copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m ()
|
||||
copyDirectoryFiles fromDir toDir = do
|
||||
createDirectoryIfMissing False toDir
|
||||
fs <- listDirectory fromDir
|
||||
forM_ fs $ \f -> do
|
||||
let fn = takeFileName f
|
||||
f' = fromDir </> fn
|
||||
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
|
||||
|
||||
deleteStorage :: ChatMonad m => m ()
|
||||
deleteStorage = do
|
||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
||||
removeFile chatDb
|
||||
removeFile agentDb
|
||||
mapM_ removePathForcibly filesPath
|
||||
|
||||
data StorageFiles = StorageFiles
|
||||
{ chatDb :: FilePath,
|
||||
agentDb :: FilePath,
|
||||
filesPath :: Maybe FilePath
|
||||
}
|
||||
|
||||
storageFiles :: ChatMonad m => m StorageFiles
|
||||
storageFiles = do
|
||||
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
||||
let SQLiteStore {dbFilePath = chatDb} = chatStore
|
||||
agentDb = agentDbPath smpAgent
|
||||
filesPath <- readTVarIO filesFolder
|
||||
pure StorageFiles {chatDb, agentDb, filesPath}
|
@ -76,6 +76,7 @@ data ChatController = ChatController
|
||||
smpAgent :: AgentClient,
|
||||
agentAsync :: TVar (Maybe (Async ())),
|
||||
chatStore :: SQLiteStore,
|
||||
chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted
|
||||
idsDrg :: TVar ChaChaDRG,
|
||||
inputQ :: TBQueue String,
|
||||
outputQ :: TBQueue (Maybe CorrId, ChatResponse),
|
||||
@ -100,8 +101,12 @@ data ChatCommand
|
||||
= ShowActiveUser
|
||||
| CreateActiveUser Profile
|
||||
| StartChat
|
||||
| APIStopChat
|
||||
| ResubscribeAllConnections
|
||||
| SetFilesFolder FilePath
|
||||
| APIExportArchive ArchiveConfig
|
||||
| APIImportArchive ArchiveConfig
|
||||
| APIDeleteStorage
|
||||
| APIGetChats {pendingConnections :: Bool}
|
||||
| APIGetChat ChatRef ChatPagination
|
||||
| APIGetChatItems Int
|
||||
@ -178,6 +183,7 @@ data ChatResponse
|
||||
= CRActiveUser {user :: User}
|
||||
| CRChatStarted
|
||||
| CRChatRunning
|
||||
| CRChatStopped
|
||||
| CRApiChats {chats :: [AChat]}
|
||||
| CRApiChat {chat :: AChat}
|
||||
| CRLastMessages {chatItems :: [AChatItem]}
|
||||
@ -279,6 +285,9 @@ instance ToJSON ChatResponse where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
|
||||
|
||||
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
contactError :: Maybe ChatError
|
||||
@ -329,6 +338,8 @@ data ChatErrorType
|
||||
= CENoActiveUser
|
||||
| CEActiveUserExists
|
||||
| CEChatNotStarted
|
||||
| CEChatNotStopped
|
||||
| CEChatStoreChanged
|
||||
| CEInvalidConnReq
|
||||
| CEInvalidChatMessage {message :: String}
|
||||
| CEContactNotReady {contact :: Contact}
|
||||
|
@ -26,13 +26,15 @@ simplexChatCore cfg@ChatConfig {dbPoolSize, yesToMigrations} opts sendToast chat
|
||||
st <- createStore f dbPoolSize yesToMigrations
|
||||
u <- getCreateActiveUser st
|
||||
cc <- newChatController st (Just u) cfg opts sendToast
|
||||
runSimplexChat u cc chat
|
||||
runSimplexChat opts u cc chat
|
||||
|
||||
runSimplexChat :: User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
||||
runSimplexChat u cc chat = do
|
||||
a1 <- async $ chat u cc
|
||||
a2 <- runReaderT (startChatController u) cc
|
||||
waitEither_ a1 a2
|
||||
runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO ()
|
||||
runSimplexChat ChatOpts {maintenance} u cc chat
|
||||
| maintenance = wait =<< async (chat u cc)
|
||||
| otherwise = do
|
||||
a1 <- async $ chat u cc
|
||||
a2 <- runReaderT (startChatController u) cc
|
||||
waitEither_ a1 a2
|
||||
|
||||
sendChatCmd :: ChatController -> String -> IO ChatResponse
|
||||
sendChatCmd cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc
|
||||
|
@ -32,12 +32,12 @@ import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgErrorType (..), MsgMeta (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, singleFieldJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (MsgBody)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
|
||||
deriving (Show, Generic)
|
||||
|
@ -54,7 +54,8 @@ mobileChatOpts =
|
||||
logAgent = False,
|
||||
chatCmd = "",
|
||||
chatCmdDelay = 3,
|
||||
chatServerPort = Nothing
|
||||
chatServerPort = Nothing,
|
||||
maintenance = True
|
||||
}
|
||||
|
||||
defaultMobileConfig :: ChatConfig
|
||||
|
@ -25,7 +25,8 @@ data ChatOpts = ChatOpts
|
||||
logAgent :: Bool,
|
||||
chatCmd :: String,
|
||||
chatCmdDelay :: Int,
|
||||
chatServerPort :: Maybe String
|
||||
chatServerPort :: Maybe String,
|
||||
maintenance :: Bool
|
||||
}
|
||||
|
||||
chatOpts :: FilePath -> FilePath -> Parser ChatOpts
|
||||
@ -88,7 +89,13 @@ chatOpts appDir defaultDbFileName = do
|
||||
<> help "Run chat server on specified port"
|
||||
<> value Nothing
|
||||
)
|
||||
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort}
|
||||
maintenance <-
|
||||
switch
|
||||
( long "maintenance"
|
||||
<> short 'm'
|
||||
<> help "Run in maintenance mode (/_start to start chat)"
|
||||
)
|
||||
pure ChatOpts {dbFilePrefix, smpServers, logConnections, logAgent, chatCmd, chatCmdDelay, chatServerPort, maintenance}
|
||||
where
|
||||
defaultDbFilePath = combine appDir defaultDbFileName
|
||||
|
||||
|
@ -31,10 +31,10 @@ import Database.SQLite.Simple.ToField (ToField (..))
|
||||
import GHC.Generics (Generic)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
import Simplex.Chat.Util (safeDecodeUtf8)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (fromTextField_)
|
||||
import Simplex.Messaging.Util ((<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
|
||||
|
||||
data ConnectionEntity
|
||||
= RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact}
|
||||
@ -437,6 +437,16 @@ instance FromField CMEventTag where fromField = fromTextField_ cmEventTagT
|
||||
|
||||
instance ToField CMEventTag where toField = toField . serializeCMEventTag
|
||||
|
||||
hasNotification :: CMEventTag -> Bool
|
||||
hasNotification = \case
|
||||
XMsgNew_ -> True
|
||||
XFile_ -> True
|
||||
XContact_ -> True
|
||||
XGrpInv_ -> True
|
||||
XGrpDel_ -> True
|
||||
XCallInv_ -> True
|
||||
_ -> False
|
||||
|
||||
appToChatMessage :: AppMessage -> Either String ChatMessage
|
||||
appToChatMessage AppMessage {msgId, event, params} = do
|
||||
eventTag <- strDecode $ encodeUtf8 event
|
||||
|
@ -206,7 +206,6 @@ import Simplex.Chat.Migrations.M20220404_files_status_fields
|
||||
import Simplex.Chat.Migrations.M20220514_profiles_user_id
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (eitherToMaybe)
|
||||
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 (..))
|
||||
@ -214,7 +213,7 @@ 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 Simplex.Messaging.Util (eitherToMaybe, liftIOEither, (<$$>))
|
||||
import UnliftIO.STM
|
||||
|
||||
schemaMigrations :: [(String, Query)]
|
||||
|
@ -1,6 +1,5 @@
|
||||
module Simplex.Chat.Util where
|
||||
|
||||
import Control.Monad (when)
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeUtf8With)
|
||||
@ -9,15 +8,3 @@ safeDecodeUtf8 :: ByteString -> Text
|
||||
safeDecodeUtf8 = decodeUtf8With onError
|
||||
where
|
||||
onError _ _ = Just '?'
|
||||
|
||||
ifM :: Monad m => m Bool -> m a -> m a -> m a
|
||||
ifM ba t f = ba >>= \b -> if b then t else f
|
||||
|
||||
whenM :: Monad m => m Bool -> m () -> m ()
|
||||
whenM ba a = ba >>= (`when` a)
|
||||
|
||||
unlessM :: Monad m => m Bool -> m () -> m ()
|
||||
unlessM b = ifM b $ pure ()
|
||||
|
||||
eitherToMaybe :: Either a b -> Maybe b
|
||||
eitherToMaybe = either (const Nothing) Just
|
||||
|
@ -53,7 +53,8 @@ responseToView :: Bool -> ChatResponse -> [StyledString]
|
||||
responseToView testView = \case
|
||||
CRActiveUser User {profile} -> viewUserProfile profile
|
||||
CRChatStarted -> ["chat started"]
|
||||
CRChatRunning -> []
|
||||
CRChatRunning -> ["chat is running"]
|
||||
CRChatStopped -> ["chat stopped"]
|
||||
CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats]
|
||||
CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
|
||||
@ -721,6 +722,8 @@ viewChatError = \case
|
||||
CENoActiveUser -> ["error: active user is required"]
|
||||
CEActiveUserExists -> ["error: active user already exists"]
|
||||
CEChatNotStarted -> ["error: chat not started"]
|
||||
CEChatNotStopped -> ["error: chat not stopped"]
|
||||
CEChatStoreChanged -> ["error: chat store changed"]
|
||||
CEInvalidConnReq -> viewInvalidConnReq
|
||||
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
|
||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||
|
@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 964daf5442e1069634762450bc28cfd69a2968a1
|
||||
commit: c1348aa54fba292d34339d6b111572cb1c74b546
|
||||
# - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977
|
||||
- github: simplex-chat/aeson
|
||||
commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7
|
||||
@ -59,8 +59,10 @@ extra-deps:
|
||||
# extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
# flags: {}
|
||||
|
||||
flags:
|
||||
zip:
|
||||
disable-bzip2: true
|
||||
disable-zstd: true
|
||||
# Extra package databases containing global packages
|
||||
# extra-package-dbs: []
|
||||
|
||||
|
@ -30,6 +30,7 @@ import Simplex.Messaging.Agent.RetryInterval
|
||||
import Simplex.Messaging.Server (runSMPServerBlocking)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Version
|
||||
import System.Directory (createDirectoryIfMissing, removePathForcibly)
|
||||
import qualified System.Terminal as C
|
||||
import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings (..), withVirtualTerminal)
|
||||
@ -42,8 +43,8 @@ testDBPrefix = "tests/tmp/test"
|
||||
serverPort :: ServiceName
|
||||
serverPort = "5001"
|
||||
|
||||
opts :: ChatOpts
|
||||
opts =
|
||||
testOpts :: ChatOpts
|
||||
testOpts =
|
||||
ChatOpts
|
||||
{ dbFilePrefix = undefined,
|
||||
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001"],
|
||||
@ -51,7 +52,8 @@ opts =
|
||||
logAgent = False,
|
||||
chatCmd = "",
|
||||
chatCmdDelay = 3,
|
||||
chatServerPort = Nothing
|
||||
chatServerPort = Nothing,
|
||||
maintenance = False
|
||||
}
|
||||
|
||||
termSettings :: VirtualTerminalSettings
|
||||
@ -74,34 +76,46 @@ data TestCC = TestCC
|
||||
aCfg :: AgentConfig
|
||||
aCfg = agentConfig defaultChatConfig
|
||||
|
||||
cfg :: ChatConfig
|
||||
cfg =
|
||||
testAgentCfg :: AgentConfig
|
||||
testAgentCfg = aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}}
|
||||
|
||||
testCfg :: ChatConfig
|
||||
testCfg =
|
||||
defaultChatConfig
|
||||
{ agentConfig =
|
||||
aCfg {reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}},
|
||||
{ agentConfig = testAgentCfg,
|
||||
testView = True
|
||||
}
|
||||
|
||||
createTestChat :: String -> Profile -> IO TestCC
|
||||
createTestChat dbPrefix profile = do
|
||||
testAgentCfgV1 :: AgentConfig
|
||||
testAgentCfgV1 =
|
||||
testAgentCfg
|
||||
{ smpAgentVersion = 1,
|
||||
smpAgentVRange = mkVersionRange 1 1
|
||||
}
|
||||
|
||||
testCfgV1 :: ChatConfig
|
||||
testCfgV1 = testCfg {agentConfig = testAgentCfgV1}
|
||||
|
||||
createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC
|
||||
createTestChat cfg opts dbPrefix profile = do
|
||||
let dbFilePrefix = testDBPrefix <> dbPrefix
|
||||
st <- createStore (dbFilePrefix <> "_chat.db") 1 False
|
||||
Right user <- runExceptT $ createUser st profile True
|
||||
startTestChat_ st dbFilePrefix user
|
||||
startTestChat_ st cfg opts dbFilePrefix user
|
||||
|
||||
startTestChat :: String -> IO TestCC
|
||||
startTestChat dbPrefix = do
|
||||
startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC
|
||||
startTestChat cfg opts dbPrefix = do
|
||||
let dbFilePrefix = testDBPrefix <> dbPrefix
|
||||
st <- createStore (dbFilePrefix <> "_chat.db") 1 False
|
||||
Just user <- find activeUser <$> getUsers st
|
||||
startTestChat_ st dbFilePrefix user
|
||||
startTestChat_ st cfg opts dbFilePrefix user
|
||||
|
||||
startTestChat_ :: SQLiteStore -> FilePath -> User -> IO TestCC
|
||||
startTestChat_ st dbFilePrefix user = do
|
||||
startTestChat_ :: SQLiteStore -> ChatConfig -> ChatOpts -> FilePath -> User -> IO TestCC
|
||||
startTestChat_ st cfg opts dbFilePrefix user = do
|
||||
t <- withVirtualTerminal termSettings pure
|
||||
ct <- newChatTerminal t
|
||||
cc <- newChatController st (Just user) cfg opts {dbFilePrefix} Nothing -- no notifications
|
||||
chatAsync <- async . runSimplexChat user cc . const $ runChatTerminal ct
|
||||
chatAsync <- async . runSimplexChat opts user cc . const $ runChatTerminal ct
|
||||
termQ <- newTQueueIO
|
||||
termAsync <- async $ readTerminalOutput t termQ
|
||||
pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ}
|
||||
@ -113,10 +127,34 @@ stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
|
||||
uninterruptibleCancel chatAsync
|
||||
|
||||
withNewTestChat :: String -> Profile -> (TestCC -> IO a) -> IO a
|
||||
withNewTestChat dbPrefix profile = bracket (createTestChat dbPrefix profile) (\cc -> cc <// 100000 >> stopTestChat cc)
|
||||
withNewTestChat = withNewTestChatCfgOpts testCfg testOpts
|
||||
|
||||
withNewTestChatV1 :: String -> Profile -> (TestCC -> IO a) -> IO a
|
||||
withNewTestChatV1 = withNewTestChatCfg testCfgV1
|
||||
|
||||
withNewTestChatCfg :: ChatConfig -> String -> Profile -> (TestCC -> IO a) -> IO a
|
||||
withNewTestChatCfg cfg = withNewTestChatCfgOpts cfg testOpts
|
||||
|
||||
withNewTestChatOpts :: ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a
|
||||
withNewTestChatOpts = withNewTestChatCfgOpts testCfg
|
||||
|
||||
withNewTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> Profile -> (TestCC -> IO a) -> IO a
|
||||
withNewTestChatCfgOpts cfg opts dbPrefix profile = bracket (createTestChat cfg opts dbPrefix profile) (\cc -> cc <// 100000 >> stopTestChat cc)
|
||||
|
||||
withTestChatV1 :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatV1 = withTestChatCfg testCfgV1
|
||||
|
||||
withTestChat :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChat dbPrefix = bracket (startTestChat dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc)
|
||||
withTestChat = withTestChatCfgOpts testCfg testOpts
|
||||
|
||||
withTestChatCfg :: ChatConfig -> String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatCfg cfg = withTestChatCfgOpts cfg testOpts
|
||||
|
||||
withTestChatOpts :: ChatOpts -> String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatOpts = withTestChatCfgOpts testCfg
|
||||
|
||||
withTestChatCfgOpts :: ChatConfig -> ChatOpts -> String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatCfgOpts cfg opts dbPrefix = bracket (startTestChat cfg opts dbPrefix) (\cc -> cc <// 100000 >> stopTestChat cc)
|
||||
|
||||
readTerminalOutput :: VirtualTerminal -> TQueue String -> IO ()
|
||||
readTerminalOutput t termQ = do
|
||||
@ -147,8 +185,8 @@ withTmpFiles =
|
||||
(createDirectoryIfMissing False "tests/tmp")
|
||||
(removePathForcibly "tests/tmp")
|
||||
|
||||
testChatN :: [Profile] -> ([TestCC] -> IO ()) -> IO ()
|
||||
testChatN ps test = withTmpFiles $ do
|
||||
testChatN :: ChatConfig -> ChatOpts -> [Profile] -> ([TestCC] -> IO ()) -> IO ()
|
||||
testChatN cfg opts ps test = withTmpFiles $ do
|
||||
tcs <- getTestCCs (zip ps [1 ..]) []
|
||||
test tcs
|
||||
concurrentlyN_ $ map (<// 100000) tcs
|
||||
@ -156,7 +194,7 @@ testChatN ps test = withTmpFiles $ do
|
||||
where
|
||||
getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC]
|
||||
getTestCCs [] tcs = pure tcs
|
||||
getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat (show db) p <*> getTestCCs envs' tcs
|
||||
getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat cfg opts (show db) p <*> getTestCCs envs' tcs
|
||||
|
||||
(<//) :: TestCC -> Int -> Expectation
|
||||
(<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing
|
||||
@ -176,21 +214,36 @@ userName :: TestCC -> IO [Char]
|
||||
userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser
|
||||
|
||||
testChat2 :: Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChat2 p1 p2 test = testChatN [p1, p2] test_
|
||||
testChat2 = testChatCfgOpts2 testCfg testOpts
|
||||
|
||||
testChatCfg2 :: ChatConfig -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChatCfg2 cfg = testChatCfgOpts2 cfg testOpts
|
||||
|
||||
testChatOpts2 :: ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChatOpts2 = testChatCfgOpts2 testCfg
|
||||
|
||||
testChatCfgOpts2 :: ChatConfig -> ChatOpts -> Profile -> Profile -> (TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_
|
||||
where
|
||||
test_ :: [TestCC] -> IO ()
|
||||
test_ [tc1, tc2] = test tc1 tc2
|
||||
test_ _ = error "expected 2 chat clients"
|
||||
|
||||
testChat3 :: Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChat3 p1 p2 p3 test = testChatN [p1, p2, p3] test_
|
||||
testChat3 = testChatCfgOpts3 testCfg testOpts
|
||||
|
||||
testChatCfg3 :: ChatConfig -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChatCfg3 cfg = testChatCfgOpts3 cfg testOpts
|
||||
|
||||
testChatCfgOpts3 :: ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_
|
||||
where
|
||||
test_ :: [TestCC] -> IO ()
|
||||
test_ [tc1, tc2, tc3] = test tc1 tc2 tc3
|
||||
test_ _ = error "expected 3 chat clients"
|
||||
|
||||
testChat4 :: Profile -> Profile -> Profile -> Profile -> (TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> IO ()
|
||||
testChat4 p1 p2 p3 p4 test = testChatN [p1, p2, p3, p4] test_
|
||||
testChat4 p1 p2 p3 p4 test = testChatN testCfg testOpts [p1, p2, p3, p4] test_
|
||||
where
|
||||
test_ :: [TestCC] -> IO ()
|
||||
test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4
|
||||
@ -216,7 +269,8 @@ serverCfg =
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt",
|
||||
logStatsInterval = Just 86400,
|
||||
logStatsStartTime = 0
|
||||
logStatsStartTime = 0,
|
||||
smpServerVRange = supportedSMPServerVRange
|
||||
}
|
||||
|
||||
withSmpServer :: IO a -> IO a
|
||||
|
@ -18,9 +18,11 @@ import Data.Char (isDigit)
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller (ChatController (..))
|
||||
import Simplex.Chat.Options (ChatOpts (..))
|
||||
import Simplex.Chat.Types (ConnStatus (..), ImageData (..), Profile (..), User (..))
|
||||
import Simplex.Chat.Util (unlessM)
|
||||
import System.Directory (copyFile, doesFileExist)
|
||||
import Simplex.Messaging.Util (unlessM)
|
||||
import System.Directory (copyFile, doesDirectoryExist, doesFileExist)
|
||||
import System.FilePath ((</>))
|
||||
import Test.Hspec
|
||||
|
||||
aliceProfile :: Profile
|
||||
@ -38,12 +40,12 @@ danProfile = Profile {displayName = "dan", fullName = "Daniel", image = Nothing}
|
||||
chatTests :: Spec
|
||||
chatTests = do
|
||||
describe "direct messages" $ do
|
||||
it "add contact and send/receive message" testAddContact
|
||||
describe "add contact and send/receive message" testAddContact
|
||||
it "direct message quoted replies" testDirectMessageQuotedReply
|
||||
it "direct message update" testDirectMessageUpdate
|
||||
it "direct message delete" testDirectMessageDelete
|
||||
describe "chat groups" $ do
|
||||
it "add contacts, create group and send/receive messages" testGroup
|
||||
describe "add contacts, create group and send/receive messages" testGroup
|
||||
it "create and join group with 4 members" testGroup2
|
||||
it "create and delete group" testGroupDelete
|
||||
it "invitee delete group when in status invited" testGroupDeleteWhenInvited
|
||||
@ -65,16 +67,16 @@ chatTests = do
|
||||
it "send and receive file to group" testGroupFileTransfer
|
||||
it "sender cancelled group file transfer before transfer" testGroupFileSndCancelBeforeTransfer
|
||||
describe "messages with files" $ do
|
||||
it "send and receive message with file" testMessageWithFile
|
||||
describe "send and receive message with file" testMessageWithFile
|
||||
it "send and receive image" testSendImage
|
||||
it "files folder: send and receive image" testFilesFoldersSendImage
|
||||
it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete
|
||||
it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete
|
||||
it "send and receive image with text and quote" testSendImageWithTextAndQuote
|
||||
it "send and receive image to group" testGroupSendImage
|
||||
describe "send and receive image to group" testGroupSendImage
|
||||
it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote
|
||||
describe "user contact link" $ do
|
||||
it "create and connect via contact link" testUserContactLink
|
||||
describe "create and connect via contact link" testUserContactLink
|
||||
it "auto accept contact requests" testUserContactLinkAutoAccept
|
||||
it "deduplicate contact requests" testDeduplicateContactRequests
|
||||
it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange
|
||||
@ -85,17 +87,64 @@ chatTests = do
|
||||
describe "async connection handshake" $ do
|
||||
it "connect when initiating client goes offline" testAsyncInitiatingOffline
|
||||
it "connect when accepting client goes offline" testAsyncAcceptingOffline
|
||||
it "connect, fully asynchronous (when clients are never simultaneously online)" testFullAsync
|
||||
xdescribe "async sending and receiving files" $ do
|
||||
it "send and receive file, fully asynchronous" testAsyncFileTransfer
|
||||
it "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
|
||||
describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do
|
||||
it "v2" testFullAsync
|
||||
it "v1" testFullAsyncV1
|
||||
it "v1 to v2" testFullAsyncV1toV2
|
||||
it "v2 to v1" testFullAsyncV2toV1
|
||||
describe "async sending and receiving files" $ do
|
||||
xdescribe "send and receive file, fully asynchronous" $ do
|
||||
it "v2" testAsyncFileTransfer
|
||||
it "v1" testAsyncFileTransferV1
|
||||
xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
|
||||
describe "webrtc calls api" $ do
|
||||
it "negotiate call" testNegotiateCall
|
||||
describe "maintenance mode" $ do
|
||||
it "start/stop/export/import chat" testMaintenanceMode
|
||||
it "export/import chat with files" testMaintenanceModeWithFiles
|
||||
|
||||
testAddContact :: IO ()
|
||||
testAddContact =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
versionTestMatrix2 :: (TestCC -> TestCC -> IO ()) -> Spec
|
||||
versionTestMatrix2 runTest = do
|
||||
it "v2" $ testChat2 aliceProfile bobProfile $ runTest
|
||||
it "v1" $ testChatCfg2 testCfgV1 aliceProfile bobProfile $ runTest
|
||||
it "v1 to v2" . withTmpFiles $
|
||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatV1 "bob" bobProfile $ \bob ->
|
||||
runTest alice bob
|
||||
it "v2 to v1" . withTmpFiles $
|
||||
withNewTestChatV1 "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat "bob" bobProfile $ \bob ->
|
||||
runTest alice bob
|
||||
|
||||
versionTestMatrix3 :: (TestCC -> TestCC -> TestCC -> IO ()) -> Spec
|
||||
versionTestMatrix3 runTest = do
|
||||
it "v2" $ testChat3 aliceProfile bobProfile cathProfile $ runTest
|
||||
it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile $ runTest
|
||||
it "v1 to v2" . withTmpFiles $
|
||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatV1 "bob" bobProfile $ \bob ->
|
||||
withNewTestChatV1 "cath" cathProfile $ \cath ->
|
||||
runTest alice bob cath
|
||||
it "v2+v1 to v2" . withTmpFiles $
|
||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat "bob" bobProfile $ \bob ->
|
||||
withNewTestChatV1 "cath" cathProfile $ \cath ->
|
||||
runTest alice bob cath
|
||||
it "v2 to v1" . withTmpFiles $
|
||||
withNewTestChatV1 "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat "bob" bobProfile $ \bob ->
|
||||
withNewTestChat "cath" cathProfile $ \cath ->
|
||||
runTest alice bob cath
|
||||
it "v2+v1 to v1" . withTmpFiles $
|
||||
withNewTestChatV1 "alice" aliceProfile $ \alice ->
|
||||
withNewTestChat "bob" bobProfile $ \bob ->
|
||||
withNewTestChatV1 "cath" cathProfile $ \cath ->
|
||||
runTest alice bob cath
|
||||
|
||||
testAddContact :: Spec
|
||||
testAddContact = versionTestMatrix2 runTestAddContact
|
||||
where
|
||||
runTestAddContact alice bob = do
|
||||
alice ##> "/c"
|
||||
inv <- getInvitation alice
|
||||
bob ##> ("/c " <> inv)
|
||||
@ -136,7 +185,6 @@ testAddContact =
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
bob #$> ("/clear alice", id, "alice: all messages are removed locally ONLY")
|
||||
bob #$> ("/_get chat @2 count=100", chat, [])
|
||||
where
|
||||
chatsEmpty alice bob = do
|
||||
alice @@@ [("@bob", "")]
|
||||
alice #$> ("/_get chat @2 count=100", chat, [])
|
||||
@ -308,10 +356,10 @@ testDirectMessageDelete =
|
||||
bob @@@ [("@alice", "do you receive my messages?")]
|
||||
bob #$> ("/_get chat @2 count=100", chat', [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))])
|
||||
|
||||
testGroup :: IO ()
|
||||
testGroup =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
testGroup :: Spec
|
||||
testGroup = versionTestMatrix3 runTestGroup
|
||||
where
|
||||
runTestGroup alice bob cath = do
|
||||
connectUsers alice bob
|
||||
connectUsers alice cath
|
||||
alice ##> "/g team"
|
||||
@ -402,7 +450,7 @@ testGroup =
|
||||
bob #$> ("/_get chat #1 count=100", chat, [])
|
||||
cath #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
|
||||
cath #$> ("/_get chat #1 count=100", chat, [])
|
||||
where
|
||||
getReadChats :: TestCC -> TestCC -> TestCC -> IO ()
|
||||
getReadChats alice bob cath = do
|
||||
alice @@@ [("#team", "hey team"), ("@cath", ""), ("@bob", "")]
|
||||
alice #$> ("/_get chat #1 count=100", chat, [(1, "hello"), (0, "hi there"), (0, "hey team")])
|
||||
@ -1199,10 +1247,10 @@ testGroupFileSndCancelBeforeTransfer =
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "file cancelled: test.txt"
|
||||
|
||||
testMessageWithFile :: IO ()
|
||||
testMessageWithFile =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice bob -> do
|
||||
testMessageWithFile :: Spec
|
||||
testMessageWithFile = versionTestMatrix2 runTestMessageWithFile
|
||||
where
|
||||
runTestMessageWithFile alice bob = do
|
||||
connectUsers alice bob
|
||||
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\": \"text\", \"text\": \"hi, sending a file\"}}"
|
||||
alice <# "@bob hi, sending a file"
|
||||
@ -1410,10 +1458,10 @@ testSendImageWithTextAndQuote =
|
||||
(alice <## "completed sending file 3 (test.jpg) to bob")
|
||||
B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src
|
||||
|
||||
testGroupSendImage :: IO ()
|
||||
testGroupSendImage =
|
||||
testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
testGroupSendImage :: Spec
|
||||
testGroupSendImage = versionTestMatrix3 runTestGroupSendImage
|
||||
where
|
||||
runTestGroupSendImage alice bob cath = do
|
||||
createGroup3 "team" alice bob cath
|
||||
alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}"
|
||||
alice <# "/f #team ./tests/fixtures/test.jpg"
|
||||
@ -1514,32 +1562,31 @@ testGroupSendImageWithTextAndQuote =
|
||||
cath #$> ("/_get chat #1 count=100", chat'', [((0, "hi team"), Nothing, Nothing), ((0, "hey bob"), Just (0, "hi team"), Just "./tests/tmp/test_1.jpg")])
|
||||
cath @@@ [("#team", "hey bob"), ("@alice", ""), ("@bob", "")]
|
||||
|
||||
testUserContactLink :: IO ()
|
||||
testUserContactLink = testChat3 aliceProfile bobProfile cathProfile $
|
||||
\alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice @@@ [("<@bob", "")]
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
alice @@@ [("@bob", "")]
|
||||
alice <##> bob
|
||||
testUserContactLink :: Spec
|
||||
testUserContactLink = versionTestMatrix3 $ \alice bob cath -> do
|
||||
alice ##> "/ad"
|
||||
cLink <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLink)
|
||||
alice <#? bob
|
||||
alice @@@ [("<@bob", "")]
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request..."
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
alice @@@ [("@bob", "")]
|
||||
alice <##> bob
|
||||
|
||||
cath ##> ("/c " <> cLink)
|
||||
alice <#? cath
|
||||
alice @@@ [("<@cath", ""), ("@bob", "hey")]
|
||||
alice ##> "/ac cath"
|
||||
alice <## "cath (Catherine): accepting contact request..."
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
alice @@@ [("@cath", ""), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
cath ##> ("/c " <> cLink)
|
||||
alice <#? cath
|
||||
alice @@@ [("<@cath", ""), ("@bob", "hey")]
|
||||
alice ##> "/ac cath"
|
||||
alice <## "cath (Catherine): accepting contact request..."
|
||||
concurrently_
|
||||
(cath <## "alice (Alice): contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
alice @@@ [("@cath", ""), ("@bob", "hey")]
|
||||
alice <##> cath
|
||||
|
||||
testUserContactLinkAutoAccept :: IO ()
|
||||
testUserContactLinkAutoAccept =
|
||||
@ -1803,11 +1850,8 @@ testFullAsync = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
withTestChat "alice" $ \_ -> pure ()
|
||||
withTestChat "bob" $ \_ -> pure ()
|
||||
withTestChat "alice" $ \alice ->
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
withTestChat "bob" $ \_ -> pure ()
|
||||
withTestChat "alice" $ \_ -> pure () -- connecting... notification in UI
|
||||
withTestChat "bob" $ \_ -> pure () -- connecting... notification in UI
|
||||
withTestChat "alice" $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
@ -1815,6 +1859,81 @@ testFullAsync = withTmpFiles $ do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
|
||||
testFullAsyncV1 :: IO ()
|
||||
testFullAsyncV1 = withTmpFiles $ do
|
||||
inv <- withNewAlice $ \alice -> do
|
||||
alice ##> "/c"
|
||||
getInvitation alice
|
||||
withNewBob $ \bob -> do
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
withAlice $ \_ -> pure ()
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice ->
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
withBob $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
where
|
||||
withNewAlice = withNewTestChatV1 "alice" aliceProfile
|
||||
withAlice = withTestChatV1 "alice"
|
||||
withNewBob = withNewTestChatV1 "bob" bobProfile
|
||||
withBob = withTestChatV1 "bob"
|
||||
|
||||
testFullAsyncV1toV2 :: IO ()
|
||||
testFullAsyncV1toV2 = withTmpFiles $ do
|
||||
inv <- withNewAlice $ \alice -> do
|
||||
alice ##> "/c"
|
||||
getInvitation alice
|
||||
withNewBob $ \bob -> do
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
withAlice $ \_ -> pure ()
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice ->
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
withBob $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
where
|
||||
withNewAlice = withNewTestChat "alice" aliceProfile
|
||||
withAlice = withTestChat "alice"
|
||||
withNewBob = withNewTestChatV1 "bob" bobProfile
|
||||
withBob = withTestChatV1 "bob"
|
||||
|
||||
testFullAsyncV2toV1 :: IO ()
|
||||
testFullAsyncV2toV1 = withTmpFiles $ do
|
||||
inv <- withNewAlice $ \alice -> do
|
||||
alice ##> "/c"
|
||||
getInvitation alice
|
||||
withNewBob $ \bob -> do
|
||||
bob ##> ("/c " <> inv)
|
||||
bob <## "confirmation sent!"
|
||||
withAlice $ \_ -> pure ()
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice ->
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
withBob $ \_ -> pure ()
|
||||
withAlice $ \alice -> do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice <## "bob (Bob): contact is connected"
|
||||
withBob $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob <## "alice (Alice): contact is connected"
|
||||
where
|
||||
withNewAlice = withNewTestChatV1 "alice" aliceProfile
|
||||
withAlice = withTestChatV1 "alice"
|
||||
withNewBob = withNewTestChat "bob" bobProfile
|
||||
withBob = withTestChat "bob"
|
||||
|
||||
testAsyncFileTransfer :: IO ()
|
||||
testAsyncFileTransfer = withTmpFiles $ do
|
||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||
@ -1831,8 +1950,8 @@ testAsyncFileTransfer = withTmpFiles $ do
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.jpg"
|
||||
withTestChatContactConnected' "alice"
|
||||
withTestChatContactConnected' "bob"
|
||||
-- withTestChatContactConnected' "alice" -- TODO not needed in v2
|
||||
-- withTestChatContactConnected' "bob" -- TODO not needed in v2
|
||||
withTestChatContactConnected' "alice"
|
||||
withTestChatContactConnected' "bob"
|
||||
withTestChatContactConnected "alice" $ \alice -> do
|
||||
@ -1845,6 +1964,36 @@ testAsyncFileTransfer = withTmpFiles $ do
|
||||
dest <- B.readFile "./tests/tmp/test.jpg"
|
||||
dest `shouldBe` src
|
||||
|
||||
testAsyncFileTransferV1 :: IO ()
|
||||
testAsyncFileTransferV1 = withTmpFiles $ do
|
||||
withNewTestChatV1 "alice" aliceProfile $ \alice ->
|
||||
withNewTestChatV1 "bob" bobProfile $ \bob ->
|
||||
connectUsers alice bob
|
||||
withTestChatContactConnectedV1 "alice" $ \alice -> do
|
||||
alice ##> "/_send @2 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"type\":\"text\", \"text\": \"hi, sending a file\"}}"
|
||||
alice <# "@bob hi, sending a file"
|
||||
alice <# "/f @bob ./tests/fixtures/test.jpg"
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
withTestChatContactConnectedV1 "bob" $ \bob -> do
|
||||
bob <# "alice> hi, sending a file"
|
||||
bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)"
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.jpg"
|
||||
withTestChatContactConnectedV1' "alice" -- TODO not needed in v2
|
||||
withTestChatContactConnectedV1' "bob" -- TODO not needed in v2
|
||||
withTestChatContactConnectedV1' "alice"
|
||||
withTestChatContactConnectedV1' "bob"
|
||||
withTestChatContactConnectedV1 "alice" $ \alice -> do
|
||||
alice <## "started sending file 1 (test.jpg) to bob"
|
||||
alice <## "completed sending file 1 (test.jpg) to bob"
|
||||
withTestChatContactConnectedV1 "bob" $ \bob -> do
|
||||
bob <## "started receiving file 1 (test.jpg) from alice"
|
||||
bob <## "completed receiving file 1 (test.jpg) from alice"
|
||||
src <- B.readFile "./tests/fixtures/test.jpg"
|
||||
dest <- B.readFile "./tests/tmp/test.jpg"
|
||||
dest `shouldBe` src
|
||||
|
||||
testAsyncGroupFileTransfer :: IO ()
|
||||
testAsyncGroupFileTransfer = withTmpFiles $ do
|
||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||
@ -1868,9 +2017,9 @@ testAsyncGroupFileTransfer = withTmpFiles $ do
|
||||
withTestChatGroup3Connected' "alice"
|
||||
withTestChatGroup3Connected' "bob"
|
||||
withTestChatGroup3Connected' "cath"
|
||||
withTestChatGroup3Connected' "alice"
|
||||
withTestChatGroup3Connected' "bob"
|
||||
withTestChatGroup3Connected' "cath"
|
||||
-- withTestChatGroup3Connected' "alice" -- TODO not needed in v2
|
||||
-- withTestChatGroup3Connected' "bob" -- TODO not needed in v2
|
||||
-- withTestChatGroup3Connected' "cath" -- TODO not needed in v2
|
||||
withTestChatGroup3Connected' "alice"
|
||||
withTestChatGroup3Connected "bob" $ \bob -> do
|
||||
bob <## "started receiving file 1 (test.jpg) from alice"
|
||||
@ -1963,6 +2112,81 @@ testNegotiateCall =
|
||||
alice <## "message updated"
|
||||
alice #$> ("/_get chat @2 count=100", chat, [(1, "outgoing call: ended (00:00)")])
|
||||
|
||||
testMaintenanceMode :: IO ()
|
||||
testMaintenanceMode = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/c"
|
||||
alice <## "error: chat not started"
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
connectUsers alice bob
|
||||
alice #> "@bob hi"
|
||||
bob <# "alice> hi"
|
||||
alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "error: chat not stopped"
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
-- chat works after start
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice #> "@bob hi again"
|
||||
bob <# "alice> hi again"
|
||||
bob #> "@alice hello"
|
||||
alice <# "bob> hello"
|
||||
-- export / delete / import
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "ok"
|
||||
doesFileExist "./tests/tmp/alice-chat.zip" `shouldReturn` True
|
||||
alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "ok"
|
||||
-- cannot start chat after import
|
||||
alice ##> "/_start"
|
||||
alice <## "error: chat store changed"
|
||||
-- works after full restart
|
||||
withTestChat "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
testChatWorking :: TestCC -> TestCC -> IO ()
|
||||
testChatWorking alice bob = do
|
||||
alice <## "1 contacts connected (use /cs for the list)"
|
||||
alice #> "@bob hello again"
|
||||
bob <# "alice> hello again"
|
||||
bob #> "@alice hello too"
|
||||
alice <# "bob> hello too"
|
||||
|
||||
testMaintenanceModeWithFiles :: IO ()
|
||||
testMaintenanceModeWithFiles = withTmpFiles $ do
|
||||
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatOpts testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
alice ##> "/_files_folder ./tests/tmp/alice_files"
|
||||
alice <## "ok"
|
||||
connectUsers alice bob
|
||||
startFileTransferWithDest' bob alice "test.jpg" "136.5 KiB / 139737 bytes" Nothing
|
||||
bob <## "completed sending file 1 (test.jpg) to alice"
|
||||
alice <## "completed receiving file 1 (test.jpg) from bob"
|
||||
src <- B.readFile "./tests/fixtures/test.jpg"
|
||||
B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src
|
||||
alice ##> "/_stop"
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/_db export {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "ok"
|
||||
alice ##> "/_db delete"
|
||||
alice <## "ok"
|
||||
-- cannot start chat after delete
|
||||
alice ##> "/_start"
|
||||
alice <## "error: chat store changed"
|
||||
doesDirectoryExist "./tests/tmp/alice_files" `shouldReturn` False
|
||||
alice ##> "/_db import {\"archivePath\": \"./tests/tmp/alice-chat.zip\"}"
|
||||
alice <## "ok"
|
||||
B.readFile "./tests/tmp/alice_files/test.jpg" `shouldReturn` src
|
||||
-- works after full restart
|
||||
withTestChat "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
withTestChatContactConnected :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatContactConnected dbPrefix action =
|
||||
withTestChat dbPrefix $ \cc -> do
|
||||
@ -1972,6 +2196,15 @@ withTestChatContactConnected dbPrefix action =
|
||||
withTestChatContactConnected' :: String -> IO ()
|
||||
withTestChatContactConnected' dbPrefix = withTestChatContactConnected dbPrefix $ \_ -> pure ()
|
||||
|
||||
withTestChatContactConnectedV1 :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatContactConnectedV1 dbPrefix action =
|
||||
withTestChatV1 dbPrefix $ \cc -> do
|
||||
cc <## "1 contacts connected (use /cs for the list)"
|
||||
action cc
|
||||
|
||||
withTestChatContactConnectedV1' :: String -> IO ()
|
||||
withTestChatContactConnectedV1' dbPrefix = withTestChatContactConnectedV1 dbPrefix $ \_ -> pure ()
|
||||
|
||||
withTestChatGroup3Connected :: String -> (TestCC -> IO a) -> IO a
|
||||
withTestChatGroup3Connected dbPrefix action = do
|
||||
withTestChat dbPrefix $ \cc -> do
|
||||
@ -1987,16 +2220,21 @@ startFileTransfer alice bob =
|
||||
startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes"
|
||||
|
||||
startFileTransfer' :: TestCC -> TestCC -> String -> String -> IO ()
|
||||
startFileTransfer' alice bob fileName fileSize = do
|
||||
alice #> ("/f @bob ./tests/fixtures/" <> fileName)
|
||||
alice <## "use /fc 1 to cancel sending"
|
||||
bob <# ("alice> sends file " <> fileName <> " (" <> fileSize <> ")")
|
||||
bob <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob <## ("saving file 1 from alice to ./tests/tmp/" <> fileName)
|
||||
startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp"
|
||||
|
||||
startFileTransferWithDest' :: TestCC -> TestCC -> String -> String -> Maybe String -> IO ()
|
||||
startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do
|
||||
name1 <- userName cc1
|
||||
name2 <- userName cc2
|
||||
cc1 #> ("/f @" <> name2 <> " ./tests/fixtures/" <> fileName)
|
||||
cc1 <## "use /fc 1 to cancel sending"
|
||||
cc2 <# (name1 <> "> sends file " <> fileName <> " (" <> fileSize <> ")")
|
||||
cc2 <## "use /fr 1 [<dir>/ | <path>] to receive it"
|
||||
cc2 ##> ("/fr 1" <> maybe "" (" " <>) fileDest_)
|
||||
cc2 <## ("saving file 1 from " <> name1 <> " to " <> maybe id (</>) fileDest_ fileName)
|
||||
concurrently_
|
||||
(bob <## ("started receiving file 1 (" <> fileName <> ") from alice"))
|
||||
(alice <## ("started sending file 1 (" <> fileName <> ") to bob"))
|
||||
(cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1))
|
||||
(cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2))
|
||||
|
||||
checkPartialTransfer :: String -> IO ()
|
||||
checkPartialTransfer fileName = do
|
||||
|
@ -16,6 +16,7 @@ import Simplex.Messaging.Crypto.Ratchet
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtocolServer (..), smpClientVRange)
|
||||
import Simplex.Messaging.Version
|
||||
import Test.Hspec
|
||||
|
||||
protocolTests :: Spec
|
||||
@ -42,7 +43,7 @@ connReqData :: ConnReqUriData
|
||||
connReqData =
|
||||
ConnReqUriData
|
||||
{ crScheme = simplexChat,
|
||||
crAgentVRange = smpAgentVRange,
|
||||
crAgentVRange = mkVersionRange 1 1,
|
||||
crSmpQueues = [queue]
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user