Implement the message send log for sender key retries.

This commit is contained in:
Greyson Parrelli
2021-06-24 11:22:20 -04:00
committed by Cody Henthorne
parent 6502ef64ce
commit f19033a7a2
31 changed files with 1077 additions and 179 deletions

View File

@@ -163,6 +163,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");

View File

@@ -91,6 +91,7 @@ import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.search.MessageResult;
import org.thoughtcrime.securesms.search.SearchResult;
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;

View File

@@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.state.SessionRecord;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -32,4 +34,12 @@ public class SessionUtil {
public static void archiveSession(Context context, RecipientId recipientId, int deviceId) {
new TextSecureSessionStore(context).archiveSession(recipientId, deviceId);
}
public static boolean ratchetKeyMatches(@NonNull Context context, @NonNull Recipient recipient, int deviceId, @NonNull ECPublicKey ratchetKey) {
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
SignalProtocolAddress address = new SignalProtocolAddress(recipient.resolve().requireServiceId(), deviceId);
SessionRecord session = sessionStore.loadSession(address);
return session.currentRatchetKeyMatches(ratchetKey);
}
}

View File

@@ -31,7 +31,6 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -70,6 +69,7 @@ public class DatabaseFactory {
private final PaymentDatabase paymentDatabase;
private final ChatColorsDatabase chatColorsDatabase;
private final EmojiSearchDatabase emojiSearchDatabase;
private final MessageSendLogDatabase messageSendLogDatabase;
public static DatabaseFactory getInstance(Context context) {
if (instance == null) {
@@ -188,16 +188,20 @@ public class DatabaseFactory {
return getInstance(context).paymentDatabase;
}
public static ChatColorsDatabase getChatColorsDatabase(Context context) {
return getInstance(context).chatColorsDatabase;
}
public static EmojiSearchDatabase getEmojiSearchDatabase(Context context) {
return getInstance(context).emojiSearchDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase();
public static MessageSendLogDatabase getMessageLogDatabase(Context context) {
return getInstance(context).messageSendLogDatabase;
}
public static ChatColorsDatabase getChatColorsDatabase(Context context) {
return getInstance(context).chatColorsDatabase;
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase();
}
public static void upgradeRestored(Context context, SQLiteDatabase database){
@@ -253,7 +257,8 @@ public class DatabaseFactory {
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -86,6 +86,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
public abstract Cursor getMessageCursor(long messageId);
public abstract OutgoingMediaMessage getOutgoingMessage(long messageId) throws MmsException, NoSuchMessageException;
public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException;
public abstract @Nullable MessageRecord getMessageRecordOrNull(long messageId);
public abstract Cursor getVerboseMessageCursor(long messageId);
public abstract boolean hasReceivedAnyCallsSince(long threadId, long timestamp);
public abstract @Nullable ViewOnceExpirationInfo getNearestExpiringViewOnceMessage();

View File

@@ -0,0 +1,265 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.database.model.MessageLogEntry
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.CursorUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SqlUtil
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.messages.SendMessageResult
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import java.util.UUID
/**
* Stores a 24-hr buffer of all outgoing messages. Used for the retry logic required for sender key.
*
* General note: This class is actually two tables -- one to store the entry, and another to store all the devices that were sent it.
*
* The general lifecycle of entries in the store goes something like this:
* - Upon sending a message, throw an entry in the 'message table' and throw an entry for each recipient you sent it to in the 'recipient table'
* - Whenever you get a delivery receipt, delete the entries in the 'recipient table'
* - Whenever there's no more records in the 'recipient table' for a given message, delete the entry in the 'message table'
* - Whenever you delete a message, delete the entry in the 'message table'
* - Whenever you read an entry from the table, first trim off all the entries that are too old
*
* Because of all of this, you can be sure that if an entry is in this store, it's safe to resend to someone upon request
*
* Worth noting that we use triggers + foreign keys to make sure entries in this table are properly cleaned up. Triggers for when you delete a message, and
* a cascading delete foreign key between these two tables.
*/
class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLCipherOpenHelper?) : Database(context, databaseHelper) {
companion object {
@JvmField
val CREATE_TABLE: Array<String> = arrayOf(MessageTable.CREATE_TABLE, RecipientTable.CREATE_TABLE)
@JvmField
val CREATE_INDEXES: Array<String> = MessageTable.CREATE_INDEXES + RecipientTable.CREATE_INDEXES
@JvmField
val CREATE_TRIGGERS: Array<String> = MessageTable.CREATE_TRIGGERS
}
private object MessageTable {
const val TABLE_NAME = "message_send_log"
const val ID = "_id"
const val DATE_SENT = "date_sent"
const val CONTENT = "content"
const val RELATED_MESSAGE_ID = "related_message_id"
const val IS_RELATED_MESSAGE_MMS = "is_related_message_mms"
const val CONTENT_HINT = "content_hint"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$DATE_SENT INTEGER NOT NULL,
$CONTENT BLOB NOT NULL,
$RELATED_MESSAGE_ID INTEGER DEFAULT -1,
$IS_RELATED_MESSAGE_MMS INTEGER DEFAULT 0,
$CONTENT_HINT INTEGER NOT NULL
)
"""
@JvmField
val CREATE_INDEXES = arrayOf(
"CREATE INDEX message_log_date_sent_index ON $TABLE_NAME ($DATE_SENT)",
"CREATE INDEX message_log_related_message_index ON $TABLE_NAME ($RELATED_MESSAGE_ID, $IS_RELATED_MESSAGE_MMS)"
)
@JvmField
val CREATE_TRIGGERS = arrayOf(
"""
CREATE TRIGGER msl_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME}
BEGIN
DELETE FROM $TABLE_NAME WHERE $RELATED_MESSAGE_ID = old.${SmsDatabase.ID} AND $IS_RELATED_MESSAGE_MMS = 0;
END
""",
"""
CREATE TRIGGER msl_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME}
BEGIN
DELETE FROM $TABLE_NAME WHERE $RELATED_MESSAGE_ID = old.${MmsDatabase.ID} AND $IS_RELATED_MESSAGE_MMS = 1;
END
"""
)
}
private object RecipientTable {
const val TABLE_NAME = "message_send_log_recipients"
const val ID = "_id"
const val MESSAGE_LOG_ID = "message_send_log_id"
const val RECIPIENT_ID = "recipient_id"
const val DEVICE = "device"
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$MESSAGE_LOG_ID INTEGER NOT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE CASCADE,
$RECIPIENT_ID INTEGER NOT NULL,
$DEVICE INTEGER NOT NULL
)
"""
val CREATE_INDEXES = arrayOf(
"CREATE INDEX message_send_log_recipients_recipient_index ON $TABLE_NAME ($RECIPIENT_ID, $DEVICE)"
)
}
fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) {
if (!FeatureFlags.senderKey()) return
if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) {
val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices))
insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, relatedMessageId, isRelatedMessageMms)
}
}
fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List<Recipient>, results: List<SendMessageResult>, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) {
if (!FeatureFlags.senderKey()) return
val recipientsByUuid: Map<UUID, Recipient> = possibleRecipients.filter(Recipient::hasUuid).associateBy(Recipient::requireUuid, { it })
val recipientsByE164: Map<String, Recipient> = possibleRecipients.filter(Recipient::hasE164).associateBy(Recipient::requireE164, { it })
val recipientDevices: List<RecipientDevice> = results
.filter { it.isSuccess && it.success.content.isPresent }
.map { result ->
val recipient: Recipient =
if (result.address.uuid.isPresent) {
recipientsByUuid[result.address.uuid.get()]!!
} else {
recipientsByE164[result.address.number.get()]!!
}
RecipientDevice(recipient.id, result.success.devices)
}
val content: SignalServiceProtos.Content = results.first { it.isSuccess && it.success.content.isPresent }.success.content.get()
insert(recipientDevices, sentTimestamp, content, contentHint, relatedMessageId, isRelatedMessageMms)
}
private fun insert(recipients: List<RecipientDevice>, dateSent: Long, content: SignalServiceProtos.Content, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) {
val db = databaseHelper.writableDatabase
db.beginTransaction()
try {
val logValues = ContentValues().apply {
put(MessageTable.DATE_SENT, dateSent)
put(MessageTable.CONTENT, content.toByteArray())
put(MessageTable.CONTENT_HINT, contentHint.type)
put(MessageTable.RELATED_MESSAGE_ID, relatedMessageId)
put(MessageTable.IS_RELATED_MESSAGE_MMS, if (isRelatedMessageMms) 1 else 0)
}
val messageLogId: Long = db.insert(MessageTable.TABLE_NAME, null, logValues)
recipients.forEach { recipientDevice ->
recipientDevice.devices.forEach { device ->
val recipientValues = ContentValues()
recipientValues.put(RecipientTable.MESSAGE_LOG_ID, messageLogId)
recipientValues.put(RecipientTable.RECIPIENT_ID, recipientDevice.recipientId.serialize())
recipientValues.put(RecipientTable.DEVICE, device)
db.insert(RecipientTable.TABLE_NAME, null, recipientValues)
}
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun getLogEntry(recipientId: RecipientId, device: Int, dateSent: Long): MessageLogEntry? {
if (!FeatureFlags.senderKey()) return null
trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())
val db = databaseHelper.readableDatabase
val table = "${MessageTable.TABLE_NAME} LEFT JOIN ${RecipientTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_LOG_ID}"
val query = "${MessageTable.DATE_SENT} = ? AND ${RecipientTable.RECIPIENT_ID} = ? AND ${RecipientTable.DEVICE} = ?"
val args = SqlUtil.buildArgs(dateSent, recipientId, device)
db.query(table, null, query, args, null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return MessageLogEntry(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RecipientTable.RECIPIENT_ID)),
dateSent = CursorUtil.requireLong(cursor, MessageTable.DATE_SENT),
content = SignalServiceProtos.Content.parseFrom(CursorUtil.requireBlob(cursor, MessageTable.CONTENT)),
contentHint = ContentHint.fromType(CursorUtil.requireInt(cursor, MessageTable.CONTENT_HINT)),
relatedMessageId = CursorUtil.requireLong(cursor, MessageTable.RELATED_MESSAGE_ID),
isRelatedMessageMms = CursorUtil.requireBoolean(cursor, MessageTable.IS_RELATED_MESSAGE_MMS)
)
}
}
return null
}
fun deleteAllRelatedToMessage(messageId: Long, mms: Boolean) {
if (!FeatureFlags.senderKey()) return
val db = databaseHelper.writableDatabase
val query = "${MessageTable.RELATED_MESSAGE_ID} = ? AND ${MessageTable.IS_RELATED_MESSAGE_MMS} = ?"
val args = SqlUtil.buildArgs(messageId, if (mms) 1 else 0)
db.delete(MessageTable.TABLE_NAME, query, args)
}
fun deleteEntryForRecipient(dateSent: Long, recipientId: RecipientId, device: Int) {
if (!FeatureFlags.senderKey()) return
deleteEntriesForRecipient(listOf(dateSent), recipientId, device)
}
fun deleteEntriesForRecipient(dateSent: List<Long>, recipientId: RecipientId, device: Int) {
if (!FeatureFlags.senderKey()) return
val db = databaseHelper.writableDatabase
db.beginTransaction()
try {
val query = """
${RecipientTable.RECIPIENT_ID} = ? AND
${RecipientTable.DEVICE} = ? AND
${RecipientTable.MESSAGE_LOG_ID} IN (
SELECT ${MessageTable.ID}
FROM ${MessageTable.TABLE_NAME}
WHERE ${MessageTable.DATE_SENT} IN (${dateSent.joinToString(",")})
)"""
val args = SqlUtil.buildArgs(recipientId, device)
db.delete(RecipientTable.TABLE_NAME, query, args)
val cleanQuery = "${MessageTable.ID} NOT IN (SELECT ${RecipientTable.MESSAGE_LOG_ID} FROM ${RecipientTable.TABLE_NAME})"
db.delete(MessageTable.TABLE_NAME, cleanQuery, null)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun deleteAll() {
if (!FeatureFlags.senderKey()) return
databaseHelper.writableDatabase.delete(MessageTable.TABLE_NAME, null, null)
}
fun trimOldMessages(currentTime: Long, maxAge: Long) {
if (!FeatureFlags.senderKey()) return
val db = databaseHelper.writableDatabase
val query = "${MessageTable.DATE_SENT} < ?"
val args = SqlUtil.buildArgs(currentTime - maxAge)
db.delete(MessageTable.TABLE_NAME, query, args)
}
private data class RecipientDevice(val recipientId: RecipientId, val devices: List<Int>)
}

View File

@@ -768,6 +768,13 @@ public class MmsDatabase extends MessageDatabase {
}
}
@Override
public @Nullable MessageRecord getMessageRecordOrNull(long messageId) {
try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) {
return new Reader(cursor).getNext();
}
}
@Override
public Reader getMessages(Collection<Long> messageIds) {
String ids = TextUtils.join(",", messageIds);
@@ -854,23 +861,32 @@ public class MmsDatabase extends MessageDatabase {
public void markAsRemoteDelete(long messageId) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(QUOTE_BODY);
values.putNull(QUOTE_AUTHOR);
values.putNull(QUOTE_ATTACHMENT);
values.putNull(QUOTE_ID);
values.putNull(LINK_PREVIEWS);
values.putNull(SHARED_CONTACTS);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
long threadId;
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
DatabaseFactory.getMentionDatabase(context).deleteMentionsForMessage(messageId);
db.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(QUOTE_BODY);
values.putNull(QUOTE_AUTHOR);
values.putNull(QUOTE_ATTACHMENT);
values.putNull(QUOTE_ID);
values.putNull(LINK_PREVIEWS);
values.putNull(SHARED_CONTACTS);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
long threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
DatabaseFactory.getMentionDatabase(context).deleteMentionsForMessage(messageId);
DatabaseFactory.getMessageLogDatabase(context).deleteAllRelatedToMessage(messageId, true);
threadId = getThreadIdForMessage(messageId);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListeners(threadId);
}
@@ -1661,6 +1677,7 @@ public class MmsDatabase extends MessageDatabase {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false);
notifyConversationListeners(threadId);
notifyStickerListeners();
@@ -1773,7 +1790,6 @@ public class MmsDatabase extends MessageDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String where = "";
Cursor cursor = null;
for (long threadId : threadIds) {
where += THREAD_ID + " = '" + threadId + "' OR ";
@@ -1781,16 +1797,10 @@ public class MmsDatabase extends MessageDatabase {
where = where.substring(0, where.length() - 4);
try {
cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null);
try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID}, where, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
deleteMessage(cursor.getLong(0));
}
} finally {
if (cursor != null)
cursor.close();
}
}

View File

@@ -414,15 +414,25 @@ public class SmsDatabase extends MessageDatabase {
public void markAsRemoteDelete(long id) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
long threadId;
long threadId = getThreadIdForMessage(id);
db.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put(REMOTE_DELETED, 1);
values.putNull(BODY);
values.putNull(REACTIONS);
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
threadId = getThreadIdForMessage(id);
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
DatabaseFactory.getMessageLogDatabase(context).deleteAllRelatedToMessage(id, false);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListeners(threadId);
}
@@ -1299,12 +1309,23 @@ public class SmsDatabase extends MessageDatabase {
public boolean deleteMessage(long messageId) {
Log.d(TAG, "deleteMessage(" + messageId + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long threadId = getThreadIdForMessage(messageId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, ID_WHERE, new String[] {messageId+""});
long threadId;
boolean threadDeleted;
boolean threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false, true);
db.beginTransaction();
try {
threadId = getThreadIdForMessage(messageId);
db.delete(TABLE_NAME, ID_WHERE, new String[] { messageId + "" });
threadDeleted = DatabaseFactory.getThreadDatabase(context).update(threadId, false, true);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListeners(threadId);
return threadDeleted;
@@ -1320,6 +1341,15 @@ public class SmsDatabase extends MessageDatabase {
return getSmsMessage(messageId);
}
@Override
public @Nullable MessageRecord getMessageRecordOrNull(long messageId) {
try {
return getSmsMessage(messageId);
} catch (NoSuchMessageException e) {
return null;
}
}
private boolean isDuplicate(IncomingTextMessage message, long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
String query = DATE_SENT + " = ? AND " + RECIPIENT_ID + " = ? AND " + THREAD_ID + " = ?";
@@ -1334,6 +1364,7 @@ public class SmsDatabase extends MessageDatabase {
void deleteThread(long threadId) {
Log.d(TAG, "deleteThread(" + threadId + ")");
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.delete(TABLE_NAME, THREAD_ID + " = ?", new String[] {threadId+""});
}

View File

@@ -6,7 +6,7 @@ import net.sqlcipher.database.SQLiteDatabaseHook;
/**
* Standard hook for setting common SQLCipher PRAGMAs.
*/
public final class SqlCipherDatabaseHook implements SQLiteDatabaseHook {
public class SqlCipherDatabaseHook implements SQLiteDatabaseHook {
@Override
public void preKey(SQLiteDatabase db) {
@@ -20,5 +20,6 @@ public final class SqlCipherDatabaseHook implements SQLiteDatabaseHook {
db.rawExecSQL("PRAGMA cipher_memory_security = OFF;");
db.rawExecSQL("PRAGMA kdf_iter = '1';");
db.rawExecSQL("PRAGMA cipher_page_size = 4096;");
db.setForeignKeyConstraintsEnabled(true);
}
}

View File

@@ -960,6 +960,7 @@ public class ThreadDatabase extends Database {
db.beginTransaction();
try {
DatabaseFactory.getMessageLogDatabase(context).deleteAll();
DatabaseFactory.getSmsDatabase(context).deleteAllThreads();
DatabaseFactory.getMmsDatabase(context).deleteAllThreads();
DatabaseFactory.getDraftDatabase(context).clearAllDrafts();

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.MessageSendLogDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PaymentDatabase;
@@ -199,8 +200,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int EMOJI_SEARCH = 102;
private static final int SENDER_KEY = 103;
private static final int MESSAGE_DUPE_INDEX = 104;
private static final int MESSAGE_LOG = 105;
private static final int DATABASE_VERSION = 104;
private static final int DATABASE_VERSION = 105;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -239,6 +241,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL(EmojiSearchDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE);
executeStatements(db, RecipientDatabase.CREATE_INDEXS);
executeStatements(db, SmsDatabase.CREATE_INDEXS);
@@ -252,6 +255,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
executeStatements(db, UnknownStorageIdDatabase.CREATE_INDEXES);
executeStatements(db, MentionDatabase.CREATE_INDEXES);
executeStatements(db, PaymentDatabase.CREATE_INDEXES);
executeStatements(db, MessageSendLogDatabase.CREATE_INDEXES);
executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS);
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
@@ -1569,6 +1575,29 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL("CREATE INDEX mms_date_sent_index on mms(date, address, thread_id)");
}
if (oldVersion < MESSAGE_LOG) {
db.execSQL("CREATE TABLE message_send_log (_id INTEGER PRIMARY KEY, " +
"date_sent INTEGER NOT NULL, " +
"content BLOB NOT NULL, " +
"related_message_id INTEGER DEFAULT -1, " +
"is_related_message_mms INTEGER DEFAULT 0, " +
"content_hint INTEGER NOT NULL, " +
"group_id BLOB DEFAULT NULL)");
db.execSQL("CREATE INDEX message_log_date_sent_index ON message_send_log (date_sent)");
db.execSQL("CREATE INDEX message_log_related_message_index ON message_send_log (related_message_id, is_related_message_mms)");
db.execSQL("CREATE TRIGGER msl_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 0; END");
db.execSQL("CREATE TRIGGER msl_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM message_send_log WHERE related_message_id = old._id AND is_related_message_mms = 1; END");
db.execSQL("CREATE TABLE message_send_log_recipients (_id INTEGER PRIMARY KEY, " +
"message_send_log_id INTEGER NOT NULL REFERENCES message_send_log (_id) ON DELETE CASCADE, " +
"recipient_id INTEGER NOT NULL, " +
"device INTEGER NOT NULL)");
db.execSQL("CREATE INDEX message_send_log_recipients_recipient_index ON message_send_log_recipients (recipient_id, device)");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.database.model
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
/**
* Model class for reading from the [org.thoughtcrime.securesms.database.MessageSendLogDatabase].
*/
data class MessageLogEntry(
val recipientId: RecipientId,
val dateSent: Long,
val content: SignalServiceProtos.Content,
val contentHint: ContentHint,
val relatedMessageId: Long,
val isRelatedMessageMms: Boolean,
) {
val hasRelatedMessage: Boolean
@JvmName("hasRelatedMessage")
get() = relatedMessageId > 0
}

View File

@@ -68,8 +68,8 @@ public class Data {
}
public byte[] getStringAsBlob(@NonNull String key) {
throwIfAbsent(strings, key);
return Base64.decodeOrThrow(strings.get(key));
String raw = getString(key);
return raw != null ? Base64.decodeOrThrow(raw) : null;
}
public String getStringOrDefault(@NonNull String key, String defaultValue) {
@@ -356,8 +356,8 @@ public class Data {
return this;
}
public Builder putBlobAsString(@NonNull String key, @NonNull byte[] value) {
String serialized = Base64.encodeBytes(value);
public Builder putBlobAsString(@NonNull String key, @Nullable byte[] value) {
String serialized = value != null ? Base64.encodeBytes(value) : null;
strings.put(key, serialized);
return this;
}

View File

@@ -87,10 +87,12 @@ public final class JobManagerFactories {
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
put(GroupCallPeekJob.KEY, new GroupCallPeekJob.Factory());
put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory());
put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory());
put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory());
put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory());
put(LocalBackupJob.KEY, new LocalBackupJob.Factory());
put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory());
put(MarkerJob.KEY, new MarkerJob.Factory());
put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory());
put(MmsReceiveJob.KEY, new MmsReceiveJob.Factory());
put(MmsSendJob.KEY, new MmsSendJob.Factory());
@@ -110,7 +112,13 @@ public final class JobManagerFactories {
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory());
put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory());
put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory());
put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory());
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
put(PaymentSendJob.KEY, new PaymentSendJob.Factory());
put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory());
put(ProfileKeySendJob.KEY, new ProfileKeySendJob.Factory());
put(ProfileUploadJob.KEY, new ProfileUploadJob.Factory());
put(PushDecryptMessageJob.KEY, new PushDecryptMessageJob.Factory());
put(PushDecryptDrainedJob.KEY, new PushDecryptDrainedJob.Factory());
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
@@ -126,12 +134,12 @@ public final class JobManagerFactories {
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());
put(ReportSpamJob.KEY, new ReportSpamJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(ResendMessageJob.KEY, new ResendMessageJob.Factory());
put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory());
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
put(RequestGroupV2InfoWorkerJob.KEY, new RequestGroupV2InfoWorkerJob.Factory());
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
@@ -148,19 +156,13 @@ public final class JobManagerFactories {
put(SmsSentJob.KEY, new SmsSentJob.Factory());
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
put(StorageForcePushJob.KEY, new StorageForcePushJob.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());
put(MarkerJob.KEY, new MarkerJob.Factory());
put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory());
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
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());

View File

@@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
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.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.util.concurrent.TimeUnit;
/**
* Just sends an empty message to a target recipient. Only suitable for individuals, NOT groups.
*/
public class NullMessageSendJob extends BaseJob {
public static final String KEY = "NullMessageSendJob";
private static final String TAG = Log.tag(NullMessageSendJob.class);
private final RecipientId recipientId;
private static final String KEY_RECIPIENT_ID = "recipient_id";
public NullMessageSendJob(@NonNull RecipientId recipientId) {
this(recipientId,
new Parameters.Builder()
.setQueue(recipientId.toQueueKey())
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.build());
}
private NullMessageSendJob(@NonNull RecipientId recipientId, @NonNull Parameters parameters) {
super(parameters);
this.recipientId = recipientId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder().putString(KEY_RECIPIENT_ID, recipientId.serialize()).build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws Exception {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isGroup()) {
Log.w(TAG, "Groups are not supported!");
return;
}
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
try {
messageSender.sendNullMessage(address, unidentifiedAccess);
} catch (UntrustedIdentityException e) {
Log.w(TAG, "Unable to send null message.");
}
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public void onFailure() {
}
public static final class Factory implements Job.Factory<NullMessageSendJob> {
@Override
public @NonNull NullMessageSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new NullMessageSendJob(RecipientId.from(data.getString(KEY_RECIPIENT_ID)),
parameters);
}
}
}

View File

@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -330,7 +329,7 @@ public final class PushGroupSendJob extends PushSendJob {
.withExpiration(groupRecipient.getExpireMessages())
.asGroupMessage(group)
.build();
return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage);
return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, messageId, true, groupDataMessage);
} else {
MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties();
@@ -372,9 +371,11 @@ public final class PushGroupSendJob extends PushSendJob {
Log.i(TAG, JobLogger.format(this, "Beginning message send."));
if (groupRecipient.isPushV2Group()) {
return GroupSendUtil.sendDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage);
return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.RESENDABLE, messageId, true, groupMessage);
} else {
return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage);
List<SendMessageResult> results = messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(groupMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, true);
return results;
}
}
} catch (ServerRejectedException e) {

View File

@@ -167,7 +167,7 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob {
.asGroupMessage(group)
.build();
List<SendMessageResult> results = GroupSendUtil.sendDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage);
List<SendMessageResult> results = GroupSendUtil.sendUnresendableDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage);
return GroupSendJobHelper.getCompletedSends(context, results);
}

View File

@@ -35,6 +35,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
@@ -222,10 +223,13 @@ public class PushMediaSendJob extends PushSendJob {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
messageSender.sendSyncMessage(syncMessage, syncAccess);
SendMessageResult result = messageSender.sendSyncMessage(syncMessage, syncAccess);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, messageId, true);
return syncAccess.isPresent();
} else {
return messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage).getSuccess().isUnidentified();
SendMessageResult result = messageSender.sendDataMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, messageId, true);
return result.getSuccess().isUnidentified();
}
} catch (UnregisteredUserException e) {
warn(TAG, String.valueOf(message.getSentTimeMillis()), e);

View File

@@ -27,6 +27,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
@@ -174,10 +175,13 @@ public class PushTextSendJob extends PushSendJob {
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
messageSender.sendSyncMessage(syncMessage, syncAccess);
SendMessageResult result = messageSender.sendSyncMessage(syncMessage, syncAccess);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getDateSent(), result, ContentHint.RESENDABLE, messageId, false);
return syncAccess.isPresent();
} else {
return messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage).getSuccess().isUnidentified();
SendMessageResult result = messageSender.sendDataMessage(address, unidentifiedAccess, ContentHint.RESENDABLE, textSecureMessage);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(messageRecipient.getId(), message.getDateSent(), result, ContentHint.RESENDABLE, messageId, false);
return result.getSuccess().isUnidentified();
}
} catch (UnregisteredUserException e) {
warn(TAG, "Failure", e);

View File

@@ -31,7 +31,6 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
@@ -219,24 +218,28 @@ public class ReactionSendJob extends BaseJob {
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, @NonNull Recipient targetAuthor, long targetSentTimestamp)
throws IOException, UntrustedIdentityException
{
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
SignalServiceDataMessage.Builder dataMessageBuilder = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withReaction(buildReaction(context, reaction, remove, targetAuthor, targetSentTimestamp));
if (conversationRecipient.isGroup()) {
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush());
}
SignalServiceDataMessage dataMessage = dataMessageBuilder.build();
List<SendMessageResult> results;
if (conversationRecipient.isPushV2Group()) {
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
results = GroupSendUtil.sendResendableDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.RESENDABLE, messageId, isMms, dataMessageBuilder.build());
} else {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);;
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.RESENDABLE, dataMessage);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(dataMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, isMms);
}
return GroupSendJobHelper.getCompletedSends(context, results);

View File

@@ -174,24 +174,28 @@ public class RemoteDeleteSendJob extends BaseJob {
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, long targetSentTimestamp)
throws IOException, UntrustedIdentityException
{
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
SignalServiceDataMessage.Builder dataMessageBuilder = SignalServiceDataMessage.newBuilder()
.withTimestamp(System.currentTimeMillis())
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
if (conversationRecipient.isGroup()) {
GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush());
GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush());
}
SignalServiceDataMessage dataMessage = dataMessageBuilder.build();
List<SendMessageResult> results;
if (conversationRecipient.isPushV2Group()) {
results = GroupSendUtil.sendDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.DEFAULT, dataMessage.build());
results = GroupSendUtil.sendResendableDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.RESENDABLE, messageId, isMms, dataMessage);
} else {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
List<SignalServiceAddress> addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build());
results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.RESENDABLE, dataMessage);
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(dataMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, isMms);
}
return GroupSendJobHelper.getCompletedSends(context, results);
@@ -209,4 +213,4 @@ public class RemoteDeleteSendJob extends BaseJob {
return new RemoteDeleteSendJob(messageId, isMms, recipients, initialRecipientCount, parameters);
}
}
}
}

View File

@@ -0,0 +1,162 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
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.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.whispersystems.libsignal.protocol.SenderKeyDistributionMessage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import java.util.concurrent.TimeUnit;
/**
* Resends a previously-sent message in response to receiving a retry receipt.
*
* Not for arbitrary retries due to network flakiness or something -- those should be handled within each individual job.
*/
public class ResendMessageJob extends BaseJob {
public static final String KEY = "ResendMessageJob";
private final RecipientId recipientId;
private final long sentTimestamp;
private final Content content;
private final ContentHint contentHint;
private final GroupId.V2 groupId;
private final DistributionId distributionId;
private static final String KEY_RECIPIENT_ID = "recipient_id";
private static final String KEY_SENT_TIMESTAMP = "sent_timestamp";
private static final String KEY_CONTENT = "content";
private static final String KEY_CONTENT_HINT = "content_hint";
private static final String KEY_GROUP_ID = "group_id";
private static final String KEY_DISTRIBUTION_ID = "distribution_id";
// TODO [greyson] Maybe just pass in the MessageLogEntry?
public ResendMessageJob(@NonNull RecipientId recipientId,
long sentTimestamp,
@NonNull Content content,
@NonNull ContentHint contentHint,
@Nullable GroupId.V2 groupId,
@Nullable DistributionId distributionId)
{
this(recipientId,
sentTimestamp,
content,
contentHint,
groupId,
distributionId,
new Parameters.Builder().setQueue(recipientId.toQueueKey())
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.addConstraint(NetworkConstraint.KEY)
.build());
}
private ResendMessageJob(@NonNull RecipientId recipientId,
long sentTimestamp,
@NonNull Content content,
@NonNull ContentHint contentHint,
@Nullable GroupId.V2 groupId,
@Nullable DistributionId distributionId,
@NonNull Parameters parameters)
{
super(parameters);
this.recipientId = recipientId;
this.sentTimestamp = sentTimestamp;
this.content = content;
this.contentHint = contentHint;
this.groupId = groupId;
this.distributionId = distributionId;
}
@Override
public @NonNull Data serialize() {
return new Data.Builder()
.putString(KEY_RECIPIENT_ID, recipientId.serialize())
.putLong(KEY_SENT_TIMESTAMP, sentTimestamp)
.putBlobAsString(KEY_CONTENT, content.toByteArray())
.putInt(KEY_CONTENT_HINT, contentHint.getType())
.putBlobAsString(KEY_GROUP_ID, groupId != null ? groupId.getDecodedId() : null)
.putString(KEY_DISTRIBUTION_ID, distributionId != null ? distributionId.toString() : null)
.build();
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws Exception {
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
Recipient recipient = Recipient.resolved(recipientId);
SignalServiceAddress address = RecipientUtil.toSignalServiceAddress(context, recipient);
Optional<UnidentifiedAccessPair> access = UnidentifiedAccessUtil.getAccessFor(context, recipient);
Content contentToSend = content;
if (distributionId != null) {
SenderKeyDistributionMessage senderKeyDistributionMessage = messageSender.getOrCreateNewGroupSession(distributionId);
ByteString distributionBytes = ByteString.copyFrom(senderKeyDistributionMessage.serialize());
contentToSend = contentToSend.toBuilder().setSenderKeyDistributionMessage(distributionBytes).build();
}
messageSender.resendContent(address, access, sentTimestamp, contentToSend, contentHint, Optional.fromNullable(groupId).transform(GroupId::getDecodedId));
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public void onFailure() {
}
public static final class Factory implements Job.Factory<ResendMessageJob> {
@Override
public @NonNull ResendMessageJob create(@NonNull Parameters parameters, @NonNull Data data) {
Content content;
try {
content = Content.parseFrom(data.getStringAsBlob(KEY_CONTENT));
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
byte[] rawGroupId = data.getStringAsBlob(KEY_GROUP_ID);
GroupId.V2 groupId = rawGroupId != null ? GroupId.pushOrThrow(rawGroupId).requireV2() : null;
String rawDistributionId = data.getString(KEY_DISTRIBUTION_ID);
DistributionId distributionId = rawDistributionId != null ? DistributionId.from(rawDistributionId) : null;
return new ResendMessageJob(RecipientId.from(data.getString(KEY_RECIPIENT_ID)),
data.getLong(KEY_SENT_TIMESTAMP),
content,
ContentHint.fromType(data.getInt(KEY_CONTENT_HINT)),
groupId,
distributionId,
parameters);
}
}
}

View File

@@ -35,6 +35,7 @@ import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -58,18 +59,41 @@ public final class GroupSendUtil {
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* Messages sent this way, if failed to be decrypted by the receiving party, can be requested to be resent.
*
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
*/
public static List<SendMessageResult> sendResendableDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
ContentHint contentHint,
long relatedMessageId,
boolean isRelatedMessageMms,
@NonNull SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException
{
return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, relatedMessageId, isRelatedMessageMms), null);
}
/**
* Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of
* {@link SendMessageResult}s just like we're used to.
*
* Messages sent this way, if failed to be decrypted by the receiving party, can *not* be requested to be resent.
*
* @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false.
*/
@WorkerThread
public static List<SendMessageResult> sendDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
ContentHint contentHint,
@NonNull SignalServiceDataMessage message)
public static List<SendMessageResult> sendUnresendableDataMessage(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull List<Recipient> allTargets,
boolean isRecipientUpdate,
ContentHint contentHint,
@NonNull SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException
{
return sendMessage(context, groupId, allTargets, isRecipientUpdate, new DataSendOperation(message, contentHint), null);
return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.unresendable(message, contentHint), null);
}
/**
@@ -173,6 +197,9 @@ public final class GroupSendUtil {
}
if (cancelationSignal != null && cancelationSignal.isCanceled()) {
if (sendOperation.shouldIncludeInMessageLog()) {
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), allTargets, allResults, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms());
}
throw new CancelationException();
}
@@ -191,6 +218,10 @@ public final class GroupSendUtil {
Log.d(TAG, "Successfully using 1:1 to " + successCount + "/" + targets.size() + " legacy targets.");
}
if (sendOperation.shouldIncludeInMessageLog()) {
DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), allTargets, allResults, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms());
}
return allResults;
}
@@ -209,15 +240,35 @@ public final class GroupSendUtil {
boolean isRecipientUpdate,
@Nullable CancelationSignal cancelationSignal)
throws IOException, UntrustedIdentityException;
@NonNull ContentHint getContentHint();
long getSentTimestamp();
boolean shouldIncludeInMessageLog();
long getRelatedMessageId();
boolean isRelatedMessageMms();
}
private static class DataSendOperation implements SendOperation {
private final SignalServiceDataMessage message;
private final ContentHint contentHint;
private final long relatedMessageId;
private final boolean isRelatedMessageMms;
private final boolean resendable;
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) {
this.message = message;
this.contentHint = contentHint;
public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, long relatedMessageId, boolean isRelatedMessageMms) {
return new DataSendOperation(message, contentHint, true, relatedMessageId, isRelatedMessageMms);
}
public static DataSendOperation unresendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) {
return new DataSendOperation(message, contentHint, false, -1, false);
}
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, long relatedMessageId, boolean isRelatedMessageMms) {
this.message = message;
this.contentHint = contentHint;
this.resendable = resendable;
this.relatedMessageId = relatedMessageId;
this.isRelatedMessageMms = isRelatedMessageMms;
}
@Override
@@ -241,6 +292,31 @@ public final class GroupSendUtil {
{
return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message);
}
@Override
public @NonNull ContentHint getContentHint() {
return contentHint;
}
@Override
public long getSentTimestamp() {
return message.getTimestamp();
}
@Override
public boolean shouldIncludeInMessageLog() {
return resendable;
}
@Override
public long getRelatedMessageId() {
return relatedMessageId;
}
@Override
public boolean isRelatedMessageMms() {
return isRelatedMessageMms;
}
}
private static class TypingSendOperation implements SendOperation {
@@ -260,7 +336,7 @@ public final class GroupSendUtil {
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException
{
messageSender.sendGroupTyping(distributionId, targets, access, message);
return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList());
return targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.absent())).collect(Collectors.toList());
}
@Override
@@ -272,7 +348,32 @@ public final class GroupSendUtil {
throws IOException
{
messageSender.sendTyping(targets, access, message, cancelationSignal);
return targets.stream().map(a -> SendMessageResult.success(a, true, false, -1)).collect(Collectors.toList());
return targets.stream().map(a -> SendMessageResult.success(a, Collections.emptyList(), true, false, -1, Optional.absent())).collect(Collectors.toList());
}
@Override
public @NonNull ContentHint getContentHint() {
return ContentHint.IMPLICIT;
}
@Override
public long getSentTimestamp() {
return message.getTimestamp();
}
@Override
public boolean shouldIncludeInMessageLog() {
return false;
}
@Override
public long getRelatedMessageId() {
return -1;
}
@Override
public boolean isRelatedMessageMms() {
return false;
}
}

View File

@@ -160,8 +160,10 @@ public class IncomingMessageProcessor {
private void processReceipt(@NonNull SignalServiceEnvelope envelope) {
Log.i(TAG, "Received server receipt for " + envelope.getTimestamp());
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(Recipient.externalHighTrustPush(context, envelope.getSourceAddress()).getId(), envelope.getTimestamp()),
System.currentTimeMillis());
Recipient sender = Recipient.externalHighTrustPush(context, envelope.getSourceAddress());
mmsSmsDatabase.incrementDeliveryReceiptCount(new SyncMessageId(sender.getId(), envelope.getTimestamp()), System.currentTimeMillis());
DatabaseFactory.getMessageLogDatabase(context).deleteEntryForRecipient(envelope.getTimestamp(), sender.getId(), envelope.getSourceDevice());
}
private boolean needsToEnqueueDecryption() {

View File

@@ -24,10 +24,12 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
import org.thoughtcrime.securesms.database.MessageDatabase;
@@ -40,10 +42,10 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageLogEntry;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -65,14 +67,15 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob;
import org.thoughtcrime.securesms.jobs.NullMessageSendJob;
import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob;
import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob;
import org.thoughtcrime.securesms.jobs.ProfileKeySendJob;
import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
import org.thoughtcrime.securesms.jobs.ResendMessageJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob;
@@ -152,6 +155,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.payments.Money;
import org.whispersystems.signalservice.api.push.DistributionId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
import java.security.SecureRandom;
@@ -241,7 +245,7 @@ public final class MessageContentProcessor {
if (isGv2Message) {
GroupId.V2 groupIdV2 = groupId.get().requireV2();
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(groupIdV2);
Optional<GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(groupIdV2);
if (possibleGv1.isPresent()) {
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1());
}
@@ -956,7 +960,7 @@ public final class MessageContentProcessor {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
if (message.getMessage().isGroupV2Message()) {
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey()));
Optional<GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey()));
if (possibleGv1.isPresent()) {
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1());
}
@@ -1611,6 +1615,7 @@ public final class MessageContentProcessor {
.toList();
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCounts(ids, System.currentTimeMillis());
DatabaseFactory.getMessageLogDatabase(context).deleteEntriesForRecipient(message.getTimestamps(), sender.getId(), content.getSenderDevice());
}
@SuppressLint("DefaultLocale")
@@ -1680,76 +1685,142 @@ public final class MessageContentProcessor {
private void handleRetryReceipt(@NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) {
if (!FeatureFlags.senderKey()) {
Log.w(TAG, "Sender key not enabled, skipping retry receipt.");
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Sender key not enabled, skipping retry receipt.");
return;
}
Recipient requester = Recipient.externalHighTrustPush(context, content.getSender());
long sentTimestamp = decryptionErrorMessage.getTimestamp();
warn(content.getTimestamp(), "[RetryReceipt] Received a retry receipt from " + requester.getId() + ", device " + decryptionErrorMessage.getDeviceId() + " for message with timestamp " + sentTimestamp + ".");
if (!requester.hasUuid()) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester " + requester.getId() + " somehow has no UUID! timestamp: " + sentTimestamp);
warn(content.getTimestamp(), "[RetryReceipt] Requester " + requester.getId() + " somehow has no UUID! timestamp: " + sentTimestamp);
return;
}
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId());
MessageLogEntry messageLogEntry = DatabaseFactory.getMessageLogDatabase(context).getLogEntry(requester.getId(), content.getSenderDevice(), sentTimestamp);
if (messageRecord == null) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find message for " + requester.getId() + " with timestamp " + sentTimestamp);
// TODO Send distribution message?
if (decryptionErrorMessage.getRatchetKey().isPresent()) {
handleIndividualRetryReceipt(requester, messageLogEntry, content, decryptionErrorMessage);
} else {
handleSenderKeyRetryReceipt(requester, messageLogEntry, content, decryptionErrorMessage);
}
}
private void handleSenderKeyRetryReceipt(@NonNull Recipient requester,
@Nullable MessageLogEntry messageLogEntry,
@NonNull SignalServiceContent content,
@NonNull DecryptionErrorMessage decryptionErrorMessage)
{
long sentTimestamp = decryptionErrorMessage.getTimestamp();
MessageRecord relatedMessage = findRetryReceiptRelatedMessage(context, messageLogEntry, sentTimestamp);
if (relatedMessage == null) {
warn(content.getTimestamp(), "[RetryReceipt-SK] The related message could not be found! There shouldn't be any sender key resends where we can't find the related message. Skipping.");
return;
}
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(messageRecord.getThreadId());
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(relatedMessage.getThreadId());
if (threadRecipient == null) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Unable to find a recipient for thread " + messageRecord.getThreadId());
warn(content.getTimestamp(), "[RetryReceipt-SK] Could not find a thread recipient! Skipping.");
return;
}
if (messageRecord.isMms()) {
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] MMS " + messageRecord.getId());
MmsMessageRecord mms = (MmsMessageRecord) messageRecord;
if (!threadRecipient.isPushV2Group()) {
warn(content.getTimestamp(), "[RetryReceipt-SK] Thread recipient is not a v2 group! Skipping.");
return;
}
if (threadRecipient.isPushV2Group()) {
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(threadRecipient.requireGroupId().requireV2());
SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId());
DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(threadRecipient.requireGroupId().requireV2());
DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress));
SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireUuid().toString(), decryptionErrorMessage.getDeviceId());
DatabaseFactory.getSenderKeySharedDatabase(context).delete(distributionId, Collections.singleton(requesterAddress));
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
GroupReceiptInfo receiptInfo = receiptDatabase.getGroupReceiptInfo(mms.getId(), requester.getId());
boolean needsDistributionMessage = true;
if (messageLogEntry != null) {
warn(content.getTimestamp(), "[RetryReceipt-SK] Found MSL entry for " + requester.getId() + " with timestamp " + sentTimestamp + ". Scheduling a resend.");
if (receiptInfo == null) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester was never sent message " + mms.getId() + "! Cannot resend it.");
} else if (receiptInfo.getStatus() >= GroupReceiptDatabase.STATUS_DELIVERED) {
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully delivered to the requester. Not resending.");
} else {
long messageAge = System.currentTimeMillis() - mms.getDateSent();
ApplicationDependencies.getJobManager().add(new ResendMessageJob(messageLogEntry.getRecipientId(),
messageLogEntry.getDateSent(),
messageLogEntry.getContent(),
messageLogEntry.getContentHint(),
threadRecipient.requireGroupId().requireV2(),
distributionId));
} else {
warn(content.getTimestamp(), "[RetryReceipt-SK] Unable to find MSL entry for " + requester.getId() + " with timestamp " + sentTimestamp + ".");
if (messageAge < FeatureFlags.retryRespondMaxAge()) {
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. Resending.");
if (!content.getGroupId().isPresent()) {
warn(content.getTimestamp(), "[RetryReceipt-SK] No groupId on the Content, so we cannot send them a SenderKeyDistributionMessage.");
return;
}
DatabaseFactory.getGroupReceiptDatabase(context).update(requester.getId(), mms.getId(), GroupReceiptDatabase.STATUS_UNDELIVERED, System.currentTimeMillis());
ApplicationDependencies.getJobManager().startChain(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2()))
.then(new PushGroupSendJob(mms.getId(), threadRecipient.getId(), requester.getId(), false))
.enqueue();
GroupId groupId;
try {
groupId = GroupId.push(content.getGroupId().get());
} catch (BadGroupIdException e) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt-SK] Bad groupId!");
return;
}
needsDistributionMessage = false;
} else {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] The message was successfully sent to the requester, but not delivered. But it's " + messageAge + " ms old, so we're not resending.");
}
}
if (!groupId.isV2()) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt-SK] Not a valid GV2 ID!");
return;
}
if (needsDistributionMessage && threadRecipient.getParticipants().contains(requester)) {
warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Requester is, however, in the group now. Sending distribution message.");
ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.requireGroupId().requireV2()));
}
Optional<GroupRecord> groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId);
if (!groupRecord.isPresent()) {
warn(content.getTimestamp(), "[RetryReceipt-SK] Could not find a record for the group!");
return;
}
if (!groupRecord.get().getMembers().contains(requester.getId())) {
warn(content.getTimestamp(), "[RetryReceipt-SK] The requester is not in the group, so we cannot send them a SenderKeyDistributionMessage.");
return;
}
warn(content.getTimestamp(), "[RetryReceipt-SK] The requester is in the group, so we'll send them a SenderKeyDistributionMessage.");
ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), groupRecord.get().getId().requireV2()));
}
}
private void handleIndividualRetryReceipt(@NonNull Recipient requester, @Nullable MessageLogEntry messageLogEntry, @NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) {
boolean archivedSession = false;
if (decryptionErrorMessage.getDeviceId() == SignalServiceAddress.DEFAULT_DEVICE_ID &&
decryptionErrorMessage.getRatchetKey().isPresent() &&
SessionUtil.ratchetKeyMatches(context, requester, content.getSenderDevice(), decryptionErrorMessage.getRatchetKey().get()))
{
warn(content.getTimestamp(), "[RetryReceipt-I] Ratchet key matches. Archiving the session.");
SessionUtil.archiveSession(context, requester.getId(), content.getSenderDevice());
archivedSession = true;
}
if (messageLogEntry != null) {
warn(content.getTimestamp(), "[RetryReceipt-I] Found an entry in the MSL. Resending.");
ApplicationDependencies.getJobManager().add(new ResendMessageJob(messageLogEntry.getRecipientId(),
messageLogEntry.getDateSent(),
messageLogEntry.getContent(),
messageLogEntry.getContentHint(),
null,
null));
} else if (archivedSession) {
warn(content.getTimestamp(), "[RetryReceipt-I] Could not find an entry in the MSL, but we archived the session, so we're sending a null message to complete the reset.");
ApplicationDependencies.getJobManager().add(new NullMessageSendJob(requester.getId()));
} else {
warn(content.getTimestamp(), "[RetryReceipt-I] Could not find an entry in the MSL. Skipping.");
}
}
private static @Nullable MessageRecord findRetryReceiptRelatedMessage(@NonNull Context context, @Nullable MessageLogEntry messageLogEntry, long sentTimestamp) {
if (messageLogEntry != null && messageLogEntry.hasRelatedMessage()) {
if (messageLogEntry.isRelatedMessageMms()) {
return DatabaseFactory.getMmsDatabase(context).getMessageRecordOrNull(messageLogEntry.getRelatedMessageId());
} else {
return DatabaseFactory.getSmsDatabase(context).getMessageRecordOrNull(messageLogEntry.getRelatedMessageId());
}
} else {
log(String.valueOf(content.getTimestamp()), "[RetryReceipt] SMS " + messageRecord.getId());
SmsMessageRecord sms = (SmsMessageRecord) messageRecord;
return DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, Recipient.self().getId());
}
}
@@ -2092,6 +2163,10 @@ public final class MessageContentProcessor {
warn(extra, message, null);
}
protected void warn(long timestamp, @NonNull String message) {
warn(String.valueOf(timestamp), message);
}
protected void warn(@NonNull String message, @Nullable Throwable t) {
warn("", message, t);
}