Merge branch 'master' into master-ios

This commit is contained in:
Evgeny Poberezkin
2023-03-29 20:46:34 +01:00
7 changed files with 120 additions and 96 deletions
+5 -5
View File
@@ -162,16 +162,16 @@ func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User {
try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd))
}
func apiUnhideUser(_ userId: Int64, viewPwd: String?) async throws -> User {
func apiUnhideUser(_ userId: Int64, viewPwd: String) async throws -> User {
try await setUserPrivacy_(.apiUnhideUser(userId: userId, viewPwd: viewPwd))
}
func apiMuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
try await setUserPrivacy_(.apiMuteUser(userId: userId, viewPwd: viewPwd))
func apiMuteUser(_ userId: Int64) async throws -> User {
try await setUserPrivacy_(.apiMuteUser(userId: userId))
}
func apiUnmuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
try await setUserPrivacy_(.apiUnmuteUser(userId: userId, viewPwd: viewPwd))
func apiUnmuteUser(_ userId: Int64) async throws -> User {
try await setUserPrivacy_(.apiUnmuteUser(userId: userId))
}
func setUserPrivacy_(_ cmd: ChatCommand) async throws -> User {
@@ -44,10 +44,12 @@ struct UserProfilesView: View {
private enum UserProfileAction: Identifiable {
case deleteUser(user: User, delSMPQueues: Bool)
case unhideUser(user: User)
var id: String {
switch self {
case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
case let .unhideUser(user): return "unhideUser \(user.userId)"
}
}
}
@@ -207,43 +209,58 @@ struct UserProfilesView: View {
@ViewBuilder private func profileActionView(_ action: UserProfileAction) -> some View {
let passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces)
switch action {
case let .deleteUser(user, delSMPQueues):
let actionEnabled = actionPassword != "" && passwordValid && correctPassword(user, actionPassword)
List {
Text("Delete user")
.font(.title)
.bold()
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowBackground(Color.clear)
Section() {
ProfilePreview(profileOf: user).padding(.leading, -8)
}
let passwordField = PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
let actionEnabled: (User) -> Bool = { user in actionPassword != "" && passwordValid && correctPassword(user, actionPassword) }
List {
switch action {
case let .deleteUser(user, delSMPQueues):
actionHeader("Delete user", user)
Section {
PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
passwordField
settingsRow("trash") {
Button("Delete user", role: .destructive) {
profileAction = nil
Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) }
}
.disabled(!actionEnabled)
.disabled(!actionEnabled(user))
}
} footer: {
if actionEnabled {
if actionEnabled(user) {
Text("All chats and messages will be deleted - this cannot be undone!")
.font(.callout)
}
}
case let .unhideUser(user):
actionHeader("Unhide user", user)
Section {
passwordField
settingsRow("lock.open") {
Button("Unhide user") {
profileAction = nil
setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) }
}
.disabled(!actionEnabled(user))
}
}
}
}
}
@ViewBuilder func actionHeader(_ title: LocalizedStringKey, _ user: User) -> some View {
Text(title)
.font(.title)
.bold()
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowBackground(Color.clear)
Section() {
ProfilePreview(profileOf: user).padding(.leading, -8)
}
}
private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
Button(title, role: .destructive) {
if let user = userToDelete {
if user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword) {
if passwordEntryRequired(user) {
profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues)
} else {
alert = .deleteUser(user: user, delSMPQueues: delSMPQueues)
@@ -252,6 +269,10 @@ struct UserProfilesView: View {
}
}
private func passwordEntryRequired(_ user: User) -> Bool {
user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword)
}
private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async {
do {
if user.activeUser {
@@ -307,7 +328,11 @@ struct UserProfilesView: View {
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if user.hidden {
Button("Unhide") {
setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: userViewPassword(user)) }
if passwordEntryRequired(user) {
profileAction = .unhideUser(user: user)
} else {
setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: searchTextOrPassword) }
}
}
.tint(.green)
} else {
@@ -321,12 +346,12 @@ struct UserProfilesView: View {
if user.showNtfs {
Button("Mute") {
setUserPrivacy(user, successAlert: showMuteProfileAlert ? .muteProfileAlert : nil) {
try await apiMuteUser(user.userId, viewPwd: userViewPassword(user))
try await apiMuteUser(user.userId)
}
}
} else {
Button("Unmute") {
setUserPrivacy(user) { try await apiUnmuteUser(user.userId, viewPwd: userViewPassword(user)) }
setUserPrivacy(user) { try await apiUnmuteUser(user.userId) }
}
}
}
+7 -12
View File
@@ -18,9 +18,9 @@ public enum ChatCommand {
case listUsers
case apiSetActiveUser(userId: Int64, viewPwd: String?)
case apiHideUser(userId: Int64, viewPwd: String)
case apiUnhideUser(userId: Int64, viewPwd: String?)
case apiMuteUser(userId: Int64, viewPwd: String?)
case apiUnmuteUser(userId: Int64, viewPwd: String?)
case apiUnhideUser(userId: Int64, viewPwd: String)
case apiMuteUser(userId: Int64)
case apiUnmuteUser(userId: Int64)
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
case startChat(subscribe: Bool, expire: Bool)
case apiStopChat
@@ -111,9 +111,9 @@ public enum ChatCommand {
case .listUsers: return "/users"
case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))"
case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))"
case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId)\(maybePwd(viewPwd))"
case let .apiMuteUser(userId, viewPwd): return "/_mute user \(userId)\(maybePwd(viewPwd))"
case let .apiUnmuteUser(userId, viewPwd): return "/_unmute user \(userId)\(maybePwd(viewPwd))"
case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))"
case let .apiMuteUser(userId): return "/_mute user \(userId)"
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
case let .startChat(subscribe, expire): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire))"
case .apiStopChat: return "/_stop"
@@ -332,10 +332,6 @@ public enum ChatCommand {
return .apiHideUser(userId: userId, viewPwd: obfuscate(viewPwd))
case let .apiUnhideUser(userId, viewPwd):
return .apiUnhideUser(userId: userId, viewPwd: obfuscate(viewPwd))
case let .apiMuteUser(userId, viewPwd):
return .apiMuteUser(userId: userId, viewPwd: obfuscate(viewPwd))
case let .apiUnmuteUser(userId, viewPwd):
return .apiUnmuteUser(userId: userId, viewPwd: obfuscate(viewPwd))
case let .apiDeleteUser(userId, delSMPQueues, viewPwd):
return .apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: obfuscate(viewPwd))
default: return self
@@ -349,9 +345,8 @@ public enum ChatCommand {
private func obfuscate(_ s: String?) -> String? {
if let s = s {
return obfuscate(s)
} else {
return nil
}
return nil
}
private func onOff(_ b: Bool) -> String {
+30 -28
View File
@@ -335,7 +335,7 @@ processChatCommand = \case
tryError (withStore (`getUserIdByName` uName)) >>= \case
Left _ -> throwChatError CEUserUnknown
Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_
APIHideUser userId' (UserPwd viewPwd) -> withUser $ \_ -> do
APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEUserAlreadyHidden userId'
@@ -344,34 +344,26 @@ processChatCommand = \case
users <- withStore' getUsers
unless (length (filter (isNothing . viewPwdHash) users) > 1) $ throwChatError $ CECantHideLastUser userId'
viewPwdHash' <- hashPassword
setUserPrivacy user' {viewPwdHash = viewPwdHash', showNtfs = False}
setUserPrivacy user user' {viewPwdHash = viewPwdHash', showNtfs = False}
where
hashPassword = do
salt <- drgRandomBytes 16
let hash = B64UrlByteString $ C.sha512Hash $ encodeUtf8 viewPwd <> salt
pure $ Just UserPwdHash {hash, salt = B64UrlByteString salt}
APIUnhideUser userId' viewPwd_ -> withUser $ \user -> do
APIUnhideUser userId' viewPwd@(UserPwd pwd) -> withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Nothing -> throwChatError $ CEUserNotHidden userId'
_ -> do
validateUserPassword user user' viewPwd_
setUserPrivacy user' {viewPwdHash = Nothing, showNtfs = True}
APIMuteUser userId' viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
setUserPrivacy user' {showNtfs = False}
APIUnmuteUser userId' viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CECantUnmuteHiddenUser userId'
_ -> do
validateUserPassword user user' viewPwd_
setUserPrivacy user' {showNtfs = True}
when (T.null pwd) $ throwChatError $ CEEmptyUserPassword userId'
validateUserPassword user user' $ Just viewPwd
setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True}
APIMuteUser userId' -> setUserNotifications userId' False
APIUnmuteUser userId' -> setUserNotifications userId' True
HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIHideUser userId viewPwd
UnhideUser -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId Nothing
MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId Nothing
UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId Nothing
UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId viewPwd
MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId
UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId
APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do
user' <- privateGetUser userId'
validateUserPassword user user' viewPwd_
@@ -1706,11 +1698,21 @@ processChatCommand = \case
validPassword :: Text -> UserPwdHash -> Bool
validPassword pwd UserPwdHash {hash = B64UrlByteString hash, salt = B64UrlByteString salt} =
hash == C.sha512Hash (encodeUtf8 pwd <> salt)
setUserPrivacy :: User -> m ChatResponse
setUserPrivacy user = do
asks currentUser >>= atomically . (`writeTVar` Just user)
withStore' (`updateUserPrivacy` user)
pure $ CRUserPrivacy user
setUserNotifications :: UserId -> Bool -> m ChatResponse
setUserNotifications userId' showNtfs = withUser $ \user -> do
user' <- privateGetUser userId'
case viewPwdHash user' of
Just _ -> throwChatError $ CEHiddenUserAlwaysMuted userId'
_ -> setUserPrivacy user user' {showNtfs}
setUserPrivacy :: User -> User -> m ChatResponse
setUserPrivacy user@User {userId} user'@User {userId = userId'}
| userId == userId' = do
asks currentUser >>= atomically . (`writeTVar` Just user')
withStore' (`updateUserPrivacy` user')
pure $ CRUserPrivacy {user = user', updatedUser = user'}
| otherwise = do
withStore' (`updateUserPrivacy` user')
pure $ CRUserPrivacy {user, updatedUser = user'}
checkDeleteChatUser :: User -> m ()
checkDeleteChatUser user@User {userId} = do
when (activeUser user) $ throwChatError (CECantDeleteActiveUser userId)
@@ -4319,11 +4321,11 @@ chatCommandP =
"/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)),
("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)),
"/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP),
"/_unhide user " *> (APIUnhideUser <$> A.decimal <*> optional (A.space *> jsonP)),
"/_mute user " *> (APIMuteUser <$> A.decimal <*> optional (A.space *> jsonP)),
"/_unmute user " *> (APIUnmuteUser <$> A.decimal <*> optional (A.space *> jsonP)),
"/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP),
"/_mute user " *> (APIMuteUser <$> A.decimal),
"/_unmute user " *> (APIUnmuteUser <$> A.decimal),
"/hide user " *> (HideUser <$> pwdP),
"/unhide user" $> UnhideUser,
"/unhide user " *> (UnhideUser <$> pwdP),
"/mute user" $> MuteUser,
"/unmute user" $> UnmuteUser,
"/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)),
+6 -6
View File
@@ -190,11 +190,11 @@ data ChatCommand
| APISetActiveUser UserId (Maybe UserPwd)
| SetActiveUser UserName (Maybe UserPwd)
| APIHideUser UserId UserPwd
| APIUnhideUser UserId (Maybe UserPwd)
| APIMuteUser UserId (Maybe UserPwd)
| APIUnmuteUser UserId (Maybe UserPwd)
| APIUnhideUser UserId UserPwd
| APIMuteUser UserId
| APIUnmuteUser UserId
| HideUser UserPwd
| UnhideUser
| UnhideUser UserPwd
| MuteUser
| UnmuteUser
| APIDeleteUser UserId Bool (Maybe UserPwd)
@@ -421,7 +421,7 @@ data ChatResponse
| CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus
| CRUserProfile {user :: User, profile :: Profile}
| CRUserProfileNoChange {user :: User}
| CRUserPrivacy {user :: User}
| CRUserPrivacy {user :: User, updatedUser :: User}
| CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]}
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation}
| CRSentConfirmation {user :: User}
@@ -741,7 +741,7 @@ data ChatErrorType
| CECantDeleteActiveUser {userId :: UserId}
| CECantDeleteLastUser {userId :: UserId}
| CECantHideLastUser {userId :: UserId}
| CECantUnmuteHiddenUser {userId :: UserId}
| CEHiddenUserAlwaysMuted {userId :: UserId}
| CEEmptyUserPassword {userId :: UserId}
| CEUserAlreadyHidden {userId :: UserId}
| CEUserNotHidden {userId :: UserId}
+8 -7
View File
@@ -116,7 +116,7 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
CRFileTransferStatus u ftStatus -> ttyUser u $ viewFileTransferStatus ftStatus
CRUserProfile u p -> ttyUser u $ viewUserProfile p
CRUserProfileNoChange u -> ttyUser u ["user profile did not change"]
CRUserPrivacy u -> ttyUserPrefix u $ viewUserPrivacy u
CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u'
CRVersionInfo info _ _ -> viewVersionInfo logLevel info
CRInvitation u cReq -> ttyUser u $ viewConnReqInvitation cReq
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
@@ -740,10 +740,11 @@ viewUserProfile Profile {displayName, fullName} =
"(the updated profile will be sent to all your contacts)"
]
viewUserPrivacy :: User -> [StyledString]
viewUserPrivacy User {showNtfs, viewPwdHash} =
[ "user messages are " <> if showNtfs then "shown" else "hidden (use /tail to view)",
"user profile is " <> if isJust viewPwdHash then "hidden" else "visible"
viewUserPrivacy :: User -> User -> [StyledString]
viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', showNtfs, viewPwdHash} =
[ (if userId == userId' then "current " else "") <> "user " <> plain n' <> ":",
"messages are " <> if showNtfs then "shown" else "hidden (use /tail to view)",
"profile is " <> if isJust viewPwdHash then "hidden" else "visible"
]
-- TODO make more generic messages or split
@@ -1241,8 +1242,8 @@ viewChatError logLevel = \case
CECantDeleteActiveUser _ -> ["cannot delete active user"]
CECantDeleteLastUser _ -> ["cannot delete last user"]
CECantHideLastUser _ -> ["cannot hide the only not hidden user"]
CECantUnmuteHiddenUser _ -> ["cannot unmute hidden user"]
CEEmptyUserPassword _ -> ["cannot set empty password"]
CEHiddenUserAlwaysMuted _ -> ["hidden user always muted when inactive"]
CEEmptyUserPassword _ -> ["user password is required"]
CEUserAlreadyHidden _ -> ["user is already hidden"]
CEUserNotHidden _ -> ["user is not hidden"]
CEChatNotStarted -> ["error: chat not started"]
+18 -17
View File
@@ -1518,7 +1518,7 @@ testUserPrivacy =
alice <# "bob> hey"
-- hide user profile
alice ##> "/hide user my_password"
userHidden alice
userHidden alice "current "
-- shows messages when active
bob #> "@alisa hello again"
alice <# "bob> hello again"
@@ -1555,13 +1555,15 @@ testUserPrivacy =
]
-- change profile password
alice ##> "/unmute user"
alice <## "cannot unmute hidden user"
alice <## "hidden user always muted when inactive"
alice ##> "/hide user password"
alice <## "user is already hidden"
alice ##> "/unhide user"
userVisible alice
alice ##> "/unhide user wrong_password"
alice <## "user does not exist or incorrect password"
alice ##> "/unhide user my_password"
userVisible alice "current "
alice ##> "/hide user new_password"
userHidden alice
userHidden alice "current "
alice ##> "/_delete user 1 del_smp=on"
alice <## "cannot delete last user"
alice ##> "/_hide user 1 \"password\""
@@ -1570,18 +1572,15 @@ testUserPrivacy =
showActiveUser alice "alice (Alice)"
-- change profile privacy for inactive user via API requires correct password
alice ##> "/_unmute user 2"
alice <## "cannot unmute hidden user"
alice <## "hidden user always muted when inactive"
alice ##> "/_hide user 2 \"password\""
alice <## "user is already hidden"
alice ##> "/_unhide user 2"
alice <## "user does not exist or incorrect password"
alice ##> "/_unhide user 2 \"wrong_password\""
alice <## "user does not exist or incorrect password"
alice ##> "/_unhide user 2 \"new_password\""
userVisible alice
userVisible alice ""
alice ##> "/_hide user 2 \"another_password\""
userHidden alice
-- check new password
userHidden alice ""
alice ##> "/user alisa another_password"
showActiveUser alice "alisa"
alice ##> "/user alice"
@@ -1594,12 +1593,14 @@ testUserPrivacy =
alice <## "ok"
alice <## "completed deleting user"
where
userHidden alice = do
alice <## "user messages are hidden (use /tail to view)"
alice <## "user profile is hidden"
userVisible alice = do
alice <## "user messages are shown"
alice <## "user profile is visible"
userHidden alice current = do
alice <## (current <> "user alisa:")
alice <## "messages are hidden (use /tail to view)"
alice <## "profile is hidden"
userVisible alice current = do
alice <## (current <> "user alisa:")
alice <## "messages are shown"
alice <## "profile is visible"
testSetChatItemTTL :: HasCallStack => FilePath -> IO ()
testSetChatItemTTL =