kopia lustrzana https://github.com/ryukoposting/Signal-Android
3464 wiersze
181 KiB
Java
3464 wiersze
181 KiB
Java
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<Long> 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<List<SignalServiceContent>> 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<Long> 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> 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<Integer> 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<Long> 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> 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<GroupRecord> 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> 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<Long> 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<Long> 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<IceUpdateMessage> messages,
|
|
@NonNull Recipient senderRecipient)
|
|
{
|
|
log(content.getTimestamp(), "handleCallIceUpdateMessage... " + messages.size());
|
|
|
|
List<byte[]> 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<Long> 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> 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<Long> 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> 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> 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<Long> 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> 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<Long> smsMessageId,
|
|
@NonNull Optional<GroupId> 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<SignalServiceGroupV2> 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> 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<StickerPackOperationMessage> 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<MobileCoinPublicAddress> 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<ReadMessage> readMessages,
|
|
long envelopeTimestamp,
|
|
boolean processingEarlyContent)
|
|
{
|
|
log(envelopeTimestamp, "Synchronize read message. Count: " + readMessages.size() + ", Timestamps: " + Stream.of(readMessages).map(ReadMessage::getTimestamp).toList());
|
|
|
|
Map<Long, Long> threadToLatestRead = new HashMap<>();
|
|
|
|
Collection<SyncMessageId> unhandled = SignalDatabase.messages().setTimestampReadFromSyncMessage(readMessages, envelopeTimestamp, threadToLatestRead);
|
|
|
|
List<MessageTable.MarkedMessageInfo> 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<ViewedMessage> viewedMessages, long envelopeTimestamp) {
|
|
log(envelopeTimestamp, "Synchronize view message. Count: " + viewedMessages.size() + ", Timestamps: " + Stream.of(viewedMessages).map(ViewedMessage::getTimestamp).toList());
|
|
|
|
List<MessageRecord> 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<Long> toMarkViewed = Stream.of(records)
|
|
.map(MessageRecord::getId)
|
|
.toList();
|
|
|
|
List<MediaMmsMessageRecord> 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> 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<Float> 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> 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> 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> 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<Long> smsMessageId,
|
|
@NonNull Recipient senderRecipient,
|
|
@NonNull Recipient threadRecipient,
|
|
long receivedTime)
|
|
throws StorageFailedException
|
|
{
|
|
log(message.getTimestamp(), "Media message.");
|
|
|
|
notifyTypingStoppedFromIncomingMessage(senderRecipient, threadRecipient, content.getSenderDevice());
|
|
|
|
Optional<InsertResult> insertResult;
|
|
|
|
MessageTable database = SignalDatabase.messages();
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
|
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
|
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().orElse(""), false);
|
|
Optional<List<Mention>> mentions = getMentions(message.getMentions());
|
|
Optional<Attachment> 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<DatabaseAttachment> allAttachments = SignalDatabase.attachments().getAttachmentsForMessage(insertResult.get().getMessageId());
|
|
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
|
List<DatabaseAttachment> 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> 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<SignalServiceDataMessage.Reaction> 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<DistributionId> distributionIds = manifest.getDistributionIdSet();
|
|
Optional<GroupId> groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey()));
|
|
String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null);
|
|
StoryType storyType = getStoryType(storyMessage);
|
|
List<LinkPreview> linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)),
|
|
"",
|
|
true).orElse(Collections.emptyList());
|
|
List<Attachment> 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<RecipientId> 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<Attachment> pendingAttachments,
|
|
long sentAtTimestamp,
|
|
@NonNull StoryType storyType,
|
|
@NonNull List<LinkPreview> 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<DatabaseAttachment> 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<DatabaseAttachment> 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<QuoteModel> quote = getValidatedQuote(message.getDataMessage().get().getQuote());
|
|
Optional<Attachment> sticker = getStickerAttachment(message.getDataMessage().get().getSticker());
|
|
Optional<List<Contact>> sharedContacts = getContacts(message.getDataMessage().get().getSharedContacts());
|
|
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getDataMessage().get().getPreviews(), message.getDataMessage().get().getBody().orElse(""), false);
|
|
Optional<List<Mention>> mentions = getMentions(message.getDataMessage().get().getMentions());
|
|
Optional<GiftBadge> giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge());
|
|
boolean viewOnce = message.getDataMessage().get().isViewOnce();
|
|
List<Attachment> 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<DatabaseAttachment> attachments;
|
|
List<DatabaseAttachment> 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<DatabaseAttachment> 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<RecipientId> messageRecipientIds = Stream.of(message.getRecipients()).map(RecipientId::from).toList();
|
|
List<Recipient> members = SignalDatabase.groups().getGroupMembers(groupString, GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
|
Map<RecipientId, Integer> 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<org.signal.libsignal.protocol.util.Pair<RecipientId, Boolean>> 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<RecipientId> messageRecipientIds = message.getRecipients().stream().map(RecipientId::from).collect(java.util.stream.Collectors.toList());
|
|
List<Recipient> members = SignalDatabase.distributionLists().getMembers(distributionListId).stream().map(Recipient::resolved).collect(java.util.stream.Collectors.toList());
|
|
Map<RecipientId, Integer> 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<org.signal.libsignal.protocol.util.Pair<RecipientId, Boolean>> 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<Long> smsMessageId,
|
|
@NonNull Optional<GroupId> 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> 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<Long> smsMessageId)
|
|
{
|
|
log(timestamp, "Invalid version message.");
|
|
|
|
MessageTable smsDatabase = SignalDatabase.messages();
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> 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<Long> smsMessageId)
|
|
{
|
|
log(timestamp, "Corrupt message.");
|
|
|
|
MessageTable smsDatabase = SignalDatabase.messages();
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> 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> groupId,
|
|
long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
log(timestamp, "Unsupported data message.");
|
|
|
|
MessageTable smsDatabase = SignalDatabase.messages();
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> 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> groupId,
|
|
long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
log(timestamp, "Invalid message.");
|
|
|
|
MessageTable smsDatabase = SignalDatabase.messages();
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> 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<Long> smsMessageId)
|
|
{
|
|
log(timestamp, "Legacy message.");
|
|
|
|
MessageTable smsDatabase = SignalDatabase.messages();
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> 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<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(senderRecipient.getId(), t))
|
|
.toList();
|
|
|
|
final Collection<SyncMessageId> 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<SyncMessageId> 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<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(senderRecipient.getId(), t))
|
|
.toList();
|
|
|
|
Collection<SyncMessageId> 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<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(senderRecipient.getId(), t))
|
|
.toList();
|
|
|
|
Collection<SyncMessageId> 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<SignalServiceAttachment> 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<QuoteModel> getValidatedQuote(Optional<SignalServiceDataMessage.Quote> 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<Attachment> attachments = new LinkedList<>();
|
|
List<Mention> 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<Attachment> getStickerAttachment(Optional<SignalServiceDataMessage.Sticker> 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<List<Contact>> getContacts(Optional<List<SharedContact>> sharedContacts) {
|
|
if (!sharedContacts.isPresent()) return Optional.empty();
|
|
|
|
List<Contact> contacts = new ArrayList<>(sharedContacts.get().size());
|
|
|
|
for (SharedContact sharedContact : sharedContacts.get()) {
|
|
contacts.add(ContactModelMapper.remoteToLocal(sharedContact));
|
|
}
|
|
|
|
return Optional.of(contacts);
|
|
}
|
|
|
|
private Optional<List<LinkPreview>> getLinkPreviews(Optional<List<SignalServicePreview>> previews, @NonNull String message, boolean isStoryEmbed) {
|
|
if (!previews.isPresent() || previews.get().isEmpty()) return Optional.empty();
|
|
|
|
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
|
|
LinkPreviewUtil.Links urlsInMessage = LinkPreviewUtil.findValidPreviewUrls(message);
|
|
|
|
for (SignalServicePreview preview : previews.get()) {
|
|
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
|
|
Optional<String> url = Optional.ofNullable(preview.getUrl());
|
|
Optional<String> title = Optional.ofNullable(preview.getTitle());
|
|
Optional<String> 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<List<Mention>> getMentions(Optional<List<SignalServiceDataMessage.Mention>> signalServiceMentions) {
|
|
if (!signalServiceMentions.isPresent()) return Optional.empty();
|
|
|
|
return Optional.of(getMentions(signalServiceMentions.get()));
|
|
}
|
|
|
|
private @Nullable BodyRangeList getBodyRangeList(Optional<List<SignalServiceProtos.BodyRange>> bodyRanges) {
|
|
if (!bodyRanges.isPresent()) {
|
|
return null;
|
|
}
|
|
|
|
return DatabaseProtosUtil.toBodyRangeList(bodyRanges.get());
|
|
}
|
|
|
|
private @NonNull List<Mention> getMentions(@Nullable List<SignalServiceDataMessage.Mention> signalServiceMentions) {
|
|
if (signalServiceMentions == null || signalServiceMentions.isEmpty()) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
List<Mention> 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<GiftBadge> getGiftBadge(Optional<SignalServiceDataMessage.GiftBadge> giftBadge) {
|
|
if (!giftBadge.isPresent()) return Optional.empty();
|
|
|
|
return Optional.of(GiftBadge.newBuilder()
|
|
.setRedemptionToken(ByteString.copyFrom(giftBadge.get().getReceiptCredentialPresentation().serialize()))
|
|
.build());
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
|
|
return insertPlaceholder(sender, senderDevice, timestamp, Optional.empty());
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<GroupId> 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<SignalServiceGroupV2> 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> 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> 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<DatabaseAttachment> 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;
|
|
}
|
|
}
|
|
}
|