That fuck shit the fascists are using
1package org.tm.archive.sharing;
2
3import android.content.Context;
4import android.graphics.Color;
5import android.net.Uri;
6
7import androidx.annotation.MainThread;
8import androidx.annotation.NonNull;
9import androidx.annotation.Nullable;
10import androidx.annotation.WorkerThread;
11import androidx.core.util.Consumer;
12
13import com.annimon.stream.Stream;
14
15import org.signal.core.util.BreakIteratorCompat;
16import org.signal.core.util.ThreadUtil;
17import org.signal.core.util.concurrent.SimpleTask;
18import org.signal.core.util.logging.Log;
19import org.tm.archive.attachments.Attachment;
20import org.tm.archive.attachments.UriAttachment;
21import org.tm.archive.contacts.paged.ContactSearchKey;
22import org.tm.archive.contactshare.Contact;
23import org.tm.archive.conversation.MessageSendType;
24import org.tm.archive.conversation.colors.ChatColors;
25import org.tm.archive.database.AttachmentTable;
26import org.tm.archive.database.SignalDatabase;
27import org.tm.archive.database.model.Mention;
28import org.tm.archive.database.model.StoryType;
29import org.tm.archive.database.model.databaseprotos.StoryTextPost;
30import org.tm.archive.dependencies.ApplicationDependencies;
31import org.tm.archive.keyvalue.SignalStore;
32import org.tm.archive.keyvalue.StorySend;
33import org.tm.archive.linkpreview.LinkPreview;
34import org.tm.archive.mediasend.Media;
35import org.tm.archive.mediasend.v2.text.TextStoryBackgroundColors;
36import org.tm.archive.mms.ImageSlide;
37import org.tm.archive.mms.OutgoingMessage;
38import org.tm.archive.mms.SentMediaQuality;
39import org.tm.archive.mms.Slide;
40import org.tm.archive.mms.SlideDeck;
41import org.tm.archive.mms.SlideFactory;
42import org.tm.archive.mms.StickerSlide;
43import org.tm.archive.mms.VideoSlide;
44import org.tm.archive.recipients.Recipient;
45import org.tm.archive.recipients.RecipientId;
46import org.tm.archive.sms.MessageSender;
47import org.tm.archive.sms.MessageSender.SendType;
48import org.tm.archive.stories.Stories;
49import org.signal.core.util.Base64;
50import org.tm.archive.util.MediaUtil;
51import org.tm.archive.util.MessageUtil;
52import org.tm.archive.util.Util;
53
54import java.util.ArrayList;
55import java.util.Collection;
56import java.util.Collections;
57import java.util.HashSet;
58import java.util.LinkedList;
59import java.util.List;
60import java.util.Objects;
61import java.util.Set;
62import java.util.concurrent.TimeUnit;
63import java.util.stream.Collectors;
64
65/**
66 * MultiShareSender encapsulates send logic (stolen from {@link org.tm.archive.conversation.ConversationActivity}
67 * and provides a means to:
68 * <p>
69 * 1. Send messages based off a {@link MultiShareArgs} object and
70 * 1. Parse through the result of the send via a {@link MultiShareSendResultCollection}
71 */
72public final class MultiShareSender {
73
74 private static final String TAG = Log.tag(MultiShareSender.class);
75
76 private MultiShareSender() {
77 }
78
79 @MainThread
80 public static void send(@NonNull MultiShareArgs multiShareArgs, @NonNull Consumer<MultiShareSendResultCollection> results) {
81 SimpleTask.run(() -> sendSync(multiShareArgs), results::accept);
82 }
83
84 @WorkerThread
85 public static MultiShareSendResultCollection sendSync(@NonNull MultiShareArgs multiShareArgs) {
86 List<MultiShareSendResult> results = new ArrayList<>(multiShareArgs.getContactSearchKeys().size());
87 Context context = ApplicationDependencies.getApplication();
88 String message = multiShareArgs.getDraftText();
89 SlideDeck slideDeck;
90 List<OutgoingMessage> storiesBatch = new LinkedList<>();
91 ChatColors generatedTextStoryBackgroundColor = TextStoryBackgroundColors.getRandomBackgroundColor();
92
93 try {
94 slideDeck = buildSlideDeck(context, multiShareArgs);
95 } catch (SlideNotFoundException e) {
96 Log.w(TAG, "Could not create slide for media message");
97 for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
98 results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.GENERIC_ERROR));
99 }
100
101 return new MultiShareSendResultCollection(results);
102 }
103
104 DistributionListMultiShareTimestampProvider distributionListSentTimestamps = DistributionListMultiShareTimestampProvider.create();
105 for (ContactSearchKey.RecipientSearchKey recipientSearchKey : multiShareArgs.getRecipientSearchKeys()) {
106 Recipient recipient = Recipient.resolved(recipientSearchKey.getRecipientId());
107
108 long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
109 List<Mention> mentions = getValidMentionsForRecipient(recipient, multiShareArgs.getMentions());
110 MessageSendType sendType = MessageSendType.SignalMessageSendType.INSTANCE;
111 long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
112 List<Contact> contacts = multiShareArgs.getSharedContacts();
113 boolean needsSplit = message != null &&
114 message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
115 boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() ||
116 (multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) ||
117 multiShareArgs.getStickerLocator() != null ||
118 recipient.isGroup() ||
119 recipient.getEmail().isPresent();
120 boolean hasPushMedia = hasMmsMedia ||
121 multiShareArgs.getLinkPreview() != null ||
122 !mentions.isEmpty() ||
123 needsSplit ||
124 !contacts.isEmpty();
125
126 MultiShareTimestampProvider sentTimestamp = recipient.isDistributionList() ? distributionListSentTimestamps : MultiShareTimestampProvider.create();
127 boolean canSendAsTextStory = recipientSearchKey.isStory() && multiShareArgs.isValidForTextStoryGeneration();
128
129 if ((recipient.isMmsGroup() || recipient.getEmail().isPresent())) {
130 results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.MMS_NOT_ENABLED));
131 } else if (hasPushMedia || canSendAsTextStory) {
132 sendMediaMessageOrCollectStoryToBatch(context,
133 multiShareArgs,
134 recipient,
135 slideDeck,
136 sendType,
137 threadId,
138 expiresIn,
139 multiShareArgs.isViewOnce(),
140 mentions,
141 recipientSearchKey.isStory(),
142 sentTimestamp,
143 canSendAsTextStory,
144 storiesBatch,
145 generatedTextStoryBackgroundColor,
146 contacts);
147 results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS));
148 } else if (recipientSearchKey.isStory()) {
149 results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.INVALID_SHARE_TO_STORY));
150 } else {
151 sendTextMessage(context, multiShareArgs, recipient, threadId, expiresIn);
152 results.add(new MultiShareSendResult(recipientSearchKey, MultiShareSendResult.Type.SUCCESS));
153 }
154
155 if (!recipientSearchKey.isStory()) {
156 SignalDatabase.threads().setRead(threadId, true);
157 }
158
159 // XXX We must do this to avoid sending out messages to the same recipient with the same
160 // sentTimestamp. If we do this, they'll be considered dupes by the receiver.
161 ThreadUtil.sleep(5);
162 }
163
164 if (!storiesBatch.isEmpty()) {
165 MessageSender.sendStories(context,
166 storiesBatch.stream()
167 .map(OutgoingMessage::makeSecure)
168 .collect(Collectors.toList()),
169 null,
170 null);
171 }
172
173 return new MultiShareSendResultCollection(results);
174 }
175
176 private static void sendMediaMessageOrCollectStoryToBatch(@NonNull Context context,
177 @NonNull MultiShareArgs multiShareArgs,
178 @NonNull Recipient recipient,
179 @NonNull SlideDeck slideDeck,
180 @NonNull MessageSendType sendType,
181 long threadId,
182 long expiresIn,
183 boolean isViewOnce,
184 @NonNull List<Mention> validatedMentions,
185 boolean isStory,
186 @NonNull MultiShareTimestampProvider sentTimestamps,
187 boolean canSendAsTextStory,
188 @NonNull List<OutgoingMessage> storiesToBatchSend,
189 @NonNull ChatColors generatedTextStoryBackgroundColor,
190 @NonNull List<Contact> contacts)
191 {
192 String body = multiShareArgs.getDraftText();
193 if (sendType.usesSignalTransport() && body != null) {
194 MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(context, body, sendType.calculateCharacters(body).maxPrimaryMessageSize);
195 body = splitMessage.getBody();
196
197 if (splitMessage.getTextSlide().isPresent()) {
198 slideDeck.addSlide(splitMessage.getTextSlide().get());
199 }
200 }
201
202 List<OutgoingMessage> outgoingMessages = new ArrayList<>();
203
204 if (isStory) {
205 final StoryType storyType;
206 if (recipient.isDistributionList()) {
207 storyType = SignalDatabase.distributionLists().getStoryType(recipient.requireDistributionListId());
208 } else {
209 storyType = StoryType.STORY_WITH_REPLIES;
210 }
211
212 if (!recipient.isMyStory()) {
213 SignalStore.storyValues().setLatestStorySend(StorySend.newSend(recipient));
214 }
215
216 if (multiShareArgs.isTextStory()) {
217 OutgoingMessage outgoingMessage = new OutgoingMessage(recipient,
218 new SlideDeck(),
219 body,
220 sentTimestamps.getMillis(0),
221 0L,
222 false,
223 storyType.toTextStoryType(),
224 buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
225 Collections.emptyList(),
226 false,
227 multiShareArgs.getBodyRanges(),
228 contacts);
229
230 outgoingMessages.add(outgoingMessage);
231 } else if (canSendAsTextStory) {
232 outgoingMessages.add(generateTextStory(context, recipient, multiShareArgs, sentTimestamps.getMillis(0), storyType, generatedTextStoryBackgroundColor));
233 } else {
234 List<Slide> storySupportedSlides = slideDeck.getSlides()
235 .stream()
236 .flatMap(slide -> {
237 if (slide instanceof VideoSlide) {
238 return expandToClips(context, (VideoSlide) slide).stream();
239 } else if (slide instanceof ImageSlide) {
240 return java.util.stream.Stream.of(ensureDefaultQuality(context, (ImageSlide) slide));
241 } else if (slide instanceof StickerSlide) {
242 return java.util.stream.Stream.empty();
243 } else {
244 return java.util.stream.Stream.of(slide);
245 }
246 })
247 .filter(it -> MediaUtil.isStorySupportedType(it.getContentType()))
248 .collect(Collectors.toList());
249
250 for (int i = 0; i < storySupportedSlides.size(); i++) {
251 Slide slide = storySupportedSlides.get(i);
252 SlideDeck singletonDeck = new SlideDeck();
253
254 singletonDeck.addSlide(slide);
255
256 OutgoingMessage outgoingMessage = new OutgoingMessage(recipient,
257 singletonDeck,
258 body,
259 sentTimestamps.getMillis(i),
260 0L,
261 false,
262 storyType,
263 Collections.emptyList(),
264 validatedMentions,
265 false,
266 multiShareArgs.getBodyRanges(),
267 contacts);
268
269 outgoingMessages.add(outgoingMessage);
270 }
271 }
272 } else {
273 OutgoingMessage outgoingMessage = new OutgoingMessage(recipient,
274 slideDeck,
275 body,
276 sentTimestamps.getMillis(0),
277 expiresIn,
278 isViewOnce,
279 StoryType.NONE,
280 buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
281 validatedMentions,
282 false,
283 multiShareArgs.getBodyRanges(),
284 contacts);
285
286 outgoingMessages.add(outgoingMessage);
287 }
288
289 if (isStory) {
290 storiesToBatchSend.addAll(outgoingMessages);
291 } else if (shouldSendAsPush(recipient)) {
292 for (final OutgoingMessage outgoingMessage : outgoingMessages) {
293 MessageSender.send(context, outgoingMessage.makeSecure(), threadId, SendType.SIGNAL, null, null);
294 }
295 } else {
296 for (final OutgoingMessage outgoingMessage : outgoingMessages) {
297 MessageSender.send(context, outgoingMessage, threadId, SendType.MMS, null, null);
298 }
299 }
300 }
301
302 private static Collection<Slide> expandToClips(@NonNull Context context, @NonNull VideoSlide videoSlide) {
303 long duration = Stories.MediaTransform.getVideoDuration(Objects.requireNonNull(videoSlide.getUri()));
304 if (duration > Stories.MAX_VIDEO_DURATION_MILLIS) {
305 return Stories.MediaTransform.clipMediaToStoryDuration(Stories.MediaTransform.videoSlideToMedia(videoSlide, duration))
306 .stream()
307 .map(media -> Stories.MediaTransform.mediaToVideoSlide(context, media))
308 .collect(Collectors.toList());
309 } else if (duration == 0L) {
310 return Collections.emptyList();
311 } else {
312 return Collections.singletonList(videoSlide);
313 }
314 }
315
316 private static List<LinkPreview> buildLinkPreviews(@NonNull Context context, @Nullable LinkPreview linkPreview) {
317 if (linkPreview == null) {
318 return Collections.emptyList();
319 } else {
320 return Collections.singletonList(new LinkPreview(
321 linkPreview.getUrl(),
322 linkPreview.getTitle(),
323 linkPreview.getDescription(),
324 linkPreview.getDate(),
325 linkPreview.getThumbnail().map(thumbnail ->
326 thumbnail instanceof UriAttachment ? thumbnail
327 : thumbnail.getUri() == null
328 ? null
329 : new ImageSlide(context,
330 thumbnail.getUri(),
331 thumbnail.contentType,
332 thumbnail.size,
333 thumbnail.width,
334 thumbnail.height,
335 thumbnail.borderless,
336 thumbnail.caption,
337 thumbnail.blurHash,
338 thumbnail.transformProperties).asAttachment()
339 )
340 ));
341 }
342 }
343
344 private static Slide ensureDefaultQuality(@NonNull Context context, @NonNull ImageSlide imageSlide) {
345 Attachment attachment = imageSlide.asAttachment();
346 final AttachmentTable.TransformProperties transformProperties = attachment.transformProperties;
347 if (transformProperties != null && transformProperties.sentMediaQuality == SentMediaQuality.HIGH.getCode()) {
348 return new ImageSlide(
349 context,
350 attachment.getUri(),
351 attachment.contentType,
352 attachment.size,
353 attachment.width,
354 attachment.height,
355 attachment.borderless,
356 attachment.caption,
357 attachment.blurHash,
358 AttachmentTable.TransformProperties.empty()
359 );
360 } else {
361 return imageSlide;
362 }
363 }
364
365 private static void sendTextMessage(@NonNull Context context,
366 @NonNull MultiShareArgs multiShareArgs,
367 @NonNull Recipient recipient,
368 long threadId,
369 long expiresIn)
370 {
371 String body = multiShareArgs.getDraftText() == null ? "" : multiShareArgs.getDraftText();
372
373 OutgoingMessage outgoingMessage;
374 if (shouldSendAsPush(recipient)) {
375 outgoingMessage = OutgoingMessage.text(recipient, body, expiresIn, System.currentTimeMillis(), multiShareArgs.getBodyRanges());
376 } else {
377 outgoingMessage = OutgoingMessage.sms(recipient, body);
378 }
379
380 MessageSender.send(context, outgoingMessage, threadId, SendType.SIGNAL, null, null);
381 }
382
383 private static @NonNull OutgoingMessage generateTextStory(@NonNull Context context,
384 @NonNull Recipient recipient,
385 @NonNull MultiShareArgs multiShareArgs,
386 long sentTimestamp,
387 @NonNull StoryType storyType,
388 @NonNull ChatColors background)
389 {
390 return OutgoingMessage.textStoryMessage(
391 recipient,
392 Base64.encodeWithPadding(new StoryTextPost.Builder()
393 .body(getBodyForTextStory(multiShareArgs.getDraftText(), multiShareArgs.getLinkPreview()))
394 .style(StoryTextPost.Style.DEFAULT)
395 .background(background.serialize())
396 .textBackgroundColor(0)
397 .textForegroundColor(Color.WHITE)
398 .build()
399 .encode()),
400 sentTimestamp,
401 storyType.toTextStoryType(),
402 buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
403 multiShareArgs.getBodyRanges());
404 }
405
406 private static @NonNull String getBodyForTextStory(@Nullable String draftText, @Nullable LinkPreview linkPreview) {
407 if (Util.isEmpty(draftText)) {
408 return "";
409 }
410
411 BreakIteratorCompat breakIteratorCompat = BreakIteratorCompat.getInstance();
412 breakIteratorCompat.setText(draftText);
413
414 String trimmed = breakIteratorCompat.take(Stories.MAX_TEXT_STORY_SIZE).toString();
415 if (linkPreview == null) {
416 return trimmed;
417 }
418
419 if (linkPreview.getUrl().equals(trimmed)) {
420 return "";
421 }
422
423 return trimmed.replace(linkPreview.getUrl(), "").trim();
424 }
425
426 private static boolean shouldSendAsPush(@NonNull Recipient recipient) {
427 return recipient.isDistributionList() ||
428 recipient.isServiceIdOnly() ||
429 recipient.isRegistered();
430 }
431
432 private static @NonNull SlideDeck buildSlideDeck(@NonNull Context context, @NonNull MultiShareArgs multiShareArgs) throws SlideNotFoundException {
433 SlideDeck slideDeck = new SlideDeck();
434 if (multiShareArgs.getStickerLocator() != null) {
435 slideDeck.addSlide(new StickerSlide(context, multiShareArgs.getDataUri(), 0, multiShareArgs.getStickerLocator(), multiShareArgs.getDataType()));
436 } else if (!multiShareArgs.getMedia().isEmpty()) {
437 for (Media media : multiShareArgs.getMedia()) {
438 Slide slide = SlideFactory.getSlide(context, media.getMimeType(), media.getUri(), media.getWidth(), media.getHeight(), media.getTransformProperties().orElse(null));
439 if (slide != null) {
440 slideDeck.addSlide(slide);
441 } else {
442 throw new SlideNotFoundException();
443 }
444 }
445 } else if (multiShareArgs.getDataUri() != null) {
446 Slide slide = SlideFactory.getSlide(context, multiShareArgs.getDataType(), multiShareArgs.getDataUri(), 0, 0, null);
447 if (slide != null) {
448 slideDeck.addSlide(slide);
449 } else {
450 throw new SlideNotFoundException();
451 }
452 }
453
454 return slideDeck;
455 }
456
457 private static @NonNull List<Mention> getValidMentionsForRecipient(@NonNull Recipient recipient, @NonNull List<Mention> mentions) {
458 if (mentions.isEmpty() || !recipient.isPushV2Group() || !recipient.isActiveGroup()) {
459 return Collections.emptyList();
460 } else {
461 Set<RecipientId> validRecipientIds = new HashSet<>(recipient.getParticipantIds());
462
463 return mentions.stream()
464 .filter(mention -> validRecipientIds.contains(mention.getRecipientId()))
465 .collect(Collectors.toList());
466 }
467 }
468
469 public static final class MultiShareSendResultCollection {
470 private final List<MultiShareSendResult> results;
471
472 private MultiShareSendResultCollection(List<MultiShareSendResult> results) {
473 this.results = results;
474 }
475
476 public boolean containsFailures() {
477 return Stream.of(results).anyMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
478 }
479
480 public boolean containsOnlyFailures() {
481 return Stream.of(results).allMatch(result -> result.type != MultiShareSendResult.Type.SUCCESS);
482 }
483 }
484
485 private static final class MultiShareSendResult {
486 private final ContactSearchKey.RecipientSearchKey recipientSearchKey;
487 private final Type type;
488
489 private MultiShareSendResult(ContactSearchKey.RecipientSearchKey contactSearchKey, Type type) {
490 this.recipientSearchKey = contactSearchKey;
491 this.type = type;
492 }
493
494 public ContactSearchKey.RecipientSearchKey getContactSearchKey() {
495 return recipientSearchKey;
496 }
497
498 public Type getType() {
499 return type;
500 }
501
502 private enum Type {
503 GENERIC_ERROR,
504 INVALID_SHARE_TO_STORY,
505 MMS_NOT_ENABLED,
506 SUCCESS
507 }
508 }
509
510 private static final class SlideNotFoundException extends Exception {
511 }
512}