mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-28 04:34:21 +01:00
Handle 428 rate limiting.
This commit is contained in:
@@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
@@ -156,6 +157,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(DownloadLatestEmojiDataJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.execute();
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
|
||||
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
|
||||
void onVoiceNotePause(@NonNull Uri uri);
|
||||
|
||||
@@ -69,4 +69,10 @@ public class AlertView extends LinearLayout {
|
||||
approvalIndicator.setVisibility(View.GONE);
|
||||
failedIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void setRateLimited() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
approvalIndicator.setVisibility(View.VISIBLE);
|
||||
failedIndicator.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,8 @@ public class ConversationItemFooter extends LinearLayout {
|
||||
dateView.setText(errorMsg);
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
|
||||
} else if (messageRecord.isRateLimited()) {
|
||||
dateView.setText(R.string.ConversationItem_send_paused);
|
||||
} else {
|
||||
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||
}
|
||||
|
||||
@@ -228,6 +228,8 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofActivity;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
@@ -579,6 +581,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
setVisibleThread(threadId);
|
||||
ConversationUtil.refreshRecipientShortcuts();
|
||||
|
||||
if (SignalStore.rateLimit().needsRecaptcha()) {
|
||||
RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2200,6 +2206,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
|
||||
this.viewModel.setArgs(args);
|
||||
this.viewModel.getWallpaper().observe(this, this::updateWallpaper);
|
||||
this.viewModel.getEvents().observe(this, this::onViewModelEvent);
|
||||
}
|
||||
|
||||
private void initializeGroupViewModel() {
|
||||
@@ -2973,6 +2980,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void onViewModelEvent(@NonNull ConversationViewModel.Event event) {
|
||||
if (event == ConversationViewModel.Event.SHOW_RECAPTCHA) {
|
||||
RecaptchaProofBottomSheetFragment.show(getSupportFragmentManager());
|
||||
} else {
|
||||
throw new AssertionError("Unexpected event!");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLinkPreviewState() {
|
||||
if (SignalStore.settings().isLinkPreviewsEnabled() && isSecureText && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
|
||||
linkPreviewViewModel.onEnabled();
|
||||
|
||||
@@ -124,6 +124,7 @@ import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -1503,6 +1504,11 @@ public class ConversationFragment extends LoggingFragment {
|
||||
listener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord) {
|
||||
RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoiceNotePause(@NonNull Uri uri) {
|
||||
voiceNoteMediaController.pausePlayback(uri);
|
||||
|
||||
@@ -97,6 +97,7 @@ import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -1054,6 +1055,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
alertView.setFailed();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
alertView.setPendingApproval();
|
||||
} else if (messageRecord.isRateLimited()) {
|
||||
alertView.setRateLimited();
|
||||
} else {
|
||||
alertView.setNone();
|
||||
}
|
||||
@@ -1166,7 +1169,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(context, locale, next.get().getTimestamp(), current.getTimestamp());
|
||||
|
||||
if (forceFooter(messageRecord) || current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() ||
|
||||
current.isFailed() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread))
|
||||
current.isFailed() || current.isRateLimited() || differentTimestamps || isEndOfMessageCluster(current, next, isGroupThread))
|
||||
{
|
||||
ConversationItemFooter activeFooter = getActiveFooter(current);
|
||||
activeFooter.setVisibility(VISIBLE);
|
||||
@@ -1211,10 +1214,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean shouldInterceptClicks(MessageRecord messageRecord) {
|
||||
return batchSelected.isEmpty() &&
|
||||
((messageRecord.isFailed() && !messageRecord.isMmsNotification()) ||
|
||||
messageRecord.isPendingInsecureSmsFallback() ||
|
||||
messageRecord.isBundleKeyExchange());
|
||||
return batchSelected.isEmpty() &&
|
||||
((messageRecord.isFailed() && !messageRecord.isMmsNotification()) ||
|
||||
(messageRecord.isRateLimited() && SignalStore.rateLimit().needsRecaptcha()) ||
|
||||
messageRecord.isPendingInsecureSmsFallback() ||
|
||||
messageRecord.isBundleKeyExchange());
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@@ -1682,6 +1686,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageWithErrorClicked(messageRecord);
|
||||
}
|
||||
} else if (messageRecord.isRateLimited() && SignalStore.rateLimit().needsRecaptcha()) {
|
||||
if (eventListener != null) {
|
||||
eventListener.onMessageWithRecaptchaNeededClicked(messageRecord);
|
||||
}
|
||||
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
|
||||
handleApproveIdentity();
|
||||
} else if (messageRecord.isPendingInsecureSmsFallback()) {
|
||||
|
||||
@@ -10,6 +10,9 @@ import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
@@ -19,8 +22,10 @@ import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -28,7 +33,7 @@ import org.whispersystems.libsignal.util.Pair;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
class ConversationViewModel extends ViewModel {
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationViewModel.class);
|
||||
|
||||
@@ -46,6 +51,7 @@ class ConversationViewModel extends ViewModel {
|
||||
private final DatabaseObserver.Observer messageObserver;
|
||||
private final MutableLiveData<RecipientId> recipientId;
|
||||
private final LiveData<ChatWallpaper> wallpaper;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
|
||||
private ConversationIntents.Args args;
|
||||
private int jumpToPosition;
|
||||
@@ -59,6 +65,7 @@ class ConversationViewModel extends ViewModel {
|
||||
this.showScrollButtons = new MutableLiveData<>(false);
|
||||
this.hasUnreadMentions = new MutableLiveData<>(false);
|
||||
this.recipientId = new MutableLiveData<>();
|
||||
this.events = new SingleLiveEvent<>();
|
||||
this.pagingController = new ProxyPagingController();
|
||||
this.messageObserver = pagingController::onDataInvalidated;
|
||||
|
||||
@@ -108,6 +115,8 @@ class ConversationViewModel extends ViewModel {
|
||||
wallpaper = Transformations.distinctUntilChanged(Transformations.map(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getWallpaper));
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
@@ -144,6 +153,10 @@ class ConversationViewModel extends ViewModel {
|
||||
return wallpaper;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
void setHasUnreadMentions(boolean hasUnreadMentions) {
|
||||
this.hasUnreadMentions.setValue(hasUnreadMentions);
|
||||
}
|
||||
@@ -184,10 +197,20 @@ class ConversationViewModel extends ViewModel {
|
||||
return Objects.requireNonNull(args);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.POSTING)
|
||||
public void onRecaptchaRequiredEvent(@NonNull RecaptchaRequiredEvent event) {
|
||||
events.postValue(Event.SHOW_RECAPTCHA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
enum Event {
|
||||
SHOW_RECAPTCHA
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
@@ -116,6 +116,8 @@ import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsFragmentArgs;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsParcelable;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofActivity;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
@@ -283,6 +285,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||
}
|
||||
|
||||
SignalProxyUtil.startListeningToWebsocket();
|
||||
|
||||
if (SignalStore.rateLimit().needsRecaptcha()) {
|
||||
Log.i(TAG, "Recaptcha required.");
|
||||
RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -89,6 +89,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage();
|
||||
public abstract boolean isSent(long messageId);
|
||||
public abstract List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp);
|
||||
public abstract Set<Long> getAllRateLimitedMessageIds();
|
||||
|
||||
public abstract void markExpireStarted(long messageId);
|
||||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
@@ -101,6 +102,8 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract void markAsInsecure(long id);
|
||||
public abstract void markAsPush(long id);
|
||||
public abstract void markAsForcedSms(long id);
|
||||
public abstract void markAsRateLimited(long id);
|
||||
public abstract void clearRateLimitStatus(Collection<Long> ids);
|
||||
public abstract void markAsDecryptFailed(long id);
|
||||
public abstract void markAsDecryptDuplicate(long id);
|
||||
public abstract void markAsNoSession(long id);
|
||||
|
||||
@@ -785,6 +785,30 @@ public class MmsDatabase extends MessageDatabase {
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsRateLimited(long messageId) {
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
updateMailboxBitmask(messageId, 0, Types.MESSAGE_RATE_LIMITED_BIT, Optional.of(threadId));
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRateLimitStatus(@NonNull Collection<Long> ids) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (long id : ids) {
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
updateMailboxBitmask(id, Types.MESSAGE_RATE_LIMITED_BIT, 0, Optional.of(threadId));
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsPendingInsecureSmsFallback(long messageId) {
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
@@ -1708,6 +1732,22 @@ public class MmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getAllRateLimitedMessageIds() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = "(" + MESSAGE_BOX + " & " + Types.TOTAL_MASK + " & " + Types.MESSAGE_RATE_LIMITED_BIT + ") > 0";
|
||||
|
||||
Set<Long> ids = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, where, null, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
ids.add(CursorUtil.requireLong(cursor, ID));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
void deleteThreads(@NonNull Set<Long> threadIds) {
|
||||
Log.d(TAG, "deleteThreads(count: " + threadIds.size() + ")");
|
||||
|
||||
@@ -28,7 +28,34 @@ public interface MmsSmsColumns {
|
||||
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
|
||||
public static final String REMOTE_DELETED = "remote_deleted";
|
||||
|
||||
/**
|
||||
* For storage efficiency, all types are stored within a single 64-bit integer column in the
|
||||
* database. There are various areas reserved for different classes of data.
|
||||
*
|
||||
* When carving out a new area, if it's storing a bunch of mutually-exclusive flags (like in
|
||||
* {@link #BASE_TYPE_MASK}, you should store integers in that area. If multiple flags can be set
|
||||
* within a category, you'll have to store them as bits. Just keep in mind that storing as bits
|
||||
* means we can store less data (i.e. 4 bits can store 16 exclusive values, or 4 non-exclusive
|
||||
* values). This was not always followed in the past, and now we've wasted some space.
|
||||
*
|
||||
* Note: We technically could use up to 64 bits, but {@link #TOTAL_MASK} is currently just set to
|
||||
* look at 32. Theoretically if we needed more bits, we could just use them and expand the size of
|
||||
* {@link #TOTAL_MASK}.
|
||||
*
|
||||
* <pre>
|
||||
* _____________________________________ ENCRYPTION ({@link #ENCRYPTION_MASK})
|
||||
* | _____________________________ SECURE MESSAGE INFORMATION (no mask, but look at {@link #SECURE_MESSAGE_BIT})
|
||||
* | | ________________________ GROUPS (no mask, but look at {@link #GROUP_UPDATE_BIT})
|
||||
* | | | _________________ KEY_EXCHANGE ({@link #KEY_EXCHANGE_MASK})
|
||||
* | | | | _________ MESSAGE_ATTRIBUTES ({@link #MESSAGE_ATTRIBUTE_MASK})
|
||||
* | | | | | ____ BASE_TYPE ({@link #BASE_TYPE_MASK})
|
||||
* ___|___ _| _| ___|__ | __|_
|
||||
* | | | | | | | | | || |
|
||||
* 0000 0000 0000 0000 0000 0000 0000 0000
|
||||
* </pre>
|
||||
*/
|
||||
public static class Types {
|
||||
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFF;
|
||||
|
||||
// Base Types
|
||||
@@ -63,8 +90,10 @@ public interface MmsSmsColumns {
|
||||
OUTGOING_AUDIO_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE};
|
||||
|
||||
// Message attributes
|
||||
protected static final long MESSAGE_ATTRIBUTE_MASK = 0xE0;
|
||||
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
|
||||
protected static final long MESSAGE_ATTRIBUTE_MASK = 0xE0;
|
||||
protected static final long MESSAGE_RATE_LIMITED_BIT = 0x80;
|
||||
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
|
||||
// Note: Might be wise to reserve 0x20 -- it would let us expand BASE_MASK by a bit if needed
|
||||
|
||||
// Key Exchange Information
|
||||
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
|
||||
@@ -210,6 +239,10 @@ public interface MmsSmsColumns {
|
||||
return (type & KEY_EXCHANGE_IDENTITY_UPDATE_BIT) != 0;
|
||||
}
|
||||
|
||||
public static boolean isRateLimited(long type) {
|
||||
return (type & MESSAGE_RATE_LIMITED_BIT) != 0;
|
||||
}
|
||||
|
||||
public static boolean isCallLog(long type) {
|
||||
return isIncomingAudioCall(type) ||
|
||||
isIncomingVideoCall(type) ||
|
||||
|
||||
@@ -47,7 +47,6 @@ import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
|
||||
@@ -321,6 +320,27 @@ public class SmsDatabase extends MessageDatabase {
|
||||
updateTypeBitmask(id, Types.PUSH_MESSAGE_BIT, Types.MESSAGE_FORCE_SMS_BIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsRateLimited(long id) {
|
||||
updateTypeBitmask(id, 0, Types.MESSAGE_RATE_LIMITED_BIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRateLimitStatus(@NonNull Collection<Long> ids) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (long id : ids) {
|
||||
updateTypeBitmask(id, Types.MESSAGE_RATE_LIMITED_BIT, 0);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsDecryptFailed(long id) {
|
||||
updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT);
|
||||
@@ -887,6 +907,22 @@ public class SmsDatabase extends MessageDatabase {
|
||||
return new Pair<>(messageId, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getAllRateLimitedMessageIds() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = "(" + TYPE + " & " + Types.TOTAL_MASK + " & " + Types.MESSAGE_RATE_LIMITED_BIT + ") > 0";
|
||||
|
||||
Set<Long> ids = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID }, where, null, null, null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
ids.add(CursorUtil.requireLong(cursor, ID));
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MessageRecord> getProfileChangeDetailsRecords(long threadId, long afterTimestamp) {
|
||||
String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " >= ? AND " + TYPE + " = ?";
|
||||
|
||||
@@ -423,6 +423,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return SmsDatabase.Types.isContentBundleKeyExchange(type);
|
||||
}
|
||||
|
||||
public boolean isRateLimited() {
|
||||
return SmsDatabase.Types.isRateLimited(type);
|
||||
}
|
||||
|
||||
public boolean isIdentityUpdate() {
|
||||
return SmsDatabase.Types.isIdentityUpdate(type);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.google.firebase.messaging.RemoteMessage;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubmitRateLimitPushChallengeJob;
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@@ -30,10 +31,14 @@ public class FcmReceiveService extends FirebaseMessagingService {
|
||||
remoteMessage.getPriority(),
|
||||
remoteMessage.getOriginalPriority()));
|
||||
|
||||
String challenge = remoteMessage.getData().get("challenge");
|
||||
if (challenge != null) {
|
||||
handlePushChallenge(challenge);
|
||||
} else {
|
||||
String registrationChallenge = remoteMessage.getData().get("challenge");
|
||||
String rateLimitChallenge = remoteMessage.getData().get("rateLimitChallenge");
|
||||
|
||||
if (registrationChallenge != null) {
|
||||
handleRegistrationPushChallenge(registrationChallenge);
|
||||
} else if (rateLimitChallenge != null) {
|
||||
handleRateLimitPushChallenge(rateLimitChallenge);
|
||||
}else {
|
||||
handleReceivedNotification(ApplicationDependencies.getApplication());
|
||||
}
|
||||
}
|
||||
@@ -75,9 +80,13 @@ public class FcmReceiveService extends FirebaseMessagingService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void handlePushChallenge(@NonNull String challenge) {
|
||||
Log.d(TAG, String.format("Got a push challenge \"%s\"", challenge));
|
||||
|
||||
private static void handleRegistrationPushChallenge(@NonNull String challenge) {
|
||||
Log.d(TAG, "Got a registration push challenge.");
|
||||
PushChallengeRequest.postChallengeResponse(challenge);
|
||||
}
|
||||
|
||||
private static void handleRateLimitPushChallenge(@NonNull String challenge) {
|
||||
Log.d(TAG, "Got a rate limit push challenge.");
|
||||
ApplicationDependencies.getJobManager().add(new SubmitRateLimitPushChallengeJob(challenge));
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,23 @@ class JobController {
|
||||
.forEach(this::cancelJob);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void update(@NonNull JobUpdater updater) {
|
||||
List<JobSpec> allJobs = jobStorage.getAllJobSpecs();
|
||||
List<JobSpec> updatedJobs = new LinkedList<>();
|
||||
|
||||
for (JobSpec job : allJobs) {
|
||||
JobSpec updated = updater.update(job, dataSerializer);
|
||||
if (updated != job) {
|
||||
updatedJobs.add(updated);
|
||||
}
|
||||
}
|
||||
|
||||
jobStorage.updateJobs(updatedJobs);
|
||||
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
synchronized void onRetry(@NonNull Job job, long backoffInterval) {
|
||||
if (backoffInterval <= 0) {
|
||||
|
||||
@@ -223,6 +223,15 @@ public class JobManager implements ConstraintObserver.Notifier {
|
||||
runOnExecutor(() -> jobController.cancelAllInQueue(queue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an arbitrary update on enqueued jobs. Will not apply to jobs that are already running.
|
||||
* You shouldn't use this if you can help it. You give yourself an opportunity to really screw
|
||||
* things up.
|
||||
*/
|
||||
public void update(@NonNull JobUpdater updater) {
|
||||
runOnExecutor(() -> jobController.update(updater));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified job synchronously. Beware: All normal dependencies are respected, meaning
|
||||
* you must take great care where you call this. It could take a very long time to complete!
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.jobmanager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
|
||||
|
||||
public interface JobUpdater {
|
||||
/**
|
||||
* Called for each enqueued job, giving you an opportunity to update each one.
|
||||
*
|
||||
* @param jobSpec An object representing data about an enqueued job.
|
||||
* @param serializer An object that can be used to serialize/deserialize data if necessary for
|
||||
* your update.
|
||||
*
|
||||
* @return The updated JobSpec you want persisted. If you do not wish to make an update, return
|
||||
* the literal same JobSpec instance you were provided.
|
||||
*/
|
||||
@NonNull JobSpec update(@NonNull JobSpec jobSpec, @NonNull Data.Serializer serializer);
|
||||
}
|
||||
@@ -49,6 +49,10 @@ public final class JobSpec {
|
||||
this.memoryOnly = memoryOnly;
|
||||
}
|
||||
|
||||
public @NonNull JobSpec withNextRunAttemptTime(long updated) {
|
||||
return new JobSpec(id, factoryKey, queueKey, createTime, updated, runAttempt, maxAttempts, lifespan, serializedData, serializedInputData, isRunning, memoryOnly);
|
||||
}
|
||||
|
||||
public @NonNull String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,8 @@ public final class JobManagerFactories {
|
||||
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
|
||||
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
|
||||
put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory());
|
||||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
||||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
||||
put(SubmitRateLimitPushChallengeJob.KEY, new SubmitRateLimitPushChallengeJob.Factory());
|
||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());
|
||||
|
||||
@@ -11,7 +11,6 @@ import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -55,6 +54,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
|
||||
@@ -151,7 +151,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
|
||||
@Override
|
||||
public void onPushSend()
|
||||
throws IOException, MmsException, NoSuchMessageException, RetryLaterException
|
||||
throws IOException, MmsException, NoSuchMessageException, RetryLaterException
|
||||
{
|
||||
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingMediaMessage message = database.getOutgoingMessage(messageId);
|
||||
@@ -197,6 +197,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
|
||||
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(findId(result.getAddress(), idByE164, idByUuid))).toList();
|
||||
List<IdentityKeyMismatch> identityMismatches = Stream.of(results).filter(result -> result.getIdentityFailure() != null).map(result -> new IdentityKeyMismatch(findId(result.getAddress(), idByE164, idByUuid), result.getIdentityFailure().getIdentityKey())).toList();
|
||||
ProofRequiredException proofRequired = Stream.of(results).filter(r -> r.getProofRequiredFailure() != null).findLast().map(SendMessageResult::getProofRequiredFailure).orElse(null);
|
||||
List<SendMessageResult> successes = Stream.of(results).filter(result -> result.getSuccess() != null).toList();
|
||||
List<Pair<RecipientId, Boolean>> successUnidentifiedStatus = Stream.of(successes).map(result -> new Pair<>(findId(result.getAddress(), idByE164, idByUuid), result.getSuccess().isUnidentified())).toList();
|
||||
Set<RecipientId> successIds = Stream.of(successUnidentifiedStatus).map(Pair::first).collect(Collectors.toSet());
|
||||
@@ -229,6 +230,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
|
||||
DatabaseFactory.getGroupReceiptDatabase(context).setUnidentified(successUnidentifiedStatus, messageId);
|
||||
|
||||
if (proofRequired != null) {
|
||||
handleProofRequiredException(proofRequired, groupRecipient, threadId, messageId, true);
|
||||
}
|
||||
|
||||
if (existingNetworkFailures.isEmpty() && networkFailures.isEmpty() && identityMismatches.isEmpty() && existingIdentityMismatches.isEmpty()) {
|
||||
database.markAsSent(messageId, true);
|
||||
|
||||
@@ -391,6 +396,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
return RecipientUtil.getEligibleForSending(members);
|
||||
}
|
||||
|
||||
public static long getMessageId(@NonNull Data data) {
|
||||
return data.getLong(KEY_MESSAGE_ID);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushGroupSendJob> {
|
||||
@Override
|
||||
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.annotation.WorkerThread;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
@@ -41,6 +40,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Pr
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
|
||||
@@ -105,7 +105,7 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
|
||||
@Override
|
||||
public void onPushSend()
|
||||
throws IOException, MmsException, NoSuchMessageException, UndeliverableMessageException
|
||||
throws IOException, MmsException, NoSuchMessageException, UndeliverableMessageException, RetryLaterException
|
||||
{
|
||||
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
||||
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
@@ -172,6 +172,8 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey());
|
||||
database.markAsSentFailed(messageId);
|
||||
RetrieveProfileJob.enqueue(recipientId);
|
||||
} catch (ProofRequiredException e) {
|
||||
handleProofRequiredException(e, DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId), threadId, messageId, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +237,10 @@ public class PushMediaSendJob extends PushSendJob {
|
||||
}
|
||||
}
|
||||
|
||||
public static long getMessageId(@NonNull Data data) {
|
||||
return data.getLong(KEY_MESSAGE_ID);
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<PushMediaSendJob> {
|
||||
@Override
|
||||
public @NonNull PushMediaSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
|
||||
@@ -11,6 +11,9 @@ import androidx.annotation.Nullable;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.signal.libsignal.metadata.certificate.SenderCertificate;
|
||||
@@ -39,6 +42,7 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||
@@ -49,6 +53,7 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@@ -60,7 +65,10 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.internal.push.ProofRequiredResponse;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -72,12 +80,14 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class PushSendJob extends SendJob {
|
||||
|
||||
private static final String TAG = Log.tag(PushSendJob.class);
|
||||
private static final long CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1);
|
||||
private static final long PUSH_CHALLENGE_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
|
||||
|
||||
protected PushSendJob(Job.Parameters parameters) {
|
||||
super(parameters);
|
||||
@@ -100,6 +110,11 @@ public abstract class PushSendJob extends SendJob {
|
||||
}
|
||||
|
||||
onPushSend();
|
||||
|
||||
if (SignalStore.rateLimit().needsRecaptcha()) {
|
||||
Log.i(TAG, "Successfully sent message. Assuming reCAPTCHA no longer needed.");
|
||||
SignalStore.rateLimit().onProofAccepted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,16 +139,28 @@ public abstract class PushSendJob extends SendJob {
|
||||
return false;
|
||||
}
|
||||
|
||||
return exception instanceof IOException ||
|
||||
exception instanceof RetryLaterException;
|
||||
return exception instanceof IOException ||
|
||||
exception instanceof RetryLaterException ||
|
||||
exception instanceof ProofRequiredException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextRunAttemptBackoff(int pastAttemptCount, @NonNull Exception exception) {
|
||||
if (exception instanceof NonSuccessfulResponseCodeException) {
|
||||
if (exception instanceof ProofRequiredException) {
|
||||
long backoff = ((ProofRequiredException) exception).getRetryAfterSeconds();
|
||||
warn(TAG, "[Proof Required] Retry-After is " + backoff + " seconds.");
|
||||
if (backoff >= 0) {
|
||||
return TimeUnit.SECONDS.toMillis(backoff);
|
||||
}
|
||||
} else if (exception instanceof NonSuccessfulResponseCodeException) {
|
||||
if (((NonSuccessfulResponseCodeException) exception).is5xx()) {
|
||||
return BackoffUtil.exponentialBackoff(pastAttemptCount, FeatureFlags.getServerErrorMaxBackoff());
|
||||
}
|
||||
} else if (exception instanceof RetryLaterException) {
|
||||
long backoff = ((RetryLaterException) exception).getBackoff();
|
||||
if (backoff >= 0) {
|
||||
return backoff;
|
||||
}
|
||||
}
|
||||
|
||||
return super.getNextRunAttemptBackoff(pastAttemptCount, exception);
|
||||
@@ -422,6 +449,81 @@ public abstract class PushSendJob extends SendJob {
|
||||
return SignalServiceSyncMessage.forSentTranscript(transcript);
|
||||
}
|
||||
|
||||
protected void handleProofRequiredException(@NonNull ProofRequiredException proofRequired, @Nullable Recipient recipient, long threadId, long messageId, boolean isMms)
|
||||
throws ProofRequiredException, RetryLaterException
|
||||
{
|
||||
try {
|
||||
if (proofRequired.getOptions().contains(ProofRequiredException.Option.PUSH_CHALLENGE)) {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().requestRateLimitPushChallenge();
|
||||
log(TAG, "[Proof Required] Successfully requested a challenge. Waiting up to " + PUSH_CHALLENGE_TIMEOUT + " ms.");
|
||||
|
||||
boolean success = new PushChallengeRequest(PUSH_CHALLENGE_TIMEOUT).blockUntilSuccess();
|
||||
|
||||
if (success) {
|
||||
log(TAG, "Successfully responded to a push challenge. Retrying message send.");
|
||||
throw new RetryLaterException(1);
|
||||
} else {
|
||||
warn(TAG, "Failed to respond to the push challenge in time. Falling back.");
|
||||
}
|
||||
}
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
warn(TAG, "[Proof Required] Could not request a push challenge (" + e.getCode() + "). Falling back.", e);
|
||||
} catch (IOException e) {
|
||||
warn(TAG, "[Proof Required] Network error when requesting push challenge. Retrying later.");
|
||||
throw new RetryLaterException(e);
|
||||
}
|
||||
|
||||
warn(TAG, "[Proof Required] Marking message as rate-limited. (id: " + messageId + ", mms: " + isMms + ", thread: " + threadId + ")");
|
||||
if (isMms) {
|
||||
DatabaseFactory.getMmsDatabase(context).markAsRateLimited(messageId);
|
||||
} else {
|
||||
DatabaseFactory.getSmsDatabase(context).markAsRateLimited(messageId);
|
||||
}
|
||||
|
||||
if (proofRequired.getOptions().contains(ProofRequiredException.Option.RECAPTCHA)) {
|
||||
log(TAG, "[Proof Required] ReCAPTCHA required.");
|
||||
SignalStore.rateLimit().markNeedsRecaptcha(proofRequired.getToken());
|
||||
|
||||
if (recipient != null) {
|
||||
ApplicationDependencies.getMessageNotifier().notifyProofRequired(context, recipient, threadId);
|
||||
} else {
|
||||
warn(TAG, "[Proof Required] No recipient! Couldn't notify.");
|
||||
}
|
||||
}
|
||||
|
||||
throw proofRequired;
|
||||
}
|
||||
|
||||
protected abstract void onPushSend() throws Exception;
|
||||
|
||||
public static class PushChallengeRequest {
|
||||
private final long timeout;
|
||||
private final CountDownLatch latch;
|
||||
private final EventBus eventBus;
|
||||
|
||||
private PushChallengeRequest(long timeout) {
|
||||
this.timeout = timeout;
|
||||
this.latch = new CountDownLatch(1);
|
||||
this.eventBus = EventBus.getDefault();
|
||||
}
|
||||
|
||||
public boolean blockUntilSuccess() {
|
||||
eventBus.register(this);
|
||||
|
||||
try {
|
||||
return latch.await(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "[Proof Required] Interrupted?", e);
|
||||
return false;
|
||||
} finally {
|
||||
eventBus.unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.POSTING)
|
||||
public void onSuccessReceived(SubmitRateLimitPushChallengeJob.SuccessEvent event) {
|
||||
Log.i(TAG, "[Proof Required] Received a successful result!");
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.jobs;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
@@ -14,7 +13,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -31,12 +29,11 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PushTextSendJob extends PushSendJob {
|
||||
|
||||
@@ -73,7 +70,7 @@ public class PushTextSendJob extends PushSendJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPushSend() throws IOException, NoSuchMessageException, UndeliverableMessageException {
|
||||
public void onPushSend() throws IOException, NoSuchMessageException, UndeliverableMessageException, RetryLaterException {
|
||||
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
||||
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getSmsMessage(messageId);
|
||||
@@ -133,6 +130,8 @@ public class PushTextSendJob extends PushSendJob {
|
||||
database.markAsSentFailed(record.getId());
|
||||
database.markAsPush(record.getId());
|
||||
RetrieveProfileJob.enqueue(recipientId);
|
||||
} catch (ProofRequiredException e) {
|
||||
handleProofRequiredException(e, record.getRecipient(), record.getThreadId(), messageId, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +186,10 @@ public class PushTextSendJob extends PushSendJob {
|
||||
}
|
||||
}
|
||||
|
||||
public static long getMessageId(@NonNull Data data) {
|
||||
return data.getLong(KEY_MESSAGE_ID);
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<PushTextSendJob> {
|
||||
@Override
|
||||
public @NonNull PushTextSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
|
||||
@@ -47,6 +47,7 @@ public class RotateProfileKeyJob extends BaseJob {
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceProfileKeyUpdateJob());
|
||||
|
||||
updateProfileKeyOnAllV2Groups();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Send a push challenge token to the service as a way of proving that your device has FCM.
|
||||
*/
|
||||
public final class SubmitRateLimitPushChallengeJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "SubmitRateLimitPushChallengeJob";
|
||||
|
||||
private static final String KEY_CHALLENGE = "challenge";
|
||||
|
||||
private final String challenge;
|
||||
|
||||
public SubmitRateLimitPushChallengeJob(@NonNull String challenge) {
|
||||
this(new Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
challenge);
|
||||
}
|
||||
|
||||
private SubmitRateLimitPushChallengeJob(@NonNull Parameters parameters, @NonNull String challenge) {
|
||||
super(parameters);
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putString(KEY_CHALLENGE, challenge).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().submitRateLimitPushChallenge(challenge);
|
||||
SignalStore.rateLimit().onProofAccepted();
|
||||
EventBus.getDefault().post(new SuccessEvent());
|
||||
RateLimitUtil.retryAllRateLimitedMessages(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof PushNetworkException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
}
|
||||
|
||||
public static final class SuccessEvent {
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<SubmitRateLimitPushChallengeJob> {
|
||||
@Override
|
||||
public @NonNull SubmitRateLimitPushChallengeJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new SubmitRateLimitPushChallengeJob(parameters, data.getString(KEY_CHALLENGE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class RateLimitValues extends SignalStoreValues {
|
||||
|
||||
private static final String TAG = Log.tag(RateLimitValues.class);
|
||||
|
||||
private static final String KEY_NEEDS_RECAPTCHA = "ratelimit.needs_recaptcha";
|
||||
private static final String KEY_CHALLENGE = "ratelimit.token";
|
||||
|
||||
RateLimitValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull List<String> getKeysToIncludeInBackup() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param challenge The token associated with the rate limit response.
|
||||
*/
|
||||
public void markNeedsRecaptcha(@NonNull String challenge) {
|
||||
Log.i(TAG, "markNeedsRecaptcha()");
|
||||
putBoolean(KEY_NEEDS_RECAPTCHA, true);
|
||||
putString(KEY_CHALLENGE, challenge);
|
||||
EventBus.getDefault().post(new RecaptchaRequiredEvent());
|
||||
}
|
||||
|
||||
public void onProofAccepted() {
|
||||
Log.i(TAG, "onProofAccepted()", new Throwable());
|
||||
putBoolean(KEY_NEEDS_RECAPTCHA, false);
|
||||
remove(KEY_CHALLENGE);
|
||||
}
|
||||
|
||||
public boolean needsRecaptcha() {
|
||||
return getBoolean(KEY_NEEDS_RECAPTCHA, false);
|
||||
}
|
||||
|
||||
public @NonNull String getChallenge() {
|
||||
return getString(KEY_CHALLENGE, "");
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public final class SignalStore {
|
||||
private final WallpaperValues wallpaperValues;
|
||||
private final PaymentsValues paymentsValues;
|
||||
private final ProxyValues proxyValues;
|
||||
private final RateLimitValues rateLimitValues;
|
||||
|
||||
private SignalStore() {
|
||||
this.store = new KeyValueStore(ApplicationDependencies.getApplication());
|
||||
@@ -55,6 +56,7 @@ public final class SignalStore {
|
||||
this.wallpaperValues = new WallpaperValues(store);
|
||||
this.paymentsValues = new PaymentsValues(store);
|
||||
this.proxyValues = new ProxyValues(store);
|
||||
this.rateLimitValues = new RateLimitValues(store);
|
||||
}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
@@ -75,6 +77,7 @@ public final class SignalStore {
|
||||
wallpaper().onFirstEverAppLaunch();
|
||||
paymentsValues().onFirstEverAppLaunch();
|
||||
proxy().onFirstEverAppLaunch();
|
||||
rateLimit().onFirstEverAppLaunch();
|
||||
}
|
||||
|
||||
public static List<String> getKeysToIncludeInBackup() {
|
||||
@@ -96,6 +99,7 @@ public final class SignalStore {
|
||||
keys.addAll(wallpaper().getKeysToIncludeInBackup());
|
||||
keys.addAll(paymentsValues().getKeysToIncludeInBackup());
|
||||
keys.addAll(proxy().getKeysToIncludeInBackup());
|
||||
keys.addAll(rateLimit().getKeysToIncludeInBackup());
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -176,6 +180,10 @@ public final class SignalStore {
|
||||
return INSTANCE.proxyValues;
|
||||
}
|
||||
|
||||
public static @NonNull RateLimitValues rateLimit() {
|
||||
return INSTANCE.rateLimitValues;
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||
}
|
||||
|
||||
@@ -67,4 +67,8 @@ abstract class SignalStoreValues {
|
||||
void putString(@NonNull String key, String value) {
|
||||
store.beginWrite().putString(key, value).apply();
|
||||
}
|
||||
|
||||
void remove(@NonNull String key) {
|
||||
store.beginWrite().remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
||||
@@ -944,6 +945,11 @@ public final class MessageContentProcessor {
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
||||
}
|
||||
|
||||
if (SignalStore.rateLimit().needsRecaptcha()) {
|
||||
Log.i(TAG, "Got a sent transcript while in reCAPTCHA mode. Assuming we're good to message again.");
|
||||
RateLimitUtil.retryAllRateLimitedMessages(context);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().setLastDesktopActivityTimestamp(message.getTimestamp());
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioManager;
|
||||
import android.media.Ringtone;
|
||||
@@ -131,7 +132,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
|
||||
public void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
if (visibleThread == threadId) {
|
||||
sendInThreadNotification(context, recipient);
|
||||
} else {
|
||||
@@ -145,6 +146,15 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
if (visibleThread == threadId) {
|
||||
sendInThreadNotification(context, recipient);
|
||||
} else {
|
||||
Log.w(TAG, "[Proof Required] Not notifying on old notifier.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDelayedNotifications() {
|
||||
executor.cancel();
|
||||
|
||||
@@ -16,7 +16,8 @@ public interface MessageNotifier {
|
||||
long getVisibleThread();
|
||||
void clearVisibleThread();
|
||||
void setLastDesktopActivityTimestamp(long timestamp);
|
||||
void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId);
|
||||
void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, long threadId);
|
||||
void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, long threadId);
|
||||
void cancelDelayedNotifications();
|
||||
void updateNotification(@NonNull Context context);
|
||||
void updateNotification(@NonNull Context context, long threadId);
|
||||
|
||||
@@ -53,10 +53,15 @@ public class OptimizedMessageNotifier implements MessageNotifier {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
|
||||
public void notifyMessageDeliveryFailed(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
getNotifier().notifyMessageDeliveryFailed(context, recipient, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyProofRequired(@NonNull Context context, @NonNull Recipient recipient, long threadId) {
|
||||
getNotifier().notifyProofRequired(context, recipient, threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelDelayedNotifications() {
|
||||
getNotifier().cancelDelayedNotifications();
|
||||
|
||||
@@ -76,6 +76,10 @@ class MessageNotifierV2(context: Application) : MessageNotifier {
|
||||
NotificationFactory.notifyMessageDeliveryFailed(context, recipient, threadId, visibleThread)
|
||||
}
|
||||
|
||||
override fun notifyProofRequired(context: Context, recipient: Recipient, threadId: Long) {
|
||||
NotificationFactory.notifyProofRequired(context, recipient, threadId, visibleThread)
|
||||
}
|
||||
|
||||
override fun cancelDelayedNotifications() {
|
||||
executor.cancel()
|
||||
}
|
||||
|
||||
@@ -315,6 +315,33 @@ object NotificationFactory {
|
||||
NotificationManagerCompat.from(context).safelyNotify(context, recipient, threadId.toInt(), builder.build())
|
||||
}
|
||||
|
||||
fun notifyProofRequired(context: Context, recipient: Recipient, threadId: Long, visibleThread: Long) {
|
||||
if (threadId == visibleThread) {
|
||||
notifyInThread(context, recipient, 0)
|
||||
return
|
||||
}
|
||||
|
||||
val intent: Intent = ConversationIntents.createBuilder(context, recipient.id, threadId)
|
||||
.build()
|
||||
.makeUniqueToPreventMerging()
|
||||
|
||||
val builder: NotificationBuilder = NotificationBuilder.create(context)
|
||||
|
||||
builder.apply {
|
||||
setSmallIcon(R.drawable.ic_notification)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.ic_info_outline))
|
||||
setContentTitle(context.getString(R.string.MessageNotifier_message_delivery_paused))
|
||||
setContentText(context.getString(R.string.MessageNotifier_verify_to_continue_messaging_on_signal))
|
||||
setContentIntent(PendingIntent.getActivity(context, 0, intent, 0))
|
||||
setOnlyAlertOnce(true)
|
||||
setAutoCancel(true)
|
||||
setAlarms(recipient)
|
||||
setChannelId(NotificationChannels.FAILURES)
|
||||
}
|
||||
|
||||
NotificationManagerCompat.from(context).safelyNotify(context, recipient, threadId.toInt(), builder.build())
|
||||
}
|
||||
|
||||
private fun NotificationManagerCompat.safelyNotify(context: Context, threadRecipient: Recipient?, notificationId: Int, notification: Notification) {
|
||||
try {
|
||||
notify(notificationId, notification)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class RateLimitUtil {
|
||||
|
||||
private static final String TAG = Log.tag(RateLimitUtil.class);
|
||||
|
||||
private RateLimitUtil() {}
|
||||
|
||||
/**
|
||||
* Forces a retry of all rate limited messages by editing jobs that are in the queue.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static void retryAllRateLimitedMessages(@NonNull Context context) {
|
||||
Set<Long> sms = DatabaseFactory.getSmsDatabase(context).getAllRateLimitedMessageIds();
|
||||
Set<Long> mms = DatabaseFactory.getMmsDatabase(context).getAllRateLimitedMessageIds();
|
||||
|
||||
if (sms.isEmpty() && mms.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Retrying " + sms.size() + " sms records and " + mms.size() + " mms records.");
|
||||
|
||||
DatabaseFactory.getSmsDatabase(context).clearRateLimitStatus(sms);
|
||||
DatabaseFactory.getMmsDatabase(context).clearRateLimitStatus(mms);
|
||||
|
||||
ApplicationDependencies.getJobManager().update((job, serializer) -> {
|
||||
Data data = serializer.deserialize(job.getSerializedData());
|
||||
|
||||
if (job.getFactoryKey().equals(PushTextSendJob.KEY) && sms.contains(PushTextSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else if (job.getFactoryKey().equals(PushMediaSendJob.KEY) && mms.contains(PushMediaSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else if (job.getFactoryKey().equals(PushGroupSendJob.KEY) && mms.contains(PushGroupSendJob.getMessageId(data))) {
|
||||
return job.withNextRunAttemptTime(System.currentTimeMillis());
|
||||
} else {
|
||||
return job;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Asks the user to solve a reCAPTCHA. If successful, triggers resends of all relevant message jobs.
|
||||
*/
|
||||
public class RecaptchaProofActivity extends PassphraseRequiredActivity {
|
||||
private static final String TAG = Log.tag(RecaptchaProofActivity.class);
|
||||
|
||||
private static final String RECAPTCHA_SCHEME = "signalcaptcha://";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
public static @NonNull Intent getIntent(@NonNull Context context) {
|
||||
return new Intent(context, RecaptchaProofActivity.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
setContentView(R.layout.recaptcha_activity);
|
||||
|
||||
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
requireSupportActionBar().setTitle(R.string.RecaptchaProofActivity_complete_verification);
|
||||
|
||||
WebView webView = findViewById(R.id.recaptcha_webview);
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.clearCache(true);
|
||||
webView.setBackgroundColor(Color.TRANSPARENT);
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url != null && url.startsWith(RECAPTCHA_SCHEME)) {
|
||||
handleToken(url.substring(RECAPTCHA_SCHEME.length()));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl(BuildConfig.RECAPTCHA_PROOF_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void handleToken(@NonNull String token) {
|
||||
SimpleProgressDialog.DismissibleDialog dialog = SimpleProgressDialog.showDelayed(this, 1000, 500);
|
||||
SimpleTask.run(() -> {
|
||||
String challenge = SignalStore.rateLimit().getChallenge();
|
||||
if (Util.isEmpty(challenge)) {
|
||||
Log.w(TAG, "No challenge available?");
|
||||
return new TokenResult(true, false);
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
try {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().submitRateLimitRecaptchaChallenge(challenge, token);
|
||||
RateLimitUtil.retryAllRateLimitedMessages(this);
|
||||
Log.i(TAG, "Successfully completed reCAPTCHA.");
|
||||
return new TokenResult(true, true);
|
||||
} catch (PushNetworkException e) {
|
||||
Log.w(TAG, "Network error during submission. Retrying.", e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Terminal failure during submission. Will clear state. May get a 428 later.", e);
|
||||
return new TokenResult(true, false);
|
||||
}
|
||||
|
||||
return new TokenResult(false, false);
|
||||
}, result -> {
|
||||
dialog.dismiss();
|
||||
|
||||
if (result.clearState) {
|
||||
Log.i(TAG, "Considering the response sufficient to clear the slate.");
|
||||
SignalStore.rateLimit().onProofAccepted();
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
Log.w(TAG, "Response was not a true success.");
|
||||
Toast.makeText(this, R.string.RecaptchaProofActivity_failed_to_submit, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
private static final class TokenResult {
|
||||
final boolean clearState;
|
||||
final boolean success;
|
||||
|
||||
private TokenResult(boolean clearState, boolean success) {
|
||||
this.clearState = clearState;
|
||||
this.success = success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
/**
|
||||
* A bottom sheet to be shown when we need to prompt the user to fill out a reCAPTCHA.
|
||||
*/
|
||||
public final class RecaptchaProofBottomSheetFragment extends BottomSheetDialogFragment {
|
||||
|
||||
private static final String TAG = Log.tag(RecaptchaProofBottomSheetFragment.class);
|
||||
|
||||
public static void show(@NonNull FragmentManager manager) {
|
||||
new RecaptchaProofBottomSheetFragment().show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Signal_DayNight_BottomSheet_Rounded);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.recaptcha_required_bottom_sheet, container, false);
|
||||
|
||||
view.findViewById(R.id.recaptcha_sheet_ok_button).setOnClickListener(v -> {
|
||||
dismissAllowingStateLoss();
|
||||
startActivity(RecaptchaProofActivity.getIntent(requireContext()));
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
if (manager.findFragmentByTag(tag) == null) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
} else {
|
||||
Log.i(TAG, "Ignoring repeat show.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.thoughtcrime.securesms.ratelimit;
|
||||
|
||||
public final class RecaptchaRequiredEvent {
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public final class PushChallengeRequest {
|
||||
|
||||
eventBus.register(this);
|
||||
try {
|
||||
accountManager.requestPushChallenge(fcmToken, e164number);
|
||||
accountManager.requestRegistrationPushChallenge(fcmToken, e164number);
|
||||
|
||||
latch.await(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
package org.thoughtcrime.securesms.transport;
|
||||
|
||||
public class RetryLaterException extends Exception {
|
||||
public RetryLaterException() {}
|
||||
|
||||
private final long backoff;
|
||||
|
||||
public RetryLaterException() {
|
||||
this(null, -1);
|
||||
}
|
||||
|
||||
public RetryLaterException(long backoff) {
|
||||
this(null, backoff);
|
||||
}
|
||||
|
||||
public RetryLaterException(Exception e) {
|
||||
this(e, -1);
|
||||
}
|
||||
|
||||
public RetryLaterException(Exception e, long backoff) {
|
||||
super(e);
|
||||
this.backoff = backoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The amount of time to wait before retrying again, or -1 if none is specified.
|
||||
*/
|
||||
public long getBackoff() {
|
||||
return backoff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user