That fuck shit the fascists are using
at master 512 lines 26 kB view raw
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}