mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 02:08:40 +00:00
Implement the message send log for sender key retries.
This commit is contained in:
committed by
Cody Henthorne
parent
6502ef64ce
commit
f19033a7a2
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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+""});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -6,7 +6,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum ContentHint {
|
||||
/** This message has content, but you shouldn’t 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. */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,4 +30,8 @@ public class OutgoingPushMessage {
|
||||
this.destinationRegistrationId = destinationRegistrationId;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public int getDestinationDeviceId() {
|
||||
return destinationDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user