core, iOS: support for self-destruct password (#2412)

* core, iOS: support for self-destruct password

* disable test logging

* core: fix tests, iOS: remove notifications on removal

* change alerts
This commit is contained in:
Evgeny Poberezkin 2023-05-09 10:33:30 +02:00 committed by GitHub
parent 57801fde1f
commit 0b8d9d11e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 318 additions and 106 deletions

View File

@ -192,7 +192,7 @@ struct ContentView: View {
private func justAuthenticate() { private func justAuthenticate() {
userAuthorized = false userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get() let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason")) { laResult in authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in
logger.debug("authenticate callback: \(String(describing: laResult))") logger.debug("authenticate callback: \(String(describing: laResult))")
switch (laResult) { switch (laResult) {
case .success: case .success:

View File

@ -247,8 +247,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
} }
} }
func removeNotifications(_ ids : [String]){ func removeAllNotifications() async {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) let nc = UNUserNotificationCenter.current()
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids) let settings = await nc.notificationSettings()
if settings.authorizationStatus == .authorized {
nc.removeAllPendingNotificationRequests()
nc.removeAllDeliveredNotifications()
}
} }
} }

View File

@ -125,7 +125,7 @@ func apiGetActiveUser() throws -> User? {
} }
} }
func apiCreateActiveUser(_ p: Profile) throws -> User { func apiCreateActiveUser(_ p: Profile?) throws -> User {
let r = chatSendCmdSync(.createActiveUser(profile: p)) let r = chatSendCmdSync(.createActiveUser(profile: p))
if case let .activeUser(user) = r { return user } if case let .activeUser(user) = r { return user }
throw r throw r

View File

@ -317,10 +317,7 @@ struct DatabaseView: View {
private func stopChat() { private func stopChat() {
Task { Task {
do { do {
try await apiStopChat() try await stopChatAsync()
ChatReceiver.shared.stop()
await MainActor.run { m.chatRunning = false }
appStateGroupDefault.set(.stopped)
} catch let error { } catch let error {
await MainActor.run { await MainActor.run {
runChat = true runChat = true
@ -374,9 +371,7 @@ struct DatabaseView: View {
progressIndicator = true progressIndicator = true
Task { Task {
do { do {
try await apiDeleteStorage() try await deleteChatAsync()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
await operationEnded(.chatDeleted) await operationEnded(.chatDeleted)
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
} catch let error { } catch let error {
@ -468,6 +463,19 @@ struct DatabaseView: View {
} }
} }
func stopChatAsync() async throws {
try await apiStopChat()
ChatReceiver.shared.stop()
await MainActor.run { ChatModel.shared.chatRunning = false }
appStateGroupDefault.set(.stopped)
}
func deleteChatAsync() async throws {
try await apiDeleteStorage()
_ = kcDatabasePassword.remove()
storeDBPassphraseGroupDefault.set(true)
}
struct DatabaseView_Previews: PreviewProvider { struct DatabaseView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none) DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none)

View File

@ -30,19 +30,26 @@ struct LocalAuthRequest {
var title: LocalizedStringKey? // if title is null, reason is shown var title: LocalizedStringKey? // if title is null, reason is shown
var reason: String var reason: String
var password: String var password: String
var selfDestruct: Bool
var completed: (LAResult) -> Void var completed: (LAResult) -> Void
static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", completed: { _ in }) static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", selfDestruct: false, completed: { _ in })
} }
func authenticate(title: LocalizedStringKey? = nil, reason: String, completed: @escaping (LAResult) -> Void) { func authenticate(title: LocalizedStringKey? = nil, reason: String, selfDestruct: Bool = false, completed: @escaping (LAResult) -> Void) {
logger.debug("authenticate") logger.debug("authenticate")
switch privacyLocalAuthModeDefault.get() { switch privacyLocalAuthModeDefault.get() {
case .system: systemAuthenticate(reason, completed) case .system: systemAuthenticate(reason, completed)
case .passcode: case .passcode:
if let password = kcAppPassword.get() { if let password = kcAppPassword.get() {
DispatchQueue.main.async { DispatchQueue.main.async {
ChatModel.shared.laRequest = LocalAuthRequest(title: title, reason: reason, password: password, completed: completed) ChatModel.shared.laRequest = LocalAuthRequest(
title: title,
reason: reason,
password: password,
selfDestruct: selfDestruct && UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT),
completed: completed
)
} }
} else { } else {
completed(.unavailable(authError: NSLocalizedString("No app password", comment: "Authentication unavailable"))) completed(.unavailable(authError: NSLocalizedString("No app password", comment: "Authentication unavailable")))

View File

@ -7,6 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat
struct LocalAuthView: View { struct LocalAuthView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@ -15,6 +16,13 @@ struct LocalAuthView: View {
var body: some View { var body: some View {
PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") { PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") {
if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword {
deleteStorageAndRestart(sdPassword) { r in
m.laRequest = nil
authRequest.completed(r)
}
return
}
let r: LAResult = password == authRequest.password let r: LAResult = password == authRequest.password
? .success ? .success
: .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) : .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))
@ -25,6 +33,41 @@ struct LocalAuthView: View {
authRequest.completed(.failed(authError: NSLocalizedString("Authentication cancelled", comment: "PIN entry"))) authRequest.completed(.failed(authError: NSLocalizedString("Authentication cancelled", comment: "PIN entry")))
} }
} }
private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) {
Task {
do {
try await stopChatAsync()
try await deleteChatAsync()
_ = kcAppPassword.set(password)
_ = kcSelfDestructPassword.remove()
await NtfManager.shared.removeAllNotifications()
let displayName = UserDefaults.standard.string(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME)
UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT)
UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME)
await MainActor.run {
m.chatDbChanged = true
m.chatInitialized = false
}
resetChatCtrl()
try initializeChat(start: true)
m.chatDbChanged = false
appStateGroupDefault.set(.active)
if m.currentUser != nil { return }
var profile: Profile? = nil
if let displayName = displayName, displayName != "" {
profile = Profile(displayName: displayName, fullName: "")
}
m.currentUser = try apiCreateActiveUser(profile)
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
try startChat()
completed(.success)
} catch {
completed(.failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")))
}
}
}
} }
struct LocalAuthView_Previews: PreviewProvider { struct LocalAuthView_Previews: PreviewProvider {

View File

@ -10,6 +10,9 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct SetAppPasscodeView: View { struct SetAppPasscodeView: View {
var passcodeKeychain: KeyChainItem = kcAppPassword
var title: LocalizedStringKey = "New Passcode"
var reason: String?
var submit: () -> Void var submit: () -> Void
var cancel: () -> Void var cancel: () -> Void
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@ -27,7 +30,7 @@ struct SetAppPasscodeView: View {
submitEnabled: { pwd in pwd == enteredPassword } submitEnabled: { pwd in pwd == enteredPassword }
) { ) {
if passcode == enteredPassword { if passcode == enteredPassword {
if kcAppPassword.set(passcode) { if passcodeKeychain.set(passcode) {
enteredPassword = "" enteredPassword = ""
passcode = "" passcode = ""
dismiss() dismiss()
@ -38,7 +41,7 @@ struct SetAppPasscodeView: View {
} }
} }
} else { } else {
setPasswordView(title: "New Passcode", submitLabel: "Save") { setPasswordView(title: title, submitLabel: "Save") {
enteredPassword = passcode enteredPassword = passcode
passcode = "" passcode = ""
confirming = true confirming = true
@ -51,7 +54,7 @@ struct SetAppPasscodeView: View {
} }
private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View { private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View {
PasscodeView(passcode: $passcode, title: title, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) {
dismiss() dismiss()
cancel() cancel()
} }

View File

@ -129,6 +129,8 @@ struct CreateProfile: View {
m.onboardingStage = .step3_CreateSimpleXAddress m.onboardingStage = .step3_CreateSimpleXAddress
} }
} else { } else {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss() dismiss()
m.users = try listUsers() m.users = try listUsers()
try getUserChatData() try getUserChatData()

View File

@ -13,6 +13,7 @@ struct IncognitoHelp: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Incognito mode") Text("Incognito mode")
.font(.largeTitle) .font(.largeTitle)
.bold()
.padding(.vertical) .padding(.vertical)
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {

View File

@ -102,9 +102,13 @@ struct SimplexLockView: View {
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
@State private var laMode: LAMode = privacyLocalAuthModeDefault.get() @State private var laMode: LAMode = privacyLocalAuthModeDefault.get()
@AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30 @AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30
@State var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
@State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT)
@AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = ""
@State private var performLAToggleReset = false @State private var performLAToggleReset = false
@State private var performLAModeReset = false @State private var performLAModeReset = false
@State private var performLASelfDestructReset = false
@State private var showPasswordAction: PasswordAction? = nil @State private var showPasswordAction: PasswordAction? = nil
@State private var showChangePassword = false @State private var showChangePassword = false
@State var laAlert: LASettingViewAlert? = nil @State var laAlert: LASettingViewAlert? = nil
@ -116,6 +120,8 @@ struct SimplexLockView: View {
case laUnavailableTurningOffAlert case laUnavailableTurningOffAlert
case laPasscodeSetAlert case laPasscodeSetAlert
case laPasscodeChangedAlert case laPasscodeChangedAlert
case laSeldDestructPasscodeSetAlert
case laSeldDestructPasscodeChangedAlert
case laPasscodeNotChangedAlert case laPasscodeNotChangedAlert
var id: Self { self } var id: Self { self }
@ -124,7 +130,10 @@ struct SimplexLockView: View {
enum PasswordAction: Identifiable { enum PasswordAction: Identifiable {
case enableAuth case enableAuth
case toggleMode case toggleMode
case changePassword case changePasscode
case enableSelfDestruct
case changeSelfDestructPasscode
case selfDestructInfo
var id: Self { self } var id: Self { self }
} }
@ -159,12 +168,34 @@ struct SimplexLockView: View {
} }
} }
if showChangePassword && laMode == .passcode { if showChangePassword && laMode == .passcode {
Button("Change Passcode") { Button("Change passcode") {
changeLAPassword() changeLAPassword()
} }
} }
} }
} }
if performLA && laMode == .passcode {
Section("Self-destruct passcode") {
Toggle(isOn: $selfDestruct) {
HStack(spacing: 6) {
Text("Enable self-destruct")
Image(systemName: "info.circle")
.foregroundColor(.accentColor)
.font(.system(size: 14))
}
.onTapGesture {
showPasswordAction = .selfDestructInfo
}
}
if selfDestruct {
TextField("New display name", text: $selfDestructDisplayName)
Button("Change self-destruct passcode") {
changeSelfDestructPassword()
}
}
}
}
} }
} }
.onChange(of: performLA) { performLAToggle in .onChange(of: performLA) { performLAToggle in
@ -192,6 +223,13 @@ struct SimplexLockView: View {
updateLAMode() updateLAMode()
} }
} }
.onChange(of: selfDestruct) { _ in
if performLASelfDestructReset {
performLASelfDestructReset = false
} else if prefPerformLA {
toggleSelfDestruct()
}
}
.alert(item: $laAlert) { alertItem in .alert(item: $laAlert) { alertItem in
switch alertItem { switch alertItem {
case .laTurnedOnAlert: return laTurnedOnAlert() case .laTurnedOnAlert: return laTurnedOnAlert()
@ -200,6 +238,8 @@ struct SimplexLockView: View {
case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert() case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert()
case .laPasscodeSetAlert: return passcodeAlert("Passcode set!") case .laPasscodeSetAlert: return passcodeAlert("Passcode set!")
case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!") case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!")
case .laSeldDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!")
case .laSeldDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!")
case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!") case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!")
} }
} }
@ -223,12 +263,27 @@ struct SimplexLockView: View {
} cancel: { } cancel: {
revertLAMode() revertLAMode()
} }
case .changePassword: case .changePasscode:
SetAppPasscodeView { SetAppPasscodeView {
showLAAlert(.laPasscodeChangedAlert) showLAAlert(.laPasscodeChangedAlert)
} cancel: { } cancel: {
showLAAlert(.laPasscodeNotChangedAlert) showLAAlert(.laPasscodeNotChangedAlert)
} }
case .enableSelfDestruct:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) {
updateSelfDestruct()
showLAAlert(.laSeldDestructPasscodeSetAlert)
} cancel: {
revertSelfDestruct()
}
case .changeSelfDestructPasscode:
SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) {
showLAAlert(.laSeldDestructPasscodeChangedAlert)
} cancel: {
showLAAlert(.laPasscodeNotChangedAlert)
}
case .selfDestructInfo:
selfDestructInfoView()
} }
} }
.onAppear { .onAppear {
@ -239,6 +294,30 @@ struct SimplexLockView: View {
} }
} }
private func selfDestructInfoView() -> some View {
VStack(alignment: .leading) {
Text("Self-desctruct")
.font(.largeTitle)
.bold()
.padding(.vertical)
ScrollView {
VStack(alignment: .leading) {
Group {
Text("If you enter your self-desctruct passcode while opening the app:")
VStack(spacing: 8) {
textListItem("1.", "All app data is deleted.")
textListItem("2.", "App passcode is replaced with self-desctruct passcode.")
textListItem("3.", "An empty chat profile with the provided name is created, and the app opens as usual.")
}
}
.padding(.bottom)
}
}
}
.frame(maxWidth: .infinity)
.padding()
}
private func showLAAlert(_ a: LASettingViewAlert) { private func showLAAlert(_ a: LASettingViewAlert) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
laAlert = a laAlert = a
@ -276,11 +355,39 @@ struct SimplexLockView: View {
} }
} }
private func toggleSelfDestruct() {
authenticate(reason: NSLocalizedString("Change self-destruct mode", comment: "authentication reason")) { laResult in
switch laResult {
case .failed:
revertSelfDestruct()
laAlert = .laFailedAlert
case .success:
if selfDestruct {
showPasswordAction = .enableSelfDestruct
} else {
resetSelfDestruct()
}
case .unavailable:
disableUnavailableLA()
}
}
}
private func changeLAPassword() { private func changeLAPassword() {
authenticate(title: "Current Passcode", reason: NSLocalizedString("Change passcode", comment: "authentication reason")) { laResult in authenticate(title: "Current Passcode", reason: NSLocalizedString("Change passcode", comment: "authentication reason")) { laResult in
switch laResult { switch laResult {
case .failed: laAlert = .laFailedAlert case .failed: laAlert = .laFailedAlert
case .success: showPasswordAction = .changePassword case .success: showPasswordAction = .changePasscode
case .unavailable: disableUnavailableLA()
}
}
}
private func changeSelfDestructPassword() {
authenticate(reason: NSLocalizedString("Change self-destruct passcode", comment: "authentication reason")) { laResult in
switch laResult {
case .failed: laAlert = .laFailedAlert
case .success: showPasswordAction = .changeSelfDestructPasscode
case .unavailable: disableUnavailableLA() case .unavailable: disableUnavailableLA()
} }
} }
@ -329,6 +436,7 @@ struct SimplexLockView: View {
_ = kcAppPassword.remove() _ = kcAppPassword.remove()
laLockDelay = 30 laLockDelay = 30
showChangePassword = false showChangePassword = false
resetSelfDestruct()
} }
private func resetLAEnabled(_ onOff: Bool) { private func resetLAEnabled(_ onOff: Bool) {
@ -347,9 +455,29 @@ struct SimplexLockView: View {
privacyLocalAuthModeDefault.set(laMode) privacyLocalAuthModeDefault.set(laMode)
} }
private func resetSelfDestruct() {
_ = kcSelfDestructPassword.remove()
selfDestruct = false
updateSelfDestruct()
}
private func revertSelfDestruct() {
performLASelfDestructReset = true
withAnimation { selfDestruct = currentSelfDestruct }
}
private func updateSelfDestruct() {
UserDefaults.standard.set(selfDestruct, forKey: DEFAULT_LA_SELF_DESTRUCT)
currentSelfDestruct = selfDestruct
}
private func passcodeAlert(_ title: LocalizedStringKey) -> Alert { private func passcodeAlert(_ title: LocalizedStringKey) -> Alert {
mkAlert(title: title, message: "Please remember or store it securely - there is no way to recover a lost passcode!") mkAlert(title: title, message: "Please remember or store it securely - there is no way to recover a lost passcode!")
} }
private func selfDestructPasscodeAlert(_ title: LocalizedStringKey) -> Alert {
mkAlert(title: title, message: "If you enter this passcode when opening the app, all app data will be irreversibly removed!")
}
} }
struct PrivacySettings_Previews: PreviewProvider { struct PrivacySettings_Previews: PreviewProvider {

View File

@ -21,6 +21,8 @@ let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown"
let DEFAULT_PERFORM_LA = "performLocalAuthentication" let DEFAULT_PERFORM_LA = "performLocalAuthentication"
let DEFAULT_LA_MODE = "localAuthenticationMode" let DEFAULT_LA_MODE = "localAuthenticationMode"
let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay" let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay"
let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct"
let DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME = "localAuthenticationSelfDestructDisplayName"
let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown" let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown"
let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay"
let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers" let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers"
@ -53,6 +55,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PERFORM_LA: false, DEFAULT_PERFORM_LA: false,
DEFAULT_LA_MODE: LAMode.system.rawValue, DEFAULT_LA_MODE: LAMode.system.rawValue,
DEFAULT_LA_LOCK_DELAY: 30, DEFAULT_LA_LOCK_DELAY: 30,
DEFAULT_LA_SELF_DESTRUCT: false,
DEFAULT_NOTIFICATION_ALERT_SHOWN: false, DEFAULT_NOTIFICATION_ALERT_SHOWN: false,
DEFAULT_WEBRTC_POLICY_RELAY: true, DEFAULT_WEBRTC_POLICY_RELAY: true,
DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false, DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false,
@ -298,9 +301,8 @@ struct SettingsView: View {
.frame(maxWidth: 24, maxHeight: 24, alignment: .center) .frame(maxWidth: 24, maxHeight: 24, alignment: .center)
.foregroundColor(chatModel.incognito ? Color.indigo : .secondary) .foregroundColor(chatModel.incognito ? Color.indigo : .secondary)
Toggle(isOn: $chatModel.incognito) { Toggle(isOn: $chatModel.incognito) {
HStack { HStack(spacing: 6) {
Text("Incognito") Text("Incognito")
Spacer().frame(width: 4)
Image(systemName: "info.circle") Image(systemName: "info.circle")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.font(.system(size: 14)) .font(.system(size: 14))

View File

@ -730,8 +730,8 @@ Dostupné ve verzi 5.1</target>
<target>Změnit</target> <target>Změnit</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Změna hesla</target> <target>Změna hesla</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -730,8 +730,8 @@ Verfügbar ab v5.1</target>
<target>Ändern</target> <target>Ändern</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Passwort ändern</target> <target>Passwort ändern</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source> <source>Change</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve"> <trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -739,9 +739,9 @@ Available in v5.1</target>
<target>Change</target> <target>Change</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Change Passcode</target> <target>Change passcode</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve"> <trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Disponible en v5.1</target>
<target>Cambiar</target> <target>Cambiar</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Cambiar el código de acceso</target> <target>Cambiar el código de acceso</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -730,8 +730,8 @@ Disponible dans la v5.1</target>
<target>Changer</target> <target>Changer</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Modifier le code d'accès</target> <target>Modifier le code d'accès</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source> <source>Change</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve"> <trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Disponibile nella v5.1</target>
<target>Cambia</target> <target>Cambia</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Cambia codice di accesso</target> <target>Cambia codice di accesso</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -730,8 +730,8 @@ Beschikbaar in v5.1</target>
<target>Veranderen</target> <target>Veranderen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Toegangscode wijzigen</target> <target>Toegangscode wijzigen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -730,8 +730,8 @@ Dostępny w v5.1</target>
<target>Zmień</target> <target>Zmień</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Zmień kod dostępu</target> <target>Zmień kod dostępu</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -569,8 +569,8 @@ Available in v5.1</source>
<source>Change</source> <source>Change</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change database passphrase?" xml:space="preserve"> <trans-unit id="Change database passphrase?" xml:space="preserve">

View File

@ -730,8 +730,8 @@ Available in v5.1</source>
<target>Поменять</target> <target>Поменять</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>Изменить код доступа</target> <target>Изменить код доступа</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -730,8 +730,8 @@ Available in v5.1</source>
<target>更改</target> <target>更改</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve"> <trans-unit id="Change passcode" xml:space="preserve">
<source>Change Passcode</source> <source>Change passcode</source>
<target>更改密码</target> <target>更改密码</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -4207,8 +4207,8 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">已取消認證</target> <target state="translated">已取消認證</target>
<note>PIN entry</note> <note>PIN entry</note>
</trans-unit> </trans-unit>
<trans-unit id="Change Passcode" xml:space="preserve" approved="no"> <trans-unit id="Change passcode" xml:space="preserve" approved="no">
<source>Change Passcode</source> <source>Change passcode</source>
<target state="translated">修改密碼</target> <target state="translated">修改密碼</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>

View File

@ -14,7 +14,7 @@ let jsonEncoder = getJSONEncoder()
public enum ChatCommand { public enum ChatCommand {
case showActiveUser case showActiveUser
case createActiveUser(profile: Profile) case createActiveUser(profile: Profile?)
case listUsers case listUsers
case apiSetActiveUser(userId: Int64, viewPwd: String?) case apiSetActiveUser(userId: Int64, viewPwd: String?)
case apiHideUser(userId: Int64, viewPwd: String) case apiHideUser(userId: Int64, viewPwd: String)
@ -110,7 +110,11 @@ public enum ChatCommand {
get { get {
switch self { switch self {
case .showActiveUser: return "/u" case .showActiveUser: return "/u"
case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)" case let .createActiveUser(profile):
if let profile = profile {
return "/create user \(profile.displayName) \(profile.fullName)"
}
return "/create user"
case .listUsers: return "/users" case .listUsers: return "/users"
case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"

View File

@ -13,11 +13,14 @@ private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDevi
private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app" private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app"
private let DATABASE_PASSWORD_ITEM: String = "databasePassword" private let DATABASE_PASSWORD_ITEM: String = "databasePassword"
private let APP_PASSWORD_ITEM: String = "appPassword" private let APP_PASSWORD_ITEM: String = "appPassword"
private let SELF_DESTRUCT_PASSWORD_ITEM: String = "selfDestructPassword"
public let kcDatabasePassword = KeyChainItem(forKey: DATABASE_PASSWORD_ITEM) public let kcDatabasePassword = KeyChainItem(forKey: DATABASE_PASSWORD_ITEM)
public let kcAppPassword = KeyChainItem(forKey: APP_PASSWORD_ITEM) public let kcAppPassword = KeyChainItem(forKey: APP_PASSWORD_ITEM)
public let kcSelfDestructPassword = KeyChainItem(forKey: SELF_DESTRUCT_PASSWORD_ITEM)
public struct KeyChainItem { public struct KeyChainItem {
var forKey: String var forKey: String

View File

@ -477,7 +477,7 @@
"Change passcode" = "Passwort ändern"; "Change passcode" = "Passwort ändern";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Passwort ändern"; "Change passcode" = "Passwort ändern";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Wechseln der Empfängeradresse"; "Change receiving address" = "Wechseln der Empfängeradresse";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Cambiar el código de acceso"; "Change passcode" = "Cambiar el código de acceso";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Cambiar el código de acceso"; "Change passcode" = "Cambiar el código de acceso";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Cambiar servidor de recepción"; "Change receiving address" = "Cambiar servidor de recepción";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Modifier le code d'accès"; "Change passcode" = "Modifier le code d'accès";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Modifier le code d'accès"; "Change passcode" = "Modifier le code d'accès";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Changer d'adresse de réception"; "Change receiving address" = "Changer d'adresse de réception";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Cambia codice di accesso"; "Change passcode" = "Cambia codice di accesso";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Cambia codice di accesso"; "Change passcode" = "Cambia codice di accesso";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Cambia indirizzo di ricezione"; "Change receiving address" = "Cambia indirizzo di ricezione";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Toegangscode wijzigen"; "Change passcode" = "Toegangscode wijzigen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Toegangscode wijzigen"; "Change passcode" = "Toegangscode wijzigen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Ontvangst adres wijzigen"; "Change receiving address" = "Ontvangst adres wijzigen";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Zmień pin"; "Change passcode" = "Zmień pin";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Zmień kod dostępu"; "Change passcode" = "Zmień kod dostępu";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Zmień adres odbioru"; "Change receiving address" = "Zmień adres odbioru";

View File

@ -477,7 +477,7 @@
"Change passcode" = "Изменить код доступа"; "Change passcode" = "Изменить код доступа";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "Изменить код доступа"; "Change passcode" = "Изменить код доступа";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "Поменять адрес получения"; "Change receiving address" = "Поменять адрес получения";

View File

@ -477,7 +477,7 @@
"Change passcode" = "更改密码"; "Change passcode" = "更改密码";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change Passcode" = "更改密码"; "Change passcode" = "更改密码";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Change receiving address" = "更改接收地址"; "Change receiving address" = "更改接收地址";

View File

@ -83,6 +83,7 @@ import Simplex.Messaging.Util
import System.Exit (exitFailure, exitSuccess) import System.Exit (exitFailure, exitSuccess)
import System.FilePath (combine, splitExtensions, takeFileName, (</>)) import System.FilePath (combine, splitExtensions, takeFileName, (</>))
import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout)
import System.Random (randomRIO)
import Text.Read (readMaybe) import Text.Read (readMaybe)
import UnliftIO.Async import UnliftIO.Async
import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay) import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay)
@ -318,7 +319,8 @@ toView event = do
processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse
processChatCommand = \case processChatCommand = \case
ShowActiveUser -> withUser' $ pure . CRActiveUser ShowActiveUser -> withUser' $ pure . CRActiveUser
CreateActiveUser p@Profile {displayName} sameServers -> do CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do
p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile
u <- asks currentUser u <- asks currentUser
(smp, smpServers) <- chooseServers SPSMP (smp, smpServers) <- chooseServers SPSMP
(xftp, xftpServers) <- chooseServers SPXFTP (xftp, xftpServers) <- chooseServers SPXFTP
@ -329,7 +331,8 @@ processChatCommand = \case
when (any (\User {localDisplayName = n} -> n == displayName) users) $ when (any (\User {localDisplayName = n} -> n == displayName) users) $
throwChatError $ CEUserExists displayName throwChatError $ CEUserExists displayName
withAgent (\a -> createUser a smp xftp) withAgent (\a -> createUser a smp xftp)
user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure
user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts
storeServers user smpServers storeServers user smpServers
storeServers user xftpServers storeServers user xftpServers
setActive ActiveNone setActive ActiveNone
@ -351,6 +354,8 @@ processChatCommand = \case
storeServers user servers = storeServers user servers =
unless (null servers) $ unless (null servers) $
withStore $ \db -> overwriteProtocolServers db user servers withStore $ \db -> overwriteProtocolServers db user servers
coupleDaysAgo t = (`addUTCTime` t) . fromInteger . (+ (2 * day)) <$> randomRIO (0, day)
day = 86400
ListUsers -> CRUsersList <$> withStore' getUsersInfo ListUsers -> CRUsersList <$> withStore' getUsersInfo
APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId' user' <- privateGetUser userId'
@ -4584,12 +4589,8 @@ chatCommandP =
choice choice
[ "/mute " *> ((`ShowMessages` False) <$> chatNameP), [ "/mute " *> ((`ShowMessages` False) <$> chatNameP),
"/unmute " *> ((`ShowMessages` True) <$> chatNameP), "/unmute " *> ((`ShowMessages` True) <$> chatNameP),
"/create user" "/_create user " *> (CreateActiveUser <$> jsonP),
*> ( do "/create user " *> (CreateActiveUser <$> newUserP),
sameSmp <- (A.space *> "same_smp=" *> onOffP) <|> pure False
uProfile <- A.space *> userProfile
pure $ CreateActiveUser uProfile sameSmp
),
"/users" $> ListUsers, "/users" $> ListUsers,
"/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)), "/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)),
("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)), ("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)),
@ -4784,7 +4785,7 @@ chatCommandP =
("/welcome" <|> "/w") $> Welcome, ("/welcome" <|> "/w") $> Welcome,
"/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP), "/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP),
"/profile_image" $> UpdateProfileImage Nothing, "/profile_image" $> UpdateProfileImage Nothing,
("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> userNames), ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames),
("/profile" <|> "/p") $> ShowProfile, ("/profile" <|> "/p") $> ShowProfile,
"/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)), "/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)),
"/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)), "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)),
@ -4823,23 +4824,19 @@ chatCommandP =
refChar c = c > ' ' && c /= '#' && c /= '@' refChar c = c > ' ' && c /= '#' && c /= '@'
liveMessageP = " live=" *> onOffP <|> pure False liveMessageP = " live=" *> onOffP <|> pure False
onOffP = ("on" $> True) <|> ("off" $> False) onOffP = ("on" $> True) <|> ("off" $> False)
userNames = do profileNames = (,) <$> displayName <*> fullNameP
cName <- displayName newUserP = do
fullName <- fullNameP cName sameServers <- "same_smp=" *> onOffP <* A.space <|> pure False
pure (cName, fullName) (cName, fullName) <- profileNames
userProfile = do let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing}
(cName, fullName) <- userNames pure NewUser {profile, sameServers, pastTimestamp = False}
pure Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing}
jsonP :: J.FromJSON a => Parser a jsonP :: J.FromJSON a => Parser a
jsonP = J.eitherDecodeStrict' <$?> A.takeByteString jsonP = J.eitherDecodeStrict' <$?> A.takeByteString
groupProfile = do groupProfile = do
gName <- displayName (gName, fullName) <- profileNames
fullName <- fullNameP gName
let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just DirectMessagesGroupPreference {enable = FEOn}} let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just DirectMessagesGroupPreference {enable = FEOn}}
pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences}
fullNameP name = do fullNameP = A.space *> textP <|> pure ""
n <- (A.space *> A.takeByteString) <|> pure ""
pure $ if B.null n then name else safeDecodeUtf8 n
textP = safeDecodeUtf8 <$> A.takeByteString textP = safeDecodeUtf8 <$> A.takeByteString
pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' '))
msgTextP = jsonP <|> textP msgTextP = jsonP <|> textP

View File

@ -180,7 +180,7 @@ instance ToJSON HelpSection where
data ChatCommand data ChatCommand
= ShowActiveUser = ShowActiveUser
| CreateActiveUser Profile Bool | CreateActiveUser NewUser
| ListUsers | ListUsers
| APISetActiveUser UserId (Maybe UserPwd) | APISetActiveUser UserId (Maybe UserPwd)
| SetActiveUser UserName (Maybe UserPwd) | SetActiveUser UserName (Maybe UserPwd)

View File

@ -27,6 +27,7 @@ module Simplex.Chat.Store
chatStoreFile, chatStoreFile,
agentStoreFile, agentStoreFile,
createUserRecord, createUserRecord,
createUserRecordAt,
getUsersInfo, getUsersInfo,
getUsers, getUsers,
setActiveUser, setActiveUser,
@ -490,9 +491,11 @@ insertedRowId :: DB.Connection -> IO Int64
insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()"
createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User
createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser = createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime
createUserRecordAt :: DB.Connection -> AgentUserId -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User
createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser currentTs =
checkConstraint SEDuplicateName . liftIO $ do checkConstraint SEDuplicateName . liftIO $ do
currentTs <- getCurrentTime
when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0"
DB.execute DB.execute
db db

View File

@ -120,6 +120,13 @@ instance ToJSON User where
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
data NewUser = NewUser
{ profile :: Maybe Profile,
sameServers :: Bool,
pastTimestamp :: Bool
}
deriving (Show, Generic, FromJSON)
newtype B64UrlByteString = B64UrlByteString ByteString newtype B64UrlByteString = B64UrlByteString ByteString
deriving (Eq, Show) deriving (Eq, Show)

View File

@ -457,7 +457,7 @@ testGroupSameName =
alice <## "group #team is created" alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team" alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/g team" alice ##> "/g team"
alice <## "group #team_1 (team) is created" alice <## "group #team_1 is created"
alice <## "to add members use /a team_1 <name> or /create link #team_1" alice <## "to add members use /a team_1 <name> or /create link #team_1"
testGroupDeleteWhenInvited :: HasCallStack => FilePath -> IO () testGroupDeleteWhenInvited :: HasCallStack => FilePath -> IO ()
@ -518,7 +518,7 @@ testGroupReAddInvited =
concurrentlyN_ concurrentlyN_
[ alice <## "invitation to join the group #team sent to bob", [ alice <## "invitation to join the group #team sent to bob",
do do
bob <## "#team_1 (team): alice invites you to join the group as admin" bob <## "#team_1: alice invites you to join the group as admin"
bob <## "use /j team_1 to accept" bob <## "use /j team_1 to accept"
] ]
@ -678,7 +678,7 @@ testGroupRemoveAdd =
] ]
alice ##> "/a team bob" alice ##> "/a team bob"
alice <## "invitation to join the group #team sent to bob" alice <## "invitation to join the group #team sent to bob"
bob <## "#team_1 (team): alice invites you to join the group as admin" bob <## "#team_1: alice invites you to join the group as admin"
bob <## "use /j team_1 to accept" bob <## "use /j team_1 to accept"
bob ##> "/j team_1" bob ##> "/j team_1"
concurrentlyN_ concurrentlyN_
@ -1058,13 +1058,13 @@ testGroupLiveMessage =
alice <## "message history:" alice <## "message history:"
alice .<## ": hello 2" alice .<## ": hello 2"
alice .<## ":" alice .<## ":"
bobItemId <- lastItemId bob -- bobItemId <- lastItemId bob
bob ##> ("/_get item info " <> bobItemId) -- bob ##> ("/_get item info " <> bobItemId)
bob <##. "sent at: " -- bob <##. "sent at: "
bob <##. "received at: " -- bob <##. "received at: "
bob <## "message history:" -- bob <## "message history:"
bob .<## ": hello 2" -- bob .<## ": hello 2"
bob .<## ":" -- bob .<## ":"
testUpdateGroupProfile :: HasCallStack => FilePath -> IO () testUpdateGroupProfile :: HasCallStack => FilePath -> IO ()
testUpdateGroupProfile = testUpdateGroupProfile =

View File

@ -1147,7 +1147,7 @@ testUpdateGroupPrefs =
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")]) alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")])
threadDelay 500000 threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:" alice <## "updated group preferences:"
alice <## "Full deletion: on" alice <## "Full deletion: on"
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")]) alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")])
@ -1156,7 +1156,7 @@ testUpdateGroupPrefs =
bob <## "Full deletion: on" bob <## "Full deletion: on"
threadDelay 500000 threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")])
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}" alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:" alice <## "updated group preferences:"
alice <## "Full deletion: off" alice <## "Full deletion: off"
alice <## "Voice messages: off" alice <## "Voice messages: off"
@ -1167,7 +1167,7 @@ testUpdateGroupPrefs =
bob <## "Voice messages: off" bob <## "Voice messages: off"
threadDelay 500000 threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")])
-- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}" -- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}"
alice ##> "/set voice #team on" alice ##> "/set voice #team on"
alice <## "updated group preferences:" alice <## "updated group preferences:"
alice <## "Voice messages: on" alice <## "Voice messages: on"
@ -1178,7 +1178,7 @@ testUpdateGroupPrefs =
threadDelay 500000 threadDelay 500000
bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")])
threadDelay 500000 threadDelay 500000
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}"
-- no update -- no update
threadDelay 500000 threadDelay 500000
alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")])
@ -1343,7 +1343,7 @@ testEnableTimedMessagesGroup =
\alice bob -> do \alice bob -> do
createGroup2 "team" alice bob createGroup2 "team" alice bob
threadDelay 1000000 threadDelay 1000000
alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}" alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}"
alice <## "updated group preferences:" alice <## "updated group preferences:"
alice <## "Disappearing messages: on (1 sec)" alice <## "Disappearing messages: on (1 sec)"
bob <## "alice updated group #team:" bob <## "alice updated group #team:"