Fix opening username links.

This commit is contained in:
Greyson Parrelli
2023-11-08 10:39:44 -05:00
committed by Cody Henthorne
parent d6fd6cb5a3
commit 73de2dfda7
6 changed files with 144 additions and 108 deletions

View File

@@ -58,7 +58,7 @@ object CallLinks {
return false
}
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
if (!url.startsWith(HTTPS_LINK_PREFIX) || !url.startsWith(SNGL_LINK_PREFIX)) {
return false
}

View File

@@ -25,6 +25,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity;
@@ -46,6 +48,8 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import java.io.IOException;
import java.util.Objects;
@@ -291,46 +295,16 @@ public class CommunicationActions {
* If the url is a signal.me link it will handle it.
*/
public static void handlePotentialSignalMeUrl(@NonNull FragmentActivity activity, @NonNull String potentialUrl) {
String e164 = SignalMeUtil.parseE164FromLink(activity, potentialUrl);
String username = SignalMeUtil.parseUsernameFromLink(potentialUrl);
String e164 = SignalMeUtil.parseE164FromLink(activity, potentialUrl);
UsernameLinkComponents username = SignalMeUtil.parseUsernameComponentsFromLink(potentialUrl);
if (e164 != null) {
handleE164Link(activity, e164);
} else if (username != null) {
handleUsernameLink(activity, username);
}
if (e164 != null || username != null) {
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500);
SimpleTask.run(() -> {
Recipient recipient = Recipient.UNKNOWN;
if (e164 != null) {
recipient = Recipient.external(activity, e164);
if (!recipient.isRegistered() || !recipient.hasServiceId()) {
try {
ContactDiscovery.refresh(activity, recipient, false, TimeUnit.SECONDS.toMillis(10));
recipient = Recipient.resolved(recipient.getId());
} catch (IOException e) {
Log.w(TAG, "[handlePotentialSignalMeUrl] Failed to refresh directory for new contact.");
}
}
} else {
Optional<ServiceId> serviceId = UsernameUtil.fetchAciForUsernameHash(username);
if (serviceId.isPresent()) {
recipient = Recipient.externalUsername(serviceId.get(), username);
}
}
return recipient;
}, recipient -> {
dialog.dismiss();
if (recipient != Recipient.UNKNOWN) {
startConversation(activity, recipient, null);
} else if (username != null) {
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(activity.getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, username))
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
}
}
@@ -456,6 +430,71 @@ public class CommunicationActions {
.execute();
}
private static void handleE164Link(Activity activity, String e164) {
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500);
SimpleTask.run(() -> {
Recipient recipient = Recipient.external(activity, e164);
if (!recipient.isRegistered() || !recipient.hasServiceId()) {
try {
ContactDiscovery.refresh(activity, recipient, false, TimeUnit.SECONDS.toMillis(10));
recipient = Recipient.resolved(recipient.getId());
} catch (IOException e) {
Log.w(TAG, "[handlePotentialSignalMeUrl] Failed to refresh directory for new contact.");
}
}
return recipient;
}, recipient -> {
dialog.dismiss();
if (recipient.isRegistered() && recipient.hasServiceId()) {
startConversation(activity, recipient, null);
} else {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.NewConversationActivity__s_is_not_a_signal_user, e164))
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
}
private static void handleUsernameLink(Activity activity, UsernameLinkComponents link) {
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(activity, 500, 500);
SimpleTask.run(() -> {
try {
byte[] encryptedUsername = ApplicationDependencies.getSignalServiceAccountManager().getEncryptedUsernameFromLinkServerId(link.getServerId());
Username username = Username.fromLink(new Username.UsernameLink(link.getEntropy(), encryptedUsername));
Optional<ServiceId> serviceId = UsernameUtil.fetchAciForUsername(username.getUsername());
if (serviceId.isPresent()) {
return Recipient.externalUsername(serviceId.get(), username.getUsername());
} else {
return null;
}
} catch (IOException e) {
Log.w(TAG, "Failed to fetch encrypted username", e);
return null;
} catch (BaseUsernameException e) {
Log.w(TAG, "Invalid username", e);
return null;
}
}, recipient -> {
dialog.dismiss();
if (recipient != null && recipient.isRegistered() && recipient.hasServiceId()) {
startConversation(activity, recipient, null);
} else {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.UsernameLinkSettings_qr_result_not_found_no_username))
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
}
private interface CallContext {
@NonNull Permissions.PermissionsBuilder getPermissionsBuilder();
void startActivity(@NonNull Intent intent);

View File

@@ -1,68 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class SignalMeUtil {
private static final String HOST = "^(https|sgnl)://" + "signal\\.me";
private static final Pattern E164_PATTERN = Pattern.compile(HOST + "/#p/(\\+[0-9]+)$");
private static final Pattern USERNAME_PATTERN = Pattern.compile(HOST + "/#u/(.+)$");
/**
* If this is a valid signal.me link and has a valid e164, it will return the e164. Otherwise, it will return null.
*/
public static @Nullable String parseE164FromLink(@NonNull Context context, @Nullable String link) {
if (Util.isEmpty(link)) {
return null;
}
Matcher matcher = E164_PATTERN.matcher(link);
if (matcher.matches()) {
String e164 = matcher.group(2);
if (PhoneNumberUtil.getInstance().isPossibleNumber(e164, Locale.getDefault().getCountry())) {
return PhoneNumberFormatter.get(context).format(e164);
} else {
return null;
}
} else {
return null;
}
}
/**
* If this is a valid signal.me link and has a valid username, it will return the username. Otherwise, it will return null.
*/
public static @Nullable String parseUsernameFromLink(@Nullable String link) {
if (Util.isEmpty(link)) {
return null;
}
Matcher matcher = USERNAME_PATTERN.matcher(link);
if (matcher.matches()) {
String username = matcher.group(2);
try {
return username == null || username.isEmpty() ? null : URLDecoder.decode(username, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
return null;
}
} else {
return null;
}
}
}

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.util
import android.content.Context
import com.google.i18n.phonenumbers.PhoneNumberUtil
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.util.UuidUtil
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.util.Locale
internal object SignalMeUtil {
private val E164_REGEX = """^(https|sgnl)://signal\.me/#p/(\+[0-9]+)$""".toRegex()
private val USERNAME_REGEX = """^(https|sgnl)://signal\.me/#eu/(.+)$""".toRegex()
/**
* If this is a valid signal.me link and has a valid e164, it will return the e164. Otherwise, it will return null.
*/
@JvmStatic
fun parseE164FromLink(context: Context, link: String?): String? {
if (link.isNullOrBlank()) {
return null
}
return E164_REGEX.find(link)?.let { match ->
val e164: String = match.groups[2]?.value ?: return@let null
if (PhoneNumberUtil.getInstance().isPossibleNumber(e164, Locale.getDefault().country)) {
PhoneNumberFormatter.get(context).format(e164)
} else {
null
}
}
}
/**
* If this is a valid signal.me link and has valid username link components, it will return those components. Otherwise, it will return null.
*/
@JvmStatic
fun parseUsernameComponentsFromLink(link: String?): UsernameLinkComponents? {
if (link.isNullOrBlank()) {
return null
}
return USERNAME_REGEX.find(link)?.let { match ->
val usernameLinkBase64: String = match.groups[2]?.value ?: return@let null
try {
val usernameLinkData: ByteArray = Base64.decode(usernameLinkBase64).takeIf { it.size == 48 } ?: return@let null
val entropy: ByteArray = usernameLinkData.sliceArray(0 until 32)
val uuidBytes: ByteArray = usernameLinkData.sliceArray(32 until usernameLinkData.size)
val uuid = UuidUtil.parseOrNull(uuidBytes)
UsernameLinkComponents(entropy, uuid)
} catch (e: UnsupportedEncodingException) {
null
} catch (e: IOException) {
null
}
}
}
}

View File

@@ -85,7 +85,7 @@ object UsernameUtil {
Log.d(TAG, "No local user with this username. Searching remotely.")
return try {
fetchAciForUsernameHash(Base64.encodeUrlSafeWithoutPadding(Username.hash(username)))
fetchAciForUsernameHash(Base64.encodeUrlSafeWithoutPadding(Username(username).hash))
} catch (e: BaseUsernameException) {
Optional.empty()
}

View File

@@ -4579,6 +4579,8 @@
<string name="NewConversationActivity__view_contact">View contact</string>
<!-- Error message shown when looking up a person by phone number and that phone number is not associated with a signal account -->
<string name="NewConversationActivity__s_is_not_a_signal_user">%1$s is not a Signal user</string>
<!-- Error message shown when we could not get a user from the username link -->
<string name="NewConversationActivity__">%1$s is not a Signal user</string>
<!-- ContactFilterView -->
<string name="ContactFilterView__search_name_or_number">Search name or number</string>