mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Fix opening username links.
This commit is contained in:
committed by
Cody Henthorne
parent
d6fd6cb5a3
commit
73de2dfda7
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user