That fuck shit the fascists are using
1/*
2 * Copyright (C) 2011 Whisper Systems
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17package org.tm.archive.sms;
18
19import android.content.Context;
20import android.os.Parcel;
21import android.os.Parcelable;
22
23import androidx.annotation.NonNull;
24import androidx.annotation.Nullable;
25import androidx.annotation.WorkerThread;
26
27import com.annimon.stream.Stream;
28
29import org.greenrobot.eventbus.EventBus;
30import org.signal.core.util.logging.Log;
31import org.tm.archive.attachments.Attachment;
32import org.tm.archive.attachments.AttachmentId;
33import org.tm.archive.attachments.DatabaseAttachment;
34import org.tm.archive.contacts.sync.ContactDiscovery;
35import org.tm.archive.contactshare.Contact;
36import org.tm.archive.database.AttachmentTable;
37import org.tm.archive.database.MessageTable;
38import org.tm.archive.database.MessageTable.SyncMessageId;
39import org.tm.archive.database.NoSuchMessageException;
40import org.tm.archive.database.RecipientTable;
41import org.tm.archive.database.SignalDatabase;
42import org.tm.archive.database.ThreadTable;
43import org.tm.archive.database.model.MessageId;
44import org.tm.archive.database.model.MessageRecord;
45import org.tm.archive.database.model.MmsMessageRecord;
46import org.tm.archive.database.model.ReactionRecord;
47import org.tm.archive.database.model.StoryType;
48import org.tm.archive.dependencies.ApplicationDependencies;
49import org.tm.archive.jobmanager.Job;
50import org.tm.archive.jobmanager.JobManager;
51import org.tm.archive.jobs.AttachmentCompressionJob;
52import org.tm.archive.jobs.AttachmentCopyJob;
53import org.tm.archive.jobs.AttachmentMarkUploadedJob;
54import org.tm.archive.jobs.AttachmentUploadJob;
55import org.tm.archive.jobs.ProfileKeySendJob;
56import org.tm.archive.jobs.PushDistributionListSendJob;
57import org.tm.archive.jobs.PushGroupSendJob;
58import org.tm.archive.jobs.IndividualSendJob;
59import org.tm.archive.jobs.ReactionSendJob;
60import org.tm.archive.jobs.RemoteDeleteSendJob;
61import org.tm.archive.keyvalue.SignalStore;
62import org.tm.archive.linkpreview.LinkPreview;
63import org.tm.archive.mediasend.Media;
64import org.tm.archive.mms.MmsException;
65import org.tm.archive.mms.OutgoingMessage;
66import org.tm.archive.recipients.Recipient;
67import org.tm.archive.recipients.RecipientId;
68import org.tm.archive.recipients.RecipientUtil;
69import org.tm.archive.service.ExpiringMessageManager;
70import org.tm.archive.util.ParcelUtil;
71import org.tm.archive.util.SignalLocalMetrics;
72import org.tm.archive.util.TextSecurePreferences;
73import org.whispersystems.signalservice.api.push.DistributionId;
74import org.whispersystems.signalservice.api.util.Preconditions;
75
76import java.io.IOException;
77import java.util.ArrayList;
78import java.util.Arrays;
79import java.util.Collection;
80import java.util.Collections;
81import java.util.LinkedList;
82import java.util.List;
83import java.util.Objects;
84import java.util.Optional;
85import java.util.Set;
86import java.util.concurrent.TimeUnit;
87import java.util.stream.Collectors;
88
89public class MessageSender {
90
91 private static final String TAG = Log.tag(MessageSender.class);
92
93 /**
94 * Suitable for a 1:1 conversation or a GV1 group only.
95 */
96 @WorkerThread
97 public static void sendProfileKey(final long threadId) {
98 ProfileKeySendJob job = ProfileKeySendJob.create(threadId, false);
99 if (job != null) {
100 ApplicationDependencies.getJobManager().add(job);
101 }
102 }
103
104 public static void sendStories(@NonNull final Context context,
105 @NonNull final List<OutgoingMessage> messages,
106 @Nullable final String metricId,
107 @Nullable final MessageTable.InsertListener insertListener)
108 {
109 Log.i(TAG, "Sending story messages to " + messages.size() + " targets.");
110 ThreadTable threadTable = SignalDatabase.threads();
111 MessageTable database = SignalDatabase.messages();
112 List<Long> messageIds = new ArrayList<>(messages.size());
113 List<Long> threads = new ArrayList<>(messages.size());
114 UploadDependencyGraph dependencyGraph;
115
116 try {
117 database.beginTransaction();
118
119 for (OutgoingMessage message : messages) {
120 long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), -1L, message.getDistributionType());
121 long messageId = database.insertMessageOutbox(message.stripAttachments(), allocatedThreadId, false, insertListener);
122
123 messageIds.add(messageId);
124 threads.add(allocatedThreadId);
125
126 if (message.getThreadRecipient().isGroup() && message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) {
127 SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId);
128 } else {
129 SignalLocalMetrics.GroupMessageSend.cancel(metricId);
130 }
131 }
132
133 for (int i = 0; i < messageIds.size(); i++) {
134 long messageId = messageIds.get(i);
135 OutgoingMessage message = messages.get(i);
136 Recipient recipient = message.getThreadRecipient();
137
138 if (recipient.isDistributionList()) {
139 DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
140 List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
141 SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
142 }
143 }
144
145 dependencyGraph = UploadDependencyGraph.create(
146 messages,
147 ApplicationDependencies.getJobManager(),
148 attachment -> {
149 try {
150 return SignalDatabase.attachments().insertAttachmentForPreUpload(attachment);
151 } catch (MmsException e) {
152 Log.e(TAG, e);
153 throw new IllegalStateException(e);
154 }
155 }
156 );
157
158 for (int i = 0; i < messageIds.size(); i++) {
159 long messageId = messageIds.get(i);
160 OutgoingMessage message = messages.get(i);
161 List<UploadDependencyGraph.Node> nodes = dependencyGraph.getDependencyMap().get(message);
162
163 if (nodes == null || nodes.isEmpty()) {
164 if (message.getStoryType().isTextStory()) {
165 Log.d(TAG, "No attachments for given text story. Skipping.");
166 continue;
167 } else {
168 Log.e(TAG, "No attachments for given media story. Aborting.");
169 throw new MmsException("No attachment for story.");
170 }
171 }
172
173 List<AttachmentId> attachmentIds = nodes.stream().map(UploadDependencyGraph.Node::getAttachmentId).collect(Collectors.toList());
174 SignalDatabase.attachments().updateMessageId(attachmentIds, messageId, true);
175 for (final AttachmentId attachmentId : attachmentIds) {
176 SignalDatabase.attachments().updateAttachmentCaption(attachmentId, message.getBody());
177 }
178 }
179
180 database.setTransactionSuccessful();
181 } catch (MmsException e) {
182 Log.w(TAG, "Failed to send stories.", e);
183 return;
184 } finally {
185 database.endTransaction();
186 }
187
188 List<JobManager.Chain> chains = dependencyGraph.consumeDeferredQueue();
189 for (final JobManager.Chain chain : chains) {
190 chain.enqueue();
191 }
192
193 for (int i = 0; i < messageIds.size(); i++) {
194 long messageId = messageIds.get(i);
195 OutgoingMessage message = messages.get(i);
196 Recipient recipient = message.getThreadRecipient();
197 List<UploadDependencyGraph.Node> dependencies = dependencyGraph.getDependencyMap().get(message);
198
199 List<String> jobDependencyIds = (dependencies != null) ? dependencies.stream().map(UploadDependencyGraph.Node::getJobId).collect(Collectors.toList())
200 : Collections.emptyList();
201
202 sendMessageInternal(context,
203 recipient,
204 SendType.SIGNAL,
205 messageId,
206 jobDependencyIds,
207 false);
208 }
209
210 onMessageSent();
211
212 for (long threadId : threads) {
213 threadTable.update(threadId, true);
214 }
215 }
216
217 public static long send(final Context context,
218 final OutgoingMessage message,
219 final long threadId,
220 @NonNull SendType sendType,
221 @Nullable final String metricId,
222 @Nullable final MessageTable.InsertListener insertListener)
223 {
224 Log.i(TAG, "Sending media message to " + message.getThreadRecipient().getId() + ", thread: " + threadId);
225 try {
226 ThreadTable threadTable = SignalDatabase.threads();
227 MessageTable database = SignalDatabase.messages();
228
229 long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId, message.getDistributionType());
230 Recipient recipient = message.getThreadRecipient();
231 long messageId = database.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId), allocatedThreadId, sendType != SendType.SIGNAL, insertListener);
232
233 if (message.getThreadRecipient().isGroup()) {
234 if (message.getAttachments().isEmpty() && message.getLinkPreviews().isEmpty() && message.getSharedContacts().isEmpty()) {
235 SignalLocalMetrics.GroupMessageSend.onInsertedIntoDatabase(messageId, metricId);
236 } else {
237 SignalLocalMetrics.GroupMessageSend.cancel(messageId);
238 }
239 } else {
240 SignalLocalMetrics.IndividualMessageSend.onInsertedIntoDatabase(messageId, metricId);
241 }
242
243 sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList(), message.getScheduledDate() > 0);
244 onMessageSent();
245 threadTable.update(allocatedThreadId, true);
246
247 return allocatedThreadId;
248 } catch (MmsException e) {
249 Log.w(TAG, e);
250 return threadId;
251 }
252 }
253
254 public static long sendPushWithPreUploadedMedia(final Context context,
255 final OutgoingMessage message,
256 final Collection<PreUploadResult> preUploadResults,
257 final long threadId,
258 final MessageTable.InsertListener insertListener)
259 {
260 Log.i(TAG, "Sending media message with pre-uploads to " + message.getThreadRecipient().getId() + ", thread: " + threadId + ", pre-uploads: " + preUploadResults);
261 Preconditions.checkArgument(message.getAttachments().isEmpty(), "If the media is pre-uploaded, there should be no attachments on the message.");
262
263 try {
264 ThreadTable threadTable = SignalDatabase.threads();
265 MessageTable mmsDatabase = SignalDatabase.messages();
266 AttachmentTable attachmentDatabase = SignalDatabase.attachments();
267
268 Recipient recipient = message.getThreadRecipient();
269 long allocatedThreadId = threadTable.getOrCreateValidThreadId(message.getThreadRecipient(), threadId);
270 long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, recipient, message, allocatedThreadId),
271 allocatedThreadId,
272 false,
273 insertListener);
274
275 List<AttachmentId> attachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
276 List<String> jobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
277
278 attachmentDatabase.updateMessageId(attachmentIds, messageId, message.getStoryType().isStory());
279
280 sendMessageInternal(context, recipient, SendType.SIGNAL, messageId, jobIds, false);
281 onMessageSent();
282 threadTable.update(allocatedThreadId, true);
283
284 return allocatedThreadId;
285 } catch (MmsException e) {
286 Log.w(TAG, e);
287 return threadId;
288 }
289 }
290
291 public static void sendMediaBroadcast(@NonNull Context context,
292 @NonNull List<OutgoingMessage> messages,
293 @NonNull Collection<PreUploadResult> preUploadResults,
294 boolean overwritePreUploadMessageIds)
295 {
296 Log.i(TAG, "Sending media broadcast (overwrite: " + overwritePreUploadMessageIds + ") to " + Stream.of(messages).map(m -> m.getThreadRecipient().getId()).toList());
297 Preconditions.checkArgument(messages.size() > 0, "No messages!");
298 Preconditions.checkArgument(Stream.of(messages).allMatch(m -> m.getAttachments().isEmpty()), "Messages can't have attachments! They should be pre-uploaded.");
299
300 JobManager jobManager = ApplicationDependencies.getJobManager();
301 AttachmentTable attachmentDatabase = SignalDatabase.attachments();
302 MessageTable mmsDatabase = SignalDatabase.messages();
303 ThreadTable threadTable = SignalDatabase.threads();
304 List<AttachmentId> preUploadAttachmentIds = Stream.of(preUploadResults).map(PreUploadResult::getAttachmentId).toList();
305 List<String> preUploadJobIds = Stream.of(preUploadResults).map(PreUploadResult::getJobIds).flatMap(Stream::of).toList();
306 List<Long> messageIds = new ArrayList<>(messages.size());
307 List<String> messageDependsOnIds = new ArrayList<>(preUploadJobIds);
308 OutgoingMessage primaryMessage = messages.get(0);
309
310 mmsDatabase.beginTransaction();
311 try {
312 if (overwritePreUploadMessageIds) {
313 long primaryThreadId = threadTable.getOrCreateThreadIdFor(primaryMessage.getThreadRecipient(), primaryMessage.getDistributionType());
314 long primaryMessageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, primaryMessage.getThreadRecipient(), primaryMessage, primaryThreadId),
315 primaryThreadId,
316 false,
317 null);
318
319 attachmentDatabase.updateMessageId(preUploadAttachmentIds, primaryMessageId, primaryMessage.getStoryType().isStory());
320 if (primaryMessage.getStoryType() != StoryType.NONE) {
321 for (final AttachmentId preUploadAttachmentId : preUploadAttachmentIds) {
322 attachmentDatabase.updateAttachmentCaption(preUploadAttachmentId, primaryMessage.getBody());
323 }
324 }
325 messageIds.add(primaryMessageId);
326 }
327
328 List<DatabaseAttachment> preUploadAttachments = Stream.of(preUploadAttachmentIds)
329 .map(attachmentDatabase::getAttachment)
330 .toList();
331
332 if (messages.size() > 0) {
333 List<OutgoingMessage> secondaryMessages = overwritePreUploadMessageIds ? messages.subList(1, messages.size()) : messages;
334 List<List<AttachmentId>> attachmentCopies = new ArrayList<>();
335
336 for (int i = 0; i < preUploadAttachmentIds.size(); i++) {
337 attachmentCopies.add(new ArrayList<>(messages.size()));
338 }
339
340 for (OutgoingMessage secondaryMessage : secondaryMessages) {
341 long allocatedThreadId = threadTable.getOrCreateThreadIdFor(secondaryMessage.getThreadRecipient(), secondaryMessage.getDistributionType());
342 long messageId = mmsDatabase.insertMessageOutbox(applyUniversalExpireTimerIfNecessary(context, secondaryMessage.getThreadRecipient(), secondaryMessage, allocatedThreadId),
343 allocatedThreadId,
344 false,
345 null);
346 List<AttachmentId> attachmentIds = new ArrayList<>(preUploadAttachmentIds.size());
347
348 for (int i = 0; i < preUploadAttachments.size(); i++) {
349 AttachmentId attachmentId = attachmentDatabase.insertAttachmentForPreUpload(preUploadAttachments.get(i)).attachmentId;
350 attachmentCopies.get(i).add(attachmentId);
351 attachmentIds.add(attachmentId);
352 }
353
354 attachmentDatabase.updateMessageId(attachmentIds, messageId, secondaryMessage.getStoryType().isStory());
355 if (primaryMessage.getStoryType() != StoryType.NONE) {
356 for (final AttachmentId preUploadAttachmentId : attachmentIds) {
357 attachmentDatabase.updateAttachmentCaption(preUploadAttachmentId, primaryMessage.getBody());
358 }
359 }
360
361 messageIds.add(messageId);
362 }
363
364 for (int i = 0; i < attachmentCopies.size(); i++) {
365 Job copyJob = new AttachmentCopyJob(preUploadAttachmentIds.get(i), attachmentCopies.get(i));
366 jobManager.add(copyJob, preUploadJobIds);
367 messageDependsOnIds.add(copyJob.getId());
368 }
369 }
370
371 for (int i = 0; i < messageIds.size(); i++) {
372 long messageId = messageIds.get(i);
373 OutgoingMessage message = messages.get(i);
374 Recipient recipient = message.getThreadRecipient();
375
376 if (recipient.isDistributionList()) {
377 List<RecipientId> members = SignalDatabase.distributionLists().getMembers(recipient.requireDistributionListId());
378 DistributionId distributionId = Objects.requireNonNull(SignalDatabase.distributionLists().getDistributionId(recipient.requireDistributionListId()));
379 SignalDatabase.storySends().insert(messageId, members, message.getSentTimeMillis(), message.getStoryType().isStoryWithReplies(), distributionId);
380 }
381 }
382
383 onMessageSent();
384 mmsDatabase.setTransactionSuccessful();
385 } catch (MmsException e) {
386 Log.w(TAG, "Failed to send messages.", e);
387 return;
388 } finally {
389 mmsDatabase.endTransaction();
390 }
391
392 for (int i = 0; i < messageIds.size(); i++) {
393 long messageId = messageIds.get(i);
394 Recipient recipient = messages.get(i).getThreadRecipient();
395
396 if (isLocalSelfSend(context, recipient, SendType.SIGNAL)) {
397 sendLocalMediaSelf(context, messageId);
398 } else if (recipient.isPushGroup()) {
399 jobManager.add(new PushGroupSendJob(messageId, recipient.getId(), Collections.emptySet(), true, false), messageDependsOnIds, recipient.getId().toQueueKey());
400 } else if (recipient.isDistributionList()) {
401 jobManager.add(new PushDistributionListSendJob(messageId, recipient.getId(), true, Collections.emptySet()), messageDependsOnIds, recipient.getId().toQueueKey());
402 } else {
403 jobManager.add(IndividualSendJob.create(messageId, recipient, true, false), messageDependsOnIds, recipient.getId().toQueueKey());
404 }
405 }
406 }
407
408 /**
409 * @return A result if the attachment was enqueued, or null if it failed to enqueue or shouldn't
410 * be enqueued (like in the case of a local self-send).
411 */
412 public static @Nullable PreUploadResult preUploadPushAttachment(@NonNull Context context, @NonNull Attachment attachment, @Nullable Recipient recipient, @NonNull Media media) {
413 if (isLocalSelfSend(context, recipient, SendType.SIGNAL)) {
414 return null;
415 }
416 Log.i(TAG, "Pre-uploading attachment for " + (recipient != null ? recipient.getId() : "null"));
417
418 try {
419 AttachmentTable attachmentDatabase = SignalDatabase.attachments();
420 DatabaseAttachment databaseAttachment = attachmentDatabase.insertAttachmentForPreUpload(attachment);
421
422 Job compressionJob = AttachmentCompressionJob.fromAttachment(databaseAttachment, false, -1);
423 Job uploadJob = new AttachmentUploadJob(databaseAttachment.attachmentId);
424
425 ApplicationDependencies.getJobManager()
426 .startChain(compressionJob)
427 .then(uploadJob)
428 .enqueue();
429
430 return new PreUploadResult(media, databaseAttachment.attachmentId, Arrays.asList(compressionJob.getId(), uploadJob.getId()));
431 } catch (MmsException e) {
432 Log.w(TAG, "preUploadPushAttachment() - Failed to upload!", e);
433 return null;
434 }
435 }
436
437 public static void sendNewReaction(@NonNull Context context, @NonNull MessageId messageId, @NonNull String emoji) {
438 ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis());
439 SignalDatabase.reactions().addReaction(messageId, reaction);
440
441 try {
442 ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, false));
443 onMessageSent();
444 } catch (NoSuchMessageException e) {
445 Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring.");
446 }
447 }
448
449 public static void sendReactionRemoval(@NonNull Context context, @NonNull MessageId messageId, @NonNull ReactionRecord reaction) {
450 SignalDatabase.reactions().deleteReaction(messageId, reaction.getAuthor());
451
452 try {
453 ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, true));
454 onMessageSent();
455 } catch (NoSuchMessageException e) {
456 Log.w(TAG, "[sendReactionRemoval] Could not find message! Ignoring.");
457 }
458 }
459
460 public static void sendRemoteDelete(long messageId) {
461 MessageTable db = SignalDatabase.messages();
462 db.markAsRemoteDelete(messageId);
463 db.markAsSending(messageId);
464
465 try {
466 RemoteDeleteSendJob.create(messageId).enqueue();
467 onMessageSent();
468 } catch (NoSuchMessageException e) {
469 Log.w(TAG, "[sendRemoteDelete] Could not find message! Ignoring.");
470 }
471 }
472
473 public static void resendGroupMessage(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set<RecipientId> filterRecipientIds) {
474 if (!messageRecord.isMms()) throw new AssertionError("Not Group");
475 sendGroupPush(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());
476 onMessageSent();
477 }
478
479 public static void resendDistributionList(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set<RecipientId> filterRecipientIds) {
480 if (!messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getStoryType().isStory()) {
481 throw new AssertionError("Not a story");
482 }
483 sendDistributionList(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());
484 onMessageSent();
485 }
486
487 public static void resend(Context context, MessageRecord messageRecord) {
488 long messageId = messageRecord.getId();
489 boolean forceSms = messageRecord.isForcedSms();
490 Recipient recipient = messageRecord.getToRecipient();
491
492 SendType sendType;
493
494 if (forceSms) {
495 Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(messageRecord.getThreadId());
496
497 if ((threadRecipient != null && threadRecipient.isGroup()) || SignalDatabase.attachments().getAttachmentsForMessage(messageId).size() > 0) {
498 sendType = SendType.MMS;
499 } else {
500 sendType = SendType.SMS;
501 }
502 } else {
503 sendType = SendType.SIGNAL;
504 }
505
506 sendMessageInternal(context, recipient, sendType, messageId, Collections.emptyList(), false);
507
508 onMessageSent();
509 }
510
511 public static void onMessageSent() {
512 EventBus.getDefault().postSticky(MessageSentEvent.INSTANCE);
513 }
514
515 private static @NonNull OutgoingMessage applyUniversalExpireTimerIfNecessary(@NonNull Context context, @NonNull Recipient recipient, @NonNull OutgoingMessage outgoingMessage, long threadId) {
516 if (!outgoingMessage.isExpirationUpdate() && outgoingMessage.getExpiresIn() == 0 && RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, recipient, threadId)) {
517 return outgoingMessage.withExpiry(TimeUnit.SECONDS.toMillis(SignalStore.settings().getUniversalExpireTimer()));
518 }
519 return outgoingMessage;
520 }
521
522 private static void sendMessageInternal(Context context,
523 Recipient recipient,
524 SendType sendType,
525 long messageId,
526 @NonNull Collection<String> uploadJobIds,
527 boolean isScheduledSend)
528 {
529 if (isLocalSelfSend(context, recipient, sendType) && !isScheduledSend) {
530 sendLocalMediaSelf(context, messageId);
531 } else if (recipient.isPushGroup()) {
532 sendGroupPush(context, recipient, messageId, Collections.emptySet(), uploadJobIds);
533 } else if (recipient.isDistributionList()) {
534 sendDistributionList(context, recipient, messageId, Collections.emptySet(), uploadJobIds);
535 } else if (sendType == SendType.SIGNAL && isPushMediaSend(context, recipient)) {
536 sendMediaPush(context, recipient, messageId, uploadJobIds);
537 } else {
538 Log.w(TAG, "Unknown send type!");
539 }
540 }
541
542 private static void sendMediaPush(Context context, Recipient recipient, long messageId, @NonNull Collection<String> uploadJobIds) {
543 JobManager jobManager = ApplicationDependencies.getJobManager();
544
545 if (uploadJobIds.size() > 0) {
546 Job mediaSend = IndividualSendJob.create(messageId, recipient, true, false);
547 jobManager.add(mediaSend, uploadJobIds);
548 } else {
549 IndividualSendJob.enqueue(context, jobManager, messageId, recipient, false);
550 }
551 }
552
553 private static void sendGroupPush(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set<RecipientId> filterRecipientIds, @NonNull Collection<String> uploadJobIds) {
554 JobManager jobManager = ApplicationDependencies.getJobManager();
555
556 if (uploadJobIds.size() > 0) {
557 Job groupSend = new PushGroupSendJob(messageId, recipient.getId(), filterRecipientIds, !uploadJobIds.isEmpty(), false);
558 jobManager.add(groupSend, uploadJobIds, uploadJobIds.isEmpty() ? null : recipient.getId().toQueueKey());
559 } else {
560 PushGroupSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientIds, false);
561 }
562 }
563
564 private static void sendDistributionList(@NonNull Context context, @NonNull Recipient recipient, long messageId, @NonNull Set<RecipientId> filterRecipientIds, @NonNull Collection<String> uploadJobIds) {
565 JobManager jobManager = ApplicationDependencies.getJobManager();
566
567 if (uploadJobIds.size() > 0) {
568 Job groupSend = new PushDistributionListSendJob(messageId, recipient.getId(), !uploadJobIds.isEmpty(), filterRecipientIds);
569 jobManager.add(groupSend, uploadJobIds, uploadJobIds.isEmpty() ? null : recipient.getId().toQueueKey());
570 } else {
571 PushDistributionListSendJob.enqueue(context, jobManager, messageId, recipient.getId(), filterRecipientIds);
572 }
573 }
574
575 private static boolean isPushMediaSend(Context context, Recipient recipient) {
576 if (!SignalStore.account().isRegistered()) {
577 return false;
578 }
579
580 if (recipient.isGroup()) {
581 return false;
582 }
583
584 return isPushDestination(context, recipient);
585 }
586
587 private static boolean isPushDestination(Context context, Recipient destination) {
588 if (destination.resolve().getRegistered() == RecipientTable.RegisteredState.REGISTERED) {
589 return true;
590 } else if (destination.resolve().getRegistered() == RecipientTable.RegisteredState.NOT_REGISTERED) {
591 return false;
592 } else {
593 try {
594 RecipientTable.RegisteredState state = ContactDiscovery.refresh(context, destination, false);
595 return state == RecipientTable.RegisteredState.REGISTERED;
596 } catch (IOException e1) {
597 Log.w(TAG, e1);
598 return false;
599 }
600 }
601 }
602
603 public static boolean isLocalSelfSend(@NonNull Context context, @Nullable Recipient recipient, SendType sendType) {
604 return recipient != null &&
605 recipient.isSelf() &&
606 sendType == SendType.SIGNAL &&
607 SignalStore.account().isRegistered() &&
608 !TextSecurePreferences.isMultiDevice(context);
609 }
610
611 private static void sendLocalMediaSelf(Context context, long messageId) {
612 try {
613 ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
614 MessageTable mmsDatabase = SignalDatabase.messages();
615 OutgoingMessage message = mmsDatabase.getOutgoingMessage(messageId);
616 SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getSentTimeMillis());
617 List<Attachment> attachments = new LinkedList<>();
618
619
620 attachments.addAll(message.getAttachments());
621
622 attachments.addAll(Stream.of(message.getLinkPreviews())
623 .map(LinkPreview::getThumbnail)
624 .filter(Optional::isPresent)
625 .map(Optional::get)
626 .toList());
627
628 attachments.addAll(Stream.of(message.getSharedContacts())
629 .map(Contact::getAvatar).withoutNulls()
630 .map(Contact.Avatar::getAttachment).withoutNulls()
631 .toList());
632
633 List<AttachmentCompressionJob> compressionJobs = Stream.of(attachments)
634 .map(a -> AttachmentCompressionJob.fromAttachment((DatabaseAttachment) a, false, -1))
635 .toList();
636
637 List<AttachmentMarkUploadedJob> fakeUploadJobs = Stream.of(attachments)
638 .map(a -> new AttachmentMarkUploadedJob(messageId, ((DatabaseAttachment) a).attachmentId))
639 .toList();
640
641 ApplicationDependencies.getJobManager().startChain(compressionJobs)
642 .then(fakeUploadJobs)
643 .enqueue();
644
645 mmsDatabase.markAsSent(messageId, true);
646 mmsDatabase.markUnidentified(messageId, true);
647
648 mmsDatabase.incrementDeliveryReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis());
649 mmsDatabase.incrementReadReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis());
650 mmsDatabase.incrementViewedReceiptCount(message.getSentTimeMillis(), Recipient.self().getId(), System.currentTimeMillis());
651
652 if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
653 mmsDatabase.markExpireStarted(messageId);
654 expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
655 }
656 } catch (NoSuchMessageException | MmsException e) {
657 Log.w(TAG, "Failed to update self-sent message.", e);
658 }
659 }
660
661 public static class PreUploadResult implements Parcelable {
662 private final Media media;
663 private final AttachmentId attachmentId;
664 private final Collection<String> jobIds;
665
666 PreUploadResult(@NonNull Media media, @NonNull AttachmentId attachmentId, @NonNull Collection<String> jobIds) {
667 this.media = media;
668 this.attachmentId = attachmentId;
669 this.jobIds = jobIds;
670 }
671
672 private PreUploadResult(Parcel in) {
673 this.attachmentId = in.readParcelable(AttachmentId.class.getClassLoader());
674 this.jobIds = ParcelUtil.readStringCollection(in);
675 this.media = in.readParcelable(Media.class.getClassLoader());
676 }
677
678 public @NonNull AttachmentId getAttachmentId() {
679 return attachmentId;
680 }
681
682 public @NonNull Collection<String> getJobIds() {
683 return jobIds;
684 }
685
686 public @NonNull Media getMedia() {
687 return media;
688 }
689
690 public static final Creator<PreUploadResult> CREATOR = new Creator<PreUploadResult>() {
691 @Override
692 public PreUploadResult createFromParcel(Parcel in) {
693 return new PreUploadResult(in);
694 }
695
696 @Override
697 public PreUploadResult[] newArray(int size) {
698 return new PreUploadResult[size];
699 }
700 };
701
702 @Override
703 public int describeContents() {
704 return 0;
705 }
706
707 @Override
708 public void writeToParcel(Parcel dest, int flags) {
709 dest.writeParcelable(attachmentId, flags);
710 ParcelUtil.writeStringCollection(dest, jobIds);
711 dest.writeParcelable(media, flags);
712 }
713
714 @Override
715 public @NonNull String toString() {
716 return "{ID: " + attachmentId.id + ", URI: " + media.getUri() + ", Jobs: " + jobIds.stream().map(j -> "JOB::" + j).collect(Collectors.toList()) + "}";
717 }
718 }
719
720 public enum MessageSentEvent {
721 INSTANCE
722 }
723
724 public enum SendType {
725 SIGNAL, SMS, MMS
726 }
727}