Update API endpoints and integration for usernames.

This commit is contained in:
Alex Hart
2023-02-09 13:59:36 -04:00
committed by Greyson Parrelli
parent 803154c544
commit 2c48d40375
22 changed files with 172 additions and 142 deletions

View File

@@ -24,6 +24,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.thoughtcrime.securesms.AvatarPreviewActivity;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
@@ -44,6 +46,7 @@ import org.thoughtcrime.securesms.util.NameUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.util.Base64UrlSafe;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@@ -242,13 +245,14 @@ public class ManageProfileFragment extends LoggingFragment {
private void presentUsername(@Nullable String username) {
if (username == null || username.isEmpty()) {
binding.manageProfileUsername.setText(R.string.ManageProfileFragment_username);
binding.manageProfileUsernameSubtitle.setText(R.string.ManageProfileFragment_your_username);
binding.manageProfileUsernameShare.setVisibility(View.GONE);
} else {
binding.manageProfileUsername.setText(username);
try {
binding.manageProfileUsernameSubtitle.setText(getString(R.string.signal_me_username_url_no_scheme, URLEncoder.encode(username, StandardCharsets.UTF_8.toString())));
} catch (UnsupportedEncodingException e) {
binding.manageProfileUsernameSubtitle.setText(getString(R.string.signal_me_username_url_no_scheme, Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))));
} catch (BaseUsernameException e) {
Log.w(TAG, "Could not format username link", e);
binding.manageProfileUsernameSubtitle.setText(R.string.ManageProfileFragment_your_username);
}

View File

@@ -5,17 +5,22 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.Result;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
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.internal.push.ReserveUsernameResponse;
import org.whispersystems.util.Base64UrlSafe;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
@@ -30,12 +35,12 @@ class UsernameEditRepository {
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
}
@NonNull Single<Result<ReserveUsernameResponse, UsernameSetResult>> reserveUsername(@NonNull String nickname) {
@NonNull Single<Result<UsernameState.Reserved, UsernameSetResult>> reserveUsername(@NonNull String nickname) {
return Single.fromCallable(() -> reserveUsernameInternal(nickname)).subscribeOn(Schedulers.io());
}
@NonNull Single<UsernameSetResult> confirmUsername(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
return Single.fromCallable(() -> confirmUsernameInternal(reserveUsernameResponse)).subscribeOn(Schedulers.io());
@NonNull Single<UsernameSetResult> confirmUsername(@NonNull UsernameState.Reserved reserved) {
return Single.fromCallable(() -> confirmUsernameInternal(reserved)).subscribeOn(Schedulers.io());
}
@NonNull Single<UsernameDeleteResult> deleteUsername() {
@@ -43,11 +48,28 @@ class UsernameEditRepository {
}
@WorkerThread
private @NonNull Result<ReserveUsernameResponse, UsernameSetResult> reserveUsernameInternal(@NonNull String nickname) {
private @NonNull Result<UsernameState.Reserved, UsernameSetResult> reserveUsernameInternal(@NonNull String nickname) {
try {
ReserveUsernameResponse username = accountManager.reserveUsername(nickname);
List<String> candidates = Username.generateCandidates(nickname, UsernameUtil.MIN_LENGTH, UsernameUtil.MAX_LENGTH);
List<String> hashes = new ArrayList<>();
for (String candidate : candidates) {
byte[] hash = Username.hash(candidate);
hashes.add(Base64UrlSafe.encodeBytesWithoutPadding(hash));
}
ReserveUsernameResponse response = accountManager.reserveUsername(hashes);
int hashIndex = hashes.indexOf(response.getUsernameHash());
if (hashIndex == -1) {
Log.w(TAG, "[reserveUsername] The response hash could not be found in our set of hashes.");
return Result.failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR);
}
Log.i(TAG, "[reserveUsername] Successfully reserved username.");
return Result.success(username);
return Result.success(new UsernameState.Reserved(candidates.get(hashIndex), response));
} catch (BaseUsernameException e) {
Log.w(TAG, "[reserveUsername] An error occurred while generating candidates.");
return Result.failure(UsernameSetResult.CANDIDATE_GENERATION_ERROR);
} catch (UsernameTakenException e) {
Log.w(TAG, "[reserveUsername] Username taken.");
return Result.failure(UsernameSetResult.USERNAME_UNAVAILABLE);
@@ -61,11 +83,10 @@ class UsernameEditRepository {
}
@WorkerThread
private @NonNull UsernameSetResult confirmUsernameInternal(@NonNull ReserveUsernameResponse reserveUsernameResponse) {
private @NonNull UsernameSetResult confirmUsernameInternal(@NonNull UsernameState.Reserved reserved) {
try {
accountManager.confirmUsername(reserveUsernameResponse);
SignalDatabase.recipients().setUsername(Recipient.self().getId(), reserveUsernameResponse.getUsername());
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileContentUpdateJob());
accountManager.confirmUsername(reserved.getUsername(), reserved.getReserveUsernameResponse());
SignalDatabase.recipients().setUsername(Recipient.self().getId(), reserved.getUsername());
Log.i(TAG, "[confirmUsername] Successfully reserved username.");
return UsernameSetResult.SUCCESS;
} catch (UsernameTakenException e) {
@@ -94,7 +115,7 @@ class UsernameEditRepository {
}
enum UsernameSetResult {
SUCCESS, USERNAME_UNAVAILABLE, USERNAME_INVALID, NETWORK_ERROR
SUCCESS, USERNAME_UNAVAILABLE, USERNAME_INVALID, NETWORK_ERROR, CANDIDATE_GENERATION_ERROR
}
enum UsernameDeleteResult {

View File

@@ -110,7 +110,7 @@ class UsernameEditViewModel extends ViewModel {
uiState.update(state -> new State(ButtonState.SUBMIT_LOADING, UsernameStatus.NONE, state.usernameState));
Disposable confirmUsernameDisposable = repo.confirmUsername(((UsernameState.Reserved) usernameState).getReserveUsernameResponse())
Disposable confirmUsernameDisposable = repo.confirmUsername((UsernameState.Reserved) usernameState)
.subscribe(result -> {
String nickname = usernameState.getNickname();
@@ -181,8 +181,8 @@ class UsernameEditViewModel extends ViewModel {
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.NONE, UsernameState.Loading.INSTANCE));
Disposable reserveDisposable = repo.reserveUsername(nickname).subscribe(result -> {
result.either(
reserveUsernameJsonResponse -> {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, new UsernameState.Reserved(reserveUsernameJsonResponse)));
reserved -> {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, reserved));
return null;
},
failure -> {
@@ -199,6 +199,10 @@ class UsernameEditViewModel extends ViewModel {
uiState.update(state -> new State(ButtonState.SUBMIT, UsernameStatus.NONE, UsernameState.NoUsername.INSTANCE));
events.onNext(Event.NETWORK_FAILURE);
break;
case CANDIDATE_GENERATION_ERROR:
// TODO -- Retry
uiState.update(state -> new State(ButtonState.SUBMIT_DISABLED, UsernameStatus.TAKEN, UsernameState.NoUsername.INSTANCE));
break;
}
return null;

View File

@@ -9,6 +9,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.findNavController
import org.signal.core.util.DimensionUnit
import org.signal.libsignal.usernames.Username
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
@@ -19,8 +20,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FragmentResultContract
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.Util
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import org.whispersystems.util.Base64UrlSafe
/**
* Allows the user to either share their username directly or to copy it to their clipboard.
@@ -71,7 +71,7 @@ class UsernameShareBottomSheet : DSLSettingsBottomSheetFragment() {
customPref(
CopyButton.Model(
text = getString(R.string.signal_me_username_url, URLEncoder.encode(username, StandardCharsets.UTF_8.toString())),
text = getString(R.string.signal_me_username_url, Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))),
onClick = {
copyToClipboard(it)
}
@@ -82,7 +82,7 @@ class UsernameShareBottomSheet : DSLSettingsBottomSheetFragment() {
customPref(
ShareButton.Model(
text = getString(R.string.signal_me_username_url, URLEncoder.encode(username, StandardCharsets.UTF_8.toString())),
text = getString(R.string.signal_me_username_url, Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))),
onClick = {
openShareSheet(it.text)
}

View File

@@ -17,10 +17,9 @@ sealed class UsernameState {
object NoUsername : UsernameState()
data class Reserved(
override val username: String,
val reserveUsernameResponse: ReserveUsernameResponse
) : UsernameState() {
override val username: String? = reserveUsernameResponse.username
}
) : UsernameState()
data class Set(
override val username: String