Perform safety number check pre-send in CFv2.

This commit is contained in:
Cody Henthorne 2023-07-13 13:50:22 -04:00 committed by Nicholas Tinsley
parent 82b3036b77
commit 1af50ba0f5
5 changed files with 106 additions and 22 deletions

View file

@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.ConversationS
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey.RecipientSearchKey
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.contactshare.ContactUtil
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity
@ -327,7 +328,8 @@ class ConversationFragment :
EmojiSearchFragment.Callback,
ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
ScheduleMessageDialogCallback,
ConversationBottomSheetCallback {
ConversationBottomSheetCallback,
SafetyNumberBottomSheet.Callbacks {
companion object {
private val TAG = Log.tag(ConversationFragment::class.java)
@ -553,7 +555,7 @@ class ConversationFragment :
motionEventRelay.setDrain(MotionEventRelayDrain())
viewModel.updateIdentityRecords()
viewModel.updateIdentityRecordsInBackground()
}
override fun onPause() {
@ -712,6 +714,24 @@ class ConversationFragment :
sendMessage(scheduledDate = scheduledDate)
}
override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List<RecipientSearchKey>) {
Log.d(TAG, "onSendAnywayAfterSafetyNumberChange")
viewModel
.updateIdentityRecords()
.subscribeBy(
onError = { t -> Log.w(TAG, "Error sending", t) },
onComplete = { sendMessage() }
)
.addTo(disposables)
}
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() {
Log.d(TAG, "onMessageResentAfterSafetyNumberChange")
viewModel.updateIdentityRecordsInBackground()
}
override fun onCanceled() = Unit
//endregion
private fun observeConversationThread() {
@ -859,6 +879,7 @@ class ConversationFragment :
viewModel
.identityRecords
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy { presentIdentityRecordsState(it) }
.addTo(disposables)
@ -1574,7 +1595,8 @@ class ConversationFragment :
bodyRanges = null,
messageToEdit = null,
quote = null,
linkPreviews = emptyList()
linkPreviews = emptyList(),
bypassPreSendSafetyNumberCheck = true
)
}
@ -1590,6 +1612,7 @@ class ConversationFragment :
clearCompose: Boolean = true,
linkPreviews: List<LinkPreview> = linkPreviewViewModel.onSend(),
preUploadResults: List<MessageSender.PreUploadResult> = emptyList(),
bypassPreSendSafetyNumberCheck: Boolean = false,
afterSendComplete: () -> Unit = {}
) {
if (scheduledDate != -1L && ReenableScheduledMessagesDialogFragment.showIfNeeded(requireContext(), childFragmentManager, null, scheduledDate)) {
@ -1622,25 +1645,26 @@ class ConversationFragment :
bodyRanges = bodyRanges,
contacts = contacts,
linkPreviews = linkPreviews,
preUploadResults = preUploadResults
preUploadResults = preUploadResults,
bypassPreSendSafetyNumberCheck = bypassPreSendSafetyNumberCheck
)
disposables += send
.doOnSubscribe {
if (clearCompose) {
composeText.setText("")
inputPanel.clearQuote()
}
}
.subscribeBy(
onError = { t ->
Log.w(TAG, "Error sending", t)
when (t) {
is InvalidMessageException -> toast(R.string.ConversationActivity_message_is_empty_exclamation)
is RecipientFormattingException -> toast(R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, Toast.LENGTH_LONG)
is RecentSafetyNumberChangeException -> handleRecentSafetyNumberChange(t.changedRecords)
}
},
onComplete = {
if (clearCompose) {
composeText.setText("")
inputPanel.clearQuote()
}
onSendComplete()
afterSendComplete()
}
@ -1667,6 +1691,13 @@ class ConversationFragment :
inputPanel.exitEditMessageMode()
}
private fun handleRecentSafetyNumberChange(changedRecords: List<IdentityRecord>) {
val recipient = viewModel.recipientSnapshot ?: return
SafetyNumberBottomSheet
.forIdentityRecordsAndDestination(changedRecords, RecipientSearchKey(recipient.getId(), false))
.show(childFragmentManager)
}
private fun toast(@StringRes toastTextId: Int, toastDuration: Int = Toast.LENGTH_SHORT) {
ThreadUtil.runOnMain {
if (context != null) {
@ -3617,7 +3648,7 @@ class ConversationFragment :
@Subscribe(threadMode = ThreadMode.POSTING)
fun onIdentityRecordUpdate(event: IdentityRecord?) {
viewModel.updateIdentityRecords()
viewModel.updateIdentityRecordsInBackground()
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)

View file

@ -203,7 +203,8 @@ class ConversationRepository(
bodyRanges: BodyRangeList?,
contacts: List<Contact>,
linkPreviews: List<LinkPreview>,
preUploadResults: List<PreUploadResult>
preUploadResults: List<PreUploadResult>,
identityRecordsState: IdentityRecordsState?
): Completable {
val sendCompletable = Completable.create { emitter ->
if (body.isEmpty() && slideDeck?.containsMediaSlide() != true && preUploadResults.isEmpty()) {
@ -216,6 +217,11 @@ class ConversationRepository(
return@create
}
if (identityRecordsState != null && identityRecordsState.hasRecentSafetyNumberChange()) {
emitter.onError(RecentSafetyNumberChangeException(identityRecordsState.getRecentSafetyNumberChangeRecords()))
return@create
}
val splitMessage: MessageUtil.SplitResult = MessageUtil.getSplitMessage(
applicationContext,
body,
@ -369,7 +375,7 @@ class ConversationRepository(
records.isVerified &&
!recipient.isSelf
IdentityRecordsState(isVerified, records, isGroup = groupRecord != null)
IdentityRecordsState(recipient, groupRecord, isVerified, records, isGroup = groupRecord != null)
}.subscribeOn(Schedulers.io())
}

View file

@ -126,7 +126,8 @@ class ConversationViewModel(
val reminder: Observable<Optional<Reminder>>
private val refreshIdentityRecords: Subject<Unit> = PublishSubject.create()
val identityRecords: Observable<IdentityRecordsState>
private val identityRecordsStore: RxStore<IdentityRecordsState> = RxStore(IdentityRecordsState())
val identityRecords: Observable<IdentityRecordsState> = identityRecordsStore.stateFlowable.toObservable()
private val _searchQuery = BehaviorSubject.createDefault("")
val searchQuery: Observable<String> = _searchQuery
@ -218,13 +219,17 @@ class ConversationViewModel(
.flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) }
.observeOn(AndroidSchedulers.mainThread())
identityRecords = Observable.combineLatest(
Observable.combineLatest(
refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()),
recipient,
recipientRepository.groupRecord
) { _, r, g -> Pair(r, g) }
.subscribeOn(Schedulers.io())
.flatMapSingle { (r, g) -> repository.getIdentityRecords(r, g.orNull()) }
.distinctUntilChanged()
.subscribeBy { newState ->
identityRecordsStore.update { newState }
}
.addTo(disposables)
}
fun setSearchQuery(query: String?) {
@ -327,7 +332,8 @@ class ConversationViewModel(
bodyRanges: BodyRangeList?,
contacts: List<Contact>,
linkPreviews: List<LinkPreview>,
preUploadResults: List<MessageSender.PreUploadResult>
preUploadResults: List<MessageSender.PreUploadResult>,
bypassPreSendSafetyNumberCheck: Boolean
): Completable {
return repository.sendMessage(
threadId = threadId,
@ -342,7 +348,8 @@ class ConversationViewModel(
bodyRanges = bodyRanges,
contacts = contacts,
linkPreviews = linkPreviews,
preUploadResults = preUploadResults
preUploadResults = preUploadResults,
identityRecordsState = if (bypassPreSendSafetyNumberCheck) null else identityRecordsStore.state
).observeOn(AndroidSchedulers.mainThread())
}
@ -353,10 +360,24 @@ class ConversationViewModel(
}
}
fun updateIdentityRecords() {
fun updateIdentityRecordsInBackground() {
refreshIdentityRecords.onNext(Unit)
}
fun updateIdentityRecords(): Completable {
val state: IdentityRecordsState = identityRecordsStore.state
if (state.recipient == null) {
return Completable.error(IllegalStateException("No recipient in records store"))
}
return repository.getIdentityRecords(state.recipient, state.group)
.doOnSuccess { newState ->
identityRecordsStore.update { newState }
}
.flatMapCompletable { Completable.complete() }
.observeOn(AndroidSchedulers.mainThread())
}
fun getTemporaryViewOnceUri(mmsMessageRecord: MmsMessageRecord): Maybe<Uri> {
return repository.getTemporaryViewOnceUri(mmsMessageRecord).observeOn(AndroidSchedulers.mainThread())
}

View file

@ -6,14 +6,27 @@
package org.thoughtcrime.securesms.conversation.v2
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Current state for all participants identity keys in a conversation excluding self.
*/
data class IdentityRecordsState(
val isVerified: Boolean,
val identityRecords: IdentityRecordList,
val isGroup: Boolean
val recipient: Recipient? = null,
val group: GroupRecord? = null,
val isVerified: Boolean = false,
val identityRecords: IdentityRecordList = IdentityRecordList(emptyList()),
val isGroup: Boolean = false
) {
val isUnverified: Boolean = identityRecords.isUnverified
fun hasRecentSafetyNumberChange(): Boolean {
return identityRecords.isUnverified(true) || identityRecords.isUntrusted(true)
}
fun getRecentSafetyNumberChangeRecords(): List<IdentityRecord> {
return identityRecords.unverifiedRecords + identityRecords.untrustedRecords
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import org.thoughtcrime.securesms.database.model.IdentityRecord
/**
* Emitted when safety numbers changed recently before a send attempt.
*/
class RecentSafetyNumberChangeException(val changedRecords: List<IdentityRecord>) : Exception()