package org.thoughtcrime.securesms.messages; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.os.Build; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.google.protobuf.ByteString; import com.mobilecoin.lib.exceptions.SerializationException; import org.signal.core.util.Hex; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.SignalProtocolAddress; import org.signal.libsignal.protocol.ecc.ECPublicKey; import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.state.SessionRecord; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.ringrtc.CallId; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.PointerAttachment; import org.thoughtcrime.securesms.attachments.TombstoneAttachment; import org.thoughtcrime.securesms.attachments.UriAttachment; import org.thoughtcrime.securesms.components.emoji.EmojiUtil; 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.database.AttachmentTable; import org.thoughtcrime.securesms.database.CallTable; import org.thoughtcrime.securesms.database.GroupReceiptTable; import org.thoughtcrime.securesms.database.GroupReceiptTable.GroupReceiptInfo; import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.MessageTable; import org.thoughtcrime.securesms.database.MessageTable.InsertResult; import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.PaymentMetaDataUtil; import org.thoughtcrime.securesms.database.PaymentTable; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.SentStorySyncManifest; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.StickerTable; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.model.DatabaseProtosUtil; import org.thoughtcrime.securesms.database.model.DistributionListId; import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageId; 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.ParentStoryId; import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.StickerRecord; import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor; import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.BadGroupIdException; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.GroupNotAMemberException; import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob; import org.thoughtcrime.securesms.jobs.AutomaticSessionResetJob; import org.thoughtcrime.securesms.jobs.GroupCallPeekJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactSyncJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDevicePniIdentityUpdateJob; 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.PushProcessEarlyMessagesJob; import org.thoughtcrime.securesms.jobs.PushProcessMessageJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob; import org.thoughtcrime.securesms.jobs.ResendMessageJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob; import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob; import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingMessage; import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.StickerSlide; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.v2.ConversationId; import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.WebRtcData; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.stories.Stories; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.LinkUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.RemoteDeleteUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTextAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; import org.whispersystems.signalservice.api.messages.calls.BusyMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; import org.whispersystems.signalservice.api.messages.multidevice.OutgoingPaymentMessage; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage; import org.whispersystems.signalservice.api.messages.multidevice.ViewedMessage; 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.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage; import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Takes data about a decrypted message, transforms it into user-presentable data, and writes that * data to our data stores. */ @SuppressWarnings({ "OptionalGetWithoutIsPresent", "OptionalIsPresent" }) public class MessageContentProcessor { private static final String TAG = Log.tag(MessageContentProcessor.class); private final Context context; public static MessageContentProcessor create(@NonNull Context context) { return new MessageContentProcessor(context); } @VisibleForTesting MessageContentProcessor(@NonNull Context context) { this.context = context; } /** * Given the details about a message decryption, this will insert the proper message content into * the database. * * This is super-stateful, and it's recommended that this be run in a transaction so that no * intermediate results are persisted to the database if the app were to crash. */ public void process(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId) throws IOException, GroupChangeBusyException { process(messageState, content, exceptionMetadata, envelopeTimestamp, smsMessageId, false); } /** * The same as {@link #process(MessageState, SignalServiceContent, ExceptionMetadata, long, long)}, except specifically targeted at early content. * Using this method will *not* store or enqueue early content jobs if we detect this as being early, to avoid recursive scenarios. */ public void processEarlyContent(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId) throws IOException, GroupChangeBusyException { process(messageState, content, exceptionMetadata, envelopeTimestamp, smsMessageId, true); } private void process(MessageState messageState, @Nullable SignalServiceContent content, @Nullable ExceptionMetadata exceptionMetadata, long envelopeTimestamp, long smsMessageId, boolean processingEarlyContent) throws IOException, GroupChangeBusyException { Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.empty(); if (messageState == MessageState.DECRYPTED_OK) { if (content != null) { Recipient senderRecipient = Recipient.externalPush(content.getSender()); handleMessage(content, senderRecipient, optionalSmsMessageId, processingEarlyContent); Optional> earlyContent = ApplicationDependencies.getEarlyMessageCache() .retrieve(senderRecipient.getId(), content.getTimestamp()); if (!processingEarlyContent && earlyContent.isPresent()) { log(String.valueOf(content.getTimestamp()), "Found " + earlyContent.get().size() + " dependent item(s) that were retrieved earlier. Processing."); for (SignalServiceContent earlyItem : earlyContent.get()) { handleMessage(earlyItem, senderRecipient, Optional.empty(), true); } } } else { warn("null", "Null content. Ignoring message."); } } else if (exceptionMetadata != null) { handleExceptionMessage(messageState, exceptionMetadata, envelopeTimestamp, optionalSmsMessageId); } else if (messageState == MessageState.NOOP) { Log.d(TAG, "Nothing to do: " + messageState.name()); } else { warn("Bad state! messageState: " + messageState); } } private void handleMessage(@NonNull SignalServiceContent content, @NonNull Recipient senderRecipient, @NonNull Optional smsMessageId, boolean processingEarlyContent) throws IOException, GroupChangeBusyException { try { Recipient threadRecipient = getMessageDestination(content, senderRecipient); if (shouldIgnore(content, senderRecipient, threadRecipient)) { log(content.getTimestamp(), "Ignoring message."); return; } PendingRetryReceiptModel pending = ApplicationDependencies.getPendingRetryReceiptCache().get(senderRecipient.getId(), content.getTimestamp()); long receivedTime = handlePendingRetry(pending, content, threadRecipient); log(String.valueOf(content.getTimestamp()), "Beginning message processing. Sender: " + formatSender(senderRecipient, content)); if (content.getDataMessage().isPresent()) { GroupTable groupDatabase = SignalDatabase.groups(); SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent() || message.getBodyRanges().isPresent(); Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); boolean isGv2Message = groupId.isPresent() && groupId.get().isV2(); if (isGv2Message) { if (handleGv2PreProcessing(groupId.orElse(null).requireV2(), content, content.getDataMessage().get().getGroupContext().get(), senderRecipient)) { return; } } MessageId messageId = null; if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId); else if (message.isEndSession()) messageId = handleEndSessionMessage(content, smsMessageId, senderRecipient); else if (message.isExpirationUpdate()) messageId = handleExpirationUpdate(content, message, smsMessageId, groupId, senderRecipient, threadRecipient, receivedTime, false); else if (message.getReaction().isPresent() && message.getStoryContext().isPresent()) messageId = handleStoryReaction(content, message, senderRecipient); else if (message.getReaction().isPresent()) messageId = handleReaction(content, message, senderRecipient, processingEarlyContent); else if (message.getRemoteDelete().isPresent()) messageId = handleRemoteDelete(content, message, senderRecipient, processingEarlyContent); else if (message.isActivatePaymentsRequest()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, true, false); else if (message.isPaymentsActivated()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, false, true); else if (message.getPayment().isPresent()) messageId = handlePayment(content, message, smsMessageId, senderRecipient, receivedTime); else if (message.getStoryContext().isPresent()) messageId = handleStoryReply(content, message, senderRecipient, receivedTime); else if (message.getGiftBadge().isPresent()) messageId = handleGiftMessage(content, message, senderRecipient, threadRecipient, receivedTime); else if (isMediaMessage) messageId = handleMediaMessage(content, message, smsMessageId, senderRecipient, threadRecipient, receivedTime); else if (message.getBody().isPresent()) messageId = handleTextMessage(content, message, smsMessageId, groupId, senderRecipient, threadRecipient, receivedTime); else if (Build.VERSION.SDK_INT > 19 && message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId, senderRecipient); if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { handleUnknownGroupMessage(content, message.getGroupContext().get(), senderRecipient); } if (message.getProfileKey().isPresent()) { handleProfileKey(content, message.getProfileKey().get(), senderRecipient); } if (content.isNeedsReceipt() && messageId != null) { handleNeedsDeliveryReceipt(senderRecipient.getId(), message, messageId); } else if (!content.isNeedsReceipt()) { if (RecipientUtil.shouldHaveProfileKey(threadRecipient)) { Log.w(TAG, "Received an unsealed sender message from " + senderRecipient.getId() + ", but they should already have our profile key. Correcting."); if (groupId.isPresent() && groupId.get().isV2()) { Log.i(TAG, "Message was to a GV2 group. Ensuring our group profile keys are up to date."); ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false)) .then(GroupV2UpdateSelfProfileKeyJob.withQueueLimits(groupId.get().requireV2())) .enqueue(); } else if (!threadRecipient.isGroup()) { Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key."); ProfileKeySendJob profileSendJob = ProfileKeySendJob.create(SignalDatabase.threads().getOrCreateThreadIdFor(threadRecipient), true); if (profileSendJob != null) { ApplicationDependencies.getJobManager() .startChain(new RefreshAttributesJob(false)) .then(profileSendJob) .enqueue(); } } } } } else if (content.getSyncMessage().isPresent()) { TextSecurePreferences.setMultiDevice(context, true); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get(), senderRecipient, processingEarlyContent); else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get(), content.getTimestamp()); else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(content, syncMessage.getRead().get(), content.getTimestamp(), processingEarlyContent); else if (syncMessage.getViewed().isPresent()) handleSynchronizeViewedMessage(syncMessage.getViewed().get(), content.getTimestamp()); else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(content, syncMessage.getViewOnceOpen().get(), content.getTimestamp(), processingEarlyContent); else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get()); else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get(), content.getTimestamp()); else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get(), content.getTimestamp()); else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get()); else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get(), content.getTimestamp()); else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get(), content.getTimestamp()); else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get()); else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp()); else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp()); else if (syncMessage.getCallEvent().isPresent()) handleSynchronizeCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp()); else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types..."); } else if (content.getCallMessage().isPresent()) { log(String.valueOf(content.getTimestamp()), "Got call message..."); SignalServiceCallMessage message = content.getCallMessage().get(); Optional destinationDeviceId = message.getDestinationDeviceId(); if (destinationDeviceId.isPresent() && destinationDeviceId.get() != SignalStore.account().getDeviceId()) { log(String.valueOf(content.getTimestamp()), String.format(Locale.US, "Ignoring call message that is not for this device! intended: %d, this: %d", destinationDeviceId.get(), SignalStore.account().getDeviceId())); return; } if (message.getOfferMessage().isPresent()) handleCallOfferMessage(content, message.getOfferMessage().get(), smsMessageId, senderRecipient); else if (message.getAnswerMessage().isPresent()) handleCallAnswerMessage(content, message.getAnswerMessage().get(), senderRecipient); else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(content, message.getIceUpdateMessages().get(), senderRecipient); else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(content, message.getHangupMessage().get(), smsMessageId, senderRecipient); else if (message.getBusyMessage().isPresent()) handleCallBusyMessage(content, message.getBusyMessage().get(), senderRecipient); else if (message.getOpaqueMessage().isPresent()) handleCallOpaqueMessage(content, message.getOpaqueMessage().get(), senderRecipient); } else if (content.getReceiptMessage().isPresent()) { SignalServiceReceiptMessage message = content.getReceiptMessage().get(); if (message.isReadReceipt()) handleReadReceipt(content, message, senderRecipient, processingEarlyContent); else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message, senderRecipient); else if (message.isViewedReceipt()) handleViewedReceipt(content, message, senderRecipient, processingEarlyContent); } else if (content.getTypingMessage().isPresent()) { handleTypingMessage(content, content.getTypingMessage().get(), senderRecipient); } else if (content.getStoryMessage().isPresent()) { handleStoryMessage(content, content.getStoryMessage().get(), senderRecipient, threadRecipient); } else if (content.getDecryptionErrorMessage().isPresent()) { handleRetryReceipt(content, content.getDecryptionErrorMessage().get(), senderRecipient); } else if (content.getSenderKeyDistributionMessage().isPresent()) { // Already handled, here in order to prevent unrecognized message log } else if (content.getPniSignatureMessage().isPresent()) { // Already handled, here in order to prevent unrecognized message log } else { warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!"); } resetRecipientToPush(senderRecipient); if (pending != null) { warn(content.getTimestamp(), "Pending retry was processed. Deleting."); ApplicationDependencies.getPendingRetryReceiptCache().delete(pending); } } catch (StorageFailedException e) { warn(String.valueOf(content.getTimestamp()), e); handleCorruptMessage(e.getSender(), e.getSenderDevice(), content.getTimestamp(), smsMessageId); } catch (BadGroupIdException e) { warn(String.valueOf(content.getTimestamp()), "Ignoring message with bad group id", e); } } private long handlePendingRetry(@Nullable PendingRetryReceiptModel pending, @NonNull SignalServiceContent content, @NonNull Recipient destination) throws BadGroupIdException { long receivedTime = System.currentTimeMillis(); if (pending != null) { warn(content.getTimestamp(), "Incoming message matches a pending retry we were expecting."); Long threadId = SignalDatabase.threads().getThreadIdFor(destination.getId()); if (threadId != null) { ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId); long visibleThread = ApplicationDependencies.getMessageNotifier().getVisibleThread().map(ConversationId::getThreadId).orElse(-1L); if (threadId != visibleThread && metadata.getLastSeen() > 0 && metadata.getLastSeen() < pending.getReceivedTimestamp()) { receivedTime = pending.getReceivedTimestamp(); warn(content.getTimestamp(), "Thread has not been opened yet. Using received timestamp of " + receivedTime); } else { warn(content.getTimestamp(), "Thread was opened after receiving the original message. Using the current time for received time. (Last seen: " + metadata.getLastSeen() + ", ThreadVisible: " + (threadId == visibleThread) + ")"); } } else { warn(content.getTimestamp(), "Could not find a thread for the pending message. Using current time for received time."); } } return receivedTime; } private @Nullable MessageId handlePayment(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient, long receivedTime) throws StorageFailedException { log(content.getTimestamp(), "Payment message."); if (!message.getPayment().isPresent()) { throw new AssertionError(); } if (!message.getPayment().get().getPaymentNotification().isPresent()) { warn(content.getTimestamp(), "Ignoring payment message without notification"); return null; } SignalServiceDataMessage.PaymentNotification paymentNotification = message.getPayment().get().getPaymentNotification().get(); PaymentTable paymentDatabase = SignalDatabase.payments(); UUID uuid = UUID.randomUUID(); String queue = "Payment_" + PushProcessMessageJob.getQueueName(senderRecipient.getId()); MessageId messageId = null; try { paymentDatabase.createIncomingPayment(uuid, senderRecipient.getId(), message.getTimestamp(), paymentNotification.getNote(), Money.MobileCoin.ZERO, Money.MobileCoin.ZERO, paymentNotification.getReceipt(), true); IncomingMediaMessage mediaMessage = IncomingMediaMessage.createIncomingPaymentNotification(senderRecipient.getId(), content, receivedTime, TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), uuid); Optional insertResult = SignalDatabase.messages().insertSecureDecryptedMessageInbox(mediaMessage, -1); smsMessageId.ifPresent(smsId -> SignalDatabase.messages().deleteMessage(smsId)); if (insertResult.isPresent()) { messageId = new MessageId(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } catch (PaymentTable.PublicKeyConflictException e) { warn(content.getTimestamp(), "Ignoring payment with public key already in database"); } catch (SerializationException e) { warn(content.getTimestamp(), "Ignoring payment with bad data.", e); } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } finally { ApplicationDependencies.getJobManager() .startChain(new PaymentTransactionCheckJob(uuid, queue)) .then(PaymentLedgerUpdateJob.updateLedger()) .enqueue(); } return messageId; } /** * @return True if the content should be ignored, otherwise false. */ private boolean handleGv2PreProcessing(@NonNull GroupId.V2 groupId, @NonNull SignalServiceContent content, @NonNull SignalServiceGroupV2 groupV2, @NonNull Recipient senderRecipient) throws IOException, GroupChangeBusyException { GroupTable groupDatabase = SignalDatabase.groups(); Optional possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(groupId); if (possibleGv1.isPresent()) { GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1()); } if (!updateGv2GroupFromServerOrP2PChange(content, groupV2)) { log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupId); return true; } Optional groupRecord = groupDatabase.getGroup(groupId); if (groupRecord.isPresent() && !groupRecord.get().getMembers().contains(senderRecipient.getId())) { log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message from member not in group " + groupId + ". Sender: " + senderRecipient.getId() + " | " + senderRecipient.requireServiceId()); return true; } if (groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(senderRecipient)) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage data = content.getDataMessage().get(); if (data.getBody().isPresent() || data.getAttachments().isPresent() || data.getQuote().isPresent() || data.getPreviews().isPresent() || data.getMentions().isPresent() || data.getSticker().isPresent()) { Log.w(TAG, "Ignoring message from " + senderRecipient.getId() + " because it has disallowed content, and they're not an admin in an announcement-only group."); return true; } } else if (content.getTypingMessage().isPresent()) { Log.w(TAG, "Ignoring typing indicator from " + senderRecipient.getId() + " because they're not an admin in an announcement-only group."); return true; } } return false; } /** * Attempts to update the group to the revision mentioned in the message. * If the local version is at least the revision in the message it will not query the server. * If the message includes a signed change proto that is sufficient (i.e. local revision is only * 1 revision behind), it will also not query the server in this case. * * @return false iff needed to query the server and was not able to because self is not a current * member of the group. */ private boolean updateGv2GroupFromServerOrP2PChange(@NonNull SignalServiceContent content, @NonNull SignalServiceGroupV2 groupV2) throws IOException, GroupChangeBusyException { try { long timestamp = groupV2.getSignedGroupChange() != null ? content.getTimestamp() : content.getTimestamp() - 1; GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), timestamp, groupV2.getSignedGroupChange()); return true; } catch (GroupNotAMemberException e) { warn(String.valueOf(content.getTimestamp()), "Ignoring message for a group we're not in"); return false; } } private void handleExceptionMessage(@NonNull MessageState messageState, @NonNull ExceptionMetadata e, long timestamp, @NonNull Optional smsMessageId) { Recipient sender = Recipient.external(context, e.sender); if (sender.isBlocked()) { warn("Ignoring exception content from blocked sender, message state:" + messageState); return; } switch (messageState) { case DECRYPTION_ERROR: warn(String.valueOf(timestamp), "Handling encryption error."); SignalDatabase.messages().insertBadDecryptMessage(sender.getId(), e.senderDevice, timestamp, System.currentTimeMillis(), getThreadIdForException(e)); break; case INVALID_VERSION: warn(String.valueOf(timestamp), "Handling invalid version."); handleInvalidVersionMessage(e.sender, e.senderDevice, timestamp, smsMessageId); break; case LEGACY_MESSAGE: warn(String.valueOf(timestamp), "Handling legacy message."); handleLegacyMessage(e.sender, e.senderDevice, timestamp, smsMessageId); break; case DUPLICATE_MESSAGE: warn(String.valueOf(timestamp), "Duplicate message. Dropping."); break; case UNSUPPORTED_DATA_MESSAGE: warn(String.valueOf(timestamp), "Handling unsupported data message."); handleUnsupportedDataMessage(e.sender, e.senderDevice, Optional.ofNullable(e.groupId), timestamp, smsMessageId); break; case CORRUPT_MESSAGE: case NO_SESSION: warn(String.valueOf(timestamp), "Discovered old enqueued bad encrypted message. Scheduling reset."); ApplicationDependencies.getJobManager().add(new AutomaticSessionResetJob(sender.getId(), e.senderDevice, timestamp)); break; default: throw new AssertionError("Not handled " + messageState + ". (" + timestamp + ")"); } } private long getThreadIdForException(ExceptionMetadata metadata) { if (metadata.groupId != null) { Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(metadata.groupId); return SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); } else { return SignalDatabase.threads().getOrCreateThreadIdFor(Recipient.external(context, metadata.sender)); } } private void handleCallOfferMessage(@NonNull SignalServiceContent content, @NonNull OfferMessage message, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient) { log(String.valueOf(content.getTimestamp()), "handleCallOfferMessage..."); if (smsMessageId.isPresent()) { MessageTable database = SignalDatabase.messages(); database.markAsMissedCall(smsMessageId.get(), message.getType() == OfferMessage.Type.VIDEO_CALL); } else { RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); ApplicationDependencies.getSignalCallManager() .receivedOffer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), new WebRtcData.OfferMetadata(message.getOpaque(), message.getSdp(), message.getType()), new WebRtcData.ReceivedOfferMetadata(remoteIdentityKey, content.getServerReceivedTimestamp(), content.getServerDeliveredTimestamp(), content.getCallMessage().get().isMultiRing())); } } private void handleCallAnswerMessage(@NonNull SignalServiceContent content, @NonNull AnswerMessage message, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "handleCallAnswerMessage..."); RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get(); ApplicationDependencies.getSignalCallManager() .receivedAnswer(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), new WebRtcData.AnswerMetadata(message.getOpaque(), message.getSdp()), new WebRtcData.ReceivedAnswerMetadata(remoteIdentityKey, content.getCallMessage().get().isMultiRing())); } private void handleCallIceUpdateMessage(@NonNull SignalServiceContent content, @NonNull List messages, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "handleCallIceUpdateMessage... " + messages.size()); List iceCandidates = new ArrayList<>(messages.size()); long callId = -1; for (IceUpdateMessage iceMessage : messages) { iceCandidates.add(iceMessage.getOpaque()); callId = iceMessage.getId(); } RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(callId)); ApplicationDependencies.getSignalCallManager() .receivedIceCandidates(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), iceCandidates); } private void handleCallHangupMessage(@NonNull SignalServiceContent content, @NonNull HangupMessage message, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "handleCallHangupMessage"); if (smsMessageId.isPresent()) { SignalDatabase.messages().markAsMissedCall(smsMessageId.get(), false); } else { RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); ApplicationDependencies.getSignalCallManager() .receivedCallHangup(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice()), new WebRtcData.HangupMetadata(message.getType(), message.isLegacy(), message.getDeviceId())); } } private void handleCallBusyMessage(@NonNull SignalServiceContent content, @NonNull BusyMessage message, @NonNull Recipient senderRecipient) { log(String.valueOf(content.getTimestamp()), "handleCallBusyMessage"); RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId())); ApplicationDependencies.getSignalCallManager() .receivedCallBusy(new WebRtcData.CallMetadata(remotePeer, content.getSenderDevice())); } private void handleCallOpaqueMessage(@NonNull SignalServiceContent content, @NonNull OpaqueMessage message, @NonNull Recipient senderRecipient) { log(String.valueOf(content.getTimestamp()), "handleCallOpaqueMessage"); long messageAgeSeconds = 0; if (content.getServerReceivedTimestamp() > 0 && content.getServerDeliveredTimestamp() >= content.getServerReceivedTimestamp()) { messageAgeSeconds = (content.getServerDeliveredTimestamp() - content.getServerReceivedTimestamp()) / 1000; } ApplicationDependencies.getSignalCallManager() .receivedOpaqueMessage(new WebRtcData.OpaqueMessageMetadata(senderRecipient.requireServiceId().uuid(), message.getOpaque(), content.getSenderDevice(), messageAgeSeconds)); } private void handleGroupCallUpdateMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional groupId, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "Group call update message."); if (!groupId.isPresent() || !groupId.get().isV2()) { Log.w(TAG, "Invalid group for group call update message"); return; } RecipientId groupRecipientId = SignalDatabase.recipients().getOrInsertFromPossiblyMigratedGroupId(groupId.get()); SignalDatabase.messages().insertOrUpdateGroupCall(groupRecipientId, senderRecipient.getId(), content.getServerReceivedTimestamp(), message.getGroupCallUpdate().get().getEraId()); GroupCallPeekJob.enqueue(groupRecipientId); } private @Nullable MessageId handleEndSessionMessage(@NonNull SignalServiceContent content, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "End session message."); MessageTable smsDatabase = SignalDatabase.messages(); IncomingTextMessage incomingTextMessage = new IncomingTextMessage(senderRecipient.getId(), content.getSenderDevice(), content.getTimestamp(), content.getServerReceivedTimestamp(), System.currentTimeMillis(), "", Optional.empty(), 0, content.isNeedsReceipt(), content.getServerUuid()); Optional insertResult; if (!smsMessageId.isPresent()) { IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage); insertResult = smsDatabase.insertMessageInbox(incomingEndSessionMessage); } else { smsDatabase.markAsEndSession(smsMessageId.get()); insertResult = Optional.of(new InsertResult(smsMessageId.get(), smsDatabase.getThreadIdForMessage(smsMessageId.get()))); } if (insertResult.isPresent()) { ApplicationDependencies.getProtocolStore().aci().deleteAllSessions(content.getSender().getIdentifier()); SecurityEvent.broadcastSecurityUpdateEvent(context); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); return new MessageId(insertResult.get().getMessageId()); } else { return null; } } private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException { log(envelopeTimestamp, "Synchronize end session message."); MessageTable database = SignalDatabase.messages(); Recipient recipient = getSyncMessageDestination(message); OutgoingMessage outgoingEndSessionMessage = OutgoingMessage.endSessionMessage(recipient, message.getTimestamp()); long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); if (!recipient.isGroup()) { ApplicationDependencies.getProtocolStore().aci().deleteAllSessions(recipient.requireServiceId().toString()); SecurityEvent.broadcastSecurityUpdateEvent(context); long messageId = database.insertMessageOutbox(outgoingEndSessionMessage, threadId, false, null); database.markAsSent(messageId, true); SignalDatabase.threads().update(threadId, true); } return threadId; } private void handleUnknownGroupMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceGroupV2 group, @NonNull Recipient senderRecipient) throws BadGroupIdException { log(content.getTimestamp(), "Unknown group message."); warn(content.getTimestamp(), "Received a GV2 message for a group we have no knowledge of -- attempting to fix this state."); ServiceId authServiceId = ServiceId.parseOrNull(content.getDestinationUuid()); if (authServiceId == null) { warn(content.getTimestamp(), "Group message missing destination uuid, defaulting to ACI"); authServiceId = SignalStore.account().requireAci(); } SignalDatabase.groups().fixMissingMasterKey(group.getMasterKey()); } /** * Inserts an expiration update if the message timer doesn't match the thread timer. */ private void handlePossibleExpirationUpdate(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, Optional groupId, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient, long receivedTime) throws StorageFailedException { if (message.getExpiresInSeconds() != threadRecipient.getExpiresInSeconds()) { warn(content.getTimestamp(), "Message expire time didn't match thread expire time. Handling timer update."); handleExpirationUpdate(content, message, Optional.empty(), groupId, senderRecipient, threadRecipient, receivedTime, true); } } /** * @param isActivatePaymentsRequest True if payments activation request message. * @param isPaymentsActivated True if payments activated message. * @throws StorageFailedException */ private @Nullable MessageId handlePaymentActivation(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient, long receivedTime, boolean isActivatePaymentsRequest, boolean isPaymentsActivated) throws StorageFailedException { try { MessageTable database = SignalDatabase.messages(); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), content.getTimestamp(), content.getServerReceivedTimestamp(), receivedTime, StoryType.NONE, null, false, -1, TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), false, false, content.isNeedsReceipt(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), content.getServerUuid(), null, isActivatePaymentsRequest, isPaymentsActivated); Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); if (smsMessageId.isPresent()) { SignalDatabase.messages().deleteMessage(smsMessageId.get()); } if (insertResult.isPresent()) { return new MessageId(insertResult.get().getMessageId()); } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } return null; } /** * @param sideEffect True if the event is side effect of a different message, false if the message itself was an expiration update. * @throws StorageFailedException */ private @Nullable MessageId handleExpirationUpdate(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Optional groupId, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient, long receivedTime, boolean sideEffect) throws StorageFailedException { log(content.getTimestamp(), "Expiration update. Side effect: " + sideEffect); if (groupId.isPresent() && groupId.get().isV2()) { warn(String.valueOf(content.getTimestamp()), "Expiration update received for GV2. Ignoring."); return null; } int expiresInSeconds = message.getExpiresInSeconds(); Optional groupContext = message.getGroupContext(); if (threadRecipient.getExpiresInSeconds() == expiresInSeconds) { log(String.valueOf(content.getTimestamp()), "No change in message expiry for group. Ignoring."); return null; } try { MessageTable database = SignalDatabase.messages(); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), content.getTimestamp() - (sideEffect ? 1 : 0), content.getServerReceivedTimestamp(), receivedTime, StoryType.NONE, null, false, -1, expiresInSeconds * 1000L, true, false, content.isNeedsReceipt(), Optional.empty(), groupContext, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), content.getServerUuid(), null, false, false); Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); SignalDatabase.recipients().setExpireMessages(threadRecipient.getId(), expiresInSeconds); if (smsMessageId.isPresent()) { SignalDatabase.messages().deleteMessage(smsMessageId.get()); } if (insertResult.isPresent()) { return new MessageId(insertResult.get().getMessageId()); } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } return null; } private @Nullable MessageId handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) throws StorageFailedException { log(content.getTimestamp(), "Handle reaction for message " + message.getReaction().get().getTargetSentTimestamp()); SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); if (!EmojiUtil.isEmoji(reaction.getEmoji())) { Log.w(TAG, "Reaction text is not a valid emoji! Ignoring the message."); return null; } Recipient targetAuthor = Recipient.externalPush(reaction.getTargetAuthor()); MessageRecord targetMessage = SignalDatabase.messages().getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId()); if (targetMessage == null) { warn(String.valueOf(content.getTimestamp()), "[handleReaction] Could not find matching message! Putting it in the early message cache. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(targetAuthor.getId(), reaction.getTargetSentTimestamp(), content); PushProcessEarlyMessagesJob.enqueue(); } return null; } if (targetMessage.isRemoteDelete()) { warn(String.valueOf(content.getTimestamp()), "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); return null; } ThreadRecord targetThread = SignalDatabase.threads().getThreadRecord(targetMessage.getThreadId()); if (targetThread == null) { warn(String.valueOf(content.getTimestamp()), "[handleReaction] Could not find a thread for the message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); return null; } Recipient threadRecipient = targetThread.getRecipient().resolve(); if (threadRecipient.isGroup() && !threadRecipient.getParticipantIds().contains(senderRecipient.getId())) { warn(String.valueOf(content.getTimestamp()), "[handleReaction] Reaction author is not in the group! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); return null; } if (!threadRecipient.isGroup() && !senderRecipient.equals(threadRecipient) && !senderRecipient.isSelf()) { warn(String.valueOf(content.getTimestamp()), "[handleReaction] Reaction author is not a part of the 1:1 thread! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId()); return null; } MessageId targetMessageId = new MessageId(targetMessage.getId()); if (reaction.isRemove()) { SignalDatabase.reactions().deleteReaction(targetMessageId, senderRecipient.getId()); ApplicationDependencies.getMessageNotifier().updateNotification(context); } else { ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), senderRecipient.getId(), message.getTimestamp(), System.currentTimeMillis()); SignalDatabase.reactions().addReaction(targetMessageId, reactionRecord); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); } return new MessageId(targetMessage.getId()); } private @Nullable MessageId handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) { log(content.getTimestamp(), "Remote delete for message " + message.getRemoteDelete().get().getTargetSentTimestamp()); SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get(); MessageRecord targetMessage = SignalDatabase.messages().getMessageFor(delete.getTargetSentTimestamp(), senderRecipient.getId()); if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, senderRecipient, content.getServerReceivedTimestamp())) { MessageTable db = targetMessage.isMms() ? SignalDatabase.messages() : SignalDatabase.messages(); db.markAsRemoteDelete(targetMessage.getId()); if (MessageRecordUtil.isStory(targetMessage)) { db.deleteRemotelyDeletedStory(targetMessage.getId()); } ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromMessageRecord(targetMessage), false); return new MessageId(targetMessage.getId()); } else if (targetMessage == null) { warn(String.valueOf(content.getTimestamp()), "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + senderRecipient.getId()); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(senderRecipient.getId(), delete.getTargetSentTimestamp(), content); PushProcessEarlyMessagesJob.enqueue(); } return null; } else { warn(String.valueOf(content.getTimestamp()), String.format(Locale.ENGLISH, "[handleRemoteDelete] Invalid remote delete! deleteTime: %d, targetTime: %d, deleteAuthor: %s, targetAuthor: %s", content.getServerReceivedTimestamp(), targetMessage.getServerTimestamp(), senderRecipient.getId(), targetMessage.getRecipient().getId())); return null; } } private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) { log(verifiedMessage.getTimestamp(), "Synchronize verified message."); IdentityUtil.processVerifiedMessage(context, verifiedMessage); } private void handleSynchronizeStickerPackOperation(@NonNull List stickerPackOperations, long envelopeTimestamp) { log(envelopeTimestamp, "Synchronize sticker pack operation."); JobManager jobManager = ApplicationDependencies.getJobManager(); for (StickerPackOperationMessage operation : stickerPackOperations) { if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) { String packId = Hex.toStringCondensed(operation.getPackId().get()); String packKey = Hex.toStringCondensed(operation.getPackKey().get()); switch (operation.getType().get()) { case INSTALL: jobManager.add(StickerPackDownloadJob.forInstall(packId, packKey, false)); break; case REMOVE: SignalDatabase.stickers().uninstallPack(packId); break; } } else { warn("Received incomplete sticker pack operation sync."); } } } private void handleSynchronizeConfigurationMessage(@NonNull ConfigurationMessage configurationMessage, long envelopeTimestamp) { log(envelopeTimestamp, "Synchronize configuration message."); if (configurationMessage.getReadReceipts().isPresent()) { TextSecurePreferences.setReadReceiptsEnabled(context, configurationMessage.getReadReceipts().get()); } if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) { TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, configurationMessage.getReadReceipts().get()); } if (configurationMessage.getTypingIndicators().isPresent()) { TextSecurePreferences.setTypingIndicatorsEnabled(context, configurationMessage.getTypingIndicators().get()); } if (configurationMessage.getLinkPreviews().isPresent()) { SignalStore.settings().setLinkPreviewsEnabled(configurationMessage.getReadReceipts().get()); } } private void handleSynchronizeBlockedListMessage(@NonNull BlockedListMessage blockMessage) { SignalDatabase.recipients().applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds()); } private void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType, long envelopeTimestamp) { log(envelopeTimestamp, "Received fetch request with type: " + fetchType); switch (fetchType) { case LOCAL_PROFILE: ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob()); break; case STORAGE_MANIFEST: StorageSyncHelper.scheduleSyncForDataChange(); break; case SUBSCRIPTION_STATUS: warn(TAG, "Dropping subscription status fetch message."); break; default: warn(TAG, "Received a fetch message for an unknown type."); } } private void handleSynchronizeMessageRequestResponse(@NonNull MessageRequestResponseMessage response, long envelopeTimestamp) throws BadGroupIdException { log(envelopeTimestamp, "Synchronize message request response."); RecipientTable recipientTable = SignalDatabase.recipients(); ThreadTable threadTable = SignalDatabase.threads(); Recipient recipient; if (response.getPerson().isPresent()) { recipient = Recipient.externalPush(response.getPerson().get()); } else if (response.getGroupId().isPresent()) { GroupId groupId = GroupId.push(response.getGroupId().get()); recipient = Recipient.externalPossiblyMigratedGroup(groupId); } else { warn("Message request response was missing a thread recipient! Skipping."); return; } long threadId = threadTable.getOrCreateThreadIdFor(recipient); switch (response.getType()) { case ACCEPT: recipientTable.setProfileSharing(recipient.getId(), true); recipientTable.setBlocked(recipient.getId(), false); break; case DELETE: recipientTable.setProfileSharing(recipient.getId(), false); if (threadId > 0) threadTable.deleteConversation(threadId); break; case BLOCK: recipientTable.setBlocked(recipient.getId(), true); recipientTable.setProfileSharing(recipient.getId(), false); break; case BLOCK_AND_DELETE: recipientTable.setBlocked(recipient.getId(), true); recipientTable.setProfileSharing(recipient.getId(), false); if (threadId > 0) threadTable.deleteConversation(threadId); break; default: warn("Got an unknown response type! Skipping"); break; } } private void handleSynchronizeOutgoingPayment(@NonNull SignalServiceContent content, @NonNull OutgoingPaymentMessage outgoingPaymentMessage) { RecipientId recipientId = outgoingPaymentMessage.getRecipient() .map(RecipientId::from) .orElse(null); long timestamp = outgoingPaymentMessage.getBlockTimestamp(); if (timestamp == 0) { timestamp = System.currentTimeMillis(); } Optional address = outgoingPaymentMessage.getAddress().map(MobileCoinPublicAddress::fromBytes); if (!address.isPresent() && recipientId == null) { log(content.getTimestamp(), "Inserting defrag"); address = Optional.of(ApplicationDependencies.getPayments().getWallet().getMobileCoinPublicAddress()); recipientId = Recipient.self().getId(); } UUID uuid = UUID.randomUUID(); try { SignalDatabase.payments() .createSuccessfulPayment(uuid, recipientId, address.get(), timestamp, outgoingPaymentMessage.getBlockIndex(), outgoingPaymentMessage.getNote().orElse(""), outgoingPaymentMessage.getAmount(), outgoingPaymentMessage.getFee(), outgoingPaymentMessage.getReceipt().toByteArray(), PaymentMetaDataUtil.fromKeysAndImages(outgoingPaymentMessage.getPublicKeys(), outgoingPaymentMessage.getKeyImages())); } catch (SerializationException e) { warn(content.getTimestamp(), "Ignoring synchronized outgoing payment with bad data.", e); } log("Inserted synchronized payment " + uuid); } private void handleSynchronizeKeys(@NonNull KeysMessage keysMessage, long envelopeTimestamp) { if (SignalStore.account().isLinkedDevice()) { log(envelopeTimestamp, "Synchronize keys."); } else { log(envelopeTimestamp, "Primary device ignores synchronize keys."); return; } SignalStore.storageService().setStorageKeyFromPrimary(keysMessage.getStorageService().get()); } private void handleSynchronizeContacts(@NonNull ContactsMessage contactsMessage, long envelopeTimestamp) throws IOException { if (SignalStore.account().isLinkedDevice()) { log(envelopeTimestamp, "Synchronize contacts."); } else { log(envelopeTimestamp, "Primary device ignores synchronize contacts."); return; } if (!(contactsMessage.getContactsStream() instanceof SignalServiceAttachmentPointer)) { warn(envelopeTimestamp, "No contact stream available."); return; } SignalServiceAttachmentPointer contactsAttachment = (SignalServiceAttachmentPointer) contactsMessage.getContactsStream(); ApplicationDependencies.getJobManager().add(new MultiDeviceContactSyncJob(contactsAttachment)); } private void handleSynchronizeCallEvent(@NonNull SyncMessage.CallEvent callEvent, long envelopeTimestamp) { if (!callEvent.hasId()) { log(envelopeTimestamp, "Synchronize call event missing call id, ignoring."); return; } long callId = callEvent.getId(); long timestamp = callEvent.getTimestamp(); CallTable.Type type = CallTable.Type.from(callEvent.getType()); CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection()); CallTable.Event event = CallTable.Event.from(callEvent.getEvent()); if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasPeerUuid()) { warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasPeerUuid()); return; } ServiceId serviceId = ServiceId.fromByteString(callEvent.getPeerUuid()); RecipientId recipientId = RecipientId.from(serviceId); log(envelopeTimestamp, "Synchronize call event call: " + callId); CallTable.Call call = SignalDatabase.calls().getCallById(callId); if (call != null) { boolean typeMismatch = call.getType() != type; boolean directionMismatch = call.getDirection() != direction; boolean eventDowngrade = call.getEvent() == CallTable.Event.ACCEPTED && event != CallTable.Event.ACCEPTED; boolean peerMismatch = !call.getPeer().equals(recipientId); if (typeMismatch || directionMismatch || eventDowngrade || peerMismatch) { warn(envelopeTimestamp, "Call event sync message is not valid for existing call record, ignoring. type: " + type + " direction: " + direction + " event: " + event + " peerMismatch: " + peerMismatch); } else { SignalDatabase.calls().updateCall(callId, event); } } else { SignalDatabase.calls().insertCall(callId, timestamp, recipientId, type, direction, event); } } private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content, @NonNull SentTranscriptMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) throws StorageFailedException, BadGroupIdException, IOException, GroupChangeBusyException { log(String.valueOf(content.getTimestamp()), "Processing sent transcript for message with ID " + message.getTimestamp()); try { GroupTable groupDatabase = SignalDatabase.groups(); if (message.getStoryMessage().isPresent() || !message.getStoryMessageRecipients().isEmpty()) { handleSynchronizeSentStoryMessage(message, content.getTimestamp()); return; } SignalServiceDataMessage dataMessage = message.getDataMessage().get(); if (dataMessage.isGroupV2Message()) { GroupId.V2 groupId = GroupId.v2(dataMessage.getGroupContext().get().getMasterKey()); if (handleGv2PreProcessing(groupId, content, dataMessage.getGroupContext().get(), senderRecipient)) { return; } } long threadId = -1; if (message.isRecipientUpdate()) { handleGroupRecipientUpdate(message, content.getTimestamp()); } else if (dataMessage.isEndSession()) { threadId = handleSynchronizeSentEndSessionMessage(message, content.getTimestamp()); } else if (dataMessage.isGroupV2Update()) { handleSynchronizeSentGv2Update(content, message); threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); } else if (Build.VERSION.SDK_INT > 19 && dataMessage.getGroupCallUpdate().isPresent()) { handleGroupCallUpdateMessage(content, dataMessage, GroupUtil.idFromGroupContext(dataMessage.getGroupContext()), senderRecipient); } else if (dataMessage.isEmptyGroupV2Message()) { warn(content.getTimestamp(), "Empty GV2 message! Doing nothing."); } else if (dataMessage.isExpirationUpdate()) { threadId = handleSynchronizeSentExpirationUpdate(message); } else if (dataMessage.getStoryContext().isPresent()) { threadId = handleSynchronizeSentStoryReply(message, content.getTimestamp()); } else if (dataMessage.getReaction().isPresent()) { handleReaction(content, dataMessage, senderRecipient, processingEarlyContent); threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message)); } else if (dataMessage.getRemoteDelete().isPresent()) { handleRemoteDelete(content, dataMessage, senderRecipient, processingEarlyContent); } else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent()) { threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp()); } else { threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp()); } if (dataMessage.getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupId.v2(dataMessage.getGroupContext().get().getMasterKey()))) { handleUnknownGroupMessage(content, dataMessage.getGroupContext().get(), senderRecipient); } if (dataMessage.getProfileKey().isPresent()) { Recipient recipient = getSyncMessageDestination(message); if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) { SignalDatabase.recipients().setProfileSharing(recipient.getId(), true); } } if (threadId != -1) { SignalDatabase.threads().setRead(threadId, true); ApplicationDependencies.getMessageNotifier().updateNotification(context); } if (SignalStore.rateLimit().needsRecaptcha()) { log(content.getTimestamp(), "Got a sent transcript while in reCAPTCHA mode. Assuming we're good to message again."); RateLimitUtil.retryAllRateLimitedMessages(context); } ApplicationDependencies.getMessageNotifier().setLastDesktopActivityTimestamp(message.getTimestamp()); } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } } private void handleSynchronizeSentGv2Update(@NonNull SignalServiceContent content, @NonNull SentTranscriptMessage message) throws IOException, GroupChangeBusyException { log(content.getTimestamp(), "Synchronize sent GV2 update for message with timestamp " + message.getTimestamp()); SignalServiceDataMessage dataMessage = message.getDataMessage().get(); SignalServiceGroupV2 signalServiceGroupV2 = dataMessage.getGroupContext().get(); GroupId.V2 groupIdV2 = GroupId.v2(signalServiceGroupV2.getMasterKey()); if (!updateGv2GroupFromServerOrP2PChange(content, signalServiceGroupV2)) { log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2); } } private void handleSynchronizeRequestMessage(@NonNull RequestMessage message, long envelopeTimestamp) { if (SignalStore.account().isPrimaryDevice()) { log(envelopeTimestamp, "Synchronize request message."); } else { log(envelopeTimestamp, "Linked device ignoring synchronize request message."); return; } if (message.isContactsRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true)); } if (message.isGroupsRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceGroupUpdateJob()); } if (message.isBlockedListRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); } if (message.isConfigurationRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(context), TextSecurePreferences.isTypingIndicatorsEnabled(context), TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context), SignalStore.settings().isLinkPreviewsEnabled())); ApplicationDependencies.getJobManager().add(new MultiDeviceStickerPackSyncJob()); } if (message.isKeysRequest()) { ApplicationDependencies.getJobManager().add(new MultiDeviceKeysUpdateJob()); } if (message.isPniIdentityRequest()) { ApplicationDependencies.getJobManager().add(new MultiDevicePniIdentityUpdateJob()); } } private void handleSynchronizeReadMessage(@NonNull SignalServiceContent content, @NonNull List readMessages, long envelopeTimestamp, boolean processingEarlyContent) { log(envelopeTimestamp, "Synchronize read message. Count: " + readMessages.size() + ", Timestamps: " + Stream.of(readMessages).map(ReadMessage::getTimestamp).toList()); Map threadToLatestRead = new HashMap<>(); Collection unhandled = SignalDatabase.messages().setTimestampReadFromSyncMessage(readMessages, envelopeTimestamp, threadToLatestRead); List markedMessages = SignalDatabase.threads().setReadSince(threadToLatestRead, false); if (Util.hasItems(markedMessages)) { Log.i(TAG, "Updating past messages: " + markedMessages.size()); MarkReadReceiver.process(context, markedMessages); } for (SyncMessageId id : unhandled) { warn(String.valueOf(content.getTimestamp()), "[handleSynchronizeReadMessage] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + id.getRecipientId()); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(id.getRecipientId(), id.getTimetamp(), content); } } if (unhandled.size() > 0 && !processingEarlyContent) { PushProcessEarlyMessagesJob.enqueue(); } MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); messageNotifier.cancelDelayedNotifications(); messageNotifier.updateNotification(context); } private void handleSynchronizeViewedMessage(@NonNull List viewedMessages, long envelopeTimestamp) { log(envelopeTimestamp, "Synchronize view message. Count: " + viewedMessages.size() + ", Timestamps: " + Stream.of(viewedMessages).map(ViewedMessage::getTimestamp).toList()); List records = Stream.of(viewedMessages) .map(message -> { RecipientId author = Recipient.externalPush(message.getSender()).getId(); return SignalDatabase.messages().getMessageFor(message.getTimestamp(), author); }) .filter(message -> message != null && message.isMms()) .toList(); List toMarkViewed = Stream.of(records) .map(MessageRecord::getId) .toList(); List toEnqueueDownload = Stream.of(records) .filter(MessageRecord::isMms) .map(it -> (MediaMmsMessageRecord) it) .filter(it -> it.getStoryType().isStory() && !it.getStoryType().isTextStory()) .toList(); for (final MediaMmsMessageRecord mediaMmsMessageRecord : toEnqueueDownload) { Stories.enqueueAttachmentsFromStoryForDownloadSync(mediaMmsMessageRecord, false); } SignalDatabase.messages().setIncomingMessagesViewed(toMarkViewed); SignalDatabase.messages().setOutgoingGiftsRevealed(toMarkViewed); MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); messageNotifier.cancelDelayedNotifications(); messageNotifier.updateNotification(context); } private void handleSynchronizeViewOnceOpenMessage(@NonNull SignalServiceContent content, @NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp, boolean processingEarlyContent) { log(envelopeTimestamp, "Handling a view-once open for message: " + openMessage.getTimestamp()); RecipientId author = Recipient.externalPush(openMessage.getSender()).getId(); long timestamp = openMessage.getTimestamp(); MessageRecord record = SignalDatabase.messages().getMessageFor(timestamp, author); if (record != null && record.isMms()) { SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(record.getId()); } else { warn(String.valueOf(envelopeTimestamp), "Got a view-once open message for a message we don't have!"); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(author, timestamp, content); PushProcessEarlyMessagesJob.enqueue(); } } MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier(); messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp); messageNotifier.cancelDelayedNotifications(); messageNotifier.updateNotification(context); } private void handleStoryMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceStoryMessage message, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient) throws StorageFailedException { log(content.getTimestamp(), "Story message."); if (threadRecipient.isInactiveGroup()) { warn(content.getTimestamp(), "Dropping a group story from a group we're no longer in."); return; } if (threadRecipient.isGroup() && !SignalDatabase.groups().isCurrentMember(threadRecipient.requireGroupId().requirePush(), senderRecipient.getId())) { warn(content.getTimestamp(), "Dropping a group story from a user who's no longer a member."); return; } if (!threadRecipient.isGroup() && !(senderRecipient.isProfileSharing() || senderRecipient.isSystemContact())) { warn(content.getTimestamp(), "Dropping story from an untrusted source."); return; } Optional insertResult; MessageTable database = SignalDatabase.messages(); database.beginTransaction(); try { final StoryType storyType; if (message.getAllowsReplies().orElse(false)) { storyType = StoryType.withReplies(message.getTextAttachment().isPresent()); } else { storyType = StoryType.withoutReplies(message.getTextAttachment().isPresent()); } IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), content.getTimestamp(), content.getServerReceivedTimestamp(), System.currentTimeMillis(), storyType, null, false, -1, 0, false, false, content.isNeedsReceipt(), message.getTextAttachment().map(this::serializeTextAttachment), Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), message.getFileAttachment().map(Collections::singletonList), Optional.empty(), Optional.empty(), getLinkPreviews(message.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)), "", true), Optional.empty(), Optional.empty(), content.getServerUuid(), null, false, false, getBodyRangeList(message.getBodyRanges())); insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); if (insertResult.isPresent()) { database.setTransactionSuccessful(); } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } finally { database.endTransaction(); } if (insertResult.isPresent()) { Stories.enqueueNextStoriesForDownload(threadRecipient.getId(), false, FeatureFlags.storiesAutoDownloadMaximum()); ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary(); } } private @NonNull String serializeTextAttachment(@NonNull SignalServiceTextAttachment textAttachment) { StoryTextPost.Builder builder = StoryTextPost.newBuilder(); if (textAttachment.getText().isPresent()) { builder.setBody(textAttachment.getText().get()); } if (textAttachment.getStyle().isPresent()) { switch (textAttachment.getStyle().get()) { case DEFAULT: builder.setStyle(StoryTextPost.Style.DEFAULT); break; case REGULAR: builder.setStyle(StoryTextPost.Style.REGULAR); break; case BOLD: builder.setStyle(StoryTextPost.Style.BOLD); break; case SERIF: builder.setStyle(StoryTextPost.Style.SERIF); break; case SCRIPT: builder.setStyle(StoryTextPost.Style.SCRIPT); break; case CONDENSED: builder.setStyle(StoryTextPost.Style.CONDENSED); break; } } if (textAttachment.getTextBackgroundColor().isPresent()) { builder.setTextBackgroundColor(textAttachment.getTextBackgroundColor().get()); } if (textAttachment.getTextForegroundColor().isPresent()) { builder.setTextForegroundColor(textAttachment.getTextForegroundColor().get()); } ChatColor.Builder chatColorBuilder = ChatColor.newBuilder(); if (textAttachment.getBackgroundColor().isPresent()) { chatColorBuilder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(textAttachment.getBackgroundColor().get())); } else if (textAttachment.getBackgroundGradient().isPresent()) { SignalServiceTextAttachment.Gradient gradient = textAttachment.getBackgroundGradient().get(); ChatColor.LinearGradient.Builder linearGradientBuilder = ChatColor.LinearGradient.newBuilder(); linearGradientBuilder.setRotation(gradient.getAngle().orElse(0).floatValue()); if (gradient.getPositions().size() > 1 && gradient.getColors().size() == gradient.getPositions().size()) { ArrayList positions = new ArrayList<>(gradient.getPositions()); positions.set(0, 0f); positions.set(positions.size() - 1, 1f); linearGradientBuilder.addAllColors(new ArrayList<>(gradient.getColors())); linearGradientBuilder.addAllPositions(positions); } else if (!gradient.getColors().isEmpty()) { Log.w(TAG, "Incoming text story has color / position mismatch. Defaulting to start and end colors."); linearGradientBuilder.addColors(gradient.getColors().get(0)); linearGradientBuilder.addColors(gradient.getColors().get(gradient.getColors().size() - 1)); linearGradientBuilder.addAllPositions(Arrays.asList(0f, 1f)); } else { Log.w(TAG, "Incoming text story did not have a valid linear gradient."); linearGradientBuilder.addAllColors(Arrays.asList(Color.BLACK, Color.BLACK)); linearGradientBuilder.addAllPositions(Arrays.asList(0f, 1f)); } chatColorBuilder.setLinearGradient(linearGradientBuilder); } builder.setBackground(chatColorBuilder); return Base64.encodeBytes(builder.build().toByteArray()); } private @Nullable MessageId handleStoryReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) throws StorageFailedException { log(content.getTimestamp(), "Story reaction."); SignalServiceDataMessage.Reaction reaction = message.getReaction().get(); if (!EmojiUtil.isEmoji(reaction.getEmoji())) { warn(content.getTimestamp(), "Story reaction text is not a valid emoji! Ignoring the message."); return null; } SignalServiceDataMessage.StoryContext storyContext = message.getStoryContext().get(); MessageTable database = SignalDatabase.messages(); database.beginTransaction(); try { RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); ParentStoryId parentStoryId; QuoteModel quoteModel = null; long expiresInMillis = 0; try { MessageId storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); if (message.getGroupContext().isPresent()) { parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); } else if (SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) { MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); String displayText = ""; BodyRangeList bodyRanges = null; if (story.getStoryType().isTextStory()) { displayText = story.getBody(); bodyRanges = story.getMessageRanges(); } parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); } else { warn(content.getTimestamp(), "Story has reactions disabled. Dropping reaction."); return null; } } catch (NoSuchMessageException e) { warn(content.getTimestamp(), "Couldn't find story for reaction.", e); return null; } IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), content.getTimestamp(), content.getServerReceivedTimestamp(), System.currentTimeMillis(), StoryType.NONE, parentStoryId, true, -1, expiresInMillis, false, false, content.isNeedsReceipt(), Optional.of(reaction.getEmoji()), Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), Optional.empty(), Optional.ofNullable(quoteModel), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), content.getServerUuid(), null, false, false); Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); if (insertResult.isPresent()) { database.setTransactionSuccessful(); if (parentStoryId.isGroupReply()) { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); } else { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); } if (parentStoryId.isDirectReply()) { return MessageId.fromNullable(insertResult.get().getMessageId()); } else { return null; } } else { warn(content.getTimestamp(), "Failed to insert story reaction"); return null; } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } finally { database.endTransaction(); } } private @Nullable MessageId handleStoryReply(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, long receivedTime) throws StorageFailedException { log(content.getTimestamp(), "Story reply."); SignalServiceDataMessage.StoryContext storyContext = message.getStoryContext().get(); MessageTable database = SignalDatabase.messages(); database.beginTransaction(); try { RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); RecipientId selfId = Recipient.self().getId(); ParentStoryId parentStoryId; QuoteModel quoteModel = null; long expiresInMillis = 0L; MessageId storyMessageId = null; try { if (selfId.equals(storyAuthorRecipient)) { storyMessageId = SignalDatabase.storySends().getStoryMessageFor(senderRecipient.getId(), storyContext.getSentTimestamp()); } if (storyMessageId == null) { storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); } MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); Recipient threadRecipient = Objects.requireNonNull(SignalDatabase.threads().getRecipientForThreadId(story.getThreadId())); boolean groupStory = threadRecipient.isActiveGroup(); if (!groupStory) { threadRecipient = senderRecipient; } handlePossibleExpirationUpdate(content, message, threadRecipient.getGroupId(), senderRecipient, threadRecipient, receivedTime); if (message.getGroupContext().isPresent() ) { parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); } else if (groupStory || SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) { parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); String displayText = ""; BodyRangeList bodyRanges = null; if (story.getStoryType().isTextStory()) { displayText = story.getBody(); bodyRanges = story.getMessageRanges(); } quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); } else { warn(content.getTimestamp(), "Story has replies disabled. Dropping reply."); return null; } } catch (NoSuchMessageException e) { warn(content.getTimestamp(), "Couldn't find story for reply.", e); return null; } BodyRangeList bodyRanges = message.getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), content.getTimestamp(), content.getServerReceivedTimestamp(), System.currentTimeMillis(), StoryType.NONE, parentStoryId, false, -1, expiresInMillis, false, false, content.isNeedsReceipt(), message.getBody(), Optional.ofNullable(GroupUtil.getGroupContextIfPresent(content)), Optional.empty(), Optional.ofNullable(quoteModel), Optional.empty(), Optional.empty(), getMentions(message.getMentions()), Optional.empty(), content.getServerUuid(), null, false, false, bodyRanges); Optional insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); if (insertResult.isPresent()) { database.setTransactionSuccessful(); if (parentStoryId.isGroupReply()) { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.fromThreadAndReply(insertResult.get().getThreadId(), (ParentStoryId.GroupReply) parentStoryId)); } else { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); } if (parentStoryId.isDirectReply()) { return MessageId.fromNullable(insertResult.get().getMessageId()); } else { return null; } } else { warn(content.getTimestamp(), "Failed to insert story reply."); return null; } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } finally { database.endTransaction(); } } private @Nullable MessageId handleGiftMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient, long receivedTime) throws StorageFailedException { log(message.getTimestamp(), "Gift message."); notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); Optional insertResult; MessageTable database = SignalDatabase.messages(); byte[] token = message.getGiftBadge().get().getReceiptCredentialPresentation().serialize(); GiftBadge giftBadge = GiftBadge.newBuilder() .setRedemptionToken(ByteString.copyFrom(token)) .setRedemptionState(GiftBadge.RedemptionState.PENDING) .build(); try { IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), message.getTimestamp(), content.getServerReceivedTimestamp(), receivedTime, StoryType.NONE, null, false, -1, TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), false, false, content.isNeedsReceipt(), Optional.of(Base64.encodeBytes(giftBadge.toByteArray())), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), content.getServerUuid(), giftBadge, false, false); insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } if (insertResult.isPresent()) { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); return new MessageId(insertResult.get().getMessageId()); } else { return null; } } private @Nullable MessageId handleMediaMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient, long receivedTime) throws StorageFailedException { log(message.getTimestamp(), "Media message."); notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); Optional insertResult; MessageTable database = SignalDatabase.messages(); database.beginTransaction(); try { Optional quote = getValidatedQuote(message.getQuote()); Optional> sharedContacts = getContacts(message.getSharedContacts()); Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().orElse(""), false); Optional> mentions = getMentions(message.getMentions()); Optional sticker = getStickerAttachment(message.getSticker()); BodyRangeList messageRanges = getBodyRangeList(message.getBodyRanges()); handlePossibleExpirationUpdate(content, message, Optional.empty(), senderRecipient, threadRecipient, receivedTime); IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(), message.getTimestamp(), content.getServerReceivedTimestamp(), receivedTime, StoryType.NONE, null, false, -1, TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), false, message.isViewOnce(), content.isNeedsReceipt(), message.getBody(), message.getGroupContext(), message.getAttachments(), quote, sharedContacts, linkPreviews, mentions, sticker, content.getServerUuid(), null, false, false, messageRanges); insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1); if (insertResult.isPresent()) { if (smsMessageId.isPresent()) { SignalDatabase.messages().deleteMessage(smsMessageId.get()); } database.setTransactionSuccessful(); } } catch (MmsException e) { throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice()); } finally { database.endTransaction(); } if (insertResult.isPresent()) { List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(insertResult.get().getMessageId()); List stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); List attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); forceStickerDownloadIfNecessary(insertResult.get().getMessageId(), stickerAttachments); for (DatabaseAttachment attachment : attachments) { ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false)); } ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); TrimThreadJob.enqueueAsync(insertResult.get().getThreadId()); if (message.isViewOnce()) { ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary(); } return new MessageId(insertResult.get().getMessageId()); } else { return null; } } private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException { log(message.getTimestamp(), "Synchronize sent expiration update."); Optional groupId = getSyncMessageDestination(message).getGroupId(); if (groupId.isPresent() && groupId.get().isV2()) { warn(String.valueOf(message.getTimestamp()), "Expiration update received for GV2. Ignoring."); return -1; } MessageTable database = SignalDatabase.messages(); Recipient recipient = getSyncMessageDestination(message); OutgoingMessage expirationUpdateMessage = OutgoingMessage.expirationUpdateMessage(recipient, message.getTimestamp(), TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null); database.markAsSent(messageId, true); SignalDatabase.recipients().setExpireMessages(recipient.getId(), message.getDataMessage().get().getExpiresInSeconds()); return threadId; } /** * Handles both story replies and reactions. */ private long handleSynchronizeSentStoryReply(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException, BadGroupIdException { log(envelopeTimestamp, "Synchronize sent story reply for " + message.getTimestamp()); try { Optional reaction = message.getDataMessage().get().getReaction(); ParentStoryId parentStoryId; SignalServiceDataMessage.StoryContext storyContext = message.getDataMessage().get().getStoryContext().get(); MessageTable database = SignalDatabase.messages(); Recipient recipient = getSyncMessageDestination(message); QuoteModel quoteModel = null; long expiresInMillis = 0L; RecipientId storyAuthorRecipient = RecipientId.from(storyContext.getAuthorServiceId()); MessageId storyMessageId = database.getStoryId(storyAuthorRecipient, storyContext.getSentTimestamp()); MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId()); Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(story.getThreadId()); boolean groupStory = threadRecipient != null && threadRecipient.isActiveGroup(); String body; if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) { body = reaction.get().getEmoji(); } else { body = message.getDataMessage().get().getBody().orElse(null); } if (message.getDataMessage().get().getGroupContext().isPresent()) { parentStoryId = new ParentStoryId.GroupReply(storyMessageId.getId()); } else if (groupStory || story.getStoryType().isStoryWithReplies()) { parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); String quoteBody = ""; BodyRangeList bodyRanges = null; if (story.getStoryType().isTextStory()) { quoteBody = story.getBody(); bodyRanges = story.getMessageRanges(); } quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()); } else { warn(envelopeTimestamp, "Story has replies disabled. Dropping reply."); return -1L; } OutgoingMessage mediaMessage = new OutgoingMessage(recipient, body, Collections.emptyList(), message.getTimestamp(), -1, expiresInMillis, false, ThreadTable.DistributionTypes.DEFAULT, StoryType.NONE, parentStoryId, message.getDataMessage().get().getReaction().isPresent(), quoteModel, Collections.emptyList(), Collections.emptyList(), getMentions(message.getDataMessage().get().getMentions()).orElse(Collections.emptyList()), Collections.emptySet(), Collections.emptySet(), null, true, null, -1); if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { handleSynchronizeSentExpirationUpdate(message); } long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long messageId; database.beginTransaction(); try { messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); if (recipient.isGroup()) { updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); } else { database.markUnidentified(messageId, isUnidentified(message, recipient)); } database.markAsSent(messageId, true); if (message.getDataMessage().get().getExpiresInSeconds() > 0) { database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); ApplicationDependencies.getExpiringMessageManager() .scheduleDeletion(messageId, true, message.getExpirationStartTimestamp(), TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); } if (recipient.isSelf()) { SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp()); SignalDatabase.messages().incrementDeliveryReceiptCount(id, System.currentTimeMillis()); SignalDatabase.messages().incrementReadReceiptCount(id, System.currentTimeMillis()); } database.setTransactionSuccessful(); } finally { database.endTransaction(); } return threadId; } catch (NoSuchMessageException e) { warn(envelopeTimestamp, "Couldn't find story for reply.", e); return -1L; } } private void handleSynchronizeSentStoryMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException { log(envelopeTimestamp, "Synchronize sent story message for " + message.getTimestamp()); SentStorySyncManifest manifest = SentStorySyncManifest.fromRecipientsSet(message.getStoryMessageRecipients()); if (message.isRecipientUpdate()) { log(envelopeTimestamp, "Processing recipient update for story message and exiting..."); SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp()); return; } SignalServiceStoryMessage storyMessage = message.getStoryMessage().get(); Set distributionIds = manifest.getDistributionIdSet(); Optional groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey())); String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null); StoryType storyType = getStoryType(storyMessage); List linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)), "", true).orElse(Collections.emptyList()); List attachments = PointerAttachment.forPointers(storyMessage.getFileAttachment() .map(SignalServiceAttachment::asPointer) .map(Collections::singletonList)); for (final DistributionId distributionId : distributionIds) { RecipientId distributionRecipientId = SignalDatabase.distributionLists().getOrCreateByDistributionId(distributionId, manifest); Recipient distributionListRecipient = Recipient.resolved(distributionRecipientId); insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews); } if (groupId.isPresent()) { Optional groupRecipient = SignalDatabase.recipients().getByGroupId(groupId.get()); if (groupRecipient.isPresent()) { insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews); } } SignalDatabase.storySends().applySentStoryManifest(manifest, message.getTimestamp()); } private void insertSentStoryMessage(@NonNull SentTranscriptMessage message, @NonNull Recipient recipient, @Nullable String textStoryBody, @NonNull List pendingAttachments, long sentAtTimestamp, @NonNull StoryType storyType, @NonNull List linkPreviews) throws MmsException { if (SignalDatabase.messages().isOutgoingStoryAlreadyInDatabase(recipient.getId(), sentAtTimestamp)) { warn(sentAtTimestamp, "Already inserted this story."); return; } OutgoingMessage mediaMessage = new OutgoingMessage(recipient, textStoryBody, pendingAttachments, sentAtTimestamp, -1, 0, false, ThreadTable.DistributionTypes.DEFAULT, storyType, null, false, null, Collections.emptyList(), linkPreviews, Collections.emptyList(), Collections.emptySet(), Collections.emptySet(), null, true, null, -1); MessageTable messageTable = SignalDatabase.messages(); long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); long messageId; List attachments; messageTable.beginTransaction(); try { messageId = messageTable.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNDELIVERED, null); if (recipient.isGroup()) { updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); } else if (recipient.getDistributionListId().isPresent()){ updateGroupReceiptStatusForDistributionList(message, messageId, recipient.getDistributionListId().get()); } else { messageTable.markUnidentified(messageId, isUnidentified(message, recipient)); } messageTable.markAsSent(messageId, true); List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId); attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); if (recipient.isSelf()) { SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp()); SignalDatabase.messages().incrementDeliveryReceiptCount(id, System.currentTimeMillis()); SignalDatabase.messages().incrementReadReceiptCount(id, System.currentTimeMillis()); } messageTable.setTransactionSuccessful(); } finally { messageTable.endTransaction(); } for (DatabaseAttachment attachment : attachments) { ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false)); } } private @NonNull StoryType getStoryType(SignalServiceStoryMessage storyMessage) { if (storyMessage.getAllowsReplies().orElse(false)) { if (storyMessage.getTextAttachment().isPresent()) { return StoryType.TEXT_STORY_WITH_REPLIES; } else { return StoryType.STORY_WITH_REPLIES; } } else { if (storyMessage.getTextAttachment().isPresent()) { return StoryType.TEXT_STORY_WITHOUT_REPLIES; } else { return StoryType.STORY_WITHOUT_REPLIES; } } } private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException, BadGroupIdException { log(envelopeTimestamp, "Synchronize sent media message for " + message.getTimestamp()); MessageTable database = SignalDatabase.messages(); Recipient recipients = getSyncMessageDestination(message); Optional quote = getValidatedQuote(message.getDataMessage().get().getQuote()); Optional sticker = getStickerAttachment(message.getDataMessage().get().getSticker()); Optional> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts()); Optional> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false); Optional> mentions = getMentions(message.getDataMessage().get().getMentions()); Optional giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge()); boolean viewOnce = message.getDataMessage().get().isViewOnce(); List syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false)) : PointerAttachment.forPointers(message.getDataMessage().get().getAttachments()); if (sticker.isPresent()) { syncAttachments.add(sticker.get()); } OutgoingMessage mediaMessage = new OutgoingMessage(recipients, message.getDataMessage().get().getBody().orElse(null), syncAttachments, message.getTimestamp(), -1, TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()), viewOnce, ThreadTable.DistributionTypes.DEFAULT, StoryType.NONE, null, false, quote.orElse(null), sharedContacts.orElse(Collections.emptyList()), previews.orElse(Collections.emptyList()), mentions.orElse(Collections.emptyList()), Collections.emptySet(), Collections.emptySet(), giftBadge.orElse(null), true, null, -1); if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { handleSynchronizeSentExpirationUpdate(message); } long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipients); long messageId; List attachments; List stickerAttachments; database.beginTransaction(); try { messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); if (recipients.isGroup()) { updateGroupReceiptStatus(message, messageId, recipients.requireGroupId()); } else { database.markUnidentified(messageId, isUnidentified(message, recipients)); } database.markAsSent(messageId, true); List allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId); stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList(); attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList(); if (message.getDataMessage().get().getExpiresInSeconds() > 0) { database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); ApplicationDependencies.getExpiringMessageManager() .scheduleDeletion(messageId, true, message.getExpirationStartTimestamp(), TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds())); } if (recipients.isSelf()) { SyncMessageId id = new SyncMessageId(recipients.getId(), message.getTimestamp()); SignalDatabase.messages().incrementDeliveryReceiptCount(id, System.currentTimeMillis()); SignalDatabase.messages().incrementReadReceiptCount(id, System.currentTimeMillis()); } database.setTransactionSuccessful(); } finally { database.endTransaction(); } for (DatabaseAttachment attachment : attachments) { ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false)); } forceStickerDownloadIfNecessary(messageId, stickerAttachments); return threadId; } private void handleGroupRecipientUpdate(@NonNull SentTranscriptMessage message, long envelopeTimestamp) { log(envelopeTimestamp, "Group recipient update."); Recipient recipient = getSyncMessageDestination(message); if (!recipient.isGroup()) { warn("Got recipient update for a non-group message! Skipping."); return; } MessageRecord record = SignalDatabase.messages().getMessageFor(message.getTimestamp(), Recipient.self().getId()); if (record == null) { warn("Got recipient update for non-existing message! Skipping."); return; } if (!record.isMms()) { warn("Recipient update matched a non-MMS message! Skipping."); return; } updateGroupReceiptStatus(message, record.getId(), recipient.requireGroupId()); } private void updateGroupReceiptStatus(@NonNull SentTranscriptMessage message, long messageId, @NonNull GroupId groupString) { GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts(); List messageRecipientIds = Stream.of(message.getRecipients()).map(RecipientId::from).toList(); List members = SignalDatabase.groups().getGroupMembers(groupString, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); Map localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId)) .collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); for (RecipientId messageRecipientId : messageRecipientIds) { //noinspection ConstantConditions if (localReceipts.containsKey(messageRecipientId) && localReceipts.get(messageRecipientId) < GroupReceiptTable.STATUS_UNDELIVERED) { receiptDatabase.update(messageRecipientId, messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); } else if (!localReceipts.containsKey(messageRecipientId)) { receiptDatabase.insert(Collections.singletonList(messageRecipientId), messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); } } List> unidentifiedStatus = Stream.of(members) .map(m -> new org.signal.libsignal.protocol.util.Pair<>(m.getId(), message.isUnidentified(m.requireServiceId()))) .toList(); receiptDatabase.setUnidentified(unidentifiedStatus, messageId); } private void updateGroupReceiptStatusForDistributionList(@NonNull SentTranscriptMessage message, long messageId, @NonNull DistributionListId distributionListId) { GroupReceiptTable receiptTable = SignalDatabase.groupReceipts(); List messageRecipientIds = message.getRecipients().stream().map(RecipientId::from).collect(java.util.stream.Collectors.toList()); List members = SignalDatabase.distributionLists().getMembers(distributionListId).stream().map(Recipient::resolved).collect(java.util.stream.Collectors.toList()); Map localReceipts = receiptTable.getGroupReceiptInfo(messageId).stream().collect(java.util.stream.Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus)); for (RecipientId messageRecipientId : messageRecipientIds) { //noinspection ConstantConditions if (localReceipts.containsKey(messageRecipientId) && localReceipts.get(messageRecipientId) < GroupReceiptTable.STATUS_UNDELIVERED) { receiptTable.update(messageRecipientId, messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); } else if (!localReceipts.containsKey(messageRecipientId)) { receiptTable.insert(Collections.singletonList(messageRecipientId), messageId, GroupReceiptTable.STATUS_UNDELIVERED, message.getTimestamp()); } } List> unidentifiedStatus = members.stream() .map(m -> new org.signal.libsignal.protocol.util.Pair<>(m.getId(), message.isUnidentified(m.requireServiceId()))) .collect(java.util.stream.Collectors.toList()); receiptTable.setUnidentified(unidentifiedStatus, messageId); } private @Nullable MessageId handleTextMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Optional smsMessageId, @NonNull Optional groupId, @NonNull Recipient senderRecipient, @NonNull Recipient threadRecipient, long receivedTime) throws StorageFailedException { log(message.getTimestamp(), "Text message."); MessageTable database = SignalDatabase.messages(); String body = message.getBody().isPresent() ? message.getBody().get() : ""; handlePossibleExpirationUpdate(content, message, groupId, senderRecipient, threadRecipient, receivedTime); Optional insertResult; if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) { insertResult = Optional.of(database.updateBundleMessageBody(smsMessageId.get(), body)); } else { notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice()); IncomingTextMessage textMessage = new IncomingTextMessage(senderRecipient.getId(), content.getSenderDevice(), message.getTimestamp(), content.getServerReceivedTimestamp(), receivedTime, body, groupId, TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()), content.isNeedsReceipt(), content.getServerUuid()); textMessage = new IncomingEncryptedMessage(textMessage, body); insertResult = database.insertMessageInbox(textMessage); if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get()); } if (insertResult.isPresent()) { ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); return new MessageId(insertResult.get().getMessageId()); } else { return null; } } private long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message, long envelopeTimestamp) throws MmsException, BadGroupIdException { log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp()); Recipient recipient = getSyncMessageDestination(message); String body = message.getDataMessage().get().getBody().orElse(""); long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds()); BodyRangeList bodyRanges = message.getDataMessage().get().getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null); if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) { handleSynchronizeSentExpirationUpdate(message); } long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient); boolean isGroup = recipient.isGroup(); MessageTable database; long messageId; if (isGroup) { OutgoingMessage outgoingMessage = new OutgoingMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, false, StoryType.NONE, Collections.emptyList(), Collections.emptyList(), true, bodyRanges); messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null); database = SignalDatabase.messages(); updateGroupReceiptStatus(message, messageId, recipient.requireGroupId()); } else { OutgoingMessage outgoingTextMessage = OutgoingMessage.text(recipient, body, expiresInMillis, message.getTimestamp(), bodyRanges); messageId = SignalDatabase.messages().insertMessageOutbox(outgoingTextMessage, threadId, false, null); database = SignalDatabase.messages(); database.markUnidentified(messageId, isUnidentified(message, recipient)); } SignalDatabase.threads().update(threadId, true); database.markAsSent(messageId, true); if (expiresInMillis > 0) { database.markExpireStarted(messageId, message.getExpirationStartTimestamp()); ApplicationDependencies.getExpiringMessageManager() .scheduleDeletion(messageId, isGroup, message.getExpirationStartTimestamp(), expiresInMillis); } if (recipient.isSelf()) { SyncMessageId id = new SyncMessageId(recipient.getId(), message.getTimestamp()); SignalDatabase.messages().incrementDeliveryReceiptCount(id, System.currentTimeMillis()); SignalDatabase.messages().incrementReadReceiptCount(id, System.currentTimeMillis()); } return threadId; } private void handleInvalidVersionMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { log(timestamp, "Invalid version message."); MessageTable smsDatabase = SignalDatabase.messages(); if (!smsMessageId.isPresent()) { Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); if (insertResult.isPresent()) { smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get()); } } private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { log(timestamp, "Corrupt message."); MessageTable smsDatabase = SignalDatabase.messages(); if (!smsMessageId.isPresent()) { Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); if (insertResult.isPresent()) { smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsDecryptFailed(smsMessageId.get()); } } private void handleUnsupportedDataMessage(@NonNull String sender, int senderDevice, @NonNull Optional groupId, long timestamp, @NonNull Optional smsMessageId) { log(timestamp, "Unsupported data message."); MessageTable smsDatabase = SignalDatabase.messages(); if (!smsMessageId.isPresent()) { Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp, groupId); if (insertResult.isPresent()) { smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); } } private void handleInvalidMessage(@NonNull SignalServiceAddress sender, int senderDevice, @NonNull Optional groupId, long timestamp, @NonNull Optional smsMessageId) { log(timestamp, "Invalid message."); MessageTable smsDatabase = SignalDatabase.messages(); if (!smsMessageId.isPresent()) { Optional insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, groupId); if (insertResult.isPresent()) { smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsNoSession(smsMessageId.get()); } } private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { log(timestamp, "Legacy message."); MessageTable smsDatabase = SignalDatabase.messages(); if (!smsMessageId.isPresent()) { Optional insertResult = insertPlaceholder(sender, senderDevice, timestamp); if (insertResult.isPresent()) { smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId()); ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId())); } } else { smsDatabase.markAsLegacyVersion(smsMessageId.get()); } } private void handleProfileKey(@NonNull SignalServiceContent content, @NonNull byte[] messageProfileKeyBytes, @NonNull Recipient senderRecipient) { RecipientTable database = SignalDatabase.recipients(); ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes); if (senderRecipient.isSelf()) { if (!Objects.equals(ProfileKeyUtil.getSelfProfileKey(), messageProfileKey)) { warn(content.getTimestamp(), "Saw a sync message whose profile key doesn't match our records. Scheduling a storage sync to check."); StorageSyncHelper.scheduleSyncForDataChange(); } } else if (messageProfileKey != null) { if (database.setProfileKey(senderRecipient.getId(), messageProfileKey)) { log(content.getTimestamp(), "Profile key on message from " + senderRecipient.getId() + " didn't match our local store. It has been updated."); ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(senderRecipient.getId())); } } else { warn(String.valueOf(content.getTimestamp()), "Ignored invalid profile key seen in message"); } } private void handleNeedsDeliveryReceipt(@NonNull RecipientId senderId, @NonNull SignalServiceDataMessage message, @NonNull MessageId messageId) { SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(senderId, message.getTimestamp(), messageId))); } private void handleViewedReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceReceiptMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) { boolean readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context); boolean storyViewedReceipts = SignalStore.storyValues().getViewedReceiptsEnabled(); if (!readReceipts && !storyViewedReceipts) { log("Ignoring viewed receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); return; } log(TAG, "Processing viewed receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Only Stories: " + (!readReceipts && storyViewedReceipts) + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); List ids = Stream.of(message.getTimestamps()) .map(t -> new SyncMessageId(senderRecipient.getId(), t)) .toList(); final Collection unhandled; if (readReceipts && storyViewedReceipts) { unhandled = SignalDatabase.messages().incrementViewedReceiptCounts(ids, content.getTimestamp()); } else if (readReceipts) { unhandled = SignalDatabase.messages().incrementViewedNonStoryReceiptCounts(ids, content.getTimestamp()); } else { unhandled = SignalDatabase.messages().incrementViewedStoryReceiptCounts(ids, content.getTimestamp()); } Set handled = new HashSet<>(ids); handled.removeAll(unhandled); SignalDatabase.messages().updateViewedStories(handled); if (unhandled.size() > 0) { RecipientId selfId = Recipient.self().getId(); for (SyncMessageId id : unhandled) { warn(String.valueOf(content.getTimestamp()), "[handleViewedReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + ", author: " + id.getRecipientId() + " | Receipt so associating with message from self (" + selfId + ")"); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(selfId, id.getTimetamp(), content); } } } if (unhandled.size() > 0 && !processingEarlyContent) { PushProcessEarlyMessagesJob.enqueue(); } } @SuppressLint("DefaultLocale") private void handleDeliveryReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceReceiptMessage message, @NonNull Recipient senderRecipient) { log(content.getTimestamp(), "Processing delivery receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); List ids = Stream.of(message.getTimestamps()) .map(t -> new SyncMessageId(senderRecipient.getId(), t)) .toList(); Collection unhandled = SignalDatabase.messages().incrementDeliveryReceiptCounts(ids, content.getTimestamp()); for (SyncMessageId id : unhandled) { warn(String.valueOf(content.getTimestamp()), "[handleDeliveryReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + id.getRecipientId()); // Early delivery receipts are special-cased in the database methods } if (unhandled.size() > 0) { PushProcessEarlyMessagesJob.enqueue(); } SignalDatabase.pendingPniSignatureMessages().acknowledgeReceipts(senderRecipient.getId(), message.getTimestamps(), content.getSenderDevice()); SignalDatabase.messageLog().deleteEntriesForRecipient(message.getTimestamps(), senderRecipient.getId(), content.getSenderDevice()); } @SuppressLint("DefaultLocale") private void handleReadReceipt(@NonNull SignalServiceContent content, @NonNull SignalServiceReceiptMessage message, @NonNull Recipient senderRecipient, boolean processingEarlyContent) { if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { log("Ignoring read receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); return; } log(TAG, "Processing read receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); List ids = Stream.of(message.getTimestamps()) .map(t -> new SyncMessageId(senderRecipient.getId(), t)) .toList(); Collection unhandled = SignalDatabase.messages().incrementReadReceiptCounts(ids, content.getTimestamp()); if (unhandled.size() > 0) { RecipientId selfId = Recipient.self().getId(); for (SyncMessageId id : unhandled) { warn(String.valueOf(content.getTimestamp()), "[handleReadReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + ", author: " + id.getRecipientId() + " | Receipt, so associating with message from self (" + selfId + ")"); if (!processingEarlyContent) { ApplicationDependencies.getEarlyMessageCache().store(selfId, id.getTimetamp(), content); } } } if (unhandled.size() > 0 && !processingEarlyContent) { PushProcessEarlyMessagesJob.enqueue(); } } private void handleTypingMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceTypingMessage typingMessage, @NonNull Recipient senderRecipient) throws BadGroupIdException { if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) { return; } long threadId; if (typingMessage.getGroupId().isPresent()) { GroupId.Push groupId = GroupId.push(typingMessage.getGroupId().get()); if (!SignalDatabase.groups().isCurrentMember(groupId, senderRecipient.getId())) { warn(String.valueOf(content.getTimestamp()), "Seen typing indicator for non-member " + senderRecipient.getId()); return; } Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId); threadId = SignalDatabase.threads().getOrCreateThreadIdFor(groupRecipient); } else { threadId = SignalDatabase.threads().getOrCreateThreadIdFor(senderRecipient); } if (threadId <= 0) { warn(String.valueOf(content.getTimestamp()), "Couldn't find a matching thread for a typing message."); return; } if (typingMessage.isTypingStarted()) { Log.d(TAG, "Typing started on thread " + threadId); ApplicationDependencies.getTypingStatusRepository().onTypingStarted(context,threadId, senderRecipient, content.getSenderDevice()); } else { Log.d(TAG, "Typing stopped on thread " + threadId); ApplicationDependencies.getTypingStatusRepository().onTypingStopped(context, threadId, senderRecipient, content.getSenderDevice(), false); } } private void handleRetryReceipt(@NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage, @NonNull Recipient senderRecipient) { if (!FeatureFlags.retryReceipts()) { warn(String.valueOf(content.getTimestamp()), "[RetryReceipt] Feature flag disabled, skipping retry receipt."); return; } if (decryptionErrorMessage.getDeviceId() != SignalStore.account().getDeviceId()) { log(String.valueOf(content.getTimestamp()), "[RetryReceipt] Received a DecryptionErrorMessage targeting a linked device. Ignoring."); return; } long sentTimestamp = decryptionErrorMessage.getTimestamp(); warn(content.getTimestamp(), "[RetryReceipt] Received a retry receipt from " + formatSender(senderRecipient, content) + " for message with timestamp " + sentTimestamp + "."); if (!senderRecipient.hasServiceId()) { warn(content.getTimestamp(), "[RetryReceipt] Requester " + senderRecipient.getId() + " somehow has no UUID! timestamp: " + sentTimestamp); return; } MessageLogEntry messageLogEntry = SignalDatabase.messageLog().getLogEntry(senderRecipient.getId(), content.getSenderDevice(), sentTimestamp); if (decryptionErrorMessage.getRatchetKey().isPresent()) { handleIndividualRetryReceipt(senderRecipient, messageLogEntry, content, decryptionErrorMessage); } else { handleSenderKeyRetryReceipt(senderRecipient, 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 = SignalDatabase.threads().getRecipientForThreadId(relatedMessage.getThreadId()); if (threadRecipient == null) { warn(content.getTimestamp(), "[RetryReceipt-SK] Could not find a thread recipient! Skipping."); return; } if (!threadRecipient.isPushV2Group() && !threadRecipient.isDistributionList()) { warn(content.getTimestamp(), "[RetryReceipt-SK] Thread recipient is not a V2 group or distribution list! Skipping."); return; } DistributionId distributionId; GroupId.V2 groupId; if (threadRecipient.isGroup()) { groupId = threadRecipient.requireGroupId().requireV2(); distributionId = SignalDatabase.groups().getOrCreateDistributionId(groupId); } else { groupId = null; distributionId = SignalDatabase.distributionLists().getDistributionId(threadRecipient.getId()); } if (distributionId == null) { Log.w(TAG, "[RetryReceipt-SK] Failed to find a distributionId! Skipping."); return; } SignalProtocolAddress requesterAddress = new SignalProtocolAddress(requester.requireServiceId().toString(), content.getSenderDevice()); SignalDatabase.senderKeyShared().delete(distributionId, Collections.singleton(requesterAddress)); if (messageLogEntry != null) { warn(content.getTimestamp(), "[RetryReceipt-SK] Found MSL entry for " + requester.getId() + " (" + requesterAddress + ") with timestamp " + sentTimestamp + ". Scheduling a resend."); ApplicationDependencies.getJobManager().add(new ResendMessageJob(messageLogEntry.getRecipientId(), messageLogEntry.getDateSent(), messageLogEntry.getContent(), messageLogEntry.getContentHint(), messageLogEntry.isUrgent(), groupId, distributionId)); } else { warn(content.getTimestamp(), "[RetryReceipt-SK] Unable to find MSL entry for " + requester.getId() + " (" + requesterAddress + ") with timestamp " + sentTimestamp + " for " + (groupId != null ? "group " + groupId : "distribution list") + ". Scheduling a job to send them the SenderKeyDistributionMessage. Membership will be checked there."); ApplicationDependencies.getJobManager().add(new SenderKeyDistributionSendJob(requester.getId(), threadRecipient.getId())); } } private void handleIndividualRetryReceipt(@NonNull Recipient requester, @Nullable MessageLogEntry messageLogEntry, @NonNull SignalServiceContent content, @NonNull DecryptionErrorMessage decryptionErrorMessage) { boolean archivedSession = false; // TODO [pnp] Ignore retry receipts that have a PNI destinationUuid if (decryptionErrorMessage.getRatchetKey().isPresent() && ratchetKeyMatches(requester, content.getSenderDevice(), decryptionErrorMessage.getRatchetKey().get())) { warn(content.getTimestamp(), "[RetryReceipt-I] Ratchet key matches. Archiving the session."); ApplicationDependencies.getProtocolStore().aci().sessions().archiveSession(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(), messageLogEntry.isUrgent(), 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 @Nullable MessageRecord findRetryReceiptRelatedMessage(@NonNull Context context, @Nullable MessageLogEntry messageLogEntry, long sentTimestamp) { if (messageLogEntry != null && messageLogEntry.hasRelatedMessage()) { MessageId relatedMessage = messageLogEntry.getRelatedMessages().get(0); return SignalDatabase.messages().getMessageRecordOrNull(relatedMessage.getId()); } else { return SignalDatabase.messages().getMessageFor(sentTimestamp, Recipient.self().getId()); } } public static boolean ratchetKeyMatches(@NonNull Recipient recipient, int deviceId, @NonNull ECPublicKey ratchetKey) { SignalProtocolAddress address = recipient.resolve().requireServiceId().toProtocolAddress(deviceId); SessionRecord session = ApplicationDependencies.getProtocolStore().aci().loadSession(address); return session.currentRatchetKeyMatches(ratchetKey); } private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) { if (message.isViewOnce()) { List attachments = message.getAttachments().orElse(Collections.emptyList()); return attachments.size() != 1 || !isViewOnceSupportedContentType(attachments.get(0).getContentType().toLowerCase()); } return false; } private static boolean isViewOnceSupportedContentType(@NonNull String contentType) { return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType); } private Optional getValidatedQuote(Optional quote) { if (!quote.isPresent()) return Optional.empty(); if (quote.get().getId() <= 0) { warn("Received quote without an ID! Ignoring..."); return Optional.empty(); } if (quote.get().getAuthor() == null) { warn("Received quote without an author! Ignoring..."); return Optional.empty(); } RecipientId author = Recipient.externalPush(quote.get().getAuthor()).getId(); MessageRecord message = SignalDatabase.messages().getMessageFor(quote.get().getId(), author); if (message != null && !message.isRemoteDelete()) { log("Found matching message record..."); List attachments = new LinkedList<>(); List mentions = new LinkedList<>(); if (message.isMms()) { MmsMessageRecord mmsMessage = (MmsMessageRecord) message; if (mmsMessage instanceof MediaMmsMessageRecord) { mmsMessage = ((MediaMmsMessageRecord) mmsMessage).withAttachments(context, SignalDatabase.attachments().getAttachmentsForMessage(mmsMessage.getId())); } mentions.addAll(SignalDatabase.mentions().getMentionsForMessage(mmsMessage.getId())); if (mmsMessage.isViewOnce()) { attachments.add(new TombstoneAttachment(MediaUtil.VIEW_ONCE, true)); } else { attachments = mmsMessage.getSlideDeck().asAttachments(); if (attachments.isEmpty()) { attachments.addAll(Stream.of(mmsMessage.getLinkPreviews()) .filter(lp -> lp.getThumbnail().isPresent()) .map(lp -> lp.getThumbnail().get()) .toList()); } } if (message.isPaymentNotification()) { message = SignalDatabase.payments().updateMessageWithPayment(message); } } String body = message.isPaymentNotification() ? message.getDisplayBody(context).toString() : message.getBody(); return Optional.of(new QuoteModel(quote.get().getId(), author, body, false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType()), message.getMessageRanges())); } else if (message != null) { warn("Found the target for the quote, but it's flagged as remotely deleted."); } warn("Didn't find matching message record..."); return Optional.of(new QuoteModel(quote.get().getId(), author, quote.get().getText(), true, PointerAttachment.forPointers(quote.get().getAttachments()), getMentions(quote.get().getMentions()), QuoteModel.Type.fromDataMessageType(quote.get().getType()), DatabaseProtosUtil.toBodyRangeList(quote.get().getBodyRanges()))); } private Optional getStickerAttachment(Optional sticker) { if (!sticker.isPresent()) { return Optional.empty(); } if (sticker.get().getPackId() == null || sticker.get().getPackKey() == null || sticker.get().getAttachment() == null) { warn("Malformed sticker!"); return Optional.empty(); } String packId = Hex.toStringCondensed(sticker.get().getPackId()); String packKey = Hex.toStringCondensed(sticker.get().getPackKey()); int stickerId = sticker.get().getStickerId(); String emoji = sticker.get().getEmoji(); StickerLocator stickerLocator = new StickerLocator(packId, packKey, stickerId, emoji); StickerTable stickerDatabase = SignalDatabase.stickers(); StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false); if (stickerRecord != null) { return Optional.of(new UriAttachment(stickerRecord.getUri(), stickerRecord.getContentType(), AttachmentTable.TRANSFER_PROGRESS_DONE, stickerRecord.getSize(), StickerSlide.WIDTH, StickerSlide.HEIGHT, null, String.valueOf(new SecureRandom().nextLong()), false, false, false, false, null, stickerLocator, null, null, null)); } else { return Optional.of(PointerAttachment.forPointer(Optional.of(sticker.get().getAttachment()), stickerLocator).get()); } } private static Optional> getContacts(Optional> sharedContacts) { if (!sharedContacts.isPresent()) return Optional.empty(); List contacts = new ArrayList<>(sharedContacts.get().size()); for (SharedContact sharedContact : sharedContacts.get()) { contacts.add(ContactModelMapper.remoteToLocal(sharedContact)); } return Optional.of(contacts); } private Optional> getLinkPreviews(Optional> previews, @NonNull String message, boolean isStoryEmbed) { if (!previews.isPresent() || previews.get().isEmpty()) return Optional.empty(); List linkPreviews = new ArrayList<>(previews.get().size()); LinkPreviewUtil.Links urlsInMessage = LinkPreviewUtil.findValidPreviewUrls(message); for (SignalServicePreview preview : previews.get()) { Optional thumbnail = PointerAttachment.forPointer(preview.getImage()); Optional url = Optional.ofNullable(preview.getUrl()); Optional title = Optional.ofNullable(preview.getTitle()); Optional description = Optional.ofNullable(preview.getDescription()); boolean hasTitle = !TextUtils.isEmpty(title.orElse("")); boolean presentInBody = url.isPresent() && urlsInMessage.containsUrl(url.get()); boolean validDomain = url.isPresent() && LinkUtil.isValidPreviewUrl(url.get()); if (hasTitle && (presentInBody || isStoryEmbed) && validDomain) { LinkPreview linkPreview = new LinkPreview(url.get(), title.orElse(""), description.orElse(""), preview.getDate(), thumbnail); linkPreviews.add(linkPreview); } else { warn(String.format("Discarding an invalid link preview. hasTitle: %b presentInBody: %b isStoryEmbed: %b validDomain: %b", hasTitle, presentInBody, isStoryEmbed, validDomain)); } } return Optional.of(linkPreviews); } private Optional> getMentions(Optional> signalServiceMentions) { if (!signalServiceMentions.isPresent()) return Optional.empty(); return Optional.of(getMentions(signalServiceMentions.get())); } private @Nullable BodyRangeList getBodyRangeList(Optional> bodyRanges) { if (!bodyRanges.isPresent()) { return null; } return DatabaseProtosUtil.toBodyRangeList(bodyRanges.get()); } private @NonNull List getMentions(@Nullable List signalServiceMentions) { if (signalServiceMentions == null || signalServiceMentions.isEmpty()) { return Collections.emptyList(); } List mentions = new ArrayList<>(signalServiceMentions.size()); for (SignalServiceDataMessage.Mention mention : signalServiceMentions) { mentions.add(new Mention(Recipient.externalPush(mention.getServiceId()).getId(), mention.getStart(), mention.getLength())); } return mentions; } private Optional getGiftBadge(Optional giftBadge) { if (!giftBadge.isPresent()) return Optional.empty(); return Optional.of(GiftBadge.newBuilder() .setRedemptionToken(ByteString.copyFrom(giftBadge.get().getReceiptCredentialPresentation().serialize())) .build()); } private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) { return insertPlaceholder(sender, senderDevice, timestamp, Optional.empty()); } private Optional insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional groupId) { MessageTable database = SignalDatabase.messages(); IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(), senderDevice, timestamp, -1, System.currentTimeMillis(), "", groupId, 0, false, null); textMessage = new IncomingEncryptedMessage(textMessage, ""); return database.insertMessageInbox(textMessage); } private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message) { if (message.getDataMessage().get().getGroupContext().isPresent()) { return Recipient.externalPossiblyMigratedGroup(GroupId.v2(message.getDataMessage().get().getGroupContext().get().getMasterKey())); } else { return Recipient.externalPush(message.getDestination().get()); } } private Recipient getMessageDestination(@NonNull SignalServiceContent content, @NonNull Recipient sender) throws BadGroupIdException { if (content.getStoryMessage().isPresent()) { SignalServiceStoryMessage message = content.getStoryMessage().get(); return getGroupRecipient(message.getGroupContext(), sender); } else { SignalServiceDataMessage message = content.getDataMessage().orElse(null); return getGroupRecipient(message != null ? message.getGroupContext() : Optional.empty(), sender); } } private @NonNull Recipient getGroupRecipient(Optional signalServiceGroup, @NonNull Recipient senderRecipient) { if (signalServiceGroup.isPresent()) { return Recipient.externalPossiblyMigratedGroup(GroupId.v2(signalServiceGroup.get().getMasterKey())); } else { return senderRecipient; } } private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient senderRecipient, @NonNull Recipient conversationRecipient, int device) { long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(conversationRecipient); if (threadId > 0 && TextSecurePreferences.isTypingIndicatorsEnabled(context)) { Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message."); ApplicationDependencies.getTypingStatusRepository().onTypingStopped(context, threadId, senderRecipient, device, true); } } private boolean shouldIgnore(@NonNull SignalServiceContent content, @NonNull Recipient sender, @NonNull Recipient conversation) throws BadGroupIdException { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); if (conversation.isGroup() && conversation.isBlocked()) { return true; } else if (conversation.isGroup()) { GroupTable groupDatabase = SignalDatabase.groups(); Optional groupId = GroupUtil.idFromGroupContext(message.getGroupContext()); if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) { return sender.isBlocked(); } boolean isTextMessage = message.getBody().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getSticker().isPresent(); boolean isExpireMessage = message.isExpirationUpdate(); boolean isGv2Update = message.isGroupV2Update(); boolean isContentMessage = !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage); boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isGv2Update); } else { return sender.isBlocked(); } } else if (content.getCallMessage().isPresent()) { return sender.isBlocked(); } else if (content.getTypingMessage().isPresent()) { if (sender.isBlocked()) { return true; } if (content.getTypingMessage().get().getGroupId().isPresent()) { GroupId groupId = GroupId.push(content.getTypingMessage().get().getGroupId().get()); Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId); if (groupRecipient.isBlocked() || !groupRecipient.isActiveGroup()) { return true; } else { Optional groupRecord = SignalDatabase.groups().getGroup(groupId); return groupRecord.isPresent() && groupRecord.get().isAnnouncementGroup() && !groupRecord.get().getAdmins().contains(sender); } } } else if (content.getStoryMessage().isPresent()) { if (conversation.isGroup() && conversation.isBlocked()) { return true; } else { return sender.isBlocked(); } } return false; } private void resetRecipientToPush(@NonNull Recipient recipient) { if (recipient.isForceSmsSelection()) { SignalDatabase.recipients().setForceSmsSelection(recipient.getId(), false); } } private void forceStickerDownloadIfNecessary(long messageId, List stickerAttachments) { if (stickerAttachments.isEmpty()) return; DatabaseAttachment stickerAttachment = stickerAttachments.get(0); if (stickerAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE) { AttachmentDownloadJob downloadJob = new AttachmentDownloadJob(messageId, stickerAttachment.getAttachmentId(), true); try { downloadJob.setContext(context); downloadJob.doWork(); } catch (Exception e) { warn("Failed to download sticker inline. Scheduling."); ApplicationDependencies.getJobManager().add(downloadJob); } } } private static boolean isUnidentified(@NonNull SentTranscriptMessage message, @NonNull Recipient recipient) { if (recipient.hasServiceId()) { return message.isUnidentified(recipient.requireServiceId()); } else { return false; } } private static void log(@NonNull String message) { Log.i(TAG, message); } private static void log(long timestamp, @NonNull String message) { log(String.valueOf(timestamp), message); } private static void log(@NonNull String extra, @NonNull String message) { String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] "; Log.i(TAG, extraLog + message); } private static void warn(@NonNull String message) { warn("", message, null); } private static void warn(@NonNull String extra, @NonNull String message) { warn(extra, message, null); } private static void warn(long timestamp, @NonNull String message) { warn(String.valueOf(timestamp), message); } private static void warn(long timestamp, @NonNull String message, @Nullable Throwable t) { warn(String.valueOf(timestamp), message, t); } private static void warn(@NonNull String message, @Nullable Throwable t) { warn("", message, t); } private static void warn(@NonNull String extra, @NonNull String message, @Nullable Throwable t) { String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] "; Log.w(TAG, extraLog + message, t); } private static String formatSender(@NonNull Recipient recipient, @Nullable SignalServiceContent content) { return formatSender(recipient.getId(), content); } private static String formatSender(@NonNull RecipientId recipientId, @Nullable SignalServiceContent content) { if (content != null) { return recipientId + " (" + content.getSender().getIdentifier() + "." + content.getSenderDevice() + ")"; } else { return recipientId.toString(); } } @SuppressWarnings("WeakerAccess") private static class StorageFailedException extends Exception { private final String sender; private final int senderDevice; private StorageFailedException(Exception e, String sender, int senderDevice) { super(e); this.sender = sender; this.senderDevice = senderDevice; } public String getSender() { return sender; } public int getSenderDevice() { return senderDevice; } } public enum MessageState { DECRYPTED_OK, INVALID_VERSION, CORRUPT_MESSAGE, // Not used, but can't remove due to serialization NO_SESSION, // Not used, but can't remove due to serialization LEGACY_MESSAGE, DUPLICATE_MESSAGE, UNSUPPORTED_DATA_MESSAGE, NOOP, DECRYPTION_ERROR } public static final class ExceptionMetadata { @NonNull private final String sender; private final int senderDevice; @Nullable private final GroupId groupId; public ExceptionMetadata(@NonNull String sender, int senderDevice, @Nullable GroupId groupId) { this.sender = sender; this.senderDevice = senderDevice; this.groupId = groupId; } public ExceptionMetadata(@NonNull String sender, int senderDevice) { this(sender, senderDevice, null); } @NonNull public String getSender() { return sender; } public int getSenderDevice() { return senderDevice; } @Nullable public GroupId getGroupId() { return groupId; } } }