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);
}

View File

@@ -219,18 +219,16 @@ public class SignalServiceMessageSender {
*
* @param recipient The sender of the received message you're acknowledging.
* @param message The read receipt to deliver.
* @throws IOException
* @throws UntrustedIdentityException
*/
public void sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceReceiptMessage message)
public SendMessageResult sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceReceiptMessage message)
throws IOException, UntrustedIdentityException
{
Content content = createReceiptContent(message);
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null);
return sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), envelopeContent, false, null);
}
/**
@@ -254,8 +252,6 @@ public class SignalServiceMessageSender {
*
* @param recipient The destination
* @param message The typing indicator to deliver
* @throws IOException
* @throws UntrustedIdentityException
*/
public void sendTyping(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
@@ -402,6 +398,23 @@ public class SignalServiceMessageSender {
new SignalGroupSessionBuilder(sessionLock, new GroupSessionBuilder(store)).process(sender, senderKeyDistributionMessage);
}
/**
* Resend a previously-sent message.
*/
public SendMessageResult resendContent(SignalServiceAddress address,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
long timestamp,
Content content,
ContentHint contentHint,
Optional<byte[]> groupId)
throws UntrustedIdentityException, IOException
{
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, groupId);
Optional<UnidentifiedAccess> access = unidentifiedAccess.isPresent() ? unidentifiedAccess.get().getTargetUnidentifiedAccess() : Optional.absent();
return sendMessage(address, access, timestamp, envelopeContent, false, null);
}
/**
* Sends a {@link SignalServiceDataMessage} to a group using sender keys.
*/
@@ -469,7 +482,7 @@ public class SignalServiceMessageSender {
return results;
}
public void sendSyncMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
Content content;
@@ -502,8 +515,7 @@ public class SignalServiceMessageSender {
} else if (message.getKeys().isPresent()) {
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
} else if (message.getVerified().isPresent()) {
sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess);
return;
return sendVerifiedMessage(message.getVerified().get(), unidentifiedAccess);
} else {
throw new IOException("Unsupported sync message!");
}
@@ -513,7 +525,7 @@ public class SignalServiceMessageSender {
EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent());
sendMessage(localAddress, Optional.absent(), timestamp, envelopeContent, false, null);
return sendMessage(localAddress, Optional.absent(), timestamp, envelopeContent, false, null);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
@@ -631,7 +643,7 @@ public class SignalServiceMessageSender {
attachment.getUploadTimestamp());
}
private void sendVerifiedMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
private SendMessageResult sendVerifiedMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
byte[] nullMessageBody = DataMessage.newBuilder()
@@ -657,6 +669,8 @@ public class SignalServiceMessageSender {
sendMessage(localAddress, Optional.absent(), message.getTimestamp(), syncMessageContent, false, null);
}
return result;
}
public SendMessageResult sendNullMessage(SignalServiceAddress address, Optional<UnidentifiedAccessPair> unidentifiedAccess)
@@ -1016,9 +1030,10 @@ public class SignalServiceMessageSender {
private Content createMultiDeviceSentTranscriptContent(SentTranscriptMessage transcript, Optional<UnidentifiedAccessPair> unidentifiedAccess) throws IOException {
SignalServiceAddress address = transcript.getDestination().get();
SendMessageResult result = SendMessageResult.success(address, unidentifiedAccess.isPresent(), true, -1);
Content content = createMessageContent(transcript.getMessage());
SendMessageResult result = SendMessageResult.success(address, Collections.emptyList(), unidentifiedAccess.isPresent(), true, -1, Optional.of(content));
return createMultiDeviceSentTranscriptContent(createMessageContent(transcript.getMessage()),
return createMultiDeviceSentTranscriptContent(content,
Optional.of(address),
transcript.getTimestamp(),
Collections.singletonList(result),
@@ -1613,7 +1628,7 @@ public class SignalServiceMessageSender {
if (pipe.isPresent() && !unidentifiedAccess.isPresent()) {
try {
SendMessageResponse response = pipe.get().send(messages, Optional.absent()).get(10, TimeUnit.SECONDS);
return SendMessageResult.success(recipient, false, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime);
return SendMessageResult.success(recipient, messages.getDevices(), false, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime, content.getContent());
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, e);
Log.w(TAG, "[sendMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
@@ -1621,7 +1636,7 @@ public class SignalServiceMessageSender {
} else if (unidentifiedPipe.isPresent() && unidentifiedAccess.isPresent()) {
try {
SendMessageResponse response = unidentifiedPipe.get().send(messages, unidentifiedAccess).get(10, TimeUnit.SECONDS);
return SendMessageResult.success(recipient, true, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime);
return SendMessageResult.success(recipient, messages.getDevices(), true, response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime, content.getContent());
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, e);
Log.w(TAG, "[sendMessage] Unidentified pipe failed, falling back...");
@@ -1634,7 +1649,7 @@ public class SignalServiceMessageSender {
SendMessageResponse response = socket.sendMessage(messages, unidentifiedAccess);
return SendMessageResult.success(recipient, unidentifiedAccess.isPresent(), response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime);
return SendMessageResult.success(recipient, messages.getDevices(), unidentifiedAccess.isPresent(), response.getNeedsSync() || isMultiDevice.get(), System.currentTimeMillis() - startTime, content.getContent());
} catch (InvalidKeyException ike) {
Log.w(TAG, ike);
@@ -1693,15 +1708,21 @@ public class SignalServiceMessageSender {
accessByUuid.put(addressIterator.next().getUuid().get(), accessIterator.next());
}
Map<SignalServiceAddress, List<Integer>> recipientDevices = recipients.stream().collect(Collectors.toMap(a -> a, a -> new LinkedList<>()));
for (int i = 0; i < RETRY_COUNT; i++) {
List<SignalProtocolAddress> destinations = new LinkedList<>();
for (SignalServiceAddress recipient : recipients) {
List<Integer> devices = recipientDevices.get(recipient);
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), SignalServiceAddress.DEFAULT_DEVICE_ID));
devices.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
for (int deviceId : store.getSubDeviceSessions(recipient.getIdentifier())) {
if (store.containsSession(new SignalProtocolAddress(recipient.getIdentifier(), deviceId))) {
destinations.add(new SignalProtocolAddress(recipient.getUuid().get().toString(), deviceId));
devices.add(deviceId);
}
}
}
@@ -1783,7 +1804,7 @@ public class SignalServiceMessageSender {
if (pipe.isPresent()) {
try {
SendGroupMessageResponse response = pipe.get().sendToGroup(ciphertext, joinedUnidentifiedAccess, timestamp, online).get(10, TimeUnit.SECONDS);
return transformGroupResponseToMessageResults(recipients, response);
return transformGroupResponseToMessageResults(recipientDevices, response, content);
} catch (IOException | ExecutionException | InterruptedException | TimeoutException e) {
Log.w(TAG, "[sendGroupMessage] Pipe failed, falling back... (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")");
}
@@ -1793,7 +1814,7 @@ public class SignalServiceMessageSender {
try {
SendGroupMessageResponse response = socket.sendGroupMessage(ciphertext, joinedUnidentifiedAccess, timestamp, online);
return transformGroupResponseToMessageResults(recipients, response);
return transformGroupResponseToMessageResults(recipientDevices, response, content);
} catch (GroupMismatchedDevicesException e) {
Log.w(TAG, "[sendGroupMessage] Handling mismatched devices.", e);
for (GroupMismatchedDevices mismatched : e.getMismatchedDevices()) {
@@ -1822,7 +1843,7 @@ public class SignalServiceMessageSender {
throw new IOException("Failed to resolve conflicts after " + RETRY_COUNT + " attempts!");
}
private List<SendMessageResult> transformGroupResponseToMessageResults(List<SignalServiceAddress> recipients, SendGroupMessageResponse response) {
private List<SendMessageResult> transformGroupResponseToMessageResults(Map<SignalServiceAddress, List<Integer>> recipients, SendGroupMessageResponse response, Content content) {
Set<UUID> unregistered = response.getUnsentTargets();
List<SendMessageResult> failures = unregistered.stream()
@@ -1830,12 +1851,14 @@ public class SignalServiceMessageSender {
.map(SendMessageResult::unregisteredFailure)
.collect(Collectors.toList());
List<SendMessageResult> success = recipients.stream()
List<SendMessageResult> success = recipients.keySet()
.stream()
.filter(r -> !unregistered.contains(r.getUuid().get()))
.map(a -> SendMessageResult.success(a, true, isMultiDevice.get(), -1))
.map(a -> SendMessageResult.success(a, recipients.get(a), true, isMultiDevice.get(), -1, Optional.of(content)))
.collect(Collectors.toList());
List<SendMessageResult> results = new LinkedList<>(success);
List<SendMessageResult> results = new ArrayList<>(success.size() + failures.size());
results.addAll(success);
results.addAll(failures);
return results;

View File

@@ -6,7 +6,7 @@ import java.util.HashMap;
import java.util.Map;
public enum ContentHint {
/** This message has content, but you shouldnt expect it to be re-sent to you. */
/** This message has content, but you shouldn't expect it to be re-sent to you. */
DEFAULT(UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT),
/** You should expect to be able to have this content be re-sent to you. */

View File

@@ -39,12 +39,17 @@ public interface EnvelopeContent {
*/
int size();
/**
* A content proto, if applicable.
*/
Optional<Content> getContent();
/**
* Wrap {@link Content} you plan on sending as an encrypted message.
* This is the default. Consider anything else exceptional.
*/
static EnvelopeContent encrypted(Content content, ContentHint contentHint, Optional<byte[]> groupId) {
return new Encrypted(content.toByteArray(), contentHint, groupId);
return new Encrypted(content, contentHint, groupId);
}
/**
@@ -56,14 +61,14 @@ public interface EnvelopeContent {
class Encrypted implements EnvelopeContent {
private final byte[] unpaddedMessage;
private final Content content;
private final ContentHint contentHint;
private final Optional<byte[]> groupId;
public Encrypted(byte[] unpaddedMessage, ContentHint contentHint, Optional<byte[]> groupId) {
this.unpaddedMessage = unpaddedMessage;
this.contentHint = contentHint;
this.groupId = groupId;
public Encrypted(Content content, ContentHint contentHint, Optional<byte[]> groupId) {
this.content = content;
this.contentHint = contentHint;
this.groupId = groupId;
}
@Override
@@ -74,7 +79,7 @@ public interface EnvelopeContent {
throws UntrustedIdentityException, InvalidKeyException
{
PushTransportDetails transportDetails = new PushTransportDetails();
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(content.toByteArray()));
UnidentifiedSenderMessageContent messageContent = new UnidentifiedSenderMessageContent(message,
senderCertificate,
contentHint.getType(),
@@ -90,7 +95,7 @@ public interface EnvelopeContent {
@Override
public OutgoingPushMessage processUnsealedSender(SignalSessionCipher sessionCipher, SignalProtocolAddress destination) throws UntrustedIdentityException {
PushTransportDetails transportDetails = new PushTransportDetails();
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage));
CiphertextMessage message = sessionCipher.encrypt(transportDetails.getPaddedMessageBody(content.toByteArray()));
int remoteRegistrationId = sessionCipher.getRemoteRegistrationId();
String body = Base64.encodeBytes(message.serialize());
@@ -107,7 +112,12 @@ public interface EnvelopeContent {
@Override
public int size() {
return unpaddedMessage.length;
return content.getSerializedSize();
}
@Override
public Optional<Content> getContent() {
return Optional.of(content);
}
}
@@ -152,5 +162,10 @@ public interface EnvelopeContent {
public int size() {
return plaintextContent.getBody().length;
}
@Override
public Optional<Content> getContent() {
return Optional.absent();
}
}
}

View File

@@ -2,8 +2,13 @@ package org.whispersystems.signalservice.api.messages;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
import java.util.List;
public class SendMessageResult {
@@ -14,8 +19,8 @@ public class SendMessageResult {
private final IdentityFailure identityFailure;
private final ProofRequiredException proofRequiredFailure;
public static SendMessageResult success(SignalServiceAddress address, boolean unidentified, boolean needsSync, long duration) {
return new SendMessageResult(address, new Success(unidentified, needsSync, duration), false, false, null, null);
public static SendMessageResult success(SignalServiceAddress address, List<Integer> devices, boolean unidentified, boolean needsSync, long duration, Optional<Content> content) {
return new SendMessageResult(address, new Success(unidentified, needsSync, duration, content, devices), false, false, null, null);
}
public static SendMessageResult networkFailure(SignalServiceAddress address) {
@@ -78,14 +83,18 @@ public class SendMessageResult {
}
public static class Success {
private final boolean unidentified;
private final boolean needsSync;
private final long duration;
private final boolean unidentified;
private final boolean needsSync;
private final long duration;
private final Optional<Content> content;
private final List<Integer> devices;
private Success(boolean unidentified, boolean needsSync, long duration) {
private Success(boolean unidentified, boolean needsSync, long duration, Optional<Content> content, List<Integer> devices) {
this.unidentified = unidentified;
this.needsSync = needsSync;
this.duration = duration;
this.content = content;
this.devices = devices;
}
public boolean isUnidentified() {
@@ -99,6 +108,14 @@ public class SendMessageResult {
public long getDuration() {
return duration;
}
public Optional<Content> getContent() {
return content;
}
public List<Integer> getDevices() {
return devices;
}
}
public static class IdentityFailure {

View File

@@ -30,4 +30,8 @@ public class OutgoingPushMessage {
this.destinationRegistrationId = destinationRegistrationId;
this.content = content;
}
public int getDestinationDeviceId() {
return destinationDeviceId;
}
}

View File

@@ -6,9 +6,11 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.stream.Collectors;
public class OutgoingPushMessageList {
@@ -50,4 +52,9 @@ public class OutgoingPushMessageList {
public boolean isOnline() {
return online;
}
@JsonIgnore
public List<Integer> getDevices() {
return messages.stream().map(OutgoingPushMessage::getDestinationDeviceId).collect(Collectors.toList());
}
}