From ef5b68eb35ee70e5238e18e43586bc10bc68a23a Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 17 May 2021 09:43:37 -0400 Subject: [PATCH] Add report spam in message request state. --- .../securesms/BlockUnblockDialog.java | 28 ++--- .../conversation/ConversationActivity.java | 7 +- .../securesms/database/MessageDatabase.java | 44 +++++++ .../securesms/database/MmsDatabase.java | 4 +- .../securesms/database/MmsSmsColumns.java | 1 + .../securesms/database/MmsSmsDatabase.java | 12 ++ .../securesms/database/PushDatabase.java | 3 +- .../securesms/database/SmsDatabase.java | 4 +- .../database/helpers/SQLCipherOpenHelper.java | 8 +- .../groups/GroupV1MessageProcessor.java | 2 +- .../v2/processing/GroupsV2StateProcessor.java | 2 +- .../securesms/jobs/JobManagerFactories.java | 1 + .../MultiDeviceMessageRequestResponseJob.java | 33 ++++-- .../securesms/jobs/ReportSpamJob.java | 107 ++++++++++++++++++ .../MessageRequestRepository.java | 13 ++- .../MessageRequestViewModel.java | 35 +++--- .../messages/MessageContentProcessor.java | 97 ++++++++-------- .../securesms/mms/IncomingMediaMessage.java | 12 +- .../securesms/sms/IncomingJoinedMessage.java | 2 +- .../securesms/sms/IncomingTextMessage.java | 15 ++- .../securesms/util/IdentityUtil.java | 8 +- app/src/main/res/values/strings.xml | 4 +- .../api/SignalServiceAccountManager.java | 4 + .../api/SignalServiceMessageReceiver.java | 10 +- .../api/crypto/SignalServiceCipher.java | 6 +- .../api/messages/SignalServiceContent.java | 21 ++++ .../api/messages/SignalServiceEnvelope.java | 23 +--- .../api/messages/SignalServiceMetadata.java | 9 +- .../internal/push/PushServiceSocket.java | 10 +- ...gnalServiceMetadataProtobufSerializer.java | 12 +- .../main/proto/InternalSerialization.proto | 1 + 31 files changed, 393 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/BlockUnblockDialog.java b/app/src/main/java/org/thoughtcrime/securesms/BlockUnblockDialog.java index 6d70b17ad..2cd48df70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BlockUnblockDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BlockUnblockDialog.java @@ -9,6 +9,8 @@ import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.Lifecycle; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; @@ -30,15 +32,15 @@ public final class BlockUnblockDialog { AlertDialog.Builder::show); } - public static void showBlockAndDeleteFor(@NonNull Context context, - @NonNull Lifecycle lifecycle, - @NonNull Recipient recipient, - @NonNull Runnable onBlock, - @NonNull Runnable onBlockAndDelete) + public static void showBlockAndReportSpamFor(@NonNull Context context, + @NonNull Lifecycle lifecycle, + @NonNull Recipient recipient, + @NonNull Runnable onBlock, + @NonNull Runnable onBlockAndReportSpam) { SimpleTask.run(lifecycle, - () -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete), - AlertDialog.Builder::show); + () -> buildBlockFor(context, recipient, onBlock, onBlockAndReportSpam), + AlertDialog.Builder::show); } public static void showUnblockFor(@NonNull Context context, @@ -55,11 +57,11 @@ public final class BlockUnblockDialog { private static AlertDialog.Builder buildBlockFor(@NonNull Context context, @NonNull Recipient recipient, @NonNull Runnable onBlock, - @Nullable Runnable onBlockAndDelete) + @Nullable Runnable onBlockAndReportSpam) { recipient = recipient.resolve(); - AlertDialog.Builder builder = new AlertDialog.Builder(context); + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context); Resources resources = context.getResources(); if (recipient.isGroup()) { @@ -78,10 +80,10 @@ public final class BlockUnblockDialog { builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context))); builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages); - if (onBlockAndDelete != null) { + if (onBlockAndReportSpam != null) { builder.setNeutralButton(android.R.string.cancel, null); - builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_delete, (d, w) -> onBlockAndDelete.run()); - builder.setNegativeButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run()); + builder.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run()); + builder.setPositiveButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run()); } else { builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run())); builder.setNegativeButton(android.R.string.cancel, null); @@ -98,7 +100,7 @@ public final class BlockUnblockDialog { { recipient = recipient.resolve(); - AlertDialog.Builder builder = new AlertDialog.Builder(context); + AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context); Resources resources = context.getResources(); if (recipient.isGroup()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index c89d2b76d..a2ec94d6f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -249,7 +249,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerManagementActivity; import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent; import org.thoughtcrime.securesms.stickers.StickerSearchRepository; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.BitmapUtil; @@ -3369,6 +3368,10 @@ public class ConversationActivity extends PassphraseRequiredActivity case ACCEPTED: hideMessageRequestBusy(); break; + case BLOCKED_AND_REPORTED: + hideMessageRequestBusy(); + Toast.makeText(this, R.string.ConversationActivity__reported_as_spam_and_blocked, Toast.LENGTH_SHORT).show(); + break; case DELETED: case BLOCKED: hideMessageRequestBusy(); @@ -3625,7 +3628,7 @@ public class ConversationActivity extends PassphraseRequiredActivity return; } - BlockUnblockDialog.showBlockAndDeleteFor(this, getLifecycle(), recipient, requestModel::onBlock, requestModel::onBlockAndDelete); + BlockUnblockDialog.showBlockAndReportSpamFor(this, getLifecycle(), recipient, requestModel::onBlock, requestModel::onBlockAndReportSpam); } private void onMessageRequestUnblockClicked(@NonNull MessageRequestViewModel requestModel) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 6e38aea50..6347f43b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; +import org.thoughtcrime.securesms.util.CursorUtil; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.Util; @@ -403,6 +404,25 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns } } + public @NonNull List getReportSpamMessageServerGuids(long threadId, long timestamp) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = THREAD_ID + " = ? AND " + getDateReceivedColumnName() + " <= ?"; + String[] args = SqlUtil.buildArgs(threadId, timestamp); + + List data = new ArrayList<>(); + try (Cursor cursor = db.query(getTableName(), new String[] { RECIPIENT_ID, SERVER_GUID, getDateReceivedColumnName() }, query, args, null, null, getDateReceivedColumnName() + " DESC", "3")) { + while (cursor.moveToNext()) { + RecipientId id = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); + String serverGuid = CursorUtil.requireString(cursor, SERVER_GUID); + long dateReceived = CursorUtil.requireLong(cursor, getDateReceivedColumnName()); + if (!Util.isEmpty(serverGuid)) { + data.add(new ReportSpamData(id, serverGuid, dateReceived)); + } + } + } + return data; + } + protected static List parseReactions(@NonNull Cursor cursor) { byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS)); @@ -770,4 +790,28 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns MessageRecord getCurrent(); void close(); } + + public static class ReportSpamData { + private final RecipientId recipientId; + private final String serverGuid; + private final long dateReceived; + + public ReportSpamData(RecipientId recipientId, String serverGuid, long dateReceived) { + this.recipientId = recipientId; + this.serverGuid = serverGuid; + this.dateReceived = dateReceived; + } + + public @NonNull RecipientId getRecipientId() { + return recipientId; + } + + public @NonNull String getServerGuid() { + return serverGuid; + } + + public long getDateReceived() { + return dateReceived; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index d17507694..210a9513c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -185,7 +185,8 @@ public class MmsDatabase extends MessageDatabase { REMOTE_DELETED + " INTEGER DEFAULT 0, " + MENTIONS_SELF + " INTEGER DEFAULT 0, " + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " + - VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0);"; + VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + + SERVER_GUID + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", @@ -1317,6 +1318,7 @@ public class MmsDatabase extends MessageDatabase { contentValues.put(VIEW_ONCE, retrieved.isViewOnce() ? 1 : 0); contentValues.put(READ, retrieved.isExpirationUpdate() ? 1 : 0); contentValues.put(UNIDENTIFIED, retrieved.isUnidentified()); + contentValues.put(SERVER_GUID, retrieved.getServerGuid()); if (!contentValues.containsKey(DATE_SENT)) { contentValues.put(DATE_SENT, contentValues.getAsLong(DATE_RECEIVED)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 3ab69763d..48658050a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -27,6 +27,7 @@ public interface MmsSmsColumns { public static final String REACTIONS_UNREAD = "reactions_unread"; public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static final String REMOTE_DELETED = "remote_deleted"; + public static final String SERVER_GUID = "server_guid"; /** * For storage efficiency, all types are stored within a single 64-bit integer column in the diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 53f2098b0..55f0308dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -38,11 +38,13 @@ import org.thoughtcrime.securesms.util.CursorUtil; import org.whispersystems.libsignal.util.Pair; import java.io.Closeable; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public class MmsSmsDatabase extends Database { @@ -564,6 +566,16 @@ public class MmsSmsDatabase extends Database { DatabaseFactory.getMmsDatabase(context).deleteAbandonedMessages(); } + public @NonNull List getReportSpamMessageServerData(long threadId, long timestamp, int limit) { + List data = new ArrayList<>(); + data.addAll(DatabaseFactory.getSmsDatabase(context).getReportSpamMessageServerGuids(threadId, timestamp)); + data.addAll(DatabaseFactory.getMmsDatabase(context).getReportSpamMessageServerGuids(threadId, timestamp)); + return data.stream() + .sorted((l, r) -> -Long.compare(l.getDateReceived(), r.getDateReceived())) + .limit(limit) + .collect(Collectors.toList()); + } + private Cursor queryTables(String[] projection, String selection, String order, String limit) { String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java index 7a558d376..27d663e09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/PushDatabase.java @@ -9,7 +9,6 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; @@ -68,7 +67,7 @@ public class PushDatabase extends Database { values.put(TIMESTAMP, envelope.getTimestamp()); values.put(SERVER_RECEIVED_TIMESTAMP, envelope.getServerReceivedTimestamp()); values.put(SERVER_DELIVERED_TIMESTAMP, envelope.getServerDeliveredTimestamp()); - values.put(SERVER_GUID, envelope.getUuid()); + values.put(SERVER_GUID, envelope.getServerGuid()); return databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index f1ba08d4d..030986519 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -123,7 +123,8 @@ public class SmsDatabase extends MessageDatabase { REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " + - NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0);"; + NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0," + + SERVER_GUID + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");", @@ -1105,6 +1106,7 @@ public class SmsDatabase extends MessageDatabase { values.put(BODY, message.getMessageBody()); values.put(TYPE, type); values.put(THREAD_ID, threadId); + values.put(SERVER_GUID, message.getServerGuid()); if (message.isPush() && isDuplicate(message, threadId)) { Log.w(TAG, "Duplicate message (" + message.getSentTimestampMillis() + "), ignoring..."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index f3332e144..db4e8a15f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -181,8 +181,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int CLEAN_REACTION_NOTIFICATIONS = 96; private static final int STORAGE_SERVICE_REFACTOR = 97; private static final int CLEAR_MMS_STORAGE_IDS = 98; + private static final int SERVER_GUID = 99; - private static final int DATABASE_VERSION = 98; + private static final int DATABASE_VERSION = 99; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1457,6 +1458,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab Log.d(TAG, "Cleared storageIds from " + deleteCount + " rows. They were either MMS groups or empty contacts."); } + if (oldVersion < SERVER_GUID) { + db.execSQL("ALTER TABLE sms ADD COLUMN server_guid TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE mms ADD COLUMN server_guid TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java index 2986c5f64..eebf8decc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupV1MessageProcessor.java @@ -248,7 +248,7 @@ public final class GroupV1MessageProcessor { } else { MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); String body = Base64.encodeBytes(storage.toByteArray()); - IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt()); + IncomingTextMessage incoming = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), body, Optional.of(GroupId.v1orThrow(group.getGroupId())), 0, content.isNeedsReceipt(), content.getServerUuid()); IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, storage, body); Optional insertResult = smsDatabase.insertMessageInbox(groupMessage); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 24e94c13d..44c314835 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -521,7 +521,7 @@ public final class GroupsV2StateProcessor { } else { MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); RecipientId sender = RecipientId.from(editor.get(), null); - IncomingTextMessage incoming = new IncomingTextMessage(sender, -1, timestamp, timestamp, "", Optional.of(groupId), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(sender, -1, timestamp, timestamp, "", Optional.of(groupId), 0, false, null); IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, decryptedGroupV2Context); if (!smsDatabase.insertMessageInbox(groupMessage).isPresent()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 52a1d0ce3..b5607cc1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -156,6 +156,7 @@ public final class JobManagerFactories { put(PaymentSendJob.KEY, new PaymentSendJob.Factory()); put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory()); put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory()); + put(ReportSpamJob.KEY, new ReportSpamJob.Factory()); // Migrations put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java index 5ba2ea846..6b14c1f74 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceMessageRequestResponseJob.java @@ -51,14 +51,18 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob { return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK_AND_DELETE); } - private MultiDeviceMessageRequestResponseJob(@NonNull RecipientId threadRecipient, @NonNull Type type) { - this(new Parameters.Builder() - .setQueue("MultiDeviceMessageRequestResponseJob") - .addConstraint(NetworkConstraint.KEY) - .setMaxAttempts(Parameters.UNLIMITED) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .build(), threadRecipient, type); + public static @NonNull MultiDeviceMessageRequestResponseJob forBlockAndReportSpam(@NonNull RecipientId threadRecipient) { + return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK); + } + private MultiDeviceMessageRequestResponseJob(@NonNull RecipientId threadRecipient, @NonNull Type type) { + this(new Parameters.Builder().setQueue("MultiDeviceMessageRequestResponseJob") + .addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(Parameters.UNLIMITED) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .build(), + threadRecipient, + type); } private MultiDeviceMessageRequestResponseJob(@NonNull Parameters parameters, @@ -111,11 +115,16 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob { private static MessageRequestResponseMessage.Type localToRemoteType(@NonNull Type type) { switch (type) { - case ACCEPT: return MessageRequestResponseMessage.Type.ACCEPT; - case DELETE: return MessageRequestResponseMessage.Type.DELETE; - case BLOCK: return MessageRequestResponseMessage.Type.BLOCK; - case BLOCK_AND_DELETE: return MessageRequestResponseMessage.Type.BLOCK_AND_DELETE; - default: return MessageRequestResponseMessage.Type.UNKNOWN; + case ACCEPT: + return MessageRequestResponseMessage.Type.ACCEPT; + case DELETE: + return MessageRequestResponseMessage.Type.DELETE; + case BLOCK: + return MessageRequestResponseMessage.Type.BLOCK; + case BLOCK_AND_DELETE: + return MessageRequestResponseMessage.Type.BLOCK_AND_DELETE; + default: + return MessageRequestResponseMessage.Type.UNKNOWN; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java new file mode 100644 index 000000000..8d560f45c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReportSpamJob.java @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageDatabase.ReportSpamData; +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.recipients.Recipient; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; + +import java.io.IOException; +import java.util.List; + +/** + * Report 1 to {@link #MAX_MESSAGE_COUNT} message guids received prior to {@link #timestamp} in {@link #threadId} to the server as spam. + */ +public class ReportSpamJob extends BaseJob { + + public static final String KEY = "ReportSpamJob"; + private static final String TAG = Log.tag(ReportSpamJob.class); + + private static final String KEY_THREAD_ID = "thread_id"; + private static final String KEY_TIMESTAMP = "timestamp"; + private static final int MAX_MESSAGE_COUNT = 3; + + private final long threadId; + private final long timestamp; + + public ReportSpamJob(long threadId, long timestamp) { + this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY) + .setMaxAttempts(5) + .setQueue("ReportSpamJob") + .build(), + threadId, + timestamp); + } + + private ReportSpamJob(@NonNull Parameters parameters, long threadId, long timestamp) { + super(parameters); + this.threadId = threadId; + this.timestamp = timestamp; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putLong(KEY_THREAD_ID, threadId) + .putLong(KEY_TIMESTAMP, timestamp) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException { + if (!TextSecurePreferences.isPushRegistered(context)) { + return; + } + + int count = 0; + List reportSpamData = DatabaseFactory.getMmsSmsDatabase(context).getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT); + SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); + for (ReportSpamData data : reportSpamData) { + Optional e164 = Recipient.resolved(data.getRecipientId()).getE164(); + if (e164.isPresent()) { + signalServiceAccountManager.reportSpam(e164.get(), data.getServerGuid()); + count++; + } else { + Log.w(TAG, "Unable to report spam without an e164 for " + data.getRecipientId()); + } + } + Log.i(TAG, "Reported " + count + " out of " + reportSpamData.size() + " messages in thread " + threadId + " as spam"); + } + + @Override + public boolean onShouldRetry(@NonNull Exception exception) { + if (exception instanceof ServerRejectedException) { + return false; + } else if (exception instanceof NonSuccessfulResponseCodeException) { + return ((NonSuccessfulResponseCodeException) exception).is5xx(); + } + + return exception instanceof IOException; + } + + @Override + public void onFailure() { + Log.w(TAG, "Canceling report spam for thread " + threadId); + } + + public static final class Factory implements Job.Factory { + @Override + public @NonNull ReportSpamJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new ReportSpamJob(parameters, data.getLong(KEY_THREAD_ID), data.getLong(KEY_TIMESTAMP)); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index 83a02ad11..ac2ec5f91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; +import org.thoughtcrime.securesms.jobs.ReportSpamJob; import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.recipients.LiveRecipient; @@ -226,10 +227,10 @@ final class MessageRequestRepository { }); } - void blockAndDeleteMessageRequest(@NonNull LiveRecipient liveRecipient, - long threadId, - @NonNull Runnable onMessageRequestBlocked, - @NonNull GroupChangeErrorCallback error) + void blockAndReportSpamMessageRequest(@NonNull LiveRecipient liveRecipient, + long threadId, + @NonNull Runnable onMessageRequestBlocked, + @NonNull GroupChangeErrorCallback error) { executor.execute(() -> { Recipient recipient = liveRecipient.resolve(); @@ -242,10 +243,10 @@ final class MessageRequestRepository { } liveRecipient.refresh(); - DatabaseFactory.getThreadDatabase(context).deleteConversation(threadId); + ApplicationDependencies.getJobManager().add(new ReportSpamJob(threadId, System.currentTimeMillis())); if (TextSecurePreferences.isMultiDevice(context)) { - ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forBlockAndDelete(liveRecipient.getId())); + ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forBlockAndReportSpam(liveRecipient.getId())); } onMessageRequestBlocked.run(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java index 3c98d03ec..991a7b491 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestViewModel.java @@ -8,7 +8,6 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; @@ -20,7 +19,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.SingleLiveEvent; -import org.thoughtcrime.securesms.util.livedata.LiveDataTriple; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.Store; @@ -29,16 +27,16 @@ import java.util.List; public class MessageRequestViewModel extends ViewModel { - private final SingleLiveEvent status = new SingleLiveEvent<>(); - private final SingleLiveEvent failures = new SingleLiveEvent<>(); - private final MutableLiveData recipient = new MutableLiveData<>(); - private final LiveData messageData; - private final MutableLiveData> groups = new MutableLiveData<>(Collections.emptyList()); - private final MutableLiveData groupInfo = new MutableLiveData<>(GroupInfo.ZERO); - private final LiveData requestReviewDisplayState; + private final SingleLiveEvent status = new SingleLiveEvent<>(); + private final SingleLiveEvent failures = new SingleLiveEvent<>(); + private final MutableLiveData recipient = new MutableLiveData<>(); + private final MutableLiveData> groups = new MutableLiveData<>(Collections.emptyList()); + private final MutableLiveData groupInfo = new MutableLiveData<>(GroupInfo.ZERO); private final Store recipientInfoStore = new Store<>(new RecipientInfo(null, null, null, null)); - private final MessageRequestRepository repository; + private final LiveData messageData; + private final LiveData requestReviewDisplayState; + private final MessageRequestRepository repository; private LiveRecipient liveRecipient; private long threadId; @@ -142,11 +140,11 @@ public class MessageRequestViewModel extends ViewModel { } @MainThread - public void onBlockAndDelete() { - repository.blockAndDeleteMessageRequest(liveRecipient, - threadId, - () -> status.postValue(Status.BLOCKED), - this::onGroupChangeError); + public void onBlockAndReportSpam() { + repository.blockAndReportSpamMessageRequest(liveRecipient, + threadId, + () -> status.postValue(Status.BLOCKED_AND_REPORTED), + this::onGroupChangeError); } private void onGroupChangeError(@NonNull GroupChangeFailureReason error) { @@ -187,8 +185,8 @@ public class MessageRequestViewModel extends ViewModel { public static class RecipientInfo { @Nullable private final Recipient recipient; - @NonNull private final GroupInfo groupInfo; - @NonNull private final List sharedGroups; + @NonNull private final GroupInfo groupInfo; + @NonNull private final List sharedGroups; @Nullable private final MessageRequestState messageRequestState; private RecipientInfo(@Nullable Recipient recipient, @Nullable GroupInfo groupInfo, @Nullable List sharedGroups, @Nullable MessageRequestState messageRequestState) { @@ -230,6 +228,7 @@ public class MessageRequestViewModel extends ViewModel { IDLE, BLOCKING, BLOCKED, + BLOCKED_AND_REPORTED, DELETING, DELETED, ACCEPTING, @@ -243,7 +242,7 @@ public class MessageRequestViewModel extends ViewModel { } public static final class MessageData { - private final Recipient recipient; + private final Recipient recipient; private final MessageRequestState messageState; public MessageData(@NonNull Recipient recipient, @NonNull MessageRequestState messageState) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index fb4ec93ab..dfa33ca64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -576,11 +576,14 @@ public final class MessageContentProcessor { { MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(), - content.getSenderDevice(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - "", Optional.absent(), 0, - content.isNeedsReceipt()); + content.getSenderDevice(), + content.getTimestamp(), + content.getServerReceivedTimestamp(), + "", + Optional.absent(), + 0, + content.isNeedsReceipt(), + content.getServerUuid()); Long threadId; @@ -686,21 +689,22 @@ public final class MessageContentProcessor { MessageDatabase database = DatabaseFactory.getMmsDatabase(context); Recipient sender = Recipient.externalHighTrustPush(context, content.getSender()); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(), - content.getTimestamp(), - content.getServerReceivedTimestamp(), - -1, - expiresInSeconds * 1000L, - true, - false, - content.isNeedsReceipt(), - Optional.absent(), - groupContext, - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent()); + content.getTimestamp(), + content.getServerReceivedTimestamp(), + -1, + expiresInSeconds * 1000L, + true, + false, + content.isNeedsReceipt(), + Optional.absent(), + groupContext, + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + content.getServerUuid()); database.insertSecureDecryptedMessageInbox(mediaMessage, -1); @@ -1104,22 +1108,24 @@ public final class MessageContentProcessor { Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional> mentions = getMentions(message.getMentions()); Optional sticker = getStickerAttachment(message.getSticker()); - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(RecipientId.fromHighTrust(content.getSender()), - message.getTimestamp(), - content.getServerReceivedTimestamp(), - -1, - message.getExpiresInSeconds() * 1000L, - false, - message.isViewOnce(), - content.isNeedsReceipt(), - message.getBody(), - message.getGroupContext(), - message.getAttachments(), - quote, - sharedContacts, - linkPreviews, - mentions, - sticker); + + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(RecipientId.fromHighTrust(content.getSender()), + message.getTimestamp(), + content.getServerReceivedTimestamp(), + -1, + message.getExpiresInSeconds() * 1000L, + false, + message.isViewOnce(), + content.isNeedsReceipt(), + message.getBody(), + message.getGroupContext(), + message.getAttachments(), + quote, + sharedContacts, + linkPreviews, + mentions, + sticker, + content.getServerUuid()); insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); @@ -1328,13 +1334,14 @@ public final class MessageContentProcessor { notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice()); IncomingTextMessage textMessage = new IncomingTextMessage(RecipientId.fromHighTrust(content.getSender()), - content.getSenderDevice(), - message.getTimestamp(), - content.getServerReceivedTimestamp(), - body, - groupId, - message.getExpiresInSeconds() * 1000L, - content.isNeedsReceipt()); + content.getSenderDevice(), + message.getTimestamp(), + content.getServerReceivedTimestamp(), + body, + groupId, + message.getExpiresInSeconds() * 1000L, + content.isNeedsReceipt(), + content.getServerUuid()); textMessage = new IncomingEncryptedMessage(textMessage, body); Optional insertResult = database.insertMessageInbox(textMessage); @@ -1803,8 +1810,8 @@ public final class MessageContentProcessor { private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional groupId) { MessageDatabase database = DatabaseFactory.getSmsDatabase(context); IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(), - senderDevice, timestamp, -1, "", - groupId, 0, false); + senderDevice, timestamp, -1, "", + groupId, 0, false, null); textMessage = new IncomingEncryptedMessage(textMessage, ""); return database.insertMessageInbox(textMessage); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java index 324e638a2..1a5dddd6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/IncomingMediaMessage.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.mms; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; @@ -32,6 +33,7 @@ public class IncomingMediaMessage { private final QuoteModel quote; private final boolean unidentified; private final boolean viewOnce; + private final String serverGuid; private final List attachments = new LinkedList<>(); private final List sharedContacts = new LinkedList<>(); @@ -63,6 +65,7 @@ public class IncomingMediaMessage { this.viewOnce = viewOnce; this.quote = null; this.unidentified = unidentified; + this.serverGuid = null; this.attachments.addAll(attachments); this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList())); @@ -84,7 +87,8 @@ public class IncomingMediaMessage { Optional> sharedContacts, Optional> linkPreviews, Optional> mentions, - Optional sticker) + Optional sticker, + @Nullable String serverGuid) { this.push = true; this.from = from; @@ -109,6 +113,8 @@ public class IncomingMediaMessage { if (sticker.isPresent()) { this.attachments.add(sticker.get()); } + + this.serverGuid = serverGuid; } public int getSubscriptionId() { @@ -178,4 +184,8 @@ public class IncomingMediaMessage { public boolean isUnidentified() { return unidentified; } + + public @Nullable String getServerGuid() { + return serverGuid; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java index 22fb7aa8f..22653794c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingJoinedMessage.java @@ -6,7 +6,7 @@ import org.whispersystems.libsignal.util.guava.Optional; public class IncomingJoinedMessage extends IncomingTextMessage { public IncomingJoinedMessage(RecipientId sender) { - super(sender, 1, System.currentTimeMillis(), -1, null, Optional.absent(), 0, false); + super(sender, 1, System.currentTimeMillis(), -1, null, Optional.absent(), 0, false, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java index d00b617fa..c4a1f9ca1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingTextMessage.java @@ -44,6 +44,7 @@ public class IncomingTextMessage implements Parcelable { private final int subscriptionId; private final long expiresInMillis; private final boolean unidentified; + @Nullable private final String serverGuid; public IncomingTextMessage(@NonNull RecipientId sender, @NonNull SmsMessage message, int subscriptionId) { this.message = message.getDisplayMessageBody(); @@ -60,6 +61,7 @@ public class IncomingTextMessage implements Parcelable { this.groupId = null; this.push = false; this.unidentified = false; + this.serverGuid = null; } public IncomingTextMessage(@NonNull RecipientId sender, @@ -69,7 +71,8 @@ public class IncomingTextMessage implements Parcelable { String encodedBody, Optional groupId, long expiresInMillis, - boolean unidentified) + boolean unidentified, + String serverGuid) { this.message = encodedBody; this.sender = sender; @@ -85,6 +88,7 @@ public class IncomingTextMessage implements Parcelable { this.expiresInMillis = expiresInMillis; this.unidentified = unidentified; this.groupId = groupId.orNull(); + this.serverGuid = serverGuid; } public IncomingTextMessage(Parcel in) { @@ -102,6 +106,7 @@ public class IncomingTextMessage implements Parcelable { this.subscriptionId = in.readInt(); this.expiresInMillis = in.readLong(); this.unidentified = in.readInt() == 1; + this.serverGuid = in.readString(); } public IncomingTextMessage(IncomingTextMessage base, String newBody) { @@ -119,6 +124,7 @@ public class IncomingTextMessage implements Parcelable { this.subscriptionId = base.getSubscriptionId(); this.expiresInMillis = base.getExpiresIn(); this.unidentified = base.isUnidentified(); + this.serverGuid = base.getServerGuid(); } public IncomingTextMessage(List fragments) { @@ -142,6 +148,7 @@ public class IncomingTextMessage implements Parcelable { this.subscriptionId = fragments.get(0).getSubscriptionId(); this.expiresInMillis = fragments.get(0).getExpiresIn(); this.unidentified = fragments.get(0).isUnidentified(); + this.serverGuid = fragments.get(0).getServerGuid(); } protected IncomingTextMessage(@NonNull RecipientId sender, @Nullable GroupId groupId) @@ -160,6 +167,7 @@ public class IncomingTextMessage implements Parcelable { this.subscriptionId = -1; this.expiresInMillis = 0; this.unidentified = false; + this.serverGuid = null; } public int getSubscriptionId() { @@ -265,6 +273,10 @@ public class IncomingTextMessage implements Parcelable { return unidentified; } + public @Nullable String getServerGuid() { + return serverGuid; + } + @Override public int describeContents() { return 0; @@ -285,5 +297,6 @@ public class IncomingTextMessage implements Parcelable { out.writeInt(subscriptionId); out.writeLong(expiresInMillis); out.writeInt(unidentified ? 1 : 0); + out.writeString(serverGuid); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java index f7be227b7..a12c31583 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IdentityUtil.java @@ -74,7 +74,7 @@ public final class IdentityUtil { if (groupRecord.getMembers().contains(recipient.getId()) && groupRecord.isActive() && !groupRecord.isMms()) { if (remote) { - IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, -1, null, Optional.of(groupRecord.getId()), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, -1, null, Optional.of(groupRecord.getId()), 0, false, null); if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming); else incoming = new IncomingIdentityDefaultMessage(incoming); @@ -96,7 +96,7 @@ public final class IdentityUtil { } if (remote) { - IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, -1, null, Optional.absent(), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipient.getId(), 1, time, -1, null, Optional.absent(), 0, false, null); if (verified) incoming = new IncomingIdentityVerifiedMessage(incoming); else incoming = new IncomingIdentityDefaultMessage(incoming); @@ -125,7 +125,7 @@ public final class IdentityUtil { while ((groupRecord = reader.getNext()) != null) { if (groupRecord.getMembers().contains(recipientId) && groupRecord.isActive()) { - IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, time, null, Optional.of(groupRecord.getId()), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, time, null, Optional.of(groupRecord.getId()), 0, false, null); IncomingIdentityUpdateMessage groupUpdate = new IncomingIdentityUpdateMessage(incoming); smsDatabase.insertMessageInbox(groupUpdate); @@ -133,7 +133,7 @@ public final class IdentityUtil { } } - IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, -1, null, Optional.absent(), 0, false); + IncomingTextMessage incoming = new IncomingTextMessage(recipientId, 1, time, -1, null, Optional.absent(), 0, false, null); IncomingIdentityUpdateMessage individualUpdate = new IncomingIdentityUpdateMessage(incoming); Optional insertResult = smsDatabase.insertMessageInbox(individualUpdate); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ba9285fd..3da51b48a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -116,7 +116,7 @@ Unblock %1$s? Block Block and Leave - Block and Delete + Report spam and block Today @@ -285,6 +285,8 @@ Error sending media + Reported as spam and blocked. + %d unread message diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 75ddb3344..1b5cb41df 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -641,6 +641,10 @@ public class SignalServiceAccountManager { return this.pushServiceSocket.getCurrencyConversions(); } + public void reportSpam(String e164, String serverGuid) throws IOException { + this.pushServiceSocket.reportSpam(e164, serverGuid); + } + /** * @return The avatar URL path, if one was written. */ diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 13b24c950..412e7fb2e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -6,7 +6,6 @@ package org.whispersystems.signalservice.api; -import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.whispersystems.libsignal.InvalidMessageException; @@ -23,8 +22,6 @@ import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; -import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -300,8 +297,11 @@ public class SignalServiceMessageReceiver { callback.onMessage(envelope); results.add(envelope); - if (envelope.hasUuid()) socket.acknowledgeMessage(envelope.getUuid()); - else socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp()); + if (envelope.hasServerGuid()) { + socket.acknowledgeMessage(envelope.getServerGuid()); + } else { + socket.acknowledgeMessage(entity.getSourceE164(), entity.getTimestamp()); + } } return results; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java index 2dcdc5019..635b80247 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/SignalServiceCipher.java @@ -180,14 +180,14 @@ public class SignalServiceCipher { SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress)); paddedMessage = sessionCipher.decrypt(new PreKeySignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid()); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isSignalMessage()) { SignalProtocolAddress sourceAddress = getPreferredProtocolAddress(signalProtocolStore, envelope.getSourceAddress(), envelope.getSourceDevice()); SignalSessionCipher sessionCipher = new SignalSessionCipher(sessionLock, new SessionCipher(signalProtocolStore, sourceAddress)); paddedMessage = sessionCipher.decrypt(new SignalMessage(ciphertext)); - metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false); + metadata = new SignalServiceMetadata(envelope.getSourceAddress(), envelope.getSourceDevice(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), false, envelope.getServerGuid()); sessionVersion = sessionCipher.getSessionVersion(); } else if (envelope.isUnidentifiedSender()) { SignalSealedSessionCipher sealedSessionCipher = new SignalSealedSessionCipher(sessionLock, new SealedSessionCipher(signalProtocolStore, localAddress.getUuid().orNull(), localAddress.getNumber().orNull(), 1)); @@ -196,7 +196,7 @@ public class SignalServiceCipher { SignalProtocolAddress protocolAddress = getPreferredProtocolAddress(signalProtocolStore, resultAddress, result.getDeviceId()); paddedMessage = result.getPaddedMessage(); - metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true); + metadata = new SignalServiceMetadata(resultAddress, result.getDeviceId(), envelope.getTimestamp(), envelope.getServerReceivedTimestamp(), envelope.getServerDeliveredTimestamp(), true, envelope.getServerGuid()); sessionVersion = sealedSessionCipher.getSessionVersion(protocolAddress); } else { throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index a11a1b09f..5882dc47a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -69,6 +69,7 @@ public final class SignalServiceContent { private final long serverDeliveredTimestamp; private final boolean needsReceipt; private final SignalServiceContentProto serializedState; + private final String serverUuid; private final Optional message; private final Optional synchronizeMessage; @@ -83,6 +84,7 @@ public final class SignalServiceContent { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, + String serverUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -91,6 +93,7 @@ public final class SignalServiceContent { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; this.serializedState = serializedState; this.message = Optional.fromNullable(message); @@ -107,6 +110,7 @@ public final class SignalServiceContent { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, + String serverUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -115,6 +119,7 @@ public final class SignalServiceContent { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; this.serializedState = serializedState; this.message = Optional.absent(); @@ -131,6 +136,7 @@ public final class SignalServiceContent { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, + String serverUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -139,6 +145,7 @@ public final class SignalServiceContent { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; this.serializedState = serializedState; this.message = Optional.absent(); @@ -155,6 +162,7 @@ public final class SignalServiceContent { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, + String serverUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -163,6 +171,7 @@ public final class SignalServiceContent { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; this.serializedState = serializedState; this.message = Optional.absent(); @@ -179,6 +188,7 @@ public final class SignalServiceContent { long serverReceivedTimestamp, long serverDeliveredTimestamp, boolean needsReceipt, + String serverUuid, SignalServiceContentProto serializedState) { this.sender = sender; @@ -187,6 +197,7 @@ public final class SignalServiceContent { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverUuid = serverUuid; this.serializedState = serializedState; this.message = Optional.absent(); @@ -240,6 +251,10 @@ public final class SignalServiceContent { return needsReceipt; } + public String getServerUuid() { + return serverUuid; + } + public byte[] serialize() { return serializedState.toByteArray(); } @@ -276,6 +291,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), + metadata.getServerGuid(), serviceContentProto); } else if (serviceContentProto.getDataCase() == SignalServiceContentProto.DataCase.CONTENT) { SignalServiceProtos.Content message = serviceContentProto.getContent(); @@ -288,6 +304,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), + metadata.getServerGuid(), serviceContentProto); } else if (message.hasSyncMessage() && localAddress.matches(metadata.getSender())) { return new SignalServiceContent(createSynchronizeMessage(metadata, message.getSyncMessage()), @@ -297,6 +314,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), + metadata.getServerGuid(), serviceContentProto); } else if (message.hasCallMessage()) { return new SignalServiceContent(createCallMessage(message.getCallMessage()), @@ -306,6 +324,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), + metadata.getServerGuid(), serviceContentProto); } else if (message.hasReceiptMessage()) { return new SignalServiceContent(createReceiptMessage(metadata, message.getReceiptMessage()), @@ -315,6 +334,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), metadata.isNeedsReceipt(), + metadata.getServerGuid(), serviceContentProto); } else if (message.hasTypingMessage()) { return new SignalServiceContent(createTypingMessage(metadata, message.getTypingMessage()), @@ -324,6 +344,7 @@ public final class SignalServiceContent { metadata.getServerReceivedTimestamp(), metadata.getServerDeliveredTimestamp(), false, + metadata.getServerGuid(), serviceContentProto); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java index ca876185e..b068efc1d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceEnvelope.java @@ -9,29 +9,14 @@ package org.whispersystems.signalservice.api.messages; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope; import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceEnvelopeProto; -import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.util.Base64; import java.io.IOException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.Mac; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; /** * This class represents an encrypted Signal Service envelope. @@ -129,11 +114,11 @@ public class SignalServiceEnvelope { this.serverDeliveredTimestamp = serverDeliveredTimestamp; } - public String getUuid() { + public String getServerGuid() { return envelope.getServerGuid(); } - public boolean hasUuid() { + public boolean hasServerGuid() { return envelope.hasServerGuid(); } @@ -285,8 +270,8 @@ public class SignalServiceEnvelope { builder.setContent(ByteString.copyFrom(getContent())); } - if (hasUuid()) { - builder.setServerGuid(getUuid()); + if (hasServerGuid()) { + builder.setServerGuid(getServerGuid()); } return builder.build().toByteArray(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java index 7fa269258..f4f500d89 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMetadata.java @@ -9,13 +9,15 @@ public final class SignalServiceMetadata { private final long serverReceivedTimestamp; private final long serverDeliveredTimestamp; private final boolean needsReceipt; + private final String serverGuid; public SignalServiceMetadata(SignalServiceAddress sender, int senderDevice, long timestamp, long serverReceivedTimestamp, long serverDeliveredTimestamp, - boolean needsReceipt) + boolean needsReceipt, + String serverGuid) { this.sender = sender; this.senderDevice = senderDevice; @@ -23,6 +25,7 @@ public final class SignalServiceMetadata { this.serverReceivedTimestamp = serverReceivedTimestamp; this.serverDeliveredTimestamp = serverDeliveredTimestamp; this.needsReceipt = needsReceipt; + this.serverGuid = serverGuid; } public SignalServiceAddress getSender() { @@ -48,4 +51,8 @@ public final class SignalServiceMetadata { public boolean isNeedsReceipt() { return needsReceipt; } + + public String getServerGuid() { + return serverGuid; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 8afe0a460..af685ed88 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -225,6 +225,8 @@ public class PushServiceSocket { private static final String SUBMIT_RATE_LIMIT_CHALLENGE = "/v1/challenge"; private static final String REQUEST_RATE_LIMIT_PUSH_CHALLENGE = "/v1/challenge/push"; + private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; + private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; private static final Map NO_HEADERS = Collections.emptyMap(); @@ -1500,7 +1502,7 @@ public class PushServiceSocket { throw new ServerRejectedException(); } - if (responseCode != 200 && responseCode != 204) { + if (responseCode != 200 && responseCode != 202 && responseCode != 204) { throw new NonSuccessfulResponseCodeException(responseCode, "Bad response: " + responseCode + " " + responseMessage); } @@ -2188,6 +2190,12 @@ public class PushServiceSocket { } } + public void reportSpam(String e164, String serverGuid) + throws NonSuccessfulResponseCodeException, MalformedResponseException, PushNetworkException + { + makeServiceRequest(String.format(REPORT_SPAM, e164, serverGuid), "POST", ""); + } + public static final class GroupHistory { private final GroupChanges groupChanges; private final Optional contentRange; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java index a48be5e46..b7ed93abf 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/serialize/SignalServiceMetadataProtobufSerializer.java @@ -16,15 +16,17 @@ public final class SignalServiceMetadataProtobufSerializer { .setTimestamp(metadata.getTimestamp()) .setServerReceivedTimestamp(metadata.getServerReceivedTimestamp()) .setServerDeliveredTimestamp(metadata.getServerDeliveredTimestamp()) + .setServerGuid(metadata.getServerGuid()) .build(); } public static SignalServiceMetadata fromProtobuf(MetadataProto metadata) { return new SignalServiceMetadata(SignalServiceAddressProtobufSerializer.fromProtobuf(metadata.getAddress()), - metadata.getSenderDevice(), - metadata.getTimestamp(), - metadata.getServerReceivedTimestamp(), - metadata.getServerDeliveredTimestamp(), - metadata.getNeedsReceipt()); + metadata.getSenderDevice(), + metadata.getTimestamp(), + metadata.getServerReceivedTimestamp(), + metadata.getServerDeliveredTimestamp(), + metadata.getNeedsReceipt(), + metadata.getServerGuid()); } } diff --git a/libsignal/service/src/main/proto/InternalSerialization.proto b/libsignal/service/src/main/proto/InternalSerialization.proto index 0a669917b..1004e7469 100644 --- a/libsignal/service/src/main/proto/InternalSerialization.proto +++ b/libsignal/service/src/main/proto/InternalSerialization.proto @@ -41,6 +41,7 @@ message MetadataProto { optional int64 serverReceivedTimestamp = 5; optional int64 serverDeliveredTimestamp = 6; optional bool needsReceipt = 4; + optional string serverGuid = 7; } message AddressProto {