Add kotlin/proto level message processing.

This commit is contained in:
Cody Henthorne
2023-03-30 11:45:13 -04:00
committed by Alex Hart
parent 28f27915c5
commit 2e45bd719a
43 changed files with 4505 additions and 84 deletions

View File

@@ -9,8 +9,11 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.LinkedList;
import java.util.List;
@@ -145,4 +148,33 @@ public class PointerAttachment extends Attachment {
null,
null));
}
public static Optional<Attachment> forPointer(SignalServiceProtos.DataMessage.Quote.QuotedAttachment quotedAttachment) {
SignalServiceAttachment thumbnail;
try {
thumbnail = quotedAttachment.hasThumbnail() ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.getThumbnail()) : null;
} catch (InvalidMessageStructureException e) {
return Optional.empty();
}
return Optional.of(new PointerAttachment(quotedAttachment.getContentType(),
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
quotedAttachment.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
null,
false,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
null,
null));
}
}

View File

@@ -62,12 +62,12 @@ public class TypingStatusRepository {
ThreadUtil.cancelRunnableOnMain(timer);
}
timer = () -> onTypingStopped(context, threadId, author, device, false);
timer = () -> onTypingStopped(threadId, author, device, false);
ThreadUtil.runOnMainDelayed(timer, RECIPIENT_TYPING_TIMEOUT);
timers.put(typist, timer);
}
public synchronized void onTypingStopped(@NonNull Context context, long threadId, @NonNull Recipient author, int device, boolean isReplacedByIncomingMessage) {
public synchronized void onTypingStopped(long threadId, @NonNull Recipient author, int device, boolean isReplacedByIncomingMessage) {
if (author.isSelf()) {
return;
}

View File

@@ -2,9 +2,14 @@ package org.thoughtcrime.securesms.contactshare;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.PointerAttachment;
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -19,6 +24,8 @@ import static org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
public class ContactModelMapper {
private static final String TAG = Log.tag(ContactModelMapper.class);
public static SharedContact.Builder localToRemoteBuilder(@NonNull Contact contact) {
List<SharedContact.Phone> phoneNumbers = new ArrayList<>(contact.getPhoneNumbers().size());
List<SharedContact.Email> emails = new ArrayList<>(contact.getEmails().size());
@@ -118,6 +125,57 @@ public class ContactModelMapper {
return new Contact(name, sharedContact.getOrganization().orElse(null), phoneNumbers, emails, postalAddresses, avatar);
}
public static Contact remoteToLocal(@NonNull SignalServiceProtos.DataMessage.Contact contact) {
Name name = new Name(contact.getName().getDisplayName(),
contact.getName().getGivenName(),
contact.getName().getFamilyName(),
contact.getName().getPrefix(),
contact.getName().getSuffix(),
contact.getName().getMiddleName());
List<Phone> phoneNumbers = new ArrayList<>(contact.getNumberCount());
for (SignalServiceProtos.DataMessage.Contact.Phone phone : contact.getNumberList()) {
phoneNumbers.add(new Phone(phone.getValue(),
remoteToLocalType(phone.getType()),
phone.getLabel()));
}
List<Email> emails = new ArrayList<>(contact.getEmailCount());
for (SignalServiceProtos.DataMessage.Contact.Email email : contact.getEmailList()) {
emails.add(new Email(email.getValue(),
remoteToLocalType(email.getType()),
email.getLabel()));
}
List<PostalAddress> postalAddresses = new ArrayList<>(contact.getAddressCount());
for (SignalServiceProtos.DataMessage.Contact.PostalAddress postalAddress : contact.getAddressList()) {
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
postalAddress.getLabel(),
postalAddress.getStreet(),
postalAddress.getPobox(),
postalAddress.getNeighborhood(),
postalAddress.getCity(),
postalAddress.getRegion(),
postalAddress.getPostcode(),
postalAddress.getCountry()));
}
Avatar avatar = null;
if (contact.hasAvatar()) {
try {
SignalServiceAttachmentPointer attachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(contact.getAvatar().getAvatar());
Attachment attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer.asPointer())).get();
boolean isProfile = contact.getAvatar().getIsProfile();
avatar = new Avatar(null, attachment, isProfile);
} catch (InvalidMessageStructureException e) {
Log.w(TAG, "Unable to create avatar for contact", e);
}
}
return new Contact(name, contact.getOrganization(), phoneNumbers, emails, postalAddresses, avatar);
}
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
switch (type) {
case HOME: return Phone.Type.HOME;
@@ -127,6 +185,15 @@ public class ContactModelMapper {
}
}
private static Phone.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Phone.Type type) {
switch (type) {
case HOME: return Phone.Type.HOME;
case MOBILE: return Phone.Type.MOBILE;
case WORK: return Phone.Type.WORK;
default: return Phone.Type.CUSTOM;
}
}
private static Email.Type remoteToLocalType(SharedContact.Email.Type type) {
switch (type) {
case HOME: return Email.Type.HOME;
@@ -136,6 +203,15 @@ public class ContactModelMapper {
}
}
private static Email.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Email.Type type) {
switch (type) {
case HOME: return Email.Type.HOME;
case MOBILE: return Email.Type.MOBILE;
case WORK: return Email.Type.WORK;
default: return Email.Type.CUSTOM;
}
}
private static PostalAddress.Type remoteToLocalType(SharedContact.PostalAddress.Type type) {
switch (type) {
case HOME: return PostalAddress.Type.HOME;
@@ -144,6 +220,14 @@ public class ContactModelMapper {
}
}
private static PostalAddress.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.PostalAddress.Type type) {
switch (type) {
case HOME: return PostalAddress.Type.HOME;
case WORK: return PostalAddress.Type.WORK;
default: return PostalAddress.Type.CUSTOM;
}
}
private static SharedContact.Phone.Type localToRemoteType(Phone.Type type) {
switch (type) {
case HOME: return SharedContact.Phone.Type.HOME;

View File

@@ -89,7 +89,7 @@ import java.util.stream.Collectors;
public class AttachmentTable extends DatabaseTable {
private static final String TAG = Log.tag(AttachmentTable.class);
public static final String TAG = Log.tag(AttachmentTable.class);
public static final String TABLE_NAME = "part";
public static final String ROW_ID = "_id";
@@ -101,7 +101,7 @@ public class AttachmentTable extends DatabaseTable {
static final String CONTENT_LOCATION = "cl";
public static final String DATA = "_data";
static final String TRANSFER_STATE = "pending_push";
private static final String TRANSFER_FILE = "transfer_file";
public static final String TRANSFER_FILE = "transfer_file";
public static final String SIZE = "data_size";
static final String FILE_NAME = "file_name";
public static final String UNIQUE_ID = "unique_id";

View File

@@ -139,6 +139,7 @@ import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.isStory
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import java.io.Closeable
import java.io.IOException
import java.util.LinkedList
@@ -4342,6 +4343,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return unhandled
}
fun setTimestampReadFromSyncMessageProto(readMessages: List<SyncMessage.Read>, proposedExpireStarted: Long, threadToLatestRead: MutableMap<Long, Long>): Collection<SyncMessageId> {
val reads: List<ReadMessage> = readMessages.map { r -> ReadMessage(ServiceId.parseOrThrow(r.senderUuid), r.timestamp) }
return setTimestampReadFromSyncMessage(reads, proposedExpireStarted, threadToLatestRead)
}
/**
* Handles a synchronized read message.
* @param messageId An id representing the author-timestamp pair of the message that was read on a linked device. Note that the author could be self when

View File

@@ -5,7 +5,9 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient
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
/**
* Represents a list of, or update to a list of, who can access a story through what
@@ -81,5 +83,17 @@ data class SentStorySyncManifest(
return SentStorySyncManifest(entries)
}
fun fromRecipientsSet(recipients: List<SignalServiceProtos.SyncMessage.Sent.StoryMessageRecipient>): SentStorySyncManifest {
val entries = recipients.toSet().map { recipient ->
Entry(
recipientId = RecipientId.from(ServiceId.parseOrThrow(recipient.destinationUuid)),
allowedToReply = recipient.isAllowedToReply,
distributionLists = recipient.distributionListIdsList.map { DistributionId.from(it) }
)
}
return SentStorySyncManifest(entries)
}
}
}

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.groups;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.protobuf.ByteString;
import org.signal.core.util.DatabaseId;
import org.signal.core.util.Hex;
import org.signal.libsignal.protocol.kdf.HKDFv3;
@@ -83,6 +85,10 @@ public abstract class GroupId implements DatabaseId {
.getGroupIdentifier());
}
public static GroupId.Push push(ByteString bytes) throws BadGroupIdException {
return push(bytes.toByteArray());
}
public static GroupId.Push push(byte[] bytes) throws BadGroupIdException {
return bytes.length == V2_BYTE_LENGTH ? v2(bytes) : v1(bytes);
}

View File

@@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.migrations.PniMigrationJob;
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob;
import org.thoughtcrime.securesms.migrations.RebuildMessageSearchIndexMigrationJob;
import org.thoughtcrime.securesms.migrations.UpdateSmsJobsMigrationJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
@@ -71,6 +70,7 @@ import org.thoughtcrime.securesms.migrations.StoryReadStateMigrationJob;
import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob;
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
import org.thoughtcrime.securesms.migrations.UpdateSmsJobsMigrationJob;
import org.thoughtcrime.securesms.migrations.UserNotificationMigrationJob;
import org.thoughtcrime.securesms.migrations.UuidMigrationJob;
@@ -162,10 +162,11 @@ public final class JobManagerFactories {
put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory());
put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory());
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
put(PushProcessMessageJobV2.KEY, new PushProcessMessageJobV2.Factory());
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
put(RefreshKbsCredentialsJob.KEY, new RefreshKbsCredentialsJob.Factory());
put(RefreshKbsCredentialsJob.KEY, new RefreshKbsCredentialsJob.Factory());
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());

View File

@@ -1,14 +1,15 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.ServiceMessageId
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.messages.MessageContentProcessor
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import java.lang.Exception
import java.util.Optional
/**
* A job that should be enqueued whenever we process a message that we think has arrived "early" (see [org.thoughtcrime.securesms.util.EarlyMessageCache]).
@@ -42,13 +43,19 @@ class PushProcessEarlyMessagesJob private constructor(parameters: Parameters) :
Log.i(TAG, "There are ${earlyIds.size} items in the early message cache with matches.")
for (id: ServiceMessageId in earlyIds) {
val contents: Optional<List<SignalServiceContent>> = ApplicationDependencies.getEarlyMessageCache().retrieve(id.sender, id.sentTimestamp)
val contents: List<SignalServiceContent>? = ApplicationDependencies.getEarlyMessageCache().retrieve(id.sender, id.sentTimestamp).orNull()
val earlyEntries: List<EarlyMessageCacheEntry>? = ApplicationDependencies.getEarlyMessageCache().retrieveV2(id.sender, id.sentTimestamp).orNull()
if (contents.isPresent) {
for (content: SignalServiceContent in contents.get()) {
if (contents != null) {
for (content: SignalServiceContent in contents) {
Log.i(TAG, "[${id.sentTimestamp}] Processing early content for $id")
MessageContentProcessor.create(context).processEarlyContent(MessageContentProcessor.MessageState.DECRYPTED_OK, content, null, id.sentTimestamp, -1)
}
} else if (earlyEntries != null) {
for (entry in earlyEntries) {
Log.i(TAG, "[${id.sentTimestamp}] Processing early V2 content for $id")
MessageContentProcessorV2.create(context).process(entry.envelope, entry.content, entry.metadata, entry.serverDeliveredTimestamp, processingEarlyContent = true)
}
} else {
Log.w(TAG, "[${id.sentTimestamp}] Saw $id in the cache, but when we went to retrieve it, it was already gone.")
}

View File

@@ -0,0 +1,141 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.WorkerThread
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
import org.thoughtcrime.securesms.groups.GroupChangeBusyException
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.GroupUtil
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.crypto.protos.CompleteMessage
import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import java.io.IOException
import java.util.concurrent.TimeUnit
import org.whispersystems.signalservice.api.crypto.protos.EnvelopeMetadata as EnvelopeMetadataProto
class PushProcessMessageJobV2 private constructor(
parameters: Parameters,
private val envelope: Envelope,
private val content: Content,
private val metadata: EnvelopeMetadata,
private val serverDeliveredTimestamp: Long
) : BaseJob(parameters) {
@WorkerThread
constructor(
envelope: Envelope,
content: Content,
metadata: EnvelopeMetadata,
serverDeliveredTimestamp: Long
) : this(createParameters(content, metadata), envelope.toBuilder().clearContent().build(), content, metadata, serverDeliveredTimestamp)
override fun shouldTrace() = true
override fun serialize(): ByteArray {
return CompleteMessage(
envelope = envelope.toByteArray().toByteString(),
content = content.toByteArray().toByteString(),
metadata = EnvelopeMetadataProto(
sourceServiceId = ByteString.of(*metadata.sourceServiceId.toByteArray()),
sourceE164 = metadata.sourceE164,
sourceDeviceId = metadata.sourceDeviceId,
sealedSender = metadata.sealedSender,
groupId = if (metadata.groupId != null) metadata.groupId!!.toByteString() else null,
destinationServiceId = ByteString.of(*metadata.destinationServiceId.toByteArray())
),
serverDeliveredTimestamp = serverDeliveredTimestamp
).encode()
}
override fun getFactoryKey(): String {
return KEY
}
public override fun onRun() {
val processor = MessageContentProcessorV2.create(context)
processor.process(envelope, content, metadata, serverDeliveredTimestamp)
}
public override fun onShouldRetry(e: Exception): Boolean {
return e is PushNetworkException ||
e is NoCredentialForRedemptionTimeException ||
e is GroupChangeBusyException
}
override fun onFailure() = Unit
class Factory : Job.Factory<PushProcessMessageJobV2?> {
override fun create(parameters: Parameters, data: ByteArray?): PushProcessMessageJobV2 {
return try {
val completeMessage = CompleteMessage.ADAPTER.decode(data!!)
PushProcessMessageJobV2(
parameters = parameters,
envelope = Envelope.parseFrom(completeMessage.envelope.toByteArray()),
content = Content.parseFrom(completeMessage.content.toByteArray()),
metadata = EnvelopeMetadata(
sourceServiceId = ServiceId.parseOrThrow(completeMessage.metadata.sourceServiceId.toByteArray()),
sourceE164 = completeMessage.metadata.sourceE164,
sourceDeviceId = completeMessage.metadata.sourceDeviceId,
sealedSender = completeMessage.metadata.sealedSender,
groupId = completeMessage.metadata.groupId?.toByteArray(),
destinationServiceId = ServiceId.parseOrThrow(completeMessage.metadata.destinationServiceId.toByteArray())
),
serverDeliveredTimestamp = completeMessage.serverDeliveredTimestamp
)
} catch (e: IOException) {
throw AssertionError(e)
}
}
}
companion object {
const val KEY = "PushProcessMessageJobV2"
const val QUEUE_PREFIX = "__PUSH_PROCESS_JOB__"
private val TAG = Log.tag(PushProcessMessageJobV2::class.java)
private fun getQueueName(recipientId: RecipientId): String {
return QUEUE_PREFIX + recipientId.toQueueKey()
}
@WorkerThread
private fun createParameters(content: Content, metadata: EnvelopeMetadata): Parameters {
val queueName: String
val builder = Parameters.Builder().setMaxAttempts(Parameters.UNLIMITED)
val groupContext = GroupUtil.getGroupContextIfPresent(content)
val groupId = groupContext?.groupId
if (groupContext != null && groupId != null) {
queueName = getQueueName(Recipient.externalPossiblyMigratedGroup(groupId).id)
if (groupId.isV2) {
val localRevision = groups.getGroupV2Revision(groupId.requireV2())
if (groupContext.revision > localRevision || groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) {
Log.i(TAG, "Adding network constraint to group-related job.")
builder.addConstraint(NetworkConstraint.KEY).setLifespan(TimeUnit.DAYS.toMillis(30))
}
}
} else if (content.hasSyncMessage() && content.syncMessage.hasSent() && content.syncMessage.sent.hasDestinationUuid()) {
queueName = getQueueName(RecipientId.from(ServiceId.parseOrThrow(content.syncMessage.sent.destinationUuid)))
} else {
queueName = getQueueName(RecipientId.from(metadata.sourceServiceId))
}
builder.setQueue(queueName)
return builder.build()
}
}
}

View File

@@ -0,0 +1,162 @@
package org.thoughtcrime.securesms.messages
import org.signal.ringrtc.CallId
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log
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.AnswerMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.HangupMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OpaqueMessageMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswerMetadata
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
import org.whispersystems.signalservice.api.messages.calls.OfferMessage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage.Offer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage.Opaque
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
object CallMessageProcessor {
fun process(
senderRecipient: Recipient,
envelope: Envelope,
content: SignalServiceProtos.Content,
metadata: EnvelopeMetadata,
serverDeliveredTimestamp: Long
) {
val callMessage = content.callMessage
when {
callMessage.hasOffer() -> handleCallOfferMessage(envelope, metadata, callMessage.offer, senderRecipient.id, serverDeliveredTimestamp, callMessage.multiRing)
callMessage.hasAnswer() -> handleCallAnswerMessage(envelope, metadata, callMessage.answer, senderRecipient.id, callMessage.multiRing)
callMessage.iceUpdateList.isNotEmpty() -> handleCallIceUpdateMessage(envelope, metadata, callMessage.iceUpdateList, senderRecipient.id)
callMessage.hasHangup() || callMessage.hasLegacyHangup() -> {
val hangup = if (callMessage.hasHangup()) callMessage.hangup else callMessage.legacyHangup
handleCallHangupMessage(envelope, metadata, hangup, senderRecipient.id, callMessage.hasLegacyHangup())
}
callMessage.hasBusy() -> handleCallBusyMessage(envelope, metadata, callMessage.busy, senderRecipient.id)
callMessage.hasOpaque() -> handleCallOpaqueMessage(envelope, metadata, callMessage.opaque, senderRecipient.requireServiceId(), serverDeliveredTimestamp)
}
}
private fun handleCallOfferMessage(envelope: Envelope, metadata: EnvelopeMetadata, offer: Offer, senderRecipientId: RecipientId, serverDeliveredTimestamp: Long, multiRing: Boolean) {
log(envelope.timestamp, "handleCallOfferMessage...")
val remotePeer = RemotePeer(senderRecipientId, CallId(offer.id))
val remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipientId).map { (_, identityKey): IdentityRecord -> identityKey.serialize() }.get()
ApplicationDependencies.getSignalCallManager()
.receivedOffer(
CallMetadata(remotePeer, metadata.sourceDeviceId),
offer.toCallOfferMetadata(),
ReceivedOfferMetadata(
remoteIdentityKey,
envelope.serverTimestamp,
serverDeliveredTimestamp,
multiRing
)
)
}
private fun handleCallAnswerMessage(
envelope: Envelope,
metadata: EnvelopeMetadata,
answer: SignalServiceProtos.CallMessage.Answer,
senderRecipientId: RecipientId,
multiRing: Boolean
) {
log(envelope.timestamp, "handleCallAnswerMessage...")
val remotePeer = RemotePeer(senderRecipientId, CallId(answer.id))
val remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipientId).map { (_, identityKey): IdentityRecord -> identityKey.serialize() }.get()
ApplicationDependencies.getSignalCallManager()
.receivedAnswer(
CallMetadata(remotePeer, metadata.sourceDeviceId),
AnswerMetadata(if (answer.hasOpaque()) answer.opaque.toByteArray() else null, if (answer.hasSdp()) answer.sdp else null),
ReceivedAnswerMetadata(remoteIdentityKey, multiRing)
)
}
private fun handleCallIceUpdateMessage(
envelope: Envelope,
metadata: EnvelopeMetadata,
iceUpdateList: MutableList<SignalServiceProtos.CallMessage.IceUpdate>,
senderRecipientId: RecipientId
) {
log(envelope.timestamp, "handleCallIceUpdateMessage... " + iceUpdateList.size)
val iceCandidates: MutableList<ByteArray> = ArrayList(iceUpdateList.size)
var callId: Long = -1
iceUpdateList
.filter { it.hasOpaque() }
.forEach { iceUpdate ->
iceCandidates += iceUpdate.opaque.toByteArray()
callId = iceUpdate.id
}
val remotePeer = RemotePeer(senderRecipientId, CallId(callId))
ApplicationDependencies.getSignalCallManager()
.receivedIceCandidates(
CallMetadata(remotePeer, metadata.sourceDeviceId),
iceCandidates
)
}
private fun handleCallHangupMessage(
envelope: Envelope,
metadata: EnvelopeMetadata,
hangup: SignalServiceProtos.CallMessage.Hangup,
senderRecipientId: RecipientId,
isLegacyHangup: Boolean
) {
log(envelope.timestamp, "handleCallHangupMessage")
val remotePeer = RemotePeer(senderRecipientId, CallId(hangup.id))
ApplicationDependencies.getSignalCallManager()
.receivedCallHangup(
CallMetadata(remotePeer, metadata.sourceDeviceId),
HangupMetadata(HangupMessage.Type.fromProto(hangup.type), isLegacyHangup, hangup.deviceId)
)
}
private fun handleCallBusyMessage(envelope: Envelope, metadata: EnvelopeMetadata, busy: SignalServiceProtos.CallMessage.Busy, senderRecipientId: RecipientId) {
log(envelope.timestamp, "handleCallBusyMessage")
val remotePeer = RemotePeer(senderRecipientId, CallId(busy.id))
ApplicationDependencies.getSignalCallManager().receivedCallBusy(CallMetadata(remotePeer, metadata.sourceDeviceId))
}
private fun handleCallOpaqueMessage(envelope: Envelope, metadata: EnvelopeMetadata, opaque: Opaque, senderServiceId: ServiceId, serverDeliveredTimestamp: Long) {
log(envelope.timestamp.toString(), "handleCallOpaqueMessage")
var messageAgeSeconds: Long = 0
if (envelope.serverTimestamp in 1..serverDeliveredTimestamp) {
messageAgeSeconds = (serverDeliveredTimestamp - envelope.serverTimestamp) / 1000
}
ApplicationDependencies.getSignalCallManager()
.receivedOpaqueMessage(
OpaqueMessageMetadata(
senderServiceId.uuid(),
opaque.data.toByteArray(),
metadata.sourceDeviceId,
messageAgeSeconds
)
)
}
private fun Offer.toCallOfferMetadata(): OfferMetadata {
val sdp = if (hasSdp()) sdp else null
val opaque = if (hasOpaque()) opaque else null
return OfferMetadata(opaque?.toByteArray(), sdp, OfferMessage.Type.fromProto(type))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil.startWhenCapable
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob
import org.thoughtcrime.securesms.jobs.PushProcessMessageJob
import org.thoughtcrime.securesms.jobs.PushProcessMessageJobV2
import org.thoughtcrime.securesms.jobs.UnableToStartException
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.MessageDecryptor.FollowUpOperation
@@ -36,17 +37,11 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
@@ -280,13 +275,7 @@ class IncomingMessageObserver(private val context: Application) {
val extraJob: Job? = when (result) {
is MessageDecryptor.Result.Success -> {
PushProcessMessageJob(
result.toMessageState(),
result.toSignalServiceContent(),
null,
-1,
result.envelope.timestamp
)
PushProcessMessageJobV2(result.envelope, result.content, result.metadata, result.serverDeliveredTimestamp)
}
is MessageDecryptor.Result.Error -> {
@@ -336,29 +325,6 @@ class IncomingMessageObserver(private val context: Application) {
}
}
private fun MessageDecryptor.Result.Success.toSignalServiceContent(): SignalServiceContent {
val localAddress = SignalServiceAddress(this.metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
val metadata = SignalServiceMetadata(
SignalServiceAddress(this.metadata.sourceServiceId, Optional.ofNullable(this.metadata.sourceE164)),
this.metadata.sourceDeviceId,
this.envelope.timestamp,
this.envelope.serverTimestamp,
this.serverDeliveredTimestamp,
this.metadata.sealedSender,
this.envelope.serverGuid,
Optional.ofNullable(this.metadata.groupId),
this.metadata.destinationServiceId.toString()
)
val contentProto = SignalServiceContentProto.newBuilder()
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(metadata))
.setContent(content)
.build()
return SignalServiceContent.createFromProto(contentProto)!!
}
private fun MessageDecryptor.ErrorMetadata.toExceptionMetadata(): MessageContentProcessor.ExceptionMetadata {
return MessageContentProcessor.ExceptionMetadata(
this.sender,

View File

@@ -2879,7 +2879,7 @@ public class MessageContentProcessor {
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);
ApplicationDependencies.getTypingStatusRepository().onTypingStopped(threadId, senderRecipient, content.getSenderDevice(), false);
}
}
@@ -3257,11 +3257,11 @@ public class MessageContentProcessor {
}
private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient senderRecipient, @NonNull Recipient conversationRecipient, int device) {
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(conversationRecipient);
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(conversationRecipient.getId());
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);
ApplicationDependencies.getTypingStatusRepository().onTypingStopped(threadId, senderRecipient, device, true);
}
}
@@ -3403,13 +3403,11 @@ public class MessageContentProcessor {
}
}
@SuppressWarnings("WeakerAccess")
private static class StorageFailedException extends Exception {
static class StorageFailedException extends Exception {
private final String sender;
private final int senderDevice;
private StorageFailedException(Exception e, String sender, int senderDevice) {
StorageFailedException(Exception e, String sender, int senderDevice) {
super(e);
this.sender = sender;
this.senderDevice = senderDevice;

View File

@@ -0,0 +1,572 @@
package org.thoughtcrime.securesms.messages
import android.content.Context
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.message.DecryptionErrorMessage
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageLogEntry
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.PendingRetryReceiptModel
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.jobs.NullMessageSendJob
import org.thoughtcrime.securesms.jobs.ResendMessageJob
import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupMasterKey
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.hasDisallowedAnnouncementOnlyContent
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.hasGroupContext
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.hasSignedGroupChange
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.hasStarted
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.isExpirationUpdate
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.isMediaMessage
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.isValid
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.signedGroupChange
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toDecryptionErrorMessage
import org.thoughtcrime.securesms.notifications.v2.ConversationId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage
import java.io.IOException
class MessageContentProcessorV2(private val context: Context) {
companion object {
const val TAG = "MessageProcessorV2"
@JvmStatic
@JvmOverloads
fun create(context: Context = ApplicationDependencies.getApplication()): MessageContentProcessorV2 {
return MessageContentProcessorV2(context)
}
fun debug(message: String) {
Log.d(TAG, message)
}
fun log(message: String) {
Log.i(TAG, message)
}
fun log(timestamp: Long, message: String) {
log(timestamp.toString(), message)
}
fun log(extra: String, message: String) {
val extraLog = if (Util.isEmpty(extra)) "" else "[$extra] "
Log.i(TAG, extraLog + message)
}
fun warn(message: String) {
warn("", message, null)
}
fun warn(extra: String, message: String) {
warn(extra, message, null)
}
fun warn(timestamp: Long, message: String) {
warn(timestamp.toString(), message)
}
fun warn(timestamp: Long, message: String, t: Throwable?) {
warn(timestamp.toString(), message, t)
}
fun warn(message: String, t: Throwable?) {
warn("", message, t)
}
fun warn(extra: String, message: String, t: Throwable?) {
val extraLog = if (Util.isEmpty(extra)) "" else "[$extra] "
Log.w(TAG, extraLog + message, t)
}
fun formatSender(recipientId: RecipientId, serviceId: ServiceId, device: Int): String {
return "$recipientId ($serviceId.$device)"
}
@Throws(BadGroupIdException::class)
private fun getMessageDestination(content: Content, sender: Recipient): Recipient {
return if (content.hasStoryMessage() && content.storyMessage.group.isValid) {
getGroupRecipient(content.storyMessage.group, sender)
} else if (content.dataMessage.hasGroupContext) {
getGroupRecipient(content.dataMessage.groupV2, sender)
} else {
sender
}
}
private fun getGroupRecipient(groupContextV2: SignalServiceProtos.GroupContextV2?, senderRecipient: Recipient): Recipient {
return if (groupContextV2 != null) {
Recipient.externalPossiblyMigratedGroup(GroupId.v2(groupContextV2.groupMasterKey))
} else {
senderRecipient
}
}
@Throws(BadGroupIdException::class)
private fun shouldIgnore(content: Content, senderRecipient: Recipient, threadRecipient: Recipient): Boolean {
if (content.hasDataMessage()) {
val message = content.dataMessage
return if (threadRecipient.isGroup && threadRecipient.isBlocked) {
true
} else if (threadRecipient.isGroup) {
val groupId = if (message.hasGroupV2()) GroupId.v2(message.groupV2.groupMasterKey) else null
if (groupId != null && SignalDatabase.groups.isUnknownGroup(groupId)) {
return senderRecipient.isBlocked
}
val isTextMessage = message.hasBody()
val isMediaMessage = message.isMediaMessage
val isExpireMessage = message.isExpirationUpdate
val isGv2Update = message.hasSignedGroupChange
val isContentMessage = !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage)
val isGroupActive = groupId != null && SignalDatabase.groups.isActive(groupId)
isContentMessage && !isGroupActive || senderRecipient.isBlocked && !isGv2Update
} else {
senderRecipient.isBlocked
}
} else if (content.hasCallMessage()) {
return senderRecipient.isBlocked
} else if (content.hasTypingMessage()) {
if (senderRecipient.isBlocked) {
return true
}
if (content.typingMessage.hasGroupId()) {
val groupId: GroupId = GroupId.push(content.typingMessage.groupId)
val groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId)
return if (groupRecipient.isBlocked || !groupRecipient.isActiveGroup) {
true
} else {
val groupRecord = SignalDatabase.groups.getGroup(groupId)
groupRecord.isPresent && groupRecord.get().isAnnouncementGroup && !groupRecord.get().admins.contains(senderRecipient)
}
}
} else if (content.hasStoryMessage()) {
return if (threadRecipient.isGroup && threadRecipient.isBlocked) {
true
} else {
senderRecipient.isBlocked
}
}
return false
}
@Throws(BadGroupIdException::class)
private fun handlePendingRetry(pending: PendingRetryReceiptModel?, timestamp: Long, destination: Recipient): Long {
var receivedTime = System.currentTimeMillis()
if (pending != null) {
warn(timestamp, "Incoming message matches a pending retry we were expecting.")
val threadId = SignalDatabase.threads.getThreadIdFor(destination.id)
if (threadId != null) {
val (lastSeen) = SignalDatabase.threads.getConversationMetadata(threadId)
val visibleThread = ApplicationDependencies.getMessageNotifier().visibleThread.map(ConversationId::threadId).orElse(-1L)
if (threadId != visibleThread && lastSeen > 0 && lastSeen < pending.receivedTimestamp) {
receivedTime = pending.receivedTimestamp
warn(timestamp, "Thread has not been opened yet. Using received timestamp of $receivedTime")
} else {
warn(timestamp, "Thread was opened after receiving the original message. Using the current time for received time. (Last seen: " + lastSeen + ", ThreadVisible: " + (threadId == visibleThread) + ")")
}
} else {
warn(timestamp, "Could not find a thread for the pending message. Using current time for received time.")
}
}
return receivedTime
}
/**
* @return True if the content should be ignored, otherwise false.
*/
@Throws(IOException::class, GroupChangeBusyException::class)
fun handleGv2PreProcessing(
context: Context,
timestamp: Long,
content: Content,
metadata: EnvelopeMetadata,
groupId: GroupId.V2,
groupV2: SignalServiceProtos.GroupContextV2,
senderRecipient: Recipient
): Boolean {
val possibleGv1 = SignalDatabase.groups.getGroupV1ByExpectedV2(groupId)
if (possibleGv1.isPresent) {
GroupsV1MigrationUtil.performLocalMigration(context, possibleGv1.get().id.requireV1())
}
if (!updateGv2GroupFromServerOrP2PChange(context, timestamp, groupV2)) {
log(timestamp, "Ignoring GV2 message for group we are not currently in $groupId")
return true
}
val groupRecord = SignalDatabase.groups.getGroup(groupId)
if (groupRecord.isPresent && !groupRecord.get().members.contains(senderRecipient.id)) {
log(timestamp, "Ignoring GV2 message from member not in group $groupId. Sender: ${formatSender(senderRecipient.id, metadata.sourceServiceId, metadata.sourceDeviceId)}")
return true
}
if (groupRecord.isPresent && groupRecord.get().isAnnouncementGroup && !groupRecord.get().admins.contains(senderRecipient)) {
if (content.hasDataMessage()) {
if (content.dataMessage.hasDisallowedAnnouncementOnlyContent) {
Log.w(TAG, "Ignoring message from ${senderRecipient.id} because it has disallowed content, and they're not an admin in an announcement-only group.")
return true
}
} else if (content.hasTypingMessage()) {
Log.w(TAG, "Ignoring typing indicator from ${senderRecipient.id} because they're not an admin in an announcement-only group.")
return true
}
}
return false
}
@Throws(IOException::class, GroupChangeBusyException::class)
fun updateGv2GroupFromServerOrP2PChange(
context: Context,
timestamp: Long,
groupV2: SignalServiceProtos.GroupContextV2
): Boolean {
return try {
val updatedTimestamp = if (groupV2.hasSignedGroupChange) timestamp else timestamp - 1
GroupManager.updateGroupFromServer(context, groupV2.groupMasterKey, groupV2.revision, updatedTimestamp, groupV2.signedGroupChange)
true
} catch (e: GroupNotAMemberException) {
warn(timestamp, "Ignoring message for a group we're not in")
false
}
}
private fun resetRecipientToPush(recipient: Recipient) {
if (recipient.isForceSmsSelection) {
SignalDatabase.recipients.setForceSmsSelection(recipient.id, false)
}
}
}
/**
* 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.
*
* @param processingEarlyContent pass `true` to specifically target 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.
*/
@JvmOverloads
fun process(envelope: Envelope, content: Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean = false) {
val senderRecipient = Recipient.externalPush(metadata.sourceServiceId)
handleMessage(senderRecipient, envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent)
val earlyCacheEntries: List<EarlyMessageCacheEntry>? = ApplicationDependencies
.getEarlyMessageCache()
.retrieveV2(senderRecipient.id, envelope.timestamp)
.orNull()
if (!processingEarlyContent && earlyCacheEntries != null) {
log(envelope.timestamp, "Found " + earlyCacheEntries.size + " dependent item(s) that were retrieved earlier. Processing.")
for (entry in earlyCacheEntries) {
handleMessage(senderRecipient, entry.envelope, entry.content, entry.metadata, entry.serverDeliveredTimestamp, processingEarlyContent = true)
}
}
}
private fun handleMessage(
senderRecipient: Recipient,
envelope: Envelope,
content: Content,
metadata: EnvelopeMetadata,
serverDeliveredTimestamp: Long,
processingEarlyContent: Boolean
) {
val threadRecipient = getMessageDestination(content, senderRecipient)
if (shouldIgnore(content, senderRecipient, threadRecipient)) {
log(envelope.timestamp, "Ignoring message.")
return
}
val pending: PendingRetryReceiptModel? = ApplicationDependencies.getPendingRetryReceiptCache().get(senderRecipient.id, envelope.timestamp)
val receivedTime: Long = handlePendingRetry(pending, envelope.timestamp, threadRecipient)
log(envelope.timestamp, "Beginning message processing. Sender: " + formatSender(senderRecipient.id, metadata.sourceServiceId, metadata.sourceDeviceId))
when {
content.hasDataMessage() -> {
DataMessageProcessor.process(
context,
senderRecipient,
threadRecipient,
envelope,
content,
metadata,
receivedTime,
if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp)
)
}
content.hasSyncMessage() -> {
TextSecurePreferences.setMultiDevice(context, true)
SyncMessageProcessor.process(
context,
senderRecipient,
envelope,
content,
metadata,
if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp)
)
}
content.hasCallMessage() -> {
log(envelope.timestamp, "Got call message...")
val message: SignalServiceProtos.CallMessage = content.callMessage
val destinationDeviceId: Int? = if (message.hasDestinationDeviceId()) message.destinationDeviceId else null
if (destinationDeviceId != null && destinationDeviceId != SignalStore.account().deviceId) {
log(envelope.timestamp, "Ignoring call message that is not for this device! intended: $destinationDeviceId, this: ${SignalStore.account().deviceId}")
return
}
CallMessageProcessor.process(senderRecipient, envelope, content, metadata, serverDeliveredTimestamp)
}
content.hasReceiptMessage() -> {
ReceiptMessageProcessor.process(
context,
senderRecipient,
envelope,
content,
metadata,
if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp)
)
}
content.hasTypingMessage() -> {
handleTypingMessage(envelope, metadata, content.typingMessage, senderRecipient)
}
content.hasStoryMessage() -> {
StoryMessageProcessor.process(
envelope,
content,
metadata,
senderRecipient,
threadRecipient
)
}
content.hasDecryptionErrorMessage() -> {
handleRetryReceipt(envelope, metadata, content.decryptionErrorMessage!!.toDecryptionErrorMessage(metadata), senderRecipient)
}
content.hasSenderKeyDistributionMessage() || content.hasPniSignatureMessage() -> {
// Already handled, here in order to prevent unrecognized message log
}
else -> {
warn(envelope.timestamp, "Got unrecognized message!")
}
}
resetRecipientToPush(senderRecipient)
if (pending != null) {
warn(envelope.timestamp, "Pending retry was processed. Deleting.")
ApplicationDependencies.getPendingRetryReceiptCache().delete(pending)
}
}
@Throws(BadGroupIdException::class)
private fun handleTypingMessage(
envelope: Envelope,
metadata: EnvelopeMetadata,
typingMessage: TypingMessage,
senderRecipient: Recipient
) {
if (!TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
return
}
val threadId: Long = if (typingMessage.hasGroupId()) {
val groupId = GroupId.push(typingMessage.groupId)
if (!SignalDatabase.groups.isCurrentMember(groupId, senderRecipient.id)) {
warn(envelope.timestamp, "Seen typing indicator for non-member " + senderRecipient.id)
return
}
val groupRecipient = Recipient.externalPossiblyMigratedGroup(groupId)
SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
} else {
SignalDatabase.threads.getOrCreateThreadIdFor(senderRecipient)
}
if (threadId <= 0) {
warn(envelope.timestamp, "Couldn't find a matching thread for a typing message.")
return
}
if (typingMessage.hasStarted) {
Log.d(TAG, "Typing started on thread $threadId")
ApplicationDependencies.getTypingStatusRepository().onTypingStarted(context, threadId, senderRecipient, metadata.sourceDeviceId)
} else {
Log.d(TAG, "Typing stopped on thread $threadId")
ApplicationDependencies.getTypingStatusRepository().onTypingStopped(threadId, senderRecipient, metadata.sourceDeviceId, false)
}
}
private fun handleRetryReceipt(envelope: Envelope, metadata: EnvelopeMetadata, decryptionErrorMessage: DecryptionErrorMessage, senderRecipient: Recipient) {
if (!FeatureFlags.retryReceipts()) {
warn(envelope.timestamp, "[RetryReceipt] Feature flag disabled, skipping retry receipt.")
return
}
if (decryptionErrorMessage.deviceId != SignalStore.account().deviceId) {
log(envelope.timestamp, "[RetryReceipt] Received a DecryptionErrorMessage targeting a linked device. Ignoring.")
return
}
val sentTimestamp = decryptionErrorMessage.timestamp
warn(envelope.timestamp, "[RetryReceipt] Received a retry receipt from ${formatSender(senderRecipient.id, metadata.sourceServiceId, metadata.sourceDeviceId)} for message with timestamp $sentTimestamp.")
if (!senderRecipient.hasServiceId()) {
warn(envelope.timestamp, "[RetryReceipt] Requester ${senderRecipient.id} somehow has no UUID! timestamp: $sentTimestamp")
return
}
val messageLogEntry = SignalDatabase.messageLog.getLogEntry(senderRecipient.id, metadata.sourceDeviceId, sentTimestamp)
if (decryptionErrorMessage.ratchetKey.isPresent) {
handleIndividualRetryReceipt(senderRecipient, messageLogEntry, envelope, metadata, decryptionErrorMessage)
} else {
handleSenderKeyRetryReceipt(senderRecipient, messageLogEntry, envelope, metadata, decryptionErrorMessage)
}
}
private fun handleSenderKeyRetryReceipt(
requester: Recipient,
messageLogEntry: MessageLogEntry?,
envelope: Envelope,
metadata: EnvelopeMetadata,
decryptionErrorMessage: DecryptionErrorMessage
) {
val sentTimestamp = decryptionErrorMessage.timestamp
val relatedMessage = findRetryReceiptRelatedMessage(messageLogEntry, sentTimestamp)
if (relatedMessage == null) {
warn(envelope.timestamp, "[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
}
val threadRecipient = SignalDatabase.threads.getRecipientForThreadId(relatedMessage.threadId)
if (threadRecipient == null) {
warn(envelope.timestamp, "[RetryReceipt-SK] Could not find a thread recipient! Skipping.")
return
}
if (!threadRecipient.isPushV2Group && !threadRecipient.isDistributionList) {
warn(envelope.timestamp, "[RetryReceipt-SK] Thread recipient is not a V2 group or distribution list! Skipping.")
return
}
val distributionId: DistributionId?
val groupId: GroupId.V2?
if (threadRecipient.isGroup) {
groupId = threadRecipient.requireGroupId().requireV2()
distributionId = SignalDatabase.groups.getOrCreateDistributionId(groupId)
} else {
groupId = null
distributionId = SignalDatabase.distributionLists.getDistributionId(threadRecipient.id)
}
if (distributionId == null) {
Log.w(TAG, "[RetryReceipt-SK] Failed to find a distributionId! Skipping.")
return
}
val requesterAddress = SignalProtocolAddress(requester.requireServiceId().toString(), metadata.sourceDeviceId)
SignalDatabase.senderKeyShared.delete(distributionId, setOf(requesterAddress))
if (messageLogEntry != null) {
warn(envelope.timestamp, "[RetryReceipt-SK] Found MSL entry for ${requester.id} ($requesterAddress) with timestamp $sentTimestamp. Scheduling a resend.")
ApplicationDependencies.getJobManager().add(
ResendMessageJob(
messageLogEntry.recipientId,
messageLogEntry.dateSent,
messageLogEntry.content,
messageLogEntry.contentHint,
messageLogEntry.urgent,
groupId,
distributionId
)
)
} else {
warn(envelope.timestamp, "[RetryReceipt-SK] Unable to find MSL entry for ${requester.id} ($requesterAddress) with timestamp $sentTimestamp for ${if (groupId != null) "group $groupId" else "distribution list"}. Scheduling a job to send them the SenderKeyDistributionMessage. Membership will be checked there.")
ApplicationDependencies.getJobManager().add(SenderKeyDistributionSendJob(requester.id, threadRecipient.id))
}
}
private fun handleIndividualRetryReceipt(requester: Recipient, messageLogEntry: MessageLogEntry?, envelope: Envelope, metadata: EnvelopeMetadata, decryptionErrorMessage: DecryptionErrorMessage) {
var archivedSession = false
// TODO [pnp] Ignore retry receipts that have a PNI destinationUuid
if (decryptionErrorMessage.ratchetKey.isPresent &&
ratchetKeyMatches(requester, metadata.sourceDeviceId, decryptionErrorMessage.ratchetKey.get())
) {
warn(envelope.timestamp, "[RetryReceipt-I] Ratchet key matches. Archiving the session.")
ApplicationDependencies.getProtocolStore().aci().sessions().archiveSession(requester.id, metadata.sourceDeviceId)
archivedSession = true
}
if (messageLogEntry != null) {
warn(envelope.timestamp, "[RetryReceipt-I] Found an entry in the MSL. Resending.")
ApplicationDependencies.getJobManager().add(
ResendMessageJob(
messageLogEntry.recipientId,
messageLogEntry.dateSent,
messageLogEntry.content,
messageLogEntry.contentHint,
messageLogEntry.urgent,
null,
null
)
)
} else if (archivedSession) {
warn(envelope.timestamp, "[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(NullMessageSendJob(requester.id))
} else {
warn(envelope.timestamp, "[RetryReceipt-I] Could not find an entry in the MSL. Skipping.")
}
}
private fun findRetryReceiptRelatedMessage(messageLogEntry: MessageLogEntry?, sentTimestamp: Long): MessageRecord? {
return if (messageLogEntry != null && messageLogEntry.hasRelatedMessage) {
val id = messageLogEntry.relatedMessages[0].id
SignalDatabase.messages.getMessageRecordOrNull(id)
} else {
SignalDatabase.messages.getMessageFor(sentTimestamp, Recipient.self().id)
}
}
private fun ratchetKeyMatches(recipient: Recipient, deviceId: Int, ratchetKey: ECPublicKey): Boolean {
val address = recipient.resolve().requireServiceId().toProtocolAddress(deviceId)
val session = ApplicationDependencies.getProtocolStore().aci().loadSession(address)
return session.currentRatchetKeyMatches(ratchetKey)
}
}

View File

@@ -439,7 +439,7 @@ object MessageDecryptor {
val followUpOperations: List<FollowUpOperation>
/** Successfully decrypted the envelope content. The plaintext [Content] is available. */
class Success(
data class Success(
override val envelope: Envelope,
override val serverDeliveredTimestamp: Long,
val content: Content,

View File

@@ -0,0 +1,138 @@
package org.thoughtcrime.securesms.messages
import android.annotation.SuppressLint
import android.content.Context
import org.thoughtcrime.securesms.database.MessageTable.SyncMessageId
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.PushProcessEarlyMessagesJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.warn
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.ReceiptMessage
object ReceiptMessageProcessor {
fun process(context: Context, senderRecipient: Recipient, envelope: Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, earlyMessageCacheEntry: EarlyMessageCacheEntry?) {
val receiptMessage = content.receiptMessage
when (receiptMessage.type) {
ReceiptMessage.Type.DELIVERY -> handleDeliveryReceipt(envelope, metadata, receiptMessage, senderRecipient.id)
ReceiptMessage.Type.READ -> handleReadReceipt(context, senderRecipient.id, envelope, metadata, receiptMessage, earlyMessageCacheEntry)
ReceiptMessage.Type.VIEWED -> handleViewedReceipt(context, envelope, metadata, receiptMessage, senderRecipient.id, earlyMessageCacheEntry)
else -> warn(envelope.timestamp, "Unknown recipient message type ${receiptMessage.type}")
}
}
@SuppressLint("DefaultLocale")
private fun handleDeliveryReceipt(
envelope: Envelope,
metadata: EnvelopeMetadata,
deliveryReceipt: ReceiptMessage,
senderRecipientId: RecipientId
) {
log(envelope.timestamp, "Processing delivery receipts. Sender: $senderRecipientId, Device: ${metadata.sourceDeviceId}, Timestamps: ${deliveryReceipt.timestampList.joinToString(", ")}")
val ids = deliveryReceipt.timestampList.map { SyncMessageId(senderRecipientId, it) }
val unhandled = SignalDatabase.messages.incrementDeliveryReceiptCounts(ids, envelope.timestamp)
for (id in unhandled) {
warn(envelope.timestamp, "[handleDeliveryReceipt] Could not find matching message! timestamp: ${id.timetamp} author: ${id.recipientId}")
// Early delivery receipts are special-cased in the database methods
}
if (unhandled.isNotEmpty()) {
PushProcessEarlyMessagesJob.enqueue()
}
SignalDatabase.pendingPniSignatureMessages.acknowledgeReceipts(senderRecipientId, deliveryReceipt.timestampList, metadata.sourceDeviceId)
SignalDatabase.messageLog.deleteEntriesForRecipient(deliveryReceipt.timestampList, senderRecipientId, metadata.sourceDeviceId)
}
@SuppressLint("DefaultLocale")
private fun handleReadReceipt(
context: Context,
senderRecipientId: RecipientId,
envelope: Envelope,
metadata: EnvelopeMetadata,
readReceipt: ReceiptMessage,
earlyMessageCacheEntry: EarlyMessageCacheEntry?
) {
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
log(envelope.timestamp, "Ignoring read receipts for IDs: " + readReceipt.timestampList.joinToString(", "))
return
}
log(envelope.timestamp, "Processing read receipts. Sender: $senderRecipientId, Device: ${metadata.sourceDeviceId}, Timestamps: ${readReceipt.timestampList.joinToString(", ")}")
val ids = readReceipt.timestampList.map { SyncMessageId(senderRecipientId, it) }
val unhandled = SignalDatabase.messages.incrementReadReceiptCounts(ids, envelope.timestamp)
if (unhandled.isNotEmpty()) {
val selfId = Recipient.self().id
for (id in unhandled) {
warn(envelope.timestamp, "[handleReadReceipt] Could not find matching message! timestamp: ${id.timetamp}, author: ${id.recipientId} | Receipt, so associating with message from self ($selfId)")
if (earlyMessageCacheEntry != null) {
ApplicationDependencies.getEarlyMessageCache().store(selfId, id.timetamp, earlyMessageCacheEntry)
}
}
}
if (unhandled.isNotEmpty() && earlyMessageCacheEntry != null) {
PushProcessEarlyMessagesJob.enqueue()
}
}
private fun handleViewedReceipt(
context: Context,
envelope: Envelope,
metadata: EnvelopeMetadata,
viewedReceipt: ReceiptMessage,
senderRecipientId: RecipientId,
earlyMessageCacheEntry: EarlyMessageCacheEntry?
) {
val readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context)
val storyViewedReceipts = SignalStore.storyValues().viewedReceiptsEnabled
if (!readReceipts && !storyViewedReceipts) {
log(envelope.timestamp, "Ignoring viewed receipts for IDs: ${viewedReceipt.timestampList.joinToString(", ")}")
return
}
log(envelope.timestamp, "Processing viewed receipts. Sender: $senderRecipientId, Device: ${metadata.sourceDeviceId}, Only Stories: ${!readReceipts}, Timestamps: ${viewedReceipt.timestampList.joinToString(", ")}")
val ids = viewedReceipt.timestampList.map { SyncMessageId(senderRecipientId, it) }
val unhandled: Collection<SyncMessageId> = if (readReceipts && storyViewedReceipts) {
SignalDatabase.messages.incrementViewedReceiptCounts(ids, envelope.timestamp)
} else if (readReceipts) {
SignalDatabase.messages.incrementViewedNonStoryReceiptCounts(ids, envelope.timestamp)
} else {
SignalDatabase.messages.incrementViewedStoryReceiptCounts(ids, envelope.timestamp)
}
val handled: Set<SyncMessageId> = ids.toSet() - unhandled.toSet()
SignalDatabase.messages.updateViewedStories(handled)
if (unhandled.isNotEmpty()) {
val selfId = Recipient.self().id
for (id in unhandled) {
warn(envelope.timestamp, "[handleViewedReceipt] Could not find matching message! timestamp: ${id.timetamp}, author: ${id.recipientId} | Receipt so associating with message from self ($selfId)")
if (earlyMessageCacheEntry != null) {
ApplicationDependencies.getEarlyMessageCache().store(selfId, id.timetamp, earlyMessageCacheEntry)
}
}
}
if (unhandled.isNotEmpty() && earlyMessageCacheEntry != null) {
PushProcessEarlyMessagesJob.enqueue()
}
}
}

View File

@@ -0,0 +1,180 @@
package org.thoughtcrime.securesms.messages
import com.google.protobuf.ByteString
import org.signal.core.util.orNull
import org.signal.libsignal.protocol.message.DecryptionErrorMessage
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toPointer
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.payments.Money
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Payment
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.StoryMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.Sent
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.TypingMessage
import java.util.Optional
private val ByteString.isNotEmpty: Boolean
get() = !this.isEmpty
object SignalServiceProtoUtil {
/** Contains some user data that affects the conversation */
val DataMessage.hasRenderableContent: Boolean
get() {
return attachmentsList.isNotEmpty() ||
hasBody() ||
hasQuote() ||
contactList.isNotEmpty() ||
previewList.isNotEmpty() ||
bodyRangesList.isNotEmpty() ||
hasSticker() ||
hasReaction() ||
hasRemoteDelete
}
val DataMessage.hasDisallowedAnnouncementOnlyContent: Boolean
get() {
return hasBody() ||
attachmentsList.isNotEmpty() ||
hasQuote() ||
previewList.isNotEmpty() ||
bodyRangesList.isNotEmpty() ||
hasSticker()
}
val DataMessage.isExpirationUpdate: Boolean
get() = flags and DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE != 0
val DataMessage.hasRemoteDelete: Boolean
get() = hasDelete() && delete.hasTargetSentTimestamp()
val DataMessage.isGroupV2Update: Boolean
get() = !hasRenderableContent && hasSignedGroupChange
val DataMessage.hasGroupContext: Boolean
get() = hasGroupV2() && groupV2.hasMasterKey() && groupV2.masterKey.isNotEmpty
val DataMessage.hasSignedGroupChange: Boolean
get() = hasGroupContext && groupV2.hasSignedGroupChange
val DataMessage.isMediaMessage: Boolean
get() = attachmentsList.isNotEmpty() || hasQuote() || contactList.isNotEmpty() || hasSticker() || bodyRangesList.isNotEmpty()
val DataMessage.isEndSession: Boolean
get() = flags and DataMessage.Flags.END_SESSION_VALUE != 0
val DataMessage.isStoryReaction: Boolean
get() = hasReaction() && hasStoryContext()
val DataMessage.isPaymentActivationRequest: Boolean
get() = hasPayment() && payment.hasActivation() && payment.activation.type == Payment.Activation.Type.REQUEST
val DataMessage.isPaymentActivated: Boolean
get() = hasPayment() && payment.hasActivation() && payment.activation.type == Payment.Activation.Type.ACTIVATED
val DataMessage.isInvalid: Boolean
get() {
if (isViewOnce) {
val contentType = attachmentsList[0].contentType.lowercase()
return attachmentsList.size != 1 || !MediaUtil.isImageOrVideoType(contentType)
}
return false
}
val DataMessage.isEmptyGroupV2Message: Boolean
get() = hasGroupContext && !isGroupV2Update && !hasRenderableContent
val GroupContextV2.hasSignedGroupChange: Boolean
get() = hasGroupChange() && groupChange.isNotEmpty
val GroupContextV2.signedGroupChange: ByteArray
get() = groupChange.toByteArray()
val GroupContextV2.groupMasterKey: GroupMasterKey
get() = GroupMasterKey(masterKey.toByteArray())
val GroupContextV2?.isValid: Boolean
get() = this != null && masterKey.isNotEmpty
val GroupContextV2.groupId: GroupId.V2?
get() = if (isValid) GroupId.v2(groupMasterKey) else null
val StoryMessage.type: StoryType
get() {
return if (allowsReplies) {
if (hasTextAttachment()) {
StoryType.TEXT_STORY_WITH_REPLIES
} else {
StoryType.STORY_WITH_REPLIES
}
} else {
if (hasTextAttachment()) {
StoryType.TEXT_STORY_WITHOUT_REPLIES
} else {
StoryType.STORY_WITHOUT_REPLIES
}
}
}
fun Sent.isUnidentified(serviceId: ServiceId?): Boolean {
return serviceId != null && unidentifiedStatusList.firstOrNull { ServiceId.parseOrNull(it.destinationUuid) == serviceId }?.unidentified ?: false
}
val Sent.serviceIdsToUnidentifiedStatus: Map<ServiceId, Boolean>
get() {
return unidentifiedStatusList
.mapNotNull { status ->
val serviceId = ServiceId.parseOrNull(status.destinationUuid)
if (serviceId != null) {
serviceId to status.unidentified
} else {
null
}
}
.toMap()
}
val TypingMessage.hasStarted: Boolean
get() = hasAction() && action == TypingMessage.Action.STARTED
fun ByteString.toDecryptionErrorMessage(metadata: EnvelopeMetadata): DecryptionErrorMessage {
try {
return DecryptionErrorMessage(toByteArray())
} catch (e: InvalidMessageStructureException) {
throw InvalidMessageStructureException(e, metadata.sourceServiceId.toString(), metadata.sourceDeviceId)
}
}
fun List<AttachmentPointer>.toPointers(): List<Attachment> {
return mapNotNull { it.toPointer() }
}
fun AttachmentPointer.toPointer(stickerLocator: StickerLocator? = null): Attachment? {
return try {
PointerAttachment.forPointer(Optional.of(toSignalServiceAttachmentPointer()), stickerLocator).orNull()
} catch (e: InvalidMessageStructureException) {
null
}
}
fun AttachmentPointer.toSignalServiceAttachmentPointer(): SignalServiceAttachmentPointer {
return AttachmentPointerUtil.createSignalAttachmentPointer(this)
}
fun Long.toMobileCoinMoney(): Money {
return Money.picoMobileCoin(this)
}
}

View File

@@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.messages
import android.graphics.Color
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.database.MessageTable.InsertResult
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.database.model.toBodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.warn
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toPointer
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
object StoryMessageProcessor {
fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, senderRecipient: Recipient, threadRecipient: Recipient) {
val storyMessage = content.storyMessage
log(envelope.timestamp, "Story message.")
if (threadRecipient.isInactiveGroup) {
warn(envelope.timestamp, "Dropping a group story from a group we're no longer in.")
return
}
if (threadRecipient.isGroup && !SignalDatabase.groups.isCurrentMember(threadRecipient.requireGroupId().requirePush(), senderRecipient.id)) {
warn(envelope.timestamp, "Dropping a group story from a user who's no longer a member.")
return
}
if (!threadRecipient.isGroup && !(senderRecipient.isProfileSharing || senderRecipient.isSystemContact)) {
warn(envelope.timestamp, "Dropping story from an untrusted source.")
return
}
val insertResult: InsertResult?
SignalDatabase.messages.beginTransaction()
try {
val storyType: StoryType = if (storyMessage.hasAllowsReplies() && storyMessage.allowsReplies) {
StoryType.withReplies(storyMessage.hasTextAttachment())
} else {
StoryType.withoutReplies(storyMessage.hasTextAttachment())
}
val mediaMessage = IncomingMediaMessage(
from = senderRecipient.id,
sentTimeMillis = envelope.timestamp,
serverTimeMillis = envelope.serverTimestamp,
receivedTimeMillis = System.currentTimeMillis(),
storyType = storyType,
isUnidentified = metadata.sealedSender,
body = serializeTextAttachment(storyMessage),
groupId = storyMessage.group.groupId,
attachments = if (storyMessage.hasFileAttachment()) listOfNotNull(storyMessage.fileAttachment.toPointer()) else emptyList(),
linkPreviews = DataMessageProcessor.getLinkPreviews(
previews = if (storyMessage.textAttachment.hasPreview()) listOf(storyMessage.textAttachment.preview) else emptyList(),
body = "",
isStoryEmbed = true
),
serverGuid = envelope.serverGuid,
messageRanges = storyMessage.bodyRangesList.filterNot { it.hasMentionUuid() }.toBodyRangeList()
)
insertResult = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mediaMessage, -1).orNull()
if (insertResult != null) {
SignalDatabase.messages.setTransactionSuccessful()
}
} catch (e: MmsException) {
throw MessageContentProcessor.StorageFailedException(e, metadata.sourceServiceId.toString(), metadata.sourceDeviceId)
} finally {
SignalDatabase.messages.endTransaction()
}
if (insertResult != null) {
Stories.enqueueNextStoriesForDownload(threadRecipient.id, false, FeatureFlags.storiesAutoDownloadMaximum())
ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary()
}
}
fun serializeTextAttachment(story: SignalServiceProtos.StoryMessage): String? {
if (!story.hasTextAttachment()) {
return null
}
val textAttachment = story.textAttachment
val builder = StoryTextPost.newBuilder()
if (textAttachment.hasText()) {
builder.body = textAttachment.text
}
if (textAttachment.hasTextStyle()) {
when (textAttachment.textStyle) {
SignalServiceProtos.TextAttachment.Style.DEFAULT -> builder.style = StoryTextPost.Style.DEFAULT
SignalServiceProtos.TextAttachment.Style.REGULAR -> builder.style = StoryTextPost.Style.REGULAR
SignalServiceProtos.TextAttachment.Style.BOLD -> builder.style = StoryTextPost.Style.BOLD
SignalServiceProtos.TextAttachment.Style.SERIF -> builder.style = StoryTextPost.Style.SERIF
SignalServiceProtos.TextAttachment.Style.SCRIPT -> builder.style = StoryTextPost.Style.SCRIPT
SignalServiceProtos.TextAttachment.Style.CONDENSED -> builder.style = StoryTextPost.Style.CONDENSED
else -> Unit
}
}
if (textAttachment.hasTextBackgroundColor()) {
builder.textBackgroundColor = textAttachment.textBackgroundColor
}
if (textAttachment.hasTextForegroundColor()) {
builder.textForegroundColor = textAttachment.textForegroundColor
}
val chatColorBuilder = ChatColor.newBuilder()
if (textAttachment.hasColor()) {
chatColorBuilder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(textAttachment.color))
} else if (textAttachment.hasGradient()) {
val gradient = textAttachment.gradient
val linearGradientBuilder = ChatColor.LinearGradient.newBuilder()
linearGradientBuilder.rotation = gradient.angle.toFloat()
if (gradient.positionsList.size > 1 && gradient.colorsList.size == gradient.positionsList.size) {
val positions = ArrayList(gradient.positionsList)
positions[0] = 0f
positions[positions.size - 1] = 1f
linearGradientBuilder.addAllColors(ArrayList(gradient.colorsList))
linearGradientBuilder.addAllPositions(positions)
} else if (gradient.colorsList.isNotEmpty()) {
warn("Incoming text story has color / position mismatch. Defaulting to start and end colors.")
linearGradientBuilder.addColors(gradient.colorsList[0])
linearGradientBuilder.addColors(gradient.colorsList[gradient.colorsList.size - 1])
linearGradientBuilder.addAllPositions(listOf(0f, 1f))
} else {
warn("Incoming text story did not have a valid linear gradient.")
linearGradientBuilder.addAllColors(listOf(Color.BLACK, Color.BLACK))
linearGradientBuilder.addAllPositions(listOf(0f, 1f))
}
chatColorBuilder.setLinearGradient(linearGradientBuilder)
}
builder.setBackground(chatColorBuilder)
return Base64.encodeBytes(builder.build().toByteArray())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
class QuoteModel(
val id: Long,
@@ -47,6 +48,14 @@ class QuoteModel(
}
return NORMAL
}
fun fromProto(type: SignalServiceProtos.DataMessage.Quote.Type): Type {
return if (type == SignalServiceProtos.DataMessage.Quote.Type.GIFT_BADGE) {
GIFT_BADGE
} else {
NORMAL
}
}
}
}
}

View File

@@ -11,6 +11,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Sometimes a message that is referencing another message can arrive out of order. In these cases,
@@ -19,7 +20,8 @@ import java.util.Optional;
*/
public final class EarlyMessageCache {
private final LRUCache<ServiceMessageId, List<SignalServiceContent>> cache = new LRUCache<>(100);
private final LRUCache<ServiceMessageId, List<SignalServiceContent>> cache = new LRUCache<>(100);
private final LRUCache<ServiceMessageId, List<EarlyMessageCacheEntry>> cacheV2 = new LRUCache<>(100);
/**
* @param targetSender The sender of the message this message depends on.
@@ -38,8 +40,25 @@ public final class EarlyMessageCache {
cache.put(messageId, contentList);
}
public synchronized void store(@NonNull RecipientId targetSender,
long targetSentTimestamp,
@NonNull EarlyMessageCacheEntry cacheEntry)
{
ServiceMessageId messageId = new ServiceMessageId(targetSender, targetSentTimestamp);
List<EarlyMessageCacheEntry> envelopeList = cacheV2.get(messageId);
if (envelopeList == null) {
envelopeList = new LinkedList<>();
}
envelopeList.add(cacheEntry);
cacheV2.put(messageId, envelopeList);
}
/**
* Returns and removes any content that is dependent on the provided message id.
*
* @param sender The sender of the message in question.
* @param sentTimestamp The sent timestamp of the message in question.
*/
@@ -47,11 +66,17 @@ public final class EarlyMessageCache {
return Optional.ofNullable(cache.remove(new ServiceMessageId(sender, sentTimestamp)));
}
public synchronized Optional<List<EarlyMessageCacheEntry>> retrieveV2(@NonNull RecipientId sender, long sentTimestamp) {
return Optional.ofNullable(cacheV2.remove(new ServiceMessageId(sender, sentTimestamp)));
}
/**
* Returns a collection of all of the {@link ServiceMessageId}s referenced in the cache at the moment of inquiry.
* Caution: There is no guarantee that this list will be relevant for any amount of time afterwards.
*/
public synchronized @NonNull Collection<ServiceMessageId> getAllReferencedIds() {
return new HashSet<>(cache.keySet());
Set<ServiceMessageId> allIds = new HashSet<>(cache.keySet());
allIds.addAll(cacheV2.keySet());
return allIds;
}
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.util
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
/**
* The tuple of information needed to process a message. Used to in [EarlyMessageCache]
* to store potentially out-of-order messages.
*/
data class EarlyMessageCacheEntry(
val envelope: Envelope,
val content: Content,
val metadata: EnvelopeMetadata,
val serverDeliveredTimestamp: Long
)

View File

@@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
@@ -16,12 +15,14 @@ import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil;
import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
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.internal.push.SignalServiceProtos;
import java.io.IOException;
import java.util.List;
@@ -55,6 +56,23 @@ public final class GroupUtil {
}
}
public static @Nullable SignalServiceProtos.GroupContextV2 getGroupContextIfPresent(@NonNull SignalServiceProtos.Content content) {
if (content.hasDataMessage() && SignalServiceProtoUtil.INSTANCE.getHasGroupContext(content.getDataMessage())) {
return content.getDataMessage().getGroupV2();
} else if (content.hasSyncMessage() &&
content.getSyncMessage().hasSent() &&
content.getSyncMessage().getSent().hasMessage() &&
SignalServiceProtoUtil.INSTANCE.getHasGroupContext(content.getSyncMessage().getSent().getMessage()))
{
return content.getSyncMessage().getSent().getMessage().getGroupV2();
} else if (content.hasStoryMessage() && SignalServiceProtoUtil.INSTANCE.isValid(content.getStoryMessage().getGroup())) {
return content.getStoryMessage().getGroup();
} else {
return null;
}
}
/**
* Result may be a v1 or v2 GroupId.
*/

View File

@@ -7,8 +7,10 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.state.SessionRecord;
import org.signal.libsignal.protocol.state.SessionStore;
@@ -23,7 +25,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.notifications.v2.ConversationId;
@@ -35,10 +36,11 @@ import org.thoughtcrime.securesms.sms.IncomingIdentityVerifiedMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.signal.core.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.List;
import java.util.Optional;
@@ -175,6 +177,28 @@ public final class IdentityUtil {
}
}
public static void processVerifiedMessage(Context context, SignalServiceProtos.Verified verified) throws InvalidKeyException {
SignalServiceAddress destination = new SignalServiceAddress(ServiceId.parseOrThrow(verified.getDestinationUuid()));
IdentityKey identityKey = new IdentityKey(verified.getIdentityKey().toByteArray(), 0);
VerifiedMessage.VerifiedState state;
switch (verified.getState()) {
case DEFAULT:
state = VerifiedMessage.VerifiedState.DEFAULT;
break;
case VERIFIED:
state = VerifiedMessage.VerifiedState.VERIFIED;
break;
case UNVERIFIED:
state = VerifiedMessage.VerifiedState.UNVERIFIED;
break;
default:
throw new IllegalArgumentException();
}
processVerifiedMessage(context, new VerifiedMessage(destination, identityKey, state, System.currentTimeMillis()));
}
public static void processVerifiedMessage(Context context, VerifiedMessage verifiedMessage) {
try(SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
SignalIdentityKeyStore identityStore = ApplicationDependencies.getProtocolStore().aci().identities();

View File

@@ -6,6 +6,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@@ -18,18 +19,23 @@ public final class RemoteDeleteUtil {
private RemoteDeleteUtil() {}
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull Recipient deleteSender, long deleteServerTimestamp) {
boolean isValidIncomingOutgoing = (deleteSender.isSelf() && targetMessage.isOutgoing()) ||
(!deleteSender.isSelf() && !targetMessage.isOutgoing());
return isValidReceive(targetMessage, deleteSender.getId(), deleteServerTimestamp);
}
boolean isValidSender = targetMessage.getIndividualRecipient().equals(deleteSender) ||
deleteSender.isSelf() && targetMessage.isOutgoing();
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull RecipientId deleteSenderId, long deleteServerTimestamp) {
boolean selfIsDeleteSender = isSelf(deleteSenderId);
long messageTimestamp = deleteSender.isSelf() && targetMessage.isOutgoing() ? targetMessage.getDateSent()
: targetMessage.getServerTimestamp();
boolean isValidIncomingOutgoing = (selfIsDeleteSender && targetMessage.isOutgoing()) ||
(!selfIsDeleteSender && !targetMessage.isOutgoing());
boolean isValidSender = targetMessage.getIndividualRecipient().getId().equals(deleteSenderId) || selfIsDeleteSender && targetMessage.isOutgoing();
long messageTimestamp = selfIsDeleteSender && targetMessage.isOutgoing() ? targetMessage.getDateSent()
: targetMessage.getServerTimestamp();
return isValidIncomingOutgoing &&
isValidSender &&
(((deleteServerTimestamp - messageTimestamp) < RECEIVE_THRESHOLD) || (deleteSender.isSelf() && targetMessage.isOutgoing()));
isValidSender &&
(((deleteServerTimestamp - messageTimestamp) < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing()));
}
public static boolean isValidSend(@NonNull Collection<MessageRecord> targetMessages, long currentTime) {
@@ -38,13 +44,17 @@ public final class RemoteDeleteUtil {
}
private static boolean isValidSend(MessageRecord message, long currentTime) {
return !message.isUpdate() &&
message.isOutgoing() &&
message.isPush() &&
return !message.isUpdate() &&
message.isOutgoing() &&
message.isPush() &&
(!message.getRecipient().isGroup() || message.getRecipient().isActiveGroup()) &&
!message.isRemoteDelete() &&
!MessageRecordUtil.hasGiftBadge(message) &&
!message.isPaymentNotification() &&
!message.isRemoteDelete() &&
!MessageRecordUtil.hasGiftBadge(message) &&
!message.isPaymentNotification() &&
(((currentTime - message.getDateSent()) < SEND_THRESHOLD) || message.getRecipient().isSelf());
}
private static boolean isSelf(@NonNull RecipientId recipientId) {
return Recipient.isSelfSet() && Recipient.self().getId().equals(recipientId);
}
}