package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import com.annimon.stream.Stream; import org.signal.core.util.SetUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import java.io.IOException; import java.util.List; import java.util.concurrent.TimeUnit; public class RemoteDeleteSendJob extends BaseJob { public static final String KEY = "RemoteDeleteSendJob"; private static final String TAG = Log.tag(RemoteDeleteSendJob.class); private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_IS_MMS = "is_mms"; private static final String KEY_RECIPIENTS = "recipients"; private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count"; private final long messageId; private final boolean isMms; private final List recipients; private final int initialRecipientCount; @WorkerThread public static @NonNull JobManager.Chain create(long messageId, boolean isMms) throws NoSuchMessageException { MessageRecord message = isMms ? SignalDatabase.mms().getMessageRecord(messageId) : SignalDatabase.sms().getSmsMessage(messageId); Recipient conversationRecipient = SignalDatabase.threads().getRecipientForThreadId(message.getThreadId()); if (conversationRecipient == null) { throw new AssertionError("We have a message, but couldn't find the thread!"); } List recipients; if (conversationRecipient.isDistributionList()) { recipients = SignalDatabase.storySends().getRemoteDeleteRecipients(message.getId(), message.getTimestamp()); if (recipients.isEmpty()) { return ApplicationDependencies.getJobManager().startChain(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId)); } } else { recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList() : Stream.of(conversationRecipient.getId()).toList(); } recipients.remove(Recipient.self().getId()); RemoteDeleteSendJob sendJob = new RemoteDeleteSendJob(messageId, isMms, recipients, recipients.size(), new Parameters.Builder() .setQueue(conversationRecipient.getId().toQueueKey()) .setLifespan(TimeUnit.DAYS.toMillis(1)) .setMaxAttempts(Parameters.UNLIMITED) .build()); if (conversationRecipient.isDistributionList()) { return ApplicationDependencies.getJobManager() .startChain(sendJob) .then(MultiDeviceStorySendSyncJob.create(message.getDateSent(), messageId)); } else { return ApplicationDependencies.getJobManager().startChain(sendJob); } } private RemoteDeleteSendJob(long messageId, boolean isMms, @NonNull List recipients, int initialRecipientCount, @NonNull Parameters parameters) { super(parameters); this.messageId = messageId; this.isMms = isMms; this.recipients = recipients; this.initialRecipientCount = initialRecipientCount; } @Override public @NonNull Data serialize() { return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) .putBoolean(KEY_IS_MMS, isMms) .putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients)) .putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount) .build(); } @Override public @NonNull String getFactoryKey() { return KEY; } @Override protected void onRun() throws Exception { if (!Recipient.self().isRegistered()) { throw new NotPushRegisteredException(); } MessageDatabase db; MessageRecord message; if (isMms) { db = SignalDatabase.mms(); message = SignalDatabase.mms().getMessageRecord(messageId); } else { db = SignalDatabase.sms(); message = SignalDatabase.sms().getSmsMessage(messageId); } long targetSentTimestamp = message.getDateSent(); Recipient conversationRecipient = SignalDatabase.threads().getRecipientForThreadId(message.getThreadId()); if (conversationRecipient == null) { throw new AssertionError("We have a message, but couldn't find the thread!"); } if (!message.isOutgoing()) { throw new IllegalStateException("Cannot delete a message that isn't yours!"); } List possible = Stream.of(recipients).map(Recipient::resolved).toList(); List eligible = RecipientUtil.getEligibleForSending(Stream.of(recipients).map(Recipient::resolved).toList()); List skipped = Stream.of(SetUtil.difference(possible, eligible)).map(Recipient::getId).toList(); GroupSendJobHelper.SendResult sendResult = deliver(conversationRecipient, eligible, targetSentTimestamp); for (Recipient completion : sendResult.completed) { recipients.remove(completion.getId()); } for (RecipientId skip : skipped) { recipients.remove(skip); } List totalSkips = Util.join(skipped, sendResult.skipped); Log.i(TAG, "Completed now: " + sendResult.completed.size() + ", Skipped: " + totalSkips.size() + ", Remaining: " + recipients.size()); if (totalSkips.size() > 0 && isMms && message.getRecipient().isGroup()) { SignalDatabase.groupReceipts().setSkipped(totalSkips, messageId); } if (recipients.isEmpty()) { db.markAsSent(messageId, true); } else { Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying."); throw new RetryLaterException(); } } @Override protected boolean onShouldRetry(@NonNull Exception e) { if (e instanceof ServerRejectedException) return false; if (e instanceof NotPushRegisteredException) return false; return e instanceof IOException || e instanceof RetryLaterException; } @Override public void onFailure() { Log.w(TAG, "Failed to send remote delete to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") ); } private @NonNull GroupSendJobHelper.SendResult deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations, long targetSentTimestamp) throws IOException, UntrustedIdentityException { SignalServiceDataMessage.Builder dataMessageBuilder = SignalServiceDataMessage.newBuilder() .withTimestamp(System.currentTimeMillis()) .withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp)); if (conversationRecipient.isGroup()) { GroupUtil.setDataMessageGroupContext(context, dataMessageBuilder, conversationRecipient.requireGroupId().requirePush()); } SignalServiceDataMessage dataMessage = dataMessageBuilder.build(); List results = GroupSendUtil.sendResendableDataMessage(context, conversationRecipient.getGroupId().map(GroupId::requireV2).orElse(null), destinations, false, ContentHint.RESENDABLE, new MessageId(messageId, isMms), dataMessage); return GroupSendJobHelper.getCompletedSends(destinations, results); } public static class Factory implements Job.Factory { @Override public @NonNull RemoteDeleteSendJob create(@NonNull Parameters parameters, @NonNull Data data) { long messageId = data.getLong(KEY_MESSAGE_ID); boolean isMms = data.getBoolean(KEY_IS_MMS); List recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS)); int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT); return new RemoteDeleteSendJob(messageId, isMms, recipients, initialRecipientCount, parameters); } } }