kopia lustrzana https://github.com/ryukoposting/Signal-Android
1978 wiersze
95 KiB
Java
1978 wiersze
95 KiB
Java
package org.thoughtcrime.securesms.messages;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.os.Build;
|
|
import android.text.TextUtils;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.annimon.stream.Collectors;
|
|
import com.annimon.stream.Stream;
|
|
|
|
import org.signal.core.util.logging.Log;
|
|
import org.signal.ringrtc.CallId;
|
|
import org.signal.zkgroup.profiles.ProfileKey;
|
|
import org.thoughtcrime.securesms.ApplicationContext;
|
|
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.Emoji;
|
|
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.crypto.storage.TextSecureSessionStore;
|
|
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
|
import org.thoughtcrime.securesms.database.GroupReceiptDatabase.GroupReceiptInfo;
|
|
import org.thoughtcrime.securesms.database.MessageDatabase;
|
|
import org.thoughtcrime.securesms.database.MessageDatabase.InsertResult;
|
|
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
|
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
|
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
|
import org.thoughtcrime.securesms.database.PaymentMetaDataUtil;
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|
import org.thoughtcrime.securesms.database.StickerDatabase;
|
|
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
|
import org.thoughtcrime.securesms.database.model.Mention;
|
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
|
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.GroupV1MessageProcessor;
|
|
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.MultiDeviceBlockedUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.MultiDeviceStickerPackSyncJob;
|
|
import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob;
|
|
import org.thoughtcrime.securesms.jobs.PaymentTransactionCheckJob;
|
|
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob;
|
|
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
|
import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob;
|
|
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
|
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob;
|
|
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.OutgoingExpirationUpdateMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
|
import org.thoughtcrime.securesms.mms.QuoteModel;
|
|
import org.thoughtcrime.securesms.mms.SlideDeck;
|
|
import org.thoughtcrime.securesms.mms.StickerSlide;
|
|
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
|
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
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.sms.OutgoingEncryptedMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
|
|
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
|
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
|
import org.thoughtcrime.securesms.util.GroupUtil;
|
|
import org.thoughtcrime.securesms.util.Hex;
|
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
|
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
import org.thoughtcrime.securesms.util.Util;
|
|
import org.whispersystems.libsignal.state.SessionStore;
|
|
import org.whispersystems.libsignal.util.Pair;
|
|
import org.whispersystems.libsignal.util.guava.Optional;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
|
import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage;
|
|
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.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.shared.SharedContact;
|
|
import org.whispersystems.signalservice.api.payments.Money;
|
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|
|
|
import java.io.IOException;
|
|
import java.security.SecureRandom;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
|
|
/**
|
|
* Takes data about a decrypted message, transforms it into user-presentable data, and writes that
|
|
* data to our data stores.
|
|
*/
|
|
public final class MessageContentProcessor {
|
|
|
|
private static final String TAG = Log.tag(MessageContentProcessor.class);
|
|
|
|
private final Context context;
|
|
|
|
public 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 timestamp, long smsMessageId)
|
|
throws IOException, GroupChangeBusyException
|
|
{
|
|
Optional<Long> optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent();
|
|
|
|
if (messageState == MessageState.DECRYPTED_OK) {
|
|
handleMessage(content, timestamp, optionalSmsMessageId);
|
|
|
|
if (content != null) {
|
|
Optional<List<SignalServiceContent>> earlyContent = ApplicationDependencies.getEarlyMessageCache()
|
|
.retrieve(Recipient.externalPush(context, content.getSender()).getId(),
|
|
content.getTimestamp());
|
|
if (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, timestamp, Optional.absent());
|
|
}
|
|
}
|
|
}
|
|
} else if (exceptionMetadata != null) {
|
|
handleExceptionMessage(messageState, exceptionMetadata, timestamp, optionalSmsMessageId);
|
|
} else if (messageState == MessageState.NOOP) {
|
|
Log.d(TAG, "Nothing to do: " + messageState.name());
|
|
} else {
|
|
warn("Bad state! messageState: " + messageState);
|
|
}
|
|
}
|
|
|
|
private void handleMessage(@Nullable SignalServiceContent content, long timestamp, @NonNull Optional<Long> smsMessageId)
|
|
throws IOException, GroupChangeBusyException
|
|
{
|
|
try {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
|
|
if (content == null || shouldIgnore(content)) {
|
|
log(content != null ? String.valueOf(content.getTimestamp()) : "null", "Ignoring message.");
|
|
return;
|
|
}
|
|
|
|
log(String.valueOf(content.getTimestamp()), "Beginning message processing.");
|
|
|
|
if (content.getDataMessage().isPresent()) {
|
|
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();
|
|
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
|
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
|
|
|
|
if (isGv2Message) {
|
|
GroupId.V2 groupIdV2 = groupId.get().requireV2();
|
|
|
|
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(groupIdV2);
|
|
if (possibleGv1.isPresent()) {
|
|
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1());
|
|
}
|
|
|
|
if (!updateGv2GroupFromServerOrP2PChange(content, message.getGroupContext().get().getGroupV2().get())) {
|
|
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message for group we are not currently in " + groupIdV2);
|
|
return;
|
|
}
|
|
|
|
Recipient sender = Recipient.externalPush(context, content.getSender());
|
|
if (!groupDatabase.isCurrentMember(groupIdV2, sender.getId())) {
|
|
log(String.valueOf(content.getTimestamp()), "Ignoring GV2 message from member not in group " + groupIdV2);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
|
|
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
|
|
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
|
|
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
|
|
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
|
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
|
|
else if (message.getPayment().isPresent()) handlePayment(content, message);
|
|
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
|
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
|
|
else if (Build.VERSION.SDK_INT > 19 && message.getGroupCallUpdate().isPresent()) handleGroupCallUpdateMessage(content, message, groupId);
|
|
|
|
if (groupId.isPresent() && groupDatabase.isUnknownGroup(groupId.get())) {
|
|
handleUnknownGroupMessage(content, message.getGroupContext().get());
|
|
}
|
|
|
|
if (message.getProfileKey().isPresent()) {
|
|
handleProfileKey(content, message.getProfileKey().get());
|
|
}
|
|
|
|
if (content.isNeedsReceipt()) {
|
|
handleNeedsDeliveryReceipt(content, message);
|
|
}
|
|
} else if (content.getSyncMessage().isPresent()) {
|
|
TextSecurePreferences.setMultiDevice(context, true);
|
|
|
|
SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
|
|
|
|
if (syncMessage.getSent().isPresent()) handleSynchronizeSentMessage(content, syncMessage.getSent().get());
|
|
else if (syncMessage.getRequest().isPresent()) handleSynchronizeRequestMessage(syncMessage.getRequest().get());
|
|
else if (syncMessage.getRead().isPresent()) handleSynchronizeReadMessage(syncMessage.getRead().get(), content.getTimestamp());
|
|
else if (syncMessage.getViewOnceOpen().isPresent()) handleSynchronizeViewOnceOpenMessage(syncMessage.getViewOnceOpen().get(), content.getTimestamp());
|
|
else if (syncMessage.getVerified().isPresent()) handleSynchronizeVerifiedMessage(syncMessage.getVerified().get());
|
|
else if (syncMessage.getStickerPackOperations().isPresent()) handleSynchronizeStickerPackOperation(syncMessage.getStickerPackOperations().get());
|
|
else if (syncMessage.getConfiguration().isPresent()) handleSynchronizeConfigurationMessage(syncMessage.getConfiguration().get());
|
|
else if (syncMessage.getBlockedList().isPresent()) handleSynchronizeBlockedListMessage(syncMessage.getBlockedList().get());
|
|
else if (syncMessage.getFetchType().isPresent()) handleSynchronizeFetchMessage(syncMessage.getFetchType().get());
|
|
else if (syncMessage.getMessageRequestResponse().isPresent()) handleSynchronizeMessageRequestResponse(syncMessage.getMessageRequestResponse().get());
|
|
else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(syncMessage.getOutgoingPaymentMessage().get());
|
|
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() != 1) {
|
|
log(String.valueOf(content.getTimestamp()), String.format(Locale.US, "Ignoring call message that is not for this device! intended: %d, this: %d", destinationDeviceId.get(), 1));
|
|
return;
|
|
}
|
|
|
|
if (message.getOfferMessage().isPresent()) handleCallOfferMessage(content, message.getOfferMessage().get(), smsMessageId);
|
|
else if (message.getAnswerMessage().isPresent()) handleCallAnswerMessage(content, message.getAnswerMessage().get());
|
|
else if (message.getIceUpdateMessages().isPresent()) handleCallIceUpdateMessage(content, message.getIceUpdateMessages().get());
|
|
else if (message.getHangupMessage().isPresent()) handleCallHangupMessage(content, message.getHangupMessage().get(), smsMessageId);
|
|
else if (message.getBusyMessage().isPresent()) handleCallBusyMessage(content, message.getBusyMessage().get());
|
|
else if (message.getOpaqueMessage().isPresent()) handleCallOpaqueMessage(content, message.getOpaqueMessage().get());
|
|
} else if (content.getReceiptMessage().isPresent()) {
|
|
SignalServiceReceiptMessage message = content.getReceiptMessage().get();
|
|
|
|
if (message.isReadReceipt()) handleReadReceipt(content, message);
|
|
else if (message.isDeliveryReceipt()) handleDeliveryReceipt(content, message);
|
|
else if (message.isViewedReceipt()) handleViewedReceipt(content, message);
|
|
} else if (content.getTypingMessage().isPresent()) {
|
|
handleTypingMessage(content, content.getTypingMessage().get());
|
|
} else {
|
|
warn(String.valueOf(content.getTimestamp()), "Got unrecognized message!");
|
|
}
|
|
|
|
resetRecipientToPush(Recipient.externalPush(context, content.getSender()));
|
|
} catch (StorageFailedException e) {
|
|
warn(String.valueOf(content.getTimestamp()), e);
|
|
handleCorruptMessage(e.getSender(), e.getSenderDevice(), timestamp, smsMessageId);
|
|
} catch (BadGroupIdException e) {
|
|
warn(String.valueOf(content.getTimestamp()), "Ignoring message with bad group id", e);
|
|
}
|
|
}
|
|
|
|
private void handlePayment(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
|
if (!message.getPayment().isPresent()) {
|
|
throw new AssertionError();
|
|
}
|
|
|
|
if (!message.getPayment().get().getPaymentNotification().isPresent()) {
|
|
Log.w(TAG, "Ignoring payment message without notification");
|
|
return;
|
|
}
|
|
|
|
SignalServiceDataMessage.PaymentNotification paymentNotification = message.getPayment().get().getPaymentNotification().get();
|
|
PaymentDatabase paymentDatabase = DatabaseFactory.getPaymentDatabase(context);
|
|
UUID uuid = UUID.randomUUID();
|
|
Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
|
|
String queue = "Payment_" + PushProcessMessageJob.getQueueName(recipient.getId());
|
|
|
|
try {
|
|
paymentDatabase.createIncomingPayment(uuid,
|
|
recipient.getId(),
|
|
message.getTimestamp(),
|
|
paymentNotification.getNote(),
|
|
Money.MobileCoin.ZERO,
|
|
Money.MobileCoin.ZERO,
|
|
paymentNotification.getReceipt());
|
|
} catch (PaymentDatabase.PublicKeyConflictException e) {
|
|
Log.w(TAG, "Ignoring payment with public key already in database");
|
|
return;
|
|
}
|
|
|
|
ApplicationDependencies.getJobManager()
|
|
.startChain(new PaymentTransactionCheckJob(uuid, queue))
|
|
.then(PaymentLedgerUpdateJob.updateLedger())
|
|
.enqueue();
|
|
}
|
|
|
|
private static @Nullable
|
|
SignalServiceGroupContext getGroupContextIfPresent(@NonNull SignalServiceContent content) {
|
|
if (content.getDataMessage().isPresent() && content.getDataMessage().get().getGroupContext().isPresent()) {
|
|
return content.getDataMessage().get().getGroupContext().get();
|
|
} else if (content.getSyncMessage().isPresent() &&
|
|
content.getSyncMessage().get().getSent().isPresent() &&
|
|
content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().isPresent())
|
|
{
|
|
return content.getSyncMessage().get().getSent().get().getMessage().getGroupContext().get();
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
GroupManager.updateGroupFromServer(context, groupV2.getMasterKey(), groupV2.getRevision(), content.getTimestamp(), 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 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.fromNullable(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(Recipient.external(context, e.sender).getId(), e.senderDevice, timestamp));
|
|
break;
|
|
|
|
default:
|
|
throw new AssertionError("Not handled " + messageState + ". (" + timestamp + ")");
|
|
}
|
|
}
|
|
|
|
private void handleCallOfferMessage(@NonNull SignalServiceContent content,
|
|
@NonNull OfferMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
log(String.valueOf(content.getTimestamp()), "handleCallOfferMessage...");
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
database.markAsMissedCall(smsMessageId.get(), message.getType() == OfferMessage.Type.VIDEO_CALL);
|
|
} else {
|
|
Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
|
|
RemotePeer remotePeer = new RemotePeer(recipient.getId());
|
|
byte[] remoteIdentityKey = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
|
|
|
|
ApplicationDependencies.getSignalCallManager()
|
|
.receivedOffer(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), 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)
|
|
{
|
|
log(String.valueOf(content), "handleCallAnswerMessage...");
|
|
Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
|
|
RemotePeer remotePeer = new RemotePeer(recipient.getId());
|
|
byte[] remoteIdentityKey = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId()).transform(record -> record.getIdentityKey().serialize()).orNull();
|
|
|
|
ApplicationDependencies.getSignalCallManager()
|
|
.receivedAnswer(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), 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)
|
|
{
|
|
log(String.valueOf(content), "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(Recipient.externalHighTrustPush(context, content.getSender()).getId());
|
|
|
|
ApplicationDependencies.getSignalCallManager()
|
|
.receivedIceCandidates(new WebRtcData.CallMetadata(remotePeer, new CallId(callId), content.getSenderDevice()),
|
|
iceCandidates);
|
|
}
|
|
|
|
private void handleCallHangupMessage(@NonNull SignalServiceContent content,
|
|
@NonNull HangupMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
log(String.valueOf(content), "handleCallHangupMessage");
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).markAsMissedCall(smsMessageId.get(), false);
|
|
} else {
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
|
|
|
|
ApplicationDependencies.getSignalCallManager()
|
|
.receivedCallHangup(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), content.getSenderDevice()),
|
|
new WebRtcData.HangupMetadata(message.getType(), message.isLegacy(), message.getDeviceId()));
|
|
}
|
|
}
|
|
|
|
private void handleCallBusyMessage(@NonNull SignalServiceContent content,
|
|
@NonNull BusyMessage message)
|
|
{
|
|
log(String.valueOf(content.getTimestamp()), "handleCallBusyMessage");
|
|
|
|
RemotePeer remotePeer = new RemotePeer(Recipient.externalHighTrustPush(context, content.getSender()).getId());
|
|
|
|
ApplicationDependencies.getSignalCallManager()
|
|
.receivedCallBusy(new WebRtcData.CallMetadata(remotePeer, new CallId(message.getId()), content.getSenderDevice()));
|
|
}
|
|
|
|
private void handleCallOpaqueMessage(@NonNull SignalServiceContent content,
|
|
@NonNull OpaqueMessage message)
|
|
{
|
|
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(Recipient.externalHighTrustPush(context, content.getSender()).requireUuid(),
|
|
message.getOpaque(),
|
|
content.getSenderDevice(),
|
|
messageAgeSeconds));
|
|
}
|
|
|
|
private void handleGroupCallUpdateMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<GroupId> groupId)
|
|
{
|
|
if (!groupId.isPresent() || !groupId.get().isV2()) {
|
|
Log.w(TAG, "Invalid group for group call update message");
|
|
return;
|
|
}
|
|
|
|
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(groupId.get());
|
|
|
|
DatabaseFactory.getSmsDatabase(context).insertOrUpdateGroupCall(groupRecipientId,
|
|
RecipientId.from(content.getSender()),
|
|
content.getServerReceivedTimestamp(),
|
|
message.getGroupCallUpdate().get().getEraId());
|
|
|
|
GroupCallPeekJob.enqueue(groupRecipientId);
|
|
}
|
|
|
|
private void handleEndSessionMessage(@NonNull SignalServiceContent content,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(Recipient.externalHighTrustPush(context, content.getSender()).getId(),
|
|
content.getSenderDevice(),
|
|
content.getTimestamp(),
|
|
content.getServerReceivedTimestamp(),
|
|
"", Optional.absent(), 0,
|
|
content.isNeedsReceipt());
|
|
|
|
Long threadId;
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
|
|
Optional<InsertResult> insertResult = smsDatabase.insertMessageInbox(incomingEndSessionMessage);
|
|
|
|
if (insertResult.isPresent()) threadId = insertResult.get().getThreadId();
|
|
else threadId = null;
|
|
} else {
|
|
smsDatabase.markAsEndSession(smsMessageId.get());
|
|
threadId = smsDatabase.getThreadIdForMessage(smsMessageId.get());
|
|
}
|
|
|
|
if (threadId != null) {
|
|
SessionStore sessionStore = new TextSecureSessionStore(context);
|
|
sessionStore.deleteAllSessions(content.getSender().getIdentifier());
|
|
|
|
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId);
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
|
|
throws BadGroupIdException
|
|
{
|
|
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1);
|
|
OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage);
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
|
|
if (!recipient.isGroup()) {
|
|
SessionStore sessionStore = new TextSecureSessionStore(context);
|
|
sessionStore.deleteAllSessions(recipient.requireServiceId());
|
|
|
|
SecurityEvent.broadcastSecurityUpdateEvent(context);
|
|
|
|
long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage,
|
|
false, message.getTimestamp(),
|
|
null);
|
|
database.markAsSent(messageId, true);
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private void handleGroupV1Message(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId,
|
|
@NonNull GroupId.V1 groupId)
|
|
throws StorageFailedException, BadGroupIdException
|
|
{
|
|
GroupV1MessageProcessor.process(context, content, message, false);
|
|
|
|
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) {
|
|
handleExpirationUpdate(content, message, Optional.absent(), Optional.of(groupId));
|
|
}
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleUnknownGroupMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceGroupContext group)
|
|
throws BadGroupIdException
|
|
{
|
|
if (group.getGroupV1().isPresent()) {
|
|
SignalServiceGroup groupV1 = group.getGroupV1().get();
|
|
if (groupV1.getType() != SignalServiceGroup.Type.REQUEST_INFO) {
|
|
ApplicationDependencies.getJobManager().add(new RequestGroupInfoJob(Recipient.externalHighTrustPush(context, content.getSender()).getId(), GroupId.v1(groupV1.getGroupId())));
|
|
} else {
|
|
warn(String.valueOf(content.getTimestamp()), "Received a REQUEST_INFO message for a group we don't know about. Ignoring.");
|
|
}
|
|
} else {
|
|
warn(String.valueOf(content.getTimestamp()), "Received a message for a group we don't know about without a GV1 context. Ignoring.");
|
|
}
|
|
}
|
|
|
|
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId,
|
|
@NonNull Optional<GroupId> groupId)
|
|
throws StorageFailedException, BadGroupIdException
|
|
{
|
|
if (groupId.isPresent() && groupId.get().isV2()) {
|
|
warn(String.valueOf(content.getTimestamp()), "Expiration update received for GV2. Ignoring.");
|
|
return;
|
|
}
|
|
|
|
int expiresInSeconds = message.getExpiresInSeconds();
|
|
Optional<SignalServiceGroupContext> groupContext = message.getGroupContext();
|
|
Recipient recipient = getMessageDestination(content, groupContext);
|
|
|
|
if (recipient.getExpireMessages() == expiresInSeconds) {
|
|
log(String.valueOf(content.getTimestamp()), "No change in message expiry for group. Ignoring.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
|
|
content.getTimestamp(),
|
|
content.getServerReceivedTimestamp(),
|
|
-1,
|
|
expiresInSeconds * 1000L,
|
|
true,
|
|
false,
|
|
content.isNeedsReceipt(),
|
|
Optional.absent(),
|
|
groupContext,
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent(),
|
|
Optional.absent());
|
|
|
|
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
|
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), expiresInSeconds);
|
|
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
|
|
}
|
|
} catch (MmsException e) {
|
|
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
|
}
|
|
}
|
|
|
|
private void handleReaction(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
|
SignalServiceDataMessage.Reaction reaction = message.getReaction().get();
|
|
|
|
if (!EmojiUtil.isEmoji(context, reaction.getEmoji())) {
|
|
Log.w(TAG, "Reaction text is not a valid emoji! Ignoring the message.");
|
|
return;
|
|
}
|
|
|
|
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
|
|
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
|
|
|
|
if (targetMessage != null && !targetMessage.isRemoteDelete()) {
|
|
Recipient reactionAuthor = Recipient.externalHighTrustPush(context, content.getSender());
|
|
MessageDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (reaction.isRemove()) {
|
|
db.deleteReaction(targetMessage.getId(), reactionAuthor.getId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context);
|
|
} else {
|
|
ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), reactionAuthor.getId(), message.getTimestamp(), System.currentTimeMillis());
|
|
db.addReaction(targetMessage.getId(), reactionRecord);
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false);
|
|
}
|
|
} else if (targetMessage != null) {
|
|
warn(String.valueOf(content.getTimestamp()), "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
|
} else {
|
|
warn(String.valueOf(content.getTimestamp()), "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
|
ApplicationDependencies.getEarlyMessageCache().store(targetAuthor.getId(), reaction.getTargetSentTimestamp(), content);
|
|
}
|
|
}
|
|
|
|
private void handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
|
SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get();
|
|
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(delete.getTargetSentTimestamp(), sender.getId());
|
|
|
|
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerReceivedTimestamp())) {
|
|
MessageDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
|
db.markAsRemoteDelete(targetMessage.getId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false);
|
|
} else if (targetMessage == null) {
|
|
warn(String.valueOf(content.getTimestamp()), "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + sender.getId());
|
|
ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), delete.getTargetSentTimestamp(), content);
|
|
} 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(), sender.getId(), targetMessage.getRecipient().getId()));
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
|
|
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
|
|
}
|
|
|
|
private void handleSynchronizeStickerPackOperation(@NonNull List<StickerPackOperationMessage> stickerPackOperations) {
|
|
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:
|
|
DatabaseFactory.getStickerDatabase(context).uninstallPack(packId);
|
|
break;
|
|
}
|
|
} else {
|
|
warn("Received incomplete sticker pack operation sync.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeConfigurationMessage(@NonNull ConfigurationMessage configurationMessage) {
|
|
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) {
|
|
DatabaseFactory.getRecipientDatabase(context).applyBlockedUpdate(blockMessage.getAddresses(), blockMessage.getGroupIds());
|
|
}
|
|
|
|
private void handleSynchronizeFetchMessage(@NonNull SignalServiceSyncMessage.FetchType fetchType) {
|
|
log("Received fetch request with type: " + fetchType);
|
|
|
|
switch (fetchType) {
|
|
case LOCAL_PROFILE:
|
|
ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
|
break;
|
|
case STORAGE_MANIFEST:
|
|
StorageSyncHelper.scheduleSyncForDataChange();
|
|
break;
|
|
default:
|
|
Log.w(TAG, "Received a fetch message for an unknown type.");
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeMessageRequestResponse(@NonNull MessageRequestResponseMessage response)
|
|
throws BadGroupIdException
|
|
{
|
|
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
|
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
|
|
|
Recipient recipient;
|
|
|
|
if (response.getPerson().isPresent()) {
|
|
recipient = Recipient.externalPush(context, response.getPerson().get());
|
|
} else if (response.getGroupId().isPresent()) {
|
|
GroupId groupId = GroupId.v1(response.getGroupId().get());
|
|
recipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
|
|
} else {
|
|
warn("Message request response was missing a thread recipient! Skipping.");
|
|
return;
|
|
}
|
|
|
|
long threadId = threadDatabase.getThreadIdFor(recipient);
|
|
|
|
switch (response.getType()) {
|
|
case ACCEPT:
|
|
recipientDatabase.setProfileSharing(recipient.getId(), true);
|
|
recipientDatabase.setBlocked(recipient.getId(), false);
|
|
break;
|
|
case DELETE:
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
|
break;
|
|
case BLOCK:
|
|
recipientDatabase.setBlocked(recipient.getId(), true);
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
break;
|
|
case BLOCK_AND_DELETE:
|
|
recipientDatabase.setBlocked(recipient.getId(), true);
|
|
recipientDatabase.setProfileSharing(recipient.getId(), false);
|
|
if (threadId > 0) threadDatabase.deleteConversation(threadId);
|
|
break;
|
|
default:
|
|
warn("Got an unknown response type! Skipping");
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeOutgoingPayment(@NonNull OutgoingPaymentMessage outgoingPaymentMessage) {
|
|
RecipientId recipientId = outgoingPaymentMessage.getRecipient()
|
|
.transform(uuid -> RecipientId.from(uuid, null))
|
|
.orNull();
|
|
long timestamp = outgoingPaymentMessage.getBlockTimestamp();
|
|
if (timestamp == 0) {
|
|
timestamp = System.currentTimeMillis();
|
|
}
|
|
|
|
Optional<MobileCoinPublicAddress> address = outgoingPaymentMessage.getAddress().transform(MobileCoinPublicAddress::fromBytes);
|
|
if (!address.isPresent() && recipientId == null) {
|
|
Log.i(TAG, "Inserting defrag");
|
|
address = Optional.of(ApplicationDependencies.getPayments().getWallet().getMobileCoinPublicAddress());
|
|
recipientId = Recipient.self().getId();
|
|
}
|
|
|
|
UUID uuid = UUID.randomUUID();
|
|
DatabaseFactory.getPaymentDatabase(context)
|
|
.createSuccessfulPayment(uuid,
|
|
recipientId,
|
|
address.get(),
|
|
timestamp,
|
|
outgoingPaymentMessage.getBlockIndex(),
|
|
outgoingPaymentMessage.getNote().or(""),
|
|
outgoingPaymentMessage.getAmount(),
|
|
outgoingPaymentMessage.getFee(),
|
|
outgoingPaymentMessage.getReceipt().toByteArray(),
|
|
PaymentMetaDataUtil.fromKeysAndImages(outgoingPaymentMessage.getPublicKeys(), outgoingPaymentMessage.getKeyImages()));
|
|
|
|
log("Inserted synchronized payment " + uuid);
|
|
}
|
|
|
|
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SentTranscriptMessage message)
|
|
throws StorageFailedException, BadGroupIdException, IOException, GroupChangeBusyException
|
|
{
|
|
log(String.valueOf(content.getTimestamp()), "Processing sent transcript for message with ID " + message.getTimestamp());
|
|
|
|
try {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
|
|
if (message.getMessage().isGroupV2Message()) {
|
|
Optional<GroupDatabase.GroupRecord> possibleGv1 = groupDatabase.getGroupV1ByExpectedV2(GroupId.v2(message.getMessage().getGroupContext().get().getGroupV2().get().getMasterKey()));
|
|
if (possibleGv1.isPresent()) {
|
|
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().getId().requireV1());
|
|
}
|
|
}
|
|
|
|
long threadId = -1;
|
|
|
|
if (message.isRecipientUpdate()) {
|
|
handleGroupRecipientUpdate(message);
|
|
} else if (message.getMessage().isEndSession()) {
|
|
threadId = handleSynchronizeSentEndSessionMessage(message);
|
|
} else if (message.getMessage().isGroupV1Update()) {
|
|
Long gv1ThreadId = GroupV1MessageProcessor.process(context, content, message.getMessage(), true);
|
|
threadId = gv1ThreadId == null ? -1 : gv1ThreadId;
|
|
} else if (message.getMessage().isGroupV2Update()) {
|
|
handleSynchronizeSentGv2Update(content, message);
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
|
} else if (Build.VERSION.SDK_INT > 19 && message.getMessage().getGroupCallUpdate().isPresent()) {
|
|
handleGroupCallUpdateMessage(content, message.getMessage(), GroupUtil.idFromGroupContext(message.getMessage().getGroupContext()));
|
|
} else if (message.getMessage().isEmptyGroupV2Message()) {
|
|
// Do nothing
|
|
} else if (message.getMessage().isExpirationUpdate()) {
|
|
threadId = handleSynchronizeSentExpirationUpdate(message);
|
|
} else if (message.getMessage().getReaction().isPresent()) {
|
|
handleReaction(content, message.getMessage());
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
|
} else if (message.getMessage().getRemoteDelete().isPresent()) {
|
|
handleRemoteDelete(content, message.getMessage());
|
|
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce() || message.getMessage().getMentions().isPresent()) {
|
|
threadId = handleSynchronizeSentMediaMessage(message);
|
|
} else {
|
|
threadId = handleSynchronizeSentTextMessage(message);
|
|
}
|
|
|
|
if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
|
|
handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get());
|
|
}
|
|
|
|
if (message.getMessage().getProfileKey().isPresent()) {
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
|
|
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient.getId(), true);
|
|
}
|
|
}
|
|
|
|
if (threadId != -1) {
|
|
DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(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
|
|
{
|
|
SignalServiceGroupV2 signalServiceGroupV2 = message.getMessage().getGroupContext().get().getGroupV2().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)
|
|
{
|
|
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());
|
|
}
|
|
}
|
|
|
|
private void handleSynchronizeReadMessage(@NonNull List<ReadMessage> readMessages, long envelopeTimestamp)
|
|
{
|
|
for (ReadMessage readMessage : readMessages) {
|
|
List<Pair<Long, Long>> expiringText = DatabaseFactory.getSmsDatabase(context).setTimestampRead(new SyncMessageId(Recipient.externalPush(context, readMessage.getSender()).getId(), readMessage.getTimestamp()), envelopeTimestamp);
|
|
List<Pair<Long, Long>> expiringMedia = DatabaseFactory.getMmsDatabase(context).setTimestampRead(new SyncMessageId(Recipient.externalPush(context, readMessage.getSender()).getId(), readMessage.getTimestamp()), envelopeTimestamp);
|
|
|
|
for (Pair<Long, Long> expiringMessage : expiringText) {
|
|
ApplicationDependencies.getExpiringMessageManager()
|
|
.scheduleDeletion(expiringMessage.first(), false, envelopeTimestamp, expiringMessage.second());
|
|
}
|
|
|
|
for (Pair<Long, Long> expiringMessage : expiringMedia) {
|
|
ApplicationDependencies.getExpiringMessageManager()
|
|
.scheduleDeletion(expiringMessage.first(), true, envelopeTimestamp, expiringMessage.second());
|
|
}
|
|
}
|
|
|
|
MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier();
|
|
messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
|
|
messageNotifier.cancelDelayedNotifications();
|
|
messageNotifier.updateNotification(context);
|
|
}
|
|
|
|
private void handleSynchronizeViewOnceOpenMessage(@NonNull ViewOnceOpenMessage openMessage, long envelopeTimestamp) {
|
|
log(String.valueOf(envelopeTimestamp), "Handling a view-once open for message: " + openMessage.getTimestamp());
|
|
|
|
RecipientId author = Recipient.externalPush(context, openMessage.getSender()).getId();
|
|
long timestamp = openMessage.getTimestamp();
|
|
MessageRecord record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(timestamp, author);
|
|
|
|
if (record != null && record.isMms()) {
|
|
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentFilesForViewOnceMessage(record.getId());
|
|
} else {
|
|
warn(String.valueOf(envelopeTimestamp), "Got a view-once open message for a message we don't have!");
|
|
}
|
|
|
|
MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier();
|
|
messageNotifier.setLastDesktopActivityTimestamp(envelopeTimestamp);
|
|
messageNotifier.cancelDelayedNotifications();
|
|
messageNotifier.updateNotification(context);
|
|
}
|
|
|
|
private void handleMediaMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
throws StorageFailedException, BadGroupIdException
|
|
{
|
|
notifyTypingStoppedFromIncomingMessage(getMessageDestination(content, message), content.getSender(), content.getSenderDevice());
|
|
|
|
Optional<InsertResult> insertResult;
|
|
|
|
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
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().or(""));
|
|
Optional<List<Mention>> mentions = getMentions(message.getMentions());
|
|
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
|
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(RecipientId.fromHighTrust(content.getSender()),
|
|
message.getTimestamp(),
|
|
content.getServerReceivedTimestamp(),
|
|
-1,
|
|
message.getExpiresInSeconds() * 1000L,
|
|
false,
|
|
message.isViewOnce(),
|
|
content.isNeedsReceipt(),
|
|
message.getBody(),
|
|
message.getGroupContext(),
|
|
message.getAttachments(),
|
|
quote,
|
|
sharedContacts,
|
|
linkPreviews,
|
|
mentions,
|
|
sticker);
|
|
|
|
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
|
|
|
if (insertResult.isPresent()) {
|
|
if (smsMessageId.isPresent()) {
|
|
DatabaseFactory.getSmsDatabase(context).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 = DatabaseFactory.getAttachmentDatabase(context).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, insertResult.get().getThreadId());
|
|
ApplicationDependencies.getJobManager().add(new TrimThreadJob(insertResult.get().getThreadId()));
|
|
|
|
if (message.isViewOnce()) {
|
|
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
|
}
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message)
|
|
throws MmsException, BadGroupIdException
|
|
{
|
|
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
OutgoingExpirationUpdateMessage expirationUpdateMessage = new OutgoingExpirationUpdateMessage(recipient,
|
|
message.getTimestamp(),
|
|
message.getMessage().getExpiresInSeconds() * 1000L);
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
long messageId = database.insertMessageOutbox(expirationUpdateMessage, threadId, false, null);
|
|
|
|
database.markAsSent(messageId, true);
|
|
|
|
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), message.getMessage().getExpiresInSeconds());
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private long handleSynchronizeSentMediaMessage(@NonNull SentTranscriptMessage message)
|
|
throws MmsException, BadGroupIdException
|
|
{
|
|
MessageDatabase database = DatabaseFactory.getMmsDatabase(context);
|
|
Recipient recipients = getSyncMessageDestination(message);
|
|
Optional<QuoteModel> quote = getValidatedQuote(message.getMessage().getQuote());
|
|
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
|
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
|
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
|
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions());
|
|
boolean viewOnce = message.getMessage().isViewOnce();
|
|
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
|
: PointerAttachment.forPointers(message.getMessage().getAttachments());
|
|
|
|
if (sticker.isPresent()) {
|
|
syncAttachments.add(sticker.get());
|
|
}
|
|
|
|
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(recipients, message.getMessage().getBody().orNull(),
|
|
syncAttachments,
|
|
message.getTimestamp(), -1,
|
|
message.getMessage().getExpiresInSeconds() * 1000,
|
|
viewOnce,
|
|
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
|
sharedContacts.or(Collections.emptyList()),
|
|
previews.or(Collections.emptyList()),
|
|
mentions.or(Collections.emptyList()),
|
|
Collections.emptyList(), Collections.emptyList());
|
|
|
|
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
|
|
|
if (recipients.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
|
|
handleSynchronizeSentExpirationUpdate(message);
|
|
}
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipients);
|
|
|
|
database.beginTransaction();
|
|
|
|
try {
|
|
long messageId = database.insertMessageOutbox(mediaMessage, threadId, false, GroupReceiptDatabase.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 = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId);
|
|
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
|
|
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
|
|
|
|
forceStickerDownloadIfNecessary(messageId, stickerAttachments);
|
|
|
|
for (DatabaseAttachment attachment : attachments) {
|
|
ApplicationDependencies.getJobManager().add(new AttachmentDownloadJob(messageId, attachment.getAttachmentId(), false));
|
|
}
|
|
|
|
if (message.getMessage().getExpiresInSeconds() > 0) {
|
|
database.markExpireStarted(messageId, message.getExpirationStartTimestamp());
|
|
ApplicationDependencies.getExpiringMessageManager()
|
|
.scheduleDeletion(messageId,
|
|
true,
|
|
message.getExpirationStartTimestamp(),
|
|
message.getMessage().getExpiresInSeconds() * 1000L);
|
|
}
|
|
|
|
if (recipients.isSelf()) {
|
|
SyncMessageId id = new SyncMessageId(recipients.getId(), message.getTimestamp());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
|
}
|
|
|
|
database.setTransactionSuccessful();
|
|
} finally {
|
|
database.endTransaction();
|
|
}
|
|
return threadId;
|
|
}
|
|
|
|
private void handleGroupRecipientUpdate(@NonNull SentTranscriptMessage message)
|
|
throws BadGroupIdException
|
|
{
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
|
|
if (!recipient.isGroup()) {
|
|
warn("Got recipient update for a non-group message! Skipping.");
|
|
return;
|
|
}
|
|
|
|
MmsSmsDatabase database = DatabaseFactory.getMmsSmsDatabase(context);
|
|
MessageRecord record = database.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) {
|
|
GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
|
|
List<Recipient> messageRecipients = Stream.of(message.getRecipients()).map(address -> Recipient.externalPush(context, address)).toList();
|
|
List<Recipient> members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupString, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
|
|
Map<RecipientId, Integer> localReceipts = Stream.of(receiptDatabase.getGroupReceiptInfo(messageId))
|
|
.collect(Collectors.toMap(GroupReceiptInfo::getRecipientId, GroupReceiptInfo::getStatus));
|
|
|
|
for (Recipient messageRecipient : messageRecipients) {
|
|
//noinspection ConstantConditions
|
|
if (localReceipts.containsKey(messageRecipient.getId()) && localReceipts.get(messageRecipient.getId()) < GroupReceiptDatabase.STATUS_UNDELIVERED) {
|
|
receiptDatabase.update(messageRecipient.getId(), messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getTimestamp());
|
|
} else if (!localReceipts.containsKey(messageRecipient.getId())) {
|
|
receiptDatabase.insert(Collections.singletonList(messageRecipient.getId()), messageId, GroupReceiptDatabase.STATUS_UNDELIVERED, message.getTimestamp());
|
|
}
|
|
}
|
|
|
|
List<org.whispersystems.libsignal.util.Pair<RecipientId, Boolean>> unidentifiedStatus = Stream.of(members)
|
|
.map(m -> new org.whispersystems.libsignal.util.Pair<>(m.getId(), message.isUnidentified(m.requireServiceId())))
|
|
.toList();
|
|
receiptDatabase.setUnidentified(unidentifiedStatus, messageId);
|
|
}
|
|
|
|
private void handleTextMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message,
|
|
@NonNull Optional<Long> smsMessageId,
|
|
@NonNull Optional<GroupId> groupId)
|
|
throws StorageFailedException, BadGroupIdException
|
|
{
|
|
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
String body = message.getBody().isPresent() ? message.getBody().get() : "";
|
|
Recipient recipient = getMessageDestination(content, message);
|
|
|
|
if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
|
|
handleExpirationUpdate(content, message, Optional.absent(), groupId);
|
|
}
|
|
|
|
Long threadId;
|
|
|
|
if (smsMessageId.isPresent() && !message.getGroupContext().isPresent()) {
|
|
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second();
|
|
} else {
|
|
notifyTypingStoppedFromIncomingMessage(recipient, content.getSender(), content.getSenderDevice());
|
|
|
|
IncomingTextMessage textMessage = new IncomingTextMessage(RecipientId.fromHighTrust(content.getSender()),
|
|
content.getSenderDevice(),
|
|
message.getTimestamp(),
|
|
content.getServerReceivedTimestamp(),
|
|
body,
|
|
groupId,
|
|
message.getExpiresInSeconds() * 1000L,
|
|
content.isNeedsReceipt());
|
|
|
|
textMessage = new IncomingEncryptedMessage(textMessage, body);
|
|
Optional<InsertResult> insertResult = database.insertMessageInbox(textMessage);
|
|
|
|
if (insertResult.isPresent()) threadId = insertResult.get().getThreadId();
|
|
else threadId = null;
|
|
|
|
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
|
|
}
|
|
|
|
if (threadId != null) {
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, threadId);
|
|
}
|
|
}
|
|
|
|
private long handleSynchronizeSentTextMessage(@NonNull SentTranscriptMessage message)
|
|
throws MmsException, BadGroupIdException
|
|
{
|
|
Recipient recipient = getSyncMessageDestination(message);
|
|
String body = message.getMessage().getBody().or("");
|
|
long expiresInMillis = message.getMessage().getExpiresInSeconds() * 1000L;
|
|
|
|
if (recipient.getExpireMessages() != message.getMessage().getExpiresInSeconds()) {
|
|
handleSynchronizeSentExpirationUpdate(message);
|
|
}
|
|
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
|
boolean isGroup = recipient.isGroup();
|
|
|
|
MessageDatabase database;
|
|
long messageId;
|
|
|
|
if (isGroup) {
|
|
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
|
new SlideDeck(),
|
|
body,
|
|
message.getTimestamp(),
|
|
-1,
|
|
expiresInMillis,
|
|
false,
|
|
ThreadDatabase.DistributionTypes.DEFAULT,
|
|
null,
|
|
Collections.emptyList(),
|
|
Collections.emptyList(),
|
|
Collections.emptyList());
|
|
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
|
|
|
|
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
|
|
database = DatabaseFactory.getMmsDatabase(context);
|
|
|
|
updateGroupReceiptStatus(message, messageId, recipient.requireGroupId());
|
|
} else {
|
|
OutgoingTextMessage outgoingTextMessage = new OutgoingEncryptedMessage(recipient, body, expiresInMillis);
|
|
|
|
messageId = DatabaseFactory.getSmsDatabase(context).insertMessageOutbox(threadId, outgoingTextMessage, false, message.getTimestamp(), null);
|
|
database = DatabaseFactory.getSmsDatabase(context);
|
|
database.markUnidentified(messageId, isUnidentified(message, recipient));
|
|
}
|
|
|
|
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());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCount(id, System.currentTimeMillis());
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCount(id, System.currentTimeMillis());
|
|
}
|
|
|
|
return threadId;
|
|
}
|
|
|
|
private void handleInvalidVersionMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsInvalidVersionKeyExchange(insertResult.get().getMessageId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsDecryptFailed(insertResult.get().getMessageId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, 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)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp, groupId);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsUnsupportedProtocolVersion(insertResult.get().getMessageId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, 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)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender.getIdentifier(), senderDevice, timestamp, groupId);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsInvalidMessage(insertResult.get().getMessageId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsNoSession(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
|
|
@NonNull Optional<Long> smsMessageId)
|
|
{
|
|
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
|
|
|
if (!smsMessageId.isPresent()) {
|
|
Optional<InsertResult> insertResult = insertPlaceholder(sender, senderDevice, timestamp);
|
|
|
|
if (insertResult.isPresent()) {
|
|
smsDatabase.markAsLegacyVersion(insertResult.get().getMessageId());
|
|
ApplicationDependencies.getMessageNotifier().updateNotification(context, insertResult.get().getThreadId());
|
|
}
|
|
} else {
|
|
smsDatabase.markAsLegacyVersion(smsMessageId.get());
|
|
}
|
|
}
|
|
|
|
private void handleProfileKey(@NonNull SignalServiceContent content,
|
|
@NonNull byte[] messageProfileKeyBytes)
|
|
{
|
|
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
|
Recipient recipient = Recipient.externalHighTrustPush(context, content.getSender());
|
|
ProfileKey messageProfileKey = ProfileKeyUtil.profileKeyOrNull(messageProfileKeyBytes);
|
|
|
|
if (messageProfileKey != null) {
|
|
if (database.setProfileKey(recipient.getId(), messageProfileKey)) {
|
|
ApplicationDependencies.getJobManager().add(RetrieveProfileJob.forRecipient(recipient.getId()));
|
|
}
|
|
} else {
|
|
warn(String.valueOf(content.getTimestamp()), "Ignored invalid profile key seen in message");
|
|
}
|
|
}
|
|
|
|
private void handleNeedsDeliveryReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message)
|
|
{
|
|
ApplicationDependencies.getJobManager().add(new SendDeliveryReceiptJob(RecipientId.fromHighTrust(content.getSender()), message.getTimestamp()));
|
|
}
|
|
|
|
private void handleViewedReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceReceiptMessage message)
|
|
{
|
|
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
|
log("Ignoring viewed receipts for IDs: " + Util.join(message.getTimestamps(), ", "));
|
|
return;
|
|
}
|
|
|
|
log("Processing viewed reciepts for IDs: " + Util.join(message.getTimestamps(), ","));
|
|
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
List<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(sender.getId(), t))
|
|
.toList();
|
|
Collection<SyncMessageId> unhandled = DatabaseFactory.getMmsSmsDatabase(context)
|
|
.incrementViewedReceiptCounts(ids, content.getTimestamp());
|
|
|
|
for (SyncMessageId id : unhandled) {
|
|
warn(String.valueOf(content.getTimestamp()), "[handleViewedReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + sender.getId());
|
|
ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), id.getTimetamp(), content);
|
|
}
|
|
}
|
|
|
|
@SuppressLint("DefaultLocale")
|
|
private void handleDeliveryReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceReceiptMessage message)
|
|
{
|
|
log(TAG, "Processing delivery receipts for IDs: " + Util.join(message.getTimestamps(), ", "));
|
|
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
List<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(sender.getId(), t))
|
|
.toList();
|
|
|
|
DatabaseFactory.getMmsSmsDatabase(context).incrementDeliveryReceiptCounts(ids, System.currentTimeMillis());
|
|
}
|
|
|
|
@SuppressLint("DefaultLocale")
|
|
private void handleReadReceipt(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceReceiptMessage message)
|
|
{
|
|
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
|
log("Ignoring read receipts for IDs: " + Util.join(message.getTimestamps(), ", "));
|
|
return;
|
|
}
|
|
|
|
log("Processing read receipts for IDs: " + Util.join(message.getTimestamps(), ", "));
|
|
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
List<SyncMessageId> ids = Stream.of(message.getTimestamps())
|
|
.map(t -> new SyncMessageId(sender.getId(), t))
|
|
.toList();
|
|
|
|
Collection<SyncMessageId> unhandled = DatabaseFactory.getMmsSmsDatabase(context).incrementReadReceiptCounts(ids, content.getTimestamp());
|
|
|
|
for (SyncMessageId id : unhandled) {
|
|
warn(String.valueOf(content.getTimestamp()), "[handleReadReceipt] Could not find matching message! timestamp: " + id.getTimetamp() + " author: " + sender.getId());
|
|
ApplicationDependencies.getEarlyMessageCache().store(sender.getId(), id.getTimetamp(), content);
|
|
}
|
|
}
|
|
|
|
private void handleTypingMessage(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceTypingMessage typingMessage)
|
|
throws BadGroupIdException
|
|
{
|
|
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
|
|
return;
|
|
}
|
|
|
|
Recipient author = Recipient.externalHighTrustPush(context, content.getSender());
|
|
|
|
long threadId;
|
|
|
|
if (typingMessage.getGroupId().isPresent()) {
|
|
GroupId.Push groupId = GroupId.push(typingMessage.getGroupId().get());
|
|
|
|
if (!DatabaseFactory.getGroupDatabase(context).isCurrentMember(groupId, author.getId())) {
|
|
warn(String.valueOf(content.getTimestamp()), "Seen typing indicator for non-member");
|
|
return;
|
|
}
|
|
|
|
Recipient groupRecipient = Recipient.externalPossiblyMigratedGroup(context, groupId);
|
|
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient);
|
|
} else {
|
|
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(author);
|
|
}
|
|
|
|
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, author, content.getSenderDevice());
|
|
} else {
|
|
Log.d(TAG, "Typing stopped on thread " + threadId);
|
|
ApplicationDependencies.getTypingStatusRepository().onTypingStopped(context, threadId, author, content.getSenderDevice(), false);
|
|
}
|
|
}
|
|
|
|
private static boolean isInvalidMessage(@NonNull SignalServiceDataMessage message) {
|
|
if (message.isViewOnce()) {
|
|
List<SignalServiceAttachment> attachments = message.getAttachments().or(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.absent();
|
|
|
|
if (quote.get().getId() <= 0) {
|
|
warn("Received quote without an ID! Ignoring...");
|
|
return Optional.absent();
|
|
}
|
|
|
|
if (quote.get().getAuthor() == null) {
|
|
warn("Received quote without an author! Ignoring...");
|
|
return Optional.absent();
|
|
}
|
|
|
|
RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId();
|
|
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).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;
|
|
|
|
mentions.addAll(DatabaseFactory.getMentionDatabase(context).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());
|
|
}
|
|
}
|
|
}
|
|
|
|
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments, mentions));
|
|
} 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())));
|
|
}
|
|
|
|
private Optional<Attachment> getStickerAttachment(Optional<SignalServiceDataMessage.Sticker> sticker) {
|
|
if (!sticker.isPresent()) {
|
|
return Optional.absent();
|
|
}
|
|
|
|
if (sticker.get().getPackId() == null || sticker.get().getPackKey() == null || sticker.get().getAttachment() == null) {
|
|
warn("Malformed sticker!");
|
|
return Optional.absent();
|
|
}
|
|
|
|
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);
|
|
StickerDatabase stickerDatabase = DatabaseFactory.getStickerDatabase(context);
|
|
StickerRecord stickerRecord = stickerDatabase.getSticker(stickerLocator.getPackId(), stickerLocator.getStickerId(), false);
|
|
|
|
if (stickerRecord != null) {
|
|
return Optional.of(new UriAttachment(stickerRecord.getUri(),
|
|
stickerRecord.getContentType(),
|
|
AttachmentDatabase.TRANSFER_PROGRESS_DONE,
|
|
stickerRecord.getSize(),
|
|
StickerSlide.WIDTH,
|
|
StickerSlide.HEIGHT,
|
|
null,
|
|
String.valueOf(new SecureRandom().nextLong()),
|
|
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.absent();
|
|
|
|
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<SignalServiceDataMessage.Preview>> previews, @NonNull String message) {
|
|
if (!previews.isPresent() || previews.get().isEmpty()) return Optional.absent();
|
|
|
|
List<LinkPreview> linkPreviews = new ArrayList<>(previews.get().size());
|
|
LinkPreviewUtil.Links urlsInMessage = LinkPreviewUtil.findValidPreviewUrls(message);
|
|
|
|
for (SignalServiceDataMessage.Preview preview : previews.get()) {
|
|
Optional<Attachment> thumbnail = PointerAttachment.forPointer(preview.getImage());
|
|
Optional<String> url = Optional.fromNullable(preview.getUrl());
|
|
Optional<String> title = Optional.fromNullable(preview.getTitle());
|
|
Optional<String> description = Optional.fromNullable(preview.getDescription());
|
|
boolean hasTitle = !TextUtils.isEmpty(title.or(""));
|
|
boolean presentInBody = url.isPresent() && urlsInMessage.containsUrl(url.get());
|
|
boolean validDomain = url.isPresent() && LinkPreviewUtil.isValidPreviewUrl(url.get());
|
|
|
|
if (hasTitle && presentInBody && validDomain) {
|
|
LinkPreview linkPreview = new LinkPreview(url.get(), title.or(""), description.or(""), preview.getDate(), thumbnail);
|
|
linkPreviews.add(linkPreview);
|
|
} else {
|
|
warn(String.format("Discarding an invalid link preview. hasTitle: %b presentInBody: %b validDomain: %b", hasTitle, presentInBody, validDomain));
|
|
}
|
|
}
|
|
|
|
return Optional.of(linkPreviews);
|
|
}
|
|
|
|
private Optional<List<Mention>> getMentions(Optional<List<SignalServiceDataMessage.Mention>> signalServiceMentions) {
|
|
if (!signalServiceMentions.isPresent()) return Optional.absent();
|
|
|
|
return Optional.of(getMentions(signalServiceMentions.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(context, mention.getUuid(), null, false).getId(), mention.getStart(), mention.getLength()));
|
|
}
|
|
|
|
return mentions;
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
|
|
return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent());
|
|
}
|
|
|
|
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp, Optional<GroupId> groupId) {
|
|
MessageDatabase database = DatabaseFactory.getSmsDatabase(context);
|
|
IncomingTextMessage textMessage = new IncomingTextMessage(Recipient.external(context, sender).getId(),
|
|
senderDevice, timestamp, -1, "",
|
|
groupId, 0, false);
|
|
|
|
textMessage = new IncomingEncryptedMessage(textMessage, "");
|
|
return database.insertMessageInbox(textMessage);
|
|
}
|
|
|
|
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
|
|
throws BadGroupIdException
|
|
{
|
|
return getGroupRecipient(message.getMessage().getGroupContext()).or(() -> Recipient.externalPush(context, message.getDestination().get()));
|
|
}
|
|
|
|
private Recipient getMessageDestination(@NonNull SignalServiceContent content,
|
|
@NonNull SignalServiceDataMessage message)
|
|
throws BadGroupIdException
|
|
{
|
|
return getGroupRecipient(message.getGroupContext()).or(() -> Recipient.externalHighTrustPush(context, content.getSender()));
|
|
}
|
|
|
|
private Recipient getMessageDestination(@NonNull SignalServiceContent content,
|
|
@NonNull Optional<SignalServiceGroupContext> groupContext)
|
|
throws BadGroupIdException
|
|
{
|
|
return getGroupRecipient(groupContext).or(() -> Recipient.externalPush(context, content.getSender()));
|
|
}
|
|
|
|
private Optional<Recipient> getGroupRecipient(Optional<SignalServiceGroupContext> message)
|
|
throws BadGroupIdException
|
|
{
|
|
if (message.isPresent()) {
|
|
return Optional.of(Recipient.externalPossiblyMigratedGroup(context, GroupUtil.idFromGroupContext(message.get())));
|
|
}
|
|
return Optional.absent();
|
|
}
|
|
|
|
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull SignalServiceAddress sender, int device) {
|
|
Recipient author = Recipient.externalPush(context, sender);
|
|
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient);
|
|
|
|
if (threadId > 0) {
|
|
Log.d(TAG, "Typing stopped on thread " + threadId + " due to an incoming message.");
|
|
ApplicationDependencies.getTypingStatusRepository().onTypingStopped(context, threadId, author, device, true);
|
|
}
|
|
}
|
|
|
|
private boolean shouldIgnore(@Nullable SignalServiceContent content)
|
|
throws BadGroupIdException
|
|
{
|
|
if (content == null) {
|
|
warn("Got a message with null content.");
|
|
return true;
|
|
}
|
|
|
|
Recipient sender = Recipient.externalHighTrustPush(context, content.getSender());
|
|
|
|
if (content.getDataMessage().isPresent()) {
|
|
SignalServiceDataMessage message = content.getDataMessage().get();
|
|
Recipient conversation = getMessageDestination(content, message);
|
|
|
|
if (conversation.isGroup() && conversation.isBlocked()) {
|
|
return true;
|
|
} else if (conversation.isGroup()) {
|
|
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
|
|
|
if (groupId.isPresent() &&
|
|
groupId.get().isV1() &&
|
|
message.isGroupV1Update() &&
|
|
groupDatabase.groupExists(groupId.get().requireV1().deriveV2MigrationGroupId()))
|
|
{
|
|
warn(String.valueOf(content.getTimestamp()), "Ignoring V1 update for a group we've already migrated to V2.");
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
boolean isExpireMessage = message.isExpirationUpdate();
|
|
boolean isGv2Update = message.isGroupV2Update();
|
|
boolean isContentMessage = !message.isGroupV1Update() && !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage);
|
|
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
|
|
boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT;
|
|
|
|
return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage && !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(context, groupId);
|
|
return groupRecipient.isBlocked() || !groupRecipient.isActiveGroup();
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void resetRecipientToPush(@NonNull Recipient recipient) {
|
|
if (recipient.isForceSmsSelection()) {
|
|
DatabaseFactory.getRecipientDatabase(context).setForceSmsSelection(recipient.getId(), false);
|
|
}
|
|
}
|
|
|
|
private void forceStickerDownloadIfNecessary(long messageId, List<DatabaseAttachment> stickerAttachments) {
|
|
if (stickerAttachments.isEmpty()) return;
|
|
|
|
DatabaseAttachment stickerAttachment = stickerAttachments.get(0);
|
|
|
|
if (stickerAttachment.getTransferState() != AttachmentDatabase.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) {
|
|
boolean unidentified = false;
|
|
|
|
if (recipient.hasE164()) {
|
|
unidentified |= message.isUnidentified(recipient.requireE164());
|
|
}
|
|
if (recipient.hasUuid()) {
|
|
unidentified |= message.isUnidentified(recipient.requireUuid());
|
|
}
|
|
|
|
return unidentified;
|
|
}
|
|
|
|
protected void log(@NonNull String message) {
|
|
Log.i(TAG, message);
|
|
}
|
|
|
|
protected void log(@NonNull String extra, @NonNull String message) {
|
|
String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] ";
|
|
Log.i(TAG, extraLog + message);
|
|
}
|
|
|
|
protected void warn(@NonNull String message) {
|
|
warn("", message, null);
|
|
}
|
|
|
|
protected void warn(@NonNull String extra, @NonNull String message) {
|
|
warn(extra, message, null);
|
|
}
|
|
|
|
protected void warn(@NonNull String message, @Nullable Throwable t) {
|
|
warn("", message, t);
|
|
}
|
|
|
|
protected void warn(@NonNull String extra, @NonNull String message, @Nullable Throwable t) {
|
|
String extraLog = Util.isEmpty(extra) ? "" : "[" + extra + "] ";
|
|
Log.w(TAG, extraLog + message, t);
|
|
}
|
|
|
|
@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
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|