Refactor username state to use Username models.

This commit is contained in:
Greyson Parrelli
2024-01-10 14:57:14 -05:00
parent b8dea25aef
commit 50369890f7
10 changed files with 161 additions and 138 deletions

View File

@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.util.Usernames
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import java.util.concurrent.TimeUnit
@@ -96,7 +97,7 @@ class UsernameEditFragmentTest {
fun testNicknameUpdateHappyPath() {
val nickname = "Spiderman"
val discriminator = "4578"
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
val username = "$nickname${Usernames.DELIMITER}$discriminator"
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/username/reserved") {

View File

@@ -104,7 +104,7 @@ class EditProfileViewModel extends ViewModel {
}
public Single<UsernameRepository.UsernameDeleteResult> deleteUsername() {
return UsernameRepository.deleteUsername().observeOn(AndroidSchedulers.mainThread());
return UsernameRepository.deleteUsernameAndLink().observeOn(AndroidSchedulers.mainThread());
}
public boolean shouldShowUsername() {

View File

@@ -101,10 +101,6 @@ public class UsernameEditFragment extends LoggingFragment {
binding.usernameDoneButton.setOnClickListener(v -> viewModel.onUsernameSubmitted());
binding.usernameSkipButton.setOnClickListener(v -> viewModel.onUsernameSkipped());
String username = SignalStore.account().getUsername();
UsernameState usernameState = username != null ? new UsernameState.Set(username) : UsernameState.NoUsername.INSTANCE;
binding.usernameText.setText(usernameState.getNickname());
binding.usernameText.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(@NonNull String text) {
@@ -114,7 +110,6 @@ public class UsernameEditFragment extends LoggingFragment {
}
});
binding.discriminatorText.setText(usernameState.getDiscriminator());
binding.discriminatorText.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(@NonNull String text) {
@@ -156,9 +151,9 @@ public class UsernameEditFragment extends LoggingFragment {
private void onUiStateChanged(@NonNull UsernameEditViewModel.State state) {
TextInputLayout usernameInputWrapper = binding.usernameTextWrapper;
presentProgressState(state.username);
presentProgressState(state.usernameState);
presentButtonState(state.buttonState);
presentSummary(state.username);
presentSummary(state.usernameState);
binding.root.setLayoutTransition(ANIMATED_LAYOUT);
@@ -227,7 +222,7 @@ public class UsernameEditFragment extends LoggingFragment {
private void presentSummary(@NonNull UsernameState usernameState) {
if (usernameState.getUsername() != null) {
binding.summary.setText(usernameState.getUsername());
binding.summary.setText(usernameState.getUsername().getUsername());
binding.summary.setAlpha(1f);
} else if (usernameState instanceof UsernameState.Loading) {
binding.summary.setAlpha(0.5f);

View File

@@ -12,6 +12,8 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.Result
import org.signal.core.util.logging.Log
import org.signal.libsignal.usernames.Username
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameDeleteResult
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSetResult
@@ -19,6 +21,7 @@ import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason
import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.util.Usernames
import java.util.concurrent.TimeUnit
/**
@@ -40,15 +43,15 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
defaultValue = State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE,
username = SignalStore.account().username?.let { UsernameState.Set(it) } ?: UsernameState.NoUsername
usernameState = SignalStore.account().username?.let { UsernameState.Set(Username(it)) } ?: UsernameState.NoUsername
),
scheduler = Schedulers.computation()
)
private val stateMachineStore = RxStore<UsernameEditStateMachine.State>(
defaultValue = UsernameEditStateMachine.NoUserEntry(
nickname = SignalStore.account().username?.split(UsernameState.DELIMITER)?.first() ?: "",
discriminator = SignalStore.account().username?.split(UsernameState.DELIMITER)?.last() ?: "",
nickname = SignalStore.account().username?.split(Usernames.DELIMITER)?.first() ?: "",
discriminator = SignalStore.account().username?.split(Usernames.DELIMITER)?.last() ?: "",
stateModifier = UsernameEditStateMachine.StateModifier.SYSTEM
),
scheduler = Schedulers.computation()
@@ -76,7 +79,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
return@update State(
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
usernameStatus = UsernameStatus.NONE,
username = UsernameState.NoUsername
usernameState = UsernameState.NoUsername
)
}
@@ -88,13 +91,13 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE,
username = state.username
usernameState = state.usernameState
)
} else {
State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE,
username = state.username
usernameState = state.usernameState
)
}
}
@@ -110,14 +113,14 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
return@update State(
buttonState = if (isInRegistration) ButtonState.SUBMIT_DISABLED else ButtonState.DELETE,
usernameStatus = UsernameStatus.NONE,
username = UsernameState.NoUsername
usernameState = UsernameState.NoUsername
)
}
State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = UsernameStatus.NONE,
username = state.username
usernameState = state.usernameState
)
}
@@ -132,61 +135,62 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
}
fun onUsernameSubmitted() {
val state = stateMachineStore.state
val editState = stateMachineStore.state
val usernameState = uiState.state.usernameState
val isCaseChange = isCaseChange(editState)
if (isCaseChange(state)) {
handleUserConfirmation(UsernameRepository::updateUsername)
} else {
handleUserConfirmation(UsernameRepository::confirmUsername)
}
}
private inline fun <reified T : UsernameState> handleUserConfirmation(
repositoryWorkerMethod: (T) -> Single<UsernameSetResult>
) {
val usernameState = uiState.state.username
if (usernameState !is T) {
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
if (usernameState !is UsernameState.Reserved && usernameState !is UsernameState.CaseChange) {
Log.w(TAG, "Username was submitted, current state is invalid! State: ${usernameState.javaClass.simpleName}")
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) }
return
}
if (usernameState.requireUsername() == SignalStore.account().username) {
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
if (usernameState.requireUsername().username == SignalStore.account().username) {
Log.d(TAG, "Username was submitted, but was identical to the current username. Ignoring.")
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = UsernameStatus.NONE) }
return
}
val invalidReason = checkUsername(usernameState.getNickname())
val invalidReason: InvalidReason? = checkUsername(usernameState.getNickname())
if (invalidReason != null) {
uiState.update { State(ButtonState.SUBMIT_DISABLED, mapNicknameError(invalidReason), it.username) }
Log.w(TAG, "Username was submitted, but did not pass validity checks. Reason: $invalidReason")
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = mapNicknameError(invalidReason)) }
return
}
uiState.update { State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, it.username) }
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_LOADING, usernameStatus = UsernameStatus.NONE) }
disposables += repositoryWorkerMethod(usernameState).subscribe { result: UsernameSetResult ->
val usernameConfirmOperation: Single<UsernameSetResult> = if (isCaseChange) {
UsernameRepository.updateUsernameDisplayForCurrentLink(usernameState.requireUsername())
} else {
val reservation = usernameState as UsernameState.Reserved
UsernameRepository.confirmUsernameAndCreateNewLink(reservation.requireUsername())
}
disposables += usernameConfirmOperation.subscribe { result: UsernameSetResult ->
val nickname = usernameState.getNickname()
when (result) {
UsernameSetResult.SUCCESS -> {
SignalStore.uiHints().markHasSetOrSkippedUsernameCreation()
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.username) }
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, it.usernameState) }
events.onNext(Event.SUBMIT_SUCCESS)
}
UsernameSetResult.USERNAME_INVALID -> {
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, it.username) }
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.INVALID_GENERIC, it.usernameState) }
events.onNext(Event.SUBMIT_FAIL_INVALID)
nickname?.let { onNicknameUpdated(it) }
}
UsernameSetResult.CANDIDATE_GENERATION_ERROR, UsernameSetResult.USERNAME_UNAVAILABLE -> {
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, it.username) }
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, it.usernameState) }
events.onNext(Event.SUBMIT_FAIL_TAKEN)
nickname?.let { onNicknameUpdated(it) }
}
UsernameSetResult.NETWORK_ERROR -> {
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, it.username) }
uiState.update { State(ButtonState.SUBMIT, UsernameStatus.NONE, it.usernameState) }
events.onNext(Event.NETWORK_FAILURE)
}
}
@@ -194,17 +198,17 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
}
fun onUsernameDeleted() {
uiState.update { state: State -> State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.username) }
uiState.update { state: State -> State(ButtonState.DELETE_LOADING, UsernameStatus.NONE, state.usernameState) }
disposables += UsernameRepository.deleteUsername().subscribe { result: UsernameDeleteResult ->
disposables += UsernameRepository.deleteUsernameAndLink().subscribe { result: UsernameDeleteResult ->
when (result) {
UsernameDeleteResult.SUCCESS -> {
uiState.update { state: State -> State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.username) }
uiState.update { state: State -> State(ButtonState.DELETE_DISABLED, UsernameStatus.NONE, state.usernameState) }
events.onNext(Event.DELETE_SUCCESS)
}
UsernameDeleteResult.NETWORK_ERROR -> {
uiState.update { state: State -> State(ButtonState.DELETE, UsernameStatus.NONE, state.username) }
uiState.update { state: State -> State(ButtonState.DELETE, UsernameStatus.NONE, state.usernameState) }
events.onNext(Event.NETWORK_FAILURE)
}
}
@@ -225,7 +229,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
}
val newLower = state.nickname.lowercase()
val oldLower = SignalStore.account().username?.split(UsernameState.DELIMITER)?.firstOrNull()?.lowercase()
val oldLower = SignalStore.account().username?.split(Usernames.DELIMITER)?.firstOrNull()?.lowercase()
return newLower == oldLower
}
@@ -241,13 +245,24 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
return
}
val invalidReason: InvalidReason? = checkUsername(nickname)
if (invalidReason != null) {
uiState.update { uiState ->
uiState.copy(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = mapNicknameError(invalidReason)
)
}
return
}
if (isCaseChange(state)) {
val discriminator = SignalStore.account().username?.split(UsernameState.DELIMITER)?.lastOrNull() ?: error("Unexpected case change, no discriminator!")
val discriminator = SignalStore.account().username?.split(Usernames.DELIMITER)?.lastOrNull() ?: error("Unexpected case change, no discriminator!")
uiState.update {
State(
buttonState = ButtonState.SUBMIT,
usernameStatus = UsernameStatus.NONE,
username = UsernameState.CaseChange("${state.nickname}${UsernameState.DELIMITER}$discriminator")
usernameState = UsernameState.CaseChange(Username("${state.nickname}${Usernames.DELIMITER}$discriminator"))
)
}
@@ -255,18 +270,6 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
return
}
val invalidReason: InvalidReason? = checkUsername(nickname)
if (invalidReason != null) {
uiState.update { uiState ->
State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = mapNicknameError(invalidReason),
username = uiState.username
)
}
return
}
uiState.update { State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading) }
val isDiscriminatorSetByUser = state is UsernameEditStateMachine.UserEnteredDiscriminator || state is UsernameEditStateMachine.UserEnteredNicknameAndDiscriminator
@@ -282,7 +285,7 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
State(
buttonState = ButtonState.SUBMIT_DISABLED,
usernameStatus = mapDiscriminatorError(discriminatorInvalidReason),
username = s.username
usernameState = s.usernameState
)
}
return
@@ -333,10 +336,10 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
}
}
class State(
data class State(
@JvmField val buttonState: ButtonState,
@JvmField val usernameStatus: UsernameStatus,
@JvmField val username: UsernameState
@JvmField val usernameState: UsernameState
)
enum class UsernameStatus {
@@ -368,6 +371,8 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
}
companion object {
private val TAG = Log.tag(UsernameEditViewModel::class.java)
private const val NICKNAME_PUBLISHER_DEBOUNCE_TIMEOUT_MILLIS: Long = 1000
private fun mapNicknameError(invalidReason: InvalidReason): UsernameStatus {
@@ -376,7 +381,6 @@ internal class UsernameEditViewModel private constructor(private val isInRegistr
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS
else -> UsernameStatus.INVALID_GENERIC
}
}

View File

@@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotAssocia
import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException
import org.whispersystems.signalservice.api.util.Usernames
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.IOException
@@ -89,16 +90,7 @@ object UsernameRepository {
private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager()
/**
* Given a username, update the username link, given that all was changed was the casing of the nickname.
*/
fun updateUsername(caseChange: UsernameState.CaseChange): Single<UsernameSetResult> {
return Single
.fromCallable { updateUsernameInternal(caseChange) }
.subscribeOn(Schedulers.io())
}
/**
* Given a nickname, this will temporarily reserve a matching discriminator that can later be confirmed via [confirmUsername].
* Given a nickname, this will temporarily reserve a matching discriminator that can later be confirmed via [confirmUsernameAndCreateNewLink].
*/
fun reserveUsername(nickname: String, discriminator: String?): Single<Result<UsernameState.Reserved, UsernameSetResult>> {
return Single
@@ -107,19 +99,32 @@ object UsernameRepository {
}
/**
* Given a reserved username (obtained via [reserveUsername]), this will confirm that reservation, assigning the user that username.
* This changes the encrypted username associated with your current username link.
* The intent of this is to allow users to change the casing of their username without changing the link,
* since usernames are case-insensitive.
*/
fun confirmUsername(reserved: UsernameState.Reserved): Single<UsernameSetResult> {
fun updateUsernameDisplayForCurrentLink(updatedUsername: Username): Single<UsernameSetResult> {
return Single
.fromCallable { confirmUsernameInternal(reserved) }
.fromCallable { updateUsernameDisplayForCurrentLinkInternal(updatedUsername) }
.subscribeOn(Schedulers.io())
}
/**
* Deletes the username from the local user's account
* Given a reserved username (obtained via [reserveUsername]), this will confirm that reservation, assigning the user that username.
* It will also create a new username link. Therefore, be sure to call [updateUsernameDisplayForCurrentLink] instead if all that has changed is the
* casing, and you want to keep the link the same.
*/
fun confirmUsernameAndCreateNewLink(username: Username): Single<UsernameSetResult> {
return Single
.fromCallable { confirmUsernameAndCreateNewLinkInternal(username) }
.subscribeOn(Schedulers.io())
}
/**
* Deletes the username and username link from the local user's account
*/
@JvmStatic
fun deleteUsername(): Single<UsernameDeleteResult> {
fun deleteUsernameAndLink(): Single<UsernameDeleteResult> {
return Single
.fromCallable { deleteUsernameInternal() }
.subscribeOn(Schedulers.io())
@@ -309,7 +314,7 @@ object UsernameRepository {
val candidates: List<Username> = if (discriminator == null) {
Username.candidatesFrom(nickname, UsernameUtil.MIN_NICKNAME_LENGTH, UsernameUtil.MAX_NICKNAME_LENGTH)
} else {
listOf(Username("$nickname${UsernameState.DELIMITER}$discriminator"))
listOf(Username("$nickname${Usernames.DELIMITER}$discriminator"))
}
val hashes: List<String> = candidates
@@ -324,7 +329,7 @@ object UsernameRepository {
}
Log.i(TAG, "[reserveUsername] Successfully reserved username.")
success(UsernameState.Reserved(candidates[hashIndex].username, response))
success(UsernameState.Reserved(candidates[hashIndex]))
} catch (e: BaseUsernameException) {
Log.w(TAG, "[reserveUsername] An error occurred while generating candidates.")
failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR)
@@ -341,63 +346,65 @@ object UsernameRepository {
}
@WorkerThread
private fun updateUsernameInternal(caseChange: UsernameState.CaseChange): UsernameSetResult {
private fun updateUsernameDisplayForCurrentLinkInternal(updatedUsername: Username): UsernameSetResult {
Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Beginning username update...")
return try {
val oldUsernameLink = SignalStore.account().usernameLink ?: return UsernameSetResult.USERNAME_INVALID
val username = Username(caseChange.username)
val newUsernameLink = username.generateLink(oldUsernameLink.entropy)
val newUsernameLink = updatedUsername.generateLink(oldUsernameLink.entropy)
val usernameLinkComponents = accountManager.updateUsernameLink(newUsernameLink)
SignalStore.account().username = username.username
SignalStore.account().username = updatedUsername.username
SignalStore.account().usernameLink = usernameLinkComponents
SignalDatabase.recipients.setUsername(Recipient.self().id, updatedUsername.username)
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[updateUsernameDisplayForCurrentLink] Successfully updated username.")
UsernameSetResult.SUCCESS
} catch (e: IOException) {
Log.w(TAG, "[updateUsernameDisplayForCurrentLink] Generic network exception.", e)
UsernameSetResult.NETWORK_ERROR
}
}
@WorkerThread
private fun confirmUsernameAndCreateNewLinkInternal(username: Username): UsernameSetResult {
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Beginning username confirmation...")
return try {
accountManager.confirmUsername(username)
SignalStore.account().username = username.username
SignalStore.account().usernameLink = null
SignalDatabase.recipients.setUsername(Recipient.self().id, username.username)
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[updateUsername] Successfully updated username.")
UsernameSetResult.SUCCESS
} catch (e: IOException) {
Log.w(TAG, "[updateUsername] Generic network exception.", e)
UsernameSetResult.NETWORK_ERROR
}
}
@WorkerThread
private fun confirmUsernameInternal(reserved: UsernameState.Reserved): UsernameSetResult {
return try {
val username = Username(reserved.username)
accountManager.confirmUsername(reserved.username, reserved.reserveUsernameResponse)
SignalStore.account().username = username.username
SignalStore.account().usernameLink = null
SignalDatabase.recipients.setUsername(Recipient.self().id, reserved.username)
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[confirmUsername] Successfully confirmed username.")
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username.")
if (tryToSetUsernameLink(username)) {
Log.i(TAG, "[confirmUsername] Successfully confirmed username link.")
Log.i(TAG, "[confirmUsernameAndCreateNewLink] Successfully confirmed username link.")
} else {
Log.w(TAG, "[confirmUsername] Failed to confirm a username link. We'll try again when the user goes to view their link.")
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Failed to confirm a username link. We'll try again when the user goes to view their link.")
}
UsernameSetResult.SUCCESS
} catch (e: UsernameTakenException) {
Log.w(TAG, "[confirmUsername] Username gone.")
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username gone.")
UsernameSetResult.USERNAME_UNAVAILABLE
} catch (e: UsernameIsNotReservedException) {
Log.w(TAG, "[confirmUsername] Username was not reserved.")
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.")
UsernameSetResult.USERNAME_INVALID
} catch (e: BaseUsernameException) {
Log.w(TAG, "[confirmUsername] Username was not reserved.")
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Username was not reserved.")
UsernameSetResult.USERNAME_INVALID
} catch (e: IOException) {
Log.w(TAG, "[confirmUsername] Generic network exception.", e)
Log.w(TAG, "[confirmUsernameAndCreateNewLink] Generic network exception.", e)
UsernameSetResult.NETWORK_ERROR
}
}

View File

@@ -1,16 +1,18 @@
package org.thoughtcrime.securesms.profiles.manage
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import org.signal.libsignal.usernames.Username
import org.whispersystems.signalservice.api.util.discriminator
import org.whispersystems.signalservice.api.util.nickname
/**
* Describes the state of the username suffix, which is a spanned CharSequence.
*/
sealed class UsernameState {
protected open val username: String? = null
protected open val username: Username? = null
open val isInProgress: Boolean = false
fun requireUsername(): String = username!!
fun requireUsername(): Username = username!!
object Loading : UsernameState() {
override val isInProgress: Boolean = true
@@ -19,27 +21,22 @@ sealed class UsernameState {
object NoUsername : UsernameState()
data class Reserved(
public override val username: String,
val reserveUsernameResponse: ReserveUsernameResponse
public override val username: Username
) : UsernameState()
data class CaseChange(
public override val username: String
public override val username: Username
) : UsernameState()
data class Set(
override val username: String
override val username: Username
) : UsernameState()
fun getNickname(): String? {
return username?.split(DELIMITER)?.firstOrNull()
return username?.nickname
}
fun getDiscriminator(): String? {
return username?.split(DELIMITER)?.lastOrNull()
}
companion object {
const val DELIMITER = "."
return username?.discriminator
}
}

View File

@@ -783,8 +783,8 @@ public class SignalServiceAccountManager {
return this.pushServiceSocket.reserveUsername(usernameHashes);
}
public void confirmUsername(String username, ReserveUsernameResponse reserveUsernameResponse) throws IOException {
this.pushServiceSocket.confirmUsername(username, reserveUsernameResponse);
public void confirmUsername(Username username) throws IOException {
this.pushServiceSocket.confirmUsername(username);
}
public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException {

View File

@@ -0,0 +1,11 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.util
import org.signal.libsignal.usernames.Username
val Username.nickname: String get() = username.split(Usernames.DELIMITER)[0]
val Username.discriminator: String get() = username.split(Usernames.DELIMITER)[1]

View File

@@ -0,0 +1,10 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.util
object Usernames {
const val DELIMITER = "."
}

View File

@@ -1109,18 +1109,16 @@ public class PushServiceSocket {
* PUT /v1/accounts/username_hash/confirm
* Set a previously reserved username for the account.
*
* @param username The username the user wishes to confirm. For example, myusername.27
* @param reserveUsernameResponse The response object from the reservation
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
* @param username The username the user wishes to confirm.
* @throws IOException Thrown when the username is invalid or taken, or when another network error occurs.
*/
public void confirmUsername(String username, ReserveUsernameResponse reserveUsernameResponse) throws IOException {
public void confirmUsername(Username username) throws IOException {
try {
byte[] randomness = new byte[32];
random.nextBytes(randomness);
byte[] proof = new Username(username).generateProofWithRandomness(randomness);
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(reserveUsernameResponse.getUsernameHash(),
Base64.encodeUrlSafeWithoutPadding(proof));
byte[] proof = username.generateProofWithRandomness(randomness);
ConfirmUsernameRequest confirmUsernameRequest = new ConfirmUsernameRequest(Base64.encodeUrlSafeWithoutPadding(username.getHash()), Base64.encodeUrlSafeWithoutPadding(proof));
makeServiceRequest(CONFIRM_USERNAME_PATH, "PUT", JsonUtil.toJson(confirmUsernameRequest), NO_HEADERS, (responseCode, body) -> {
switch (responseCode) {
@@ -1159,7 +1157,7 @@ public class PushServiceSocket {
makeServiceRequest(USERNAME_LINK_PATH, "DELETE", null);
}
/** Given a link serverId (see {@link #createUsernameLink(String)}), this will return the encrypted username associate with the link. */
/** Given a link serverId (see {@link #createUsernameLink(String, boolean)}}), this will return the encrypted username associate with the link. */
public byte[] getEncryptedUsernameFromLinkServerId(UUID serverId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(USERNAME_FROM_LINK_PATH, serverId.toString()), "GET", null);
GetUsernameFromLinkResponseBody parsed = JsonUtil.fromJson(response, GetUsernameFromLinkResponseBody.class);