diff --git a/.gitignore b/.gitignore index b83d22266..de4b15720 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target/ +.test/ diff --git a/pom.xml b/pom.xml index 527d7e3f7..94a7e148f 100644 --- a/pom.xml +++ b/pom.xml @@ -139,7 +139,7 @@ 1.6.13 1.70 3.5.1 - 3.0.0 + 3.0.1 3.37.0 5.10.0-M1 5.13.0 diff --git a/src/main/java/it/auties/whatsapp/api/ErrorHandler.java b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java index 146793b42..e9c6fe51b 100644 --- a/src/main/java/it/auties/whatsapp/api/ErrorHandler.java +++ b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java @@ -69,10 +69,19 @@ static ErrorHandler defaultErrorHandler(Consumer printer) { if(printer != null) { printer.accept(throwable); } - if (location == INITIAL_APP_STATE_SYNC || (location == CRYPTOGRAPHY && type != ClientType.MOBILE) || (location == MESSAGE && throwable instanceof HmacValidationException)) { + + if(location == CRYPTOGRAPHY && type == ClientType.MOBILE) { + logger.log(WARNING, "Reconnecting"); + return Result.RECONNECT; + } + + if (location == INITIAL_APP_STATE_SYNC + || location == CRYPTOGRAPHY + || (location == MESSAGE && throwable instanceof HmacValidationException)) { logger.log(WARNING, "Socket failure at %s".formatted(location)); return Result.RESTORE; } + logger.log(WARNING, "Ignored failure"); return Result.DISCARD; }; diff --git a/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java b/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java index d11f47982..5c0463d5e 100644 --- a/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java +++ b/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java @@ -47,27 +47,27 @@ public T verificationCodeSupplier(@NonNull Supplier verificationCodeSupp } /** - * Sets the handler that provides the captcha newsletters when verifying an account - * Happens only on business devices + * Sets the handler that provides the verification code when verifying an account * - * @param verificationCaptchaSupplier the non-null supplier + * @param verificationCodeSupplier the non-null supplier * @return the same instance */ @SuppressWarnings("unchecked") - public T verificationCaptchaSupplier(@NonNull Function verificationCaptchaSupplier) { - this.verificationCaptchaSupplier = AsyncCaptchaCodeSupplier.of(verificationCaptchaSupplier); + public T verificationCodeSupplier(@NonNull AsyncVerificationCodeSupplier verificationCodeSupplier) { + this.verificationCodeSupplier = verificationCodeSupplier; return (T) this; } /** - * Sets the handler that provides the verification code when verifying an account + * Sets the handler that provides the captcha newsletters when verifying an account + * Happens only on business devices * - * @param verificationCodeSupplier the non-null supplier + * @param verificationCaptchaSupplier the non-null supplier * @return the same instance */ @SuppressWarnings("unchecked") - public T verificationCodeSupplier(@NonNull AsyncVerificationCodeSupplier verificationCodeSupplier) { - this.verificationCodeSupplier = verificationCodeSupplier; + public T verificationCaptchaSupplier(@NonNull Function verificationCaptchaSupplier) { + this.verificationCaptchaSupplier = AsyncCaptchaCodeSupplier.of(verificationCaptchaSupplier); return (T) this; } diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index 357b88506..e47d1af14 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -8,24 +8,21 @@ import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeReader; import it.auties.curve25519.Curve25519; -import it.auties.linkpreview.LinkPreview; -import it.auties.linkpreview.LinkPreviewMedia; -import it.auties.linkpreview.LinkPreviewResult; import it.auties.whatsapp.controller.Keys; import it.auties.whatsapp.controller.Store; -import it.auties.whatsapp.crypto.*; +import it.auties.whatsapp.crypto.AesGcm; +import it.auties.whatsapp.crypto.Hkdf; +import it.auties.whatsapp.crypto.Hmac; +import it.auties.whatsapp.crypto.SessionCipher; import it.auties.whatsapp.listener.*; import it.auties.whatsapp.model.action.*; import it.auties.whatsapp.model.business.*; -import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplate; -import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplate; import it.auties.whatsapp.model.call.Call; import it.auties.whatsapp.model.call.CallStatus; import it.auties.whatsapp.model.chat.*; import it.auties.whatsapp.model.companion.CompanionLinkResult; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.contact.ContactStatus; -import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.info.MessageInfoBuilder; import it.auties.whatsapp.model.jid.Jid; @@ -33,32 +30,25 @@ import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.media.AttachmentType; import it.auties.whatsapp.model.media.MediaFile; -import it.auties.whatsapp.model.media.MutableAttachmentProvider; -import it.auties.whatsapp.model.message.button.ButtonsMessage; -import it.auties.whatsapp.model.message.button.InteractiveMessage; -import it.auties.whatsapp.model.message.button.TemplateMessage; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.message.model.reserved.LocalMediaMessage; import it.auties.whatsapp.model.message.server.ProtocolMessage; import it.auties.whatsapp.model.message.server.ProtocolMessageBuilder; -import it.auties.whatsapp.model.message.standard.*; +import it.auties.whatsapp.model.message.standard.CallMessageBuilder; +import it.auties.whatsapp.model.message.standard.ReactionMessageBuilder; +import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.node.Node; -import it.auties.whatsapp.model.poll.PollAdditionalMetadata; -import it.auties.whatsapp.model.poll.PollUpdateEncryptedMetadata; -import it.auties.whatsapp.model.poll.PollUpdateEncryptedOptions; -import it.auties.whatsapp.model.poll.PollUpdateEncryptedOptionsSpec; import it.auties.whatsapp.model.privacy.GdprAccountReport; import it.auties.whatsapp.model.privacy.PrivacySettingEntry; import it.auties.whatsapp.model.privacy.PrivacySettingType; import it.auties.whatsapp.model.privacy.PrivacySettingValue; import it.auties.whatsapp.model.request.*; -import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.model.request.UpdateChannelRequest.UpdatePayload; -import it.auties.whatsapp.model.response.NewsletterResponse; -import it.auties.whatsapp.model.response.RecommendedNewslettersResponse; import it.auties.whatsapp.model.response.ContactStatusResponse; import it.auties.whatsapp.model.response.HasWhatsappResponse; +import it.auties.whatsapp.model.response.NewsletterResponse; +import it.auties.whatsapp.model.response.RecommendedNewslettersResponse; import it.auties.whatsapp.model.setting.LocaleSettings; import it.auties.whatsapp.model.setting.PushNameSettings; import it.auties.whatsapp.model.signal.auth.*; @@ -85,7 +75,10 @@ import java.time.ZonedDateTime; import java.time.chrono.ChronoZonedDateTime; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -483,28 +476,10 @@ public CompletableFuture sendMessage(@NonNull JidProvider chat, @No * @return a CompletableFuture */ public CompletableFuture sendMessage(@NonNull MessageInfo info) { - store().attribute(info); - return attributeMessageMetadata(info) - .thenComposeAsync(ignored -> socketHandler.sendMessage(new MessageSendRequest(info))) + return socketHandler.sendMessage(new MessageSendRequest(info)) .thenApply(ignored -> info); } - private CompletableFuture attributeMessageMetadata(MessageInfo info) { - info.key().setChatJid(info.chatJid().withoutDevice()); - info.key().setSenderJid(info.senderJid() == null ? null : info.senderJid().withoutDevice()); - fixEphemeralMessage(info); - var content = info.message().content(); - return switch (content) { - case LocalMediaMessage mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage); - case ButtonMessage buttonMessage -> attributeButtonMessage(info, buttonMessage); - case TextMessage textMessage -> attributeTextMessage(textMessage); - case PollCreationMessage pollCreationMessage -> attributePollCreationMessage(info, pollCreationMessage); - case PollUpdateMessage pollUpdateMessage -> attributePollUpdateMessage(info, pollUpdateMessage); - case GroupInviteMessage groupInviteMessage -> attributeGroupInviteMessage(info, groupInviteMessage); - case null, default -> CompletableFuture.completedFuture(null); - }; - } - /** * Marks a chat as read. * @@ -515,203 +490,6 @@ public CompletableFuture markRead(@NonNull T chat) { return mark(chat, true).thenComposeAsync(ignored -> markAllAsRead(chat)).thenApplyAsync(ignored -> chat); } - private void fixEphemeralMessage(MessageInfo info) { - if (info.message().hasCategory(MessageCategory.SERVER)) { - return; - } - - var chat = info.chat().orElse(null); - if (chat != null && chat.isEphemeral()) { - info.message() - .contentWithContext() - .flatMap(ContextualMessage::contextInfo) - .ifPresent(contextInfo -> createEphemeralContext(chat, contextInfo)); - info.setMessage(info.message().toEphemeral()); - return; - } - - if (info.message().type() != MessageType.EPHEMERAL) { - return; - } - - info.setMessage(info.message().unbox()); - } - - // TODO: Async - private CompletableFuture attributeTextMessage(TextMessage textMessage) { - if (store().textPreviewSetting() == TextPreviewSetting.DISABLED) { - return CompletableFuture.completedFuture(null); - } - - var match = LinkPreview.createPreview(textMessage.text()) - .orElse(null); - if (match == null) { - return CompletableFuture.completedFuture(null); - } - - var uri = match.result().uri().toString(); - if (store().textPreviewSetting() == TextPreviewSetting.ENABLED_WITH_INFERENCE && !match.text() - .equals(uri)) { - textMessage.setText(textMessage.text().replace(match.text(), uri)); - } - - var imageUri = match.result() - .images() - .stream() - .reduce(this::compareDimensions) - .map(LinkPreviewMedia::uri) - .orElse(null); - var videoUri = match.result() - .videos() - .stream() - .reduce(this::compareDimensions) - .map(LinkPreviewMedia::uri) - .orElse(null); - textMessage.setMatchedText(uri); - textMessage.setCanonicalUrl(Objects.requireNonNullElse(videoUri, match.result().uri()).toString()); - textMessage.setThumbnail(Medias.download(imageUri).orElse(null)); - textMessage.setDescription(match.result().siteDescription()); - textMessage.setTitle(match.result().title()); - textMessage.setPreviewType(videoUri != null ? TextMessage.PreviewType.VIDEO : TextMessage.PreviewType.NONE); - return CompletableFuture.completedFuture(null); - } - - private CompletableFuture attributeMediaMessage(Jid chatJid, LocalMediaMessage mediaMessage) { - var media = mediaMessage.decodedMedia() - .orElseThrow(() -> new IllegalArgumentException("Missing media to upload")); - var attachmentType = getAttachmentType(chatJid, mediaMessage); - var mediaConnection = store().mediaConnection(); - return Medias.upload(media, attachmentType, mediaConnection) - .thenAccept(upload -> attributeMediaMessage(mediaMessage, upload)); - } - - private AttachmentType getAttachmentType(Jid chatJid, LocalMediaMessage mediaMessage) { - if (!chatJid.hasServer(JidServer.NEWSLETTER)) { - return mediaMessage.attachmentType(); - } - - return switch (mediaMessage.mediaType()) { - case IMAGE -> AttachmentType.NEWSLETTER_IMAGE; - case DOCUMENT -> AttachmentType.NEWSLETTER_DOCUMENT; - case AUDIO -> AttachmentType.NEWSLETTER_AUDIO; - case VIDEO -> AttachmentType.NEWSLETTER_VIDEO; - case STICKER -> AttachmentType.NEWSLETTER_STICKER; - case NONE -> throw new IllegalArgumentException("Unexpected empty message"); - }; - } - - - private MutableAttachmentProvider attributeMediaMessage(MutableAttachmentProvider mediaMessage, MediaFile upload) { - if(mediaMessage instanceof LocalMediaMessage localMediaMessage) { - localMediaMessage.setHandle(upload.handle()); - } - - return mediaMessage.setMediaSha256(upload.fileSha256()) - .setMediaEncryptedSha256(upload.fileEncSha256()) - .setMediaKey(upload.mediaKey()) - .setMediaUrl(upload.url()) - .setMediaKeyTimestamp(upload.timestamp()) - .setMediaDirectPath(upload.directPath()) - .setMediaSize(upload.fileLength()); - } - - private CompletableFuture attributePollCreationMessage(MessageInfo info, PollCreationMessage pollCreationMessage) { - var pollEncryptionKey = pollCreationMessage.encryptionKey() - .orElseGet(KeyHelper::senderKey); - pollCreationMessage.setEncryptionKey(pollEncryptionKey); - info.setMessageSecret(pollEncryptionKey); - info.message() - .deviceInfo() - .ifPresent(deviceContextInfo -> deviceContextInfo.setMessageSecret(pollEncryptionKey)); - var metadata = new PollAdditionalMetadata(false); - info.setPollAdditionalMetadata(metadata); - return CompletableFuture.completedFuture(null); - } - - private CompletableFuture attributePollUpdateMessage(MessageInfo info, PollUpdateMessage pollUpdateMessage) { - if (pollUpdateMessage.encryptedMetadata().isPresent()) { - return CompletableFuture.completedFuture(null); - } - - var iv = BytesHelper.random(12); - var additionalData = "%s\0%s".formatted(pollUpdateMessage.pollCreationMessageKey().id(), jidOrThrowError().withoutDevice()); - var encryptedOptions = pollUpdateMessage.votes().stream().map(entry -> Sha256.calculate(entry.name())).toList(); - var pollUpdateEncryptedOptions = PollUpdateEncryptedOptionsSpec.encode(new PollUpdateEncryptedOptions(encryptedOptions)); - var originalPollInfo = store() - .findMessageByKey(pollUpdateMessage.pollCreationMessageKey()) - .orElseThrow(() -> new NoSuchElementException("Missing original poll message")); - var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content(); - var originalPollSender = originalPollInfo.senderJid().withoutDevice().toString().getBytes(StandardCharsets.UTF_8); - var modificationSenderJid = info.senderJid().withoutDevice(); - pollUpdateMessage.setVoter(modificationSenderJid); - var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8); - var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8); - var useSecretPayload = BytesHelper.concat( - pollUpdateMessage.pollCreationMessageKey().id().getBytes(StandardCharsets.UTF_8), - originalPollSender, - modificationSender, - secretName - ); - var encryptionKey = originalPollMessage.encryptionKey() - .orElseThrow(() -> new NoSuchElementException("Missing encryption key")); - var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32); - var pollUpdateEncryptedPayload = AesGcm.encrypt(iv, pollUpdateEncryptedOptions, useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8)); - var pollUpdateEncryptedMetadata = new PollUpdateEncryptedMetadata(pollUpdateEncryptedPayload, iv); - pollUpdateMessage.setEncryptedMetadata(pollUpdateEncryptedMetadata); - return CompletableFuture.completedFuture(null); - } - - private CompletableFuture attributeButtonMessage(MessageInfo info, ButtonMessage buttonMessage) { - return switch (buttonMessage) { - case ButtonsMessage buttonsMessage when buttonsMessage.header().isPresent() - && buttonsMessage.header().get() instanceof LocalMediaMessage mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage); - case TemplateMessage templateMessage when templateMessage.format().isPresent() -> { - var templateFormatter = templateMessage.format().get(); - yield switch (templateFormatter) { - case HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate - when highlyStructuredFourRowTemplate.title().isPresent() && highlyStructuredFourRowTemplate.title().get() instanceof LocalMediaMessage fourRowMedia -> - attributeMediaMessage(info.chatJid(), fourRowMedia); - case HydratedFourRowTemplate hydratedFourRowTemplate when hydratedFourRowTemplate.title().isPresent() && hydratedFourRowTemplate.title().get() instanceof LocalMediaMessage hydratedFourRowMedia -> - attributeMediaMessage(info.chatJid(), hydratedFourRowMedia); - case null, default -> CompletableFuture.completedFuture(null); - }; - } - case InteractiveMessage interactiveMessage - when interactiveMessage.header().isPresent() - && interactiveMessage.header().get().attachment().isPresent() - && interactiveMessage.header().get().attachment().get() instanceof LocalMediaMessage interactiveMedia -> attributeMediaMessage(info.chatJid(), interactiveMedia); - default -> CompletableFuture.completedFuture(null); - }; - } - - // TODO: Fix this - private CompletableFuture attributeGroupInviteMessage(MessageInfo info, GroupInviteMessage groupInviteMessage) { - var url = "https://chat.whatsapp.com/%s".formatted(groupInviteMessage.code()); - var preview = LinkPreview.createPreview(URI.create(url)) - .stream() - .map(LinkPreviewResult::images) - .map(Collection::stream) - .map(Stream::findFirst) - .flatMap(Optional::stream) - .findFirst() - .map(LinkPreviewMedia::uri) - .orElse(null); - var parsedCaption = groupInviteMessage.caption() - .map(caption -> "%s: %s".formatted(caption, url)) - .orElse(url); - var replacement = new TextMessageBuilder() - .text(parsedCaption) - .description("WhatsApp Group Invite") - .title(groupInviteMessage.groupName()) - .previewType(TextMessage.PreviewType.NONE) - .thumbnail(Medias.download(preview).orElse(null)) - .matchedText(url) - .canonicalUrl(url) - .build(); - info.setMessage(MessageContainer.of(replacement)); - return CompletableFuture.completedFuture(null); - } - private CompletableFuture mark(@NonNull T chat, boolean read) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions @@ -739,10 +517,6 @@ private CompletableFuture markAllAsRead(JidProvider chat) { return CompletableFuture.allOf(all); } - private LinkPreviewMedia compareDimensions(LinkPreviewMedia first, LinkPreviewMedia second) { - return first.width() * first.height() > second.width() * second.height() ? first : second; - } - private ActionMessageRangeSync createRange(JidProvider chat, boolean allMessages) { var known = store().findChatByJid(chat.toJid()).orElseGet(() -> store().addNewChat(chat.toJid())); return new ActionMessageRangeSync(known, allMessages); @@ -768,13 +542,6 @@ public CompletableFuture markRead(@NonNull MessageInfo info) { return CompletableFuture.completedFuture(info); } - private void createEphemeralContext(Chat chat, ContextInfo contextInfo) { - var period = chat.ephemeralMessageDuration() - .period() - .toSeconds(); - contextInfo.setEphemeralExpiration((int) period); - } - /** * Awaits for a single newsletters to a message * @@ -812,17 +579,24 @@ public CompletableFuture hasWhatsapp(@NonNull JidProvider c * @return a CompletableFuture that wraps a non-null map */ public CompletableFuture> hasWhatsapp(@NonNull JidProvider... contacts) { - var contactNodes = Arrays.stream(contacts) - .map(jid -> Node.of("user", Node.of("contact", jid.toJid().toPhoneNumber()))) + var jids = Arrays.stream(contacts) + .map(JidProvider::toJid) + .toList(); + var contactNodes = jids.stream() + .map(jid -> Node.of("user", Node.of("contact", jid.toPhoneNumber()))) .toArray(Node[]::new); return socketHandler.sendInteractiveQuery(Node.of("contact"), contactNodes) - .thenApplyAsync(this::parseHasWhatsappResponse); + .thenApplyAsync(result -> parseHasWhatsappResponse(jids, result)); } - private Map parseHasWhatsappResponse(List nodes) { - return nodes.stream() + private Map parseHasWhatsappResponse(List contacts, List nodes) { + var result = nodes.stream() .map(this::parseHasWhatsappResponse) - .collect(Collectors.toMap(HasWhatsappResponse::contact, Function.identity())); + .collect(Collectors.toMap(HasWhatsappResponse::contact, Function.identity(), (first, second) -> first, HashMap::new)); + contacts.stream() + .filter(contact -> !result.containsKey(contact)) + .forEach(contact -> result.put(contact, new HasWhatsappResponse(contact, false))); + return Collections.unmodifiableMap(result); } private HasWhatsappResponse parseHasWhatsappResponse(Node node) { @@ -1001,6 +775,7 @@ private void updateSelfPresence(JidProvider chatJid, ContactStatus presence) { if (self.isEmpty()) { return; } + if (presence == ContactStatus.AVAILABLE || presence == ContactStatus.UNAVAILABLE) { self.get().setLastKnownPresence(presence); } @@ -2011,7 +1786,7 @@ public CompletableFuture requireMediaReupload(@NonNull MessageInfo } private MessageInfo parseMediaReupload(MessageInfo info, MediaMessage mediaMessage, byte[] retryKey, byte[] retryIdData, Node node) { - Validate.isTrue(!node.hasNode("error"), "Erroneous newsletters from media reupload: %s", node.attributes() + Validate.isTrue(!node.hasNode("error"), "Erroneous response from media reupload: %s", node.attributes() .getInt("code")); var encryptNode = node.findNode("encrypt") .orElseThrow(() -> new NoSuchElementException("Missing encrypt node in media reupload")); @@ -2047,17 +1822,15 @@ public CompletableFuture sendNode(@NonNull Node node) { * @param body the nullable description of the new community * @return a CompletableFuture */ - public CompletableFuture createCommunity(@NonNull String subject, String body) { + public CompletableFuture> createCommunity(@NonNull String subject, String body) { var descriptionId = HexFormat.of().formatHex(BytesHelper.random(12)); var entry = Node.of("create", Map.of("subject", subject), Node.of("description", Map.of("id", descriptionId), Node.of("body", Objects.requireNonNullElse(body, "").getBytes(StandardCharsets.UTF_8))), Node.of("parent", Map.of("default_membership_approval_mode", "request_required")), Node.of("allow_non_admin_sub_group_creation")); - return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", entry).thenApplyAsync(node -> { - node.assertNode("group", () -> "Missing community newsletters, something went wrong: " + findErrorNode(node)); - return socketHandler.parseGroupMetadata(node); - }); + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", entry) + .thenApplyAsync(node -> node.findNode("group").map(socketHandler::parseGroupMetadata)); } /** @@ -2970,7 +2743,7 @@ public Whatsapp addNewContactListener(OnNewContact onNewContact) { * @param onNewMessage the listener to register * @return the same instance */ - public Whatsapp addNewMessageListener(OnNewMessage onNewMessage) { + public Whatsapp addNewChatMessageListener(OnNewMessage onNewMessage) { return addListener(onNewMessage); } @@ -2980,7 +2753,7 @@ public Whatsapp addNewMessageListener(OnNewMessage onNewMessage) { * @param onNewMessage the listener to register * @return the same instance */ - public Whatsapp addNewMessageListener(OnNewMarkedMessage onNewMessage) { + public Whatsapp addNewNewsletterMessageListener(OnNewNewsletterMessage onNewMessage) { return addListener(onNewMessage); } @@ -3180,7 +2953,7 @@ public Whatsapp addMetadataListener(OnWhatsappMetadata onMetadata) { * @param onNewMessage the listener to register * @return the same instance */ - public Whatsapp addNewMessageListener(OnWhatsappNewMessage onNewMessage) { + public Whatsapp addNewChatMessageListener(OnWhatsappNewMessage onNewMessage) { return addListener(onNewMessage); } @@ -3190,7 +2963,7 @@ public Whatsapp addNewMessageListener(OnWhatsappNewMessage onNewMessage) { * @param onNewMessage the listener to register * @return the same instance */ - public Whatsapp addNewMessageListener(OnWhatsappNewMarkedMessage onNewMessage) { + public Whatsapp addNewNewsletterMessageListener(OnWhatsappNewNewsletterMessage onNewMessage) { return addListener(onNewMessage); } diff --git a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java index 2109a5be1..e91f2ab2d 100644 --- a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java @@ -5,6 +5,7 @@ import it.auties.whatsapp.model.chat.ChatBuilder; import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.util.Smile; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; @@ -18,6 +19,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -29,6 +31,7 @@ public class DefaultControllerSerializer implements ControllerSerializer { private static final Path DEFAULT_DIRECTORY = Path.of(System.getProperty("user.home") + "/.cobalt/"); private static final String CHAT_PREFIX = "chat_"; + private static final String NEWSLETTER_PREFIX = "newsletter_"; private static final String STORE_NAME = "store.smile"; private static final String KEYS_NAME = "keys.smile"; private static final ControllerSerializer DEFAULT_SERIALIZER = new DefaultControllerSerializer(); @@ -162,7 +165,11 @@ public CompletableFuture serializeStore(Store store, boolean async) { } var chatsFutures = serializeChatsAsync(store); - var result = CompletableFuture.allOf(chatsFutures).thenRunAsync(() -> { + var newslettersFutures = serializeNewslettersAsync(store); + var dependableFutures = Stream.of(chatsFutures, newslettersFutures) + .flatMap(Arrays::stream) + .toArray(CompletableFuture[]::new); + var result = CompletableFuture.allOf(dependableFutures).thenRunAsync(() -> { var storePath = getSessionFile(store, STORE_NAME); writeFile(store, STORE_NAME, storePath); }); @@ -191,6 +198,19 @@ private CompletableFuture serializeChatAsync(Store store, Chat chat) { return CompletableFuture.runAsync(() -> writeFile(chat, fileName, outputFile)); } + private CompletableFuture[] serializeNewslettersAsync(Store store) { + return store.newsletters() + .stream() + .map(newsletter -> serializeNewsletterAsync(store, newsletter)) + .toArray(CompletableFuture[]::new); + } + + private CompletableFuture serializeNewsletterAsync(Store store, Newsletter newsletter) { + var fileName = NEWSLETTER_PREFIX + newsletter.jid() + ".smile"; + var outputFile = getSessionFile(store, fileName); + return CompletableFuture.runAsync(() -> writeFile(newsletter, fileName, outputFile)); + } + private void writeFile(Object object, String fileName, Path outputFile) { try { var tempFile = Files.createTempFile(fileName, ".tmp"); @@ -298,8 +318,8 @@ public CompletableFuture attributeStore(Store store) { return CompletableFuture.completedFuture(null); } try (var walker = Files.walk(directory)) { - var futures = walker.filter(entry -> entry.getFileName().toString().startsWith(CHAT_PREFIX)) - .map(entry -> CompletableFuture.runAsync(() -> deserializeChat(store, entry))) + var futures = walker.map(entry -> handleStoreFile(store, entry)) + .filter(Objects::nonNull) .toArray(CompletableFuture[]::new); var result = CompletableFuture.allOf(futures); attributeStoreSerializers.put(store.uuid(), result); @@ -309,6 +329,37 @@ public CompletableFuture attributeStore(Store store) { } } + private CompletableFuture handleStoreFile(Store store, Path entry) { + return switch (FileType.of(entry)) { + case UNKNOWN -> null; + case NEWSLETTER -> CompletableFuture.runAsync(() -> deserializeNewsletter(store, entry)); + case CHAT -> CompletableFuture.runAsync(() -> deserializeChat(store, entry)); + }; + } + + private enum FileType { + UNKNOWN(null), + CHAT(CHAT_PREFIX), + NEWSLETTER(NEWSLETTER_PREFIX); + + private final String prefix; + + FileType(String prefix) { + this.prefix = prefix; + } + + private static FileType of(Path path) { + return Arrays.stream(values()) + .filter(entry -> entry.prefix() != null && path.getFileName().toString().startsWith(entry.prefix())) + .findFirst() + .orElse(UNKNOWN); + } + + private String prefix() { + return prefix; + } + } + @Override public void deleteSession(@NonNull Controller controller) { try { @@ -381,12 +432,39 @@ private Chat rescueChat(Path entry) { .build(); } + private void deserializeNewsletter(Store store, Path newsletterFile) { + try (var input = new GZIPInputStream(Files.newInputStream(newsletterFile))) { + store.addNewsletter(Smile.readValue(input, Newsletter.class)); + } catch (IOException exception) { + store.addNewsletter(rescueNewsletter(newsletterFile)); + } + } + + private Newsletter rescueNewsletter(Path entry) { + try { + Files.deleteIfExists(entry); + } catch (IOException ignored) { + + } + var newsletterName = entry.getFileName().toString() + .replaceFirst(CHAT_PREFIX, "") + .replace(".smile", "") + .replaceAll("~~", ":"); + return new Newsletter(Jid.of(newsletterName), null, null, null); + } + private Path getHome(ClientType type) { return baseDirectory.resolve(type == ClientType.MOBILE ? "mobile" : "web"); } private Path getSessionDirectory(ClientType clientType, String path) { - return getHome(clientType).resolve(path); + try { + var result = getHome(clientType).resolve(path); + Files.createDirectories(result.getParent()); + return result; + }catch (IOException exception) { + throw new UncheckedIOException(exception); + } } private Path getSessionFile(Store store, String fileName) { diff --git a/src/main/java/it/auties/whatsapp/controller/Keys.java b/src/main/java/it/auties/whatsapp/controller/Keys.java index d8185cc43..f97ed0476 100644 --- a/src/main/java/it/auties/whatsapp/controller/Keys.java +++ b/src/main/java/it/auties/whatsapp/controller/Keys.java @@ -576,12 +576,12 @@ public boolean initialAppSync() { return this.readCounter; } - public byte[] writeKey() { - return this.writeKey; + public Optional writeKey() { + return Optional.ofNullable(this.writeKey); } - public byte[] readKey() { - return this.readKey; + public Optional readKey() { + return Optional.ofNullable(this.readKey); } public Keys setCompanionKeyPair(SignalKeyPair companionKeyPair) { diff --git a/src/main/java/it/auties/whatsapp/controller/Store.java b/src/main/java/it/auties/whatsapp/controller/Store.java index 3f0dfa120..60a225c80 100644 --- a/src/main/java/it/auties/whatsapp/controller/Store.java +++ b/src/main/java/it/auties/whatsapp/controller/Store.java @@ -6,8 +6,6 @@ import it.auties.whatsapp.api.ClientType; import it.auties.whatsapp.api.TextPreviewSetting; import it.auties.whatsapp.api.WebHistoryLength; -import it.auties.whatsapp.crypto.AesGcm; -import it.auties.whatsapp.crypto.Hkdf; import it.auties.whatsapp.listener.Listener; import it.auties.whatsapp.model.business.BusinessCategory; import it.auties.whatsapp.model.call.Call; @@ -17,7 +15,6 @@ import it.auties.whatsapp.model.companion.CompanionDevice; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.info.ContextInfo; -import it.auties.whatsapp.model.info.DeviceContextInfo; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.jid.JidProvider; @@ -25,14 +22,9 @@ import it.auties.whatsapp.model.media.MediaConnection; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageKey; -import it.auties.whatsapp.model.message.standard.PollCreationMessage; -import it.auties.whatsapp.model.message.standard.PollUpdateMessage; -import it.auties.whatsapp.model.message.standard.ReactionMessage; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.model.node.Node; -import it.auties.whatsapp.model.poll.PollUpdate; -import it.auties.whatsapp.model.poll.PollUpdateEncryptedOptionsSpec; import it.auties.whatsapp.model.privacy.PrivacySettingEntry; import it.auties.whatsapp.model.privacy.PrivacySettingType; import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; @@ -40,12 +32,14 @@ import it.auties.whatsapp.model.signal.auth.Version; import it.auties.whatsapp.model.sync.HistorySyncMessage; import it.auties.whatsapp.socket.SocketRequest; -import it.auties.whatsapp.util.*; +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.FutureReference; +import it.auties.whatsapp.util.MetadataHelper; +import it.auties.whatsapp.util.ProxyAuthenticator; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; import java.util.concurrent.*; @@ -201,7 +195,7 @@ public final class Store extends Controller { * The non-null list of status messages */ @NonNull - private final ConcurrentHashMap> status; + private final ConcurrentHashMap> status; /** * The non-null map of newsletters @@ -338,7 +332,7 @@ public final class Store extends Controller { * All args constructor */ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - Store(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, @Nullable URI proxy, @NonNull FutureReference version, boolean online, @Nullable String locale, @NonNull String name, boolean business, @Nullable String businessAddress, @Nullable Double businessLongitude, @Nullable Double businessLatitude, @Nullable String businessDescription, @Nullable String businessWebsite, @Nullable String businessEmail, @Nullable BusinessCategory businessCategory, @Nullable String deviceHash, @NonNull LinkedHashMap linkedDevicesKeys, @Nullable URI profilePicture, @Nullable String about, @Nullable Jid jid, @Nullable Jid lid, @NonNull ConcurrentHashMap properties, @NonNull ConcurrentHashMap contacts, @NonNull ConcurrentHashMap> status, @NonNull ConcurrentHashMap newsletters, @NonNull ConcurrentHashMap privacySettings, @NonNull ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, long initializationTimeStamp, @NonNull ChatEphemeralTimer newChatsEphemeralTimer, @NonNull TextPreviewSetting textPreviewSetting, @NonNull WebHistoryLength historyLength, boolean autodetectListeners, boolean automaticPresenceUpdates, @NonNull ReleaseChannel releaseChannel, @Nullable CompanionDevice device, @Nullable PlatformType companionDeviceOs, boolean checkPatchMacs) { + Store(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, @Nullable URI proxy, @NonNull FutureReference version, boolean online, @Nullable String locale, @NonNull String name, boolean business, @Nullable String businessAddress, @Nullable Double businessLongitude, @Nullable Double businessLatitude, @Nullable String businessDescription, @Nullable String businessWebsite, @Nullable String businessEmail, @Nullable BusinessCategory businessCategory, @Nullable String deviceHash, @NonNull LinkedHashMap linkedDevicesKeys, @Nullable URI profilePicture, @Nullable String about, @Nullable Jid jid, @Nullable Jid lid, @NonNull ConcurrentHashMap properties, @NonNull ConcurrentHashMap contacts, @NonNull ConcurrentHashMap> status, @NonNull ConcurrentHashMap newsletters, @NonNull ConcurrentHashMap privacySettings, @NonNull ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, long initializationTimeStamp, @NonNull ChatEphemeralTimer newChatsEphemeralTimer, @NonNull TextPreviewSetting textPreviewSetting, @NonNull WebHistoryLength historyLength, boolean autodetectListeners, boolean automaticPresenceUpdates, @NonNull ReleaseChannel releaseChannel, @Nullable CompanionDevice device, @Nullable PlatformType companionDeviceOs, boolean checkPatchMacs) { super(uuid, phoneNumber, serializer, clientType, alias); if(proxy != null) { ProxyAuthenticator.register(proxy); @@ -753,7 +747,6 @@ public Chat addNewChat(@NonNull Jid chatJid) { * @return the old chat, if present */ public Optional addChat(@NonNull Chat chat) { - chat.messages().forEach(this::attribute); if (chat.hasName() && chat.jid().hasServer(JidServer.WHATSAPP)) { var contact = findContactByJid(chat.jid()) .orElseGet(() -> addContact(new Contact(chat.jid()))); @@ -854,133 +847,6 @@ public Optional removeContact(@NonNull JidProvider contactJid) { return Optional.ofNullable(contacts.remove(contactJid.toJid())); } - /** - * Attributes a message Usually used by the socket handler - * - * @param historySyncMessage a non-null message - * @return the same incoming message - */ - public MessageInfo attribute(@NonNull HistorySyncMessage historySyncMessage) { - return attribute(historySyncMessage.messageInfo()); - } - - // TODO: Move attribution logic to the correct Socket handler - /** - * Attributes a message Usually used by the socket handler - * - * @param info a non-null message - * @return the same incoming message - */ - public MessageInfo attribute(@NonNull MessageInfo info) { - var chat = findChatByJid(info.chatJid()) - .orElseGet(() -> addNewChat(info.chatJid())); - info.setChat(chat); - if (info.fromMe() && jid != null) { - info.key().setSenderJid(jid.withoutDevice()); - } - info.key() - .senderJid() - .ifPresent(senderJid -> attributeSender(info, senderJid)); - info.message() - .contentWithContext() - .flatMap(ContextualMessage::contextInfo) - .ifPresent(this::attributeContext); - processMessage(info); - return info; - } - - private MessageKey attributeSender(MessageInfo info, Jid senderJid) { - var contact = findContactByJid(senderJid) - .orElseGet(() -> addContact(new Contact(senderJid))); - info.setSender(contact); - return info.key(); - } - - private void attributeContext(ContextInfo contextInfo) { - contextInfo.quotedMessageSenderJid().ifPresent(senderJid -> attributeContextSender(contextInfo, senderJid)); - contextInfo.quotedMessageChatJid().ifPresent(chatJid -> attributeContextChat(contextInfo, chatJid)); - } - - private void attributeContextChat(ContextInfo contextInfo, Jid chatJid) { - var chat = findChatByJid(chatJid) - .orElseGet(() -> addNewChat(chatJid)); - contextInfo.setQuotedMessageChat(chat); - } - - private void attributeContextSender(ContextInfo contextInfo, Jid senderJid) { - var contact = findContactByJid(senderJid) - .orElseGet(() -> addContact(new Contact(senderJid))); - contextInfo.setQuotedMessageSender(contact); - } - - private void processMessage(MessageInfo info) { - switch (info.message().content()) { - case PollCreationMessage pollCreationMessage -> handlePollCreation(info, pollCreationMessage); - case PollUpdateMessage pollUpdateMessage -> handlePollUpdate(info, pollUpdateMessage); - case ReactionMessage reactionMessage -> handleReactionMessage(info, reactionMessage); - default -> {} - } - } - - private void handlePollCreation(MessageInfo info, PollCreationMessage pollCreationMessage) { - if (pollCreationMessage.encryptionKey().isPresent()) { - return; - } - - info.message() - .deviceInfo() - .flatMap(DeviceContextInfo::messageSecret) - .or(info::messageSecret) - .ifPresent(pollCreationMessage::setEncryptionKey); - } - - private void handlePollUpdate(MessageInfo info, PollUpdateMessage pollUpdateMessage) { - var originalPollInfo = findMessageByKey(pollUpdateMessage.pollCreationMessageKey()) - .orElseThrow(() -> new NoSuchElementException("Missing original poll message")); - var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content(); - pollUpdateMessage.setPollCreationMessage(originalPollMessage); - var originalPollSender = originalPollInfo.senderJid() - .withoutDevice() - .toString() - .getBytes(StandardCharsets.UTF_8); - var modificationSenderJid = info.senderJid().withoutDevice(); - pollUpdateMessage.setVoter(modificationSenderJid); - var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8); - var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8); - var useSecretPayload = BytesHelper.concat( - originalPollInfo.id().getBytes(StandardCharsets.UTF_8), - originalPollSender, - modificationSender, - secretName - ); - var encryptionKey = originalPollMessage.encryptionKey() - .orElseThrow(() -> new NoSuchElementException("Missing encryption key")); - var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32); - var additionalData = "%s\0%s".formatted( - originalPollInfo.id(), - modificationSenderJid - ); - var metadata = pollUpdateMessage.encryptedMetadata() - .orElseThrow(() -> new NoSuchElementException("Missing encrypted metadata")); - var decrypted = AesGcm.decrypt(metadata.iv(), metadata.payload(), useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8)); - var pollVoteMessage = PollUpdateEncryptedOptionsSpec.decode(decrypted); - var selectedOptions = pollVoteMessage.selectedOptions() - .stream() - .map(sha256 -> originalPollMessage.getSelectableOption(HexFormat.of().formatHex(sha256))) - .flatMap(Optional::stream) - .toList(); - originalPollMessage.addSelectedOptions(modificationSenderJid, selectedOptions); - pollUpdateMessage.setVotes(selectedOptions); - var update = new PollUpdate(info.key(), pollVoteMessage, Clock.nowMilliseconds()); - info.pollUpdates().add(update); - } - - private void handleReactionMessage(MessageInfo info, ReactionMessage reactionMessage) { - info.setIgnore(true); - findMessageByKey(reactionMessage.key()) - .ifPresent(message -> message.reactions().add(reactionMessage)); - } - /** * Returns the chats pinned to the top sorted new to old * @@ -1083,8 +949,7 @@ public Collection blockedContacts() { * @return the same instance */ public Store addStatus(@NonNull MessageInfo info) { - attribute(info); - var wrapper = Objects.requireNonNullElseGet(status.get(info.senderJid()), ConcurrentLinkedDeque::new); + var wrapper = Objects.requireNonNullElseGet(status.get(info.senderJid()), CopyOnWriteArrayList::new); wrapper.add(info); status.put(info.senderJid(), wrapper); return this; diff --git a/src/main/java/it/auties/whatsapp/listener/Listener.java b/src/main/java/it/auties/whatsapp/listener/Listener.java index 0a533f5b2..e9e22a639 100644 --- a/src/main/java/it/auties/whatsapp/listener/Listener.java +++ b/src/main/java/it/auties/whatsapp/listener/Listener.java @@ -8,10 +8,11 @@ import it.auties.whatsapp.model.call.Call; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; import it.auties.whatsapp.model.info.MessageIndexInfo; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.info.NewsletterMessageInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.MessageStatus; import it.auties.whatsapp.model.message.model.QuotedMessage; import it.auties.whatsapp.model.newsletter.Newsletter; @@ -325,22 +326,20 @@ default void onNewMessage(MessageInfo info) { } /** - * Called when a new message is received in a chat + * Called when a new message is received in a newsletter * * @param whatsapp an instance to the calling api * @param info the message that was sent - * @param offline whether this message was received while the client was offline */ - default void onNewMessage(Whatsapp whatsapp, MessageInfo info, boolean offline) { + default void onNewMessage(Whatsapp whatsapp, NewsletterMessageInfo info) { } /** - * Called when a new message is received in a chat + * Called when a new message is received in a newsletter * * @param info the message that was sent - * @param offline whether this message was received while the client was offline */ - default void onNewMessage(MessageInfo info, boolean offline) { + default void onNewMessage(NewsletterMessageInfo info) { } /** diff --git a/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java index 2fd3e66ed..c51b3043a 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java +++ b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java @@ -1,8 +1,8 @@ package it.auties.whatsapp.listener; import it.auties.whatsapp.model.chat.Chat; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.jid.Jid; public interface OnContactPresence extends Listener { /** diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewMarkedMessage.java b/src/main/java/it/auties/whatsapp/listener/OnNewMarkedMessage.java deleted file mode 100644 index 119a08f89..000000000 --- a/src/main/java/it/auties/whatsapp/listener/OnNewMarkedMessage.java +++ /dev/null @@ -1,14 +0,0 @@ -package it.auties.whatsapp.listener; - -import it.auties.whatsapp.model.info.MessageInfo; - -public interface OnNewMarkedMessage extends Listener { - /** - * Called when a new message is received in a chat - * - * @param info the message that was sent - * @param offline whether this message was received while the client was offline - */ - @Override - void onNewMessage(MessageInfo info, boolean offline); -} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewNewsletterMessage.java b/src/main/java/it/auties/whatsapp/listener/OnNewNewsletterMessage.java new file mode 100644 index 000000000..fcba1723f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNewNewsletterMessage.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.NewsletterMessageInfo; + +public interface OnNewNewsletterMessage extends Listener { + /** + * Called when a new message is received in a newsletter + * + * @param info the message that was sent + */ + @Override + void onNewMessage(NewsletterMessageInfo info); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java index 6a914c5fc..72f9719bc 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java @@ -2,8 +2,8 @@ import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.model.chat.Chat; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.jid.Jid; public interface OnWhatsappContactPresence extends Listener { /** diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMarkedMessage.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMarkedMessage.java deleted file mode 100644 index 31b00b53c..000000000 --- a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMarkedMessage.java +++ /dev/null @@ -1,16 +0,0 @@ -package it.auties.whatsapp.listener; - -import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.info.MessageInfo; - -public interface OnWhatsappNewMarkedMessage extends Listener { - /** - * Called when a new message is received in a chat - * - * @param whatsapp an instance to the calling api - * @param info the message that was sent - * @param offline whether this message was received while the client was offline - */ - @Override - void onNewMessage(Whatsapp whatsapp, MessageInfo info, boolean offline); -} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewNewsletterMessage.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewNewsletterMessage.java new file mode 100644 index 000000000..44e6f79a1 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewNewsletterMessage.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.NewsletterMessageInfo; + +public interface OnWhatsappNewNewsletterMessage extends Listener { + /** + * Called when a new message is received in a newsletter + * + * @param whatsapp an instance to the calling api + * @param info the message that was sent + */ + @Override + void onNewMessage(Whatsapp whatsapp, NewsletterMessageInfo info); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/Chat.java b/src/main/java/it/auties/whatsapp/model/chat/Chat.java index 146660203..16a65069f 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/Chat.java +++ b/src/main/java/it/auties/whatsapp/model/chat/Chat.java @@ -17,7 +17,7 @@ import it.auties.whatsapp.model.message.model.MessageCategory; import it.auties.whatsapp.model.sync.HistorySyncMessage; import it.auties.whatsapp.util.Clock; -import it.auties.whatsapp.util.ConcurrentDoublyLinkedList; +import it.auties.whatsapp.util.MessagesSet; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -39,7 +39,7 @@ public final class Chat implements ProtobufMessage, JidProvider { private final @NonNull Jid jid; @ProtobufProperty(index = 2, type = ProtobufType.OBJECT, repeated = true) - private final @NonNull ConcurrentDoublyLinkedList historySyncMessages; + private final @NonNull MessagesSet historySyncMessages; @ProtobufProperty(index = 3, type = ProtobufType.STRING) private final Jid newJid; @@ -156,7 +156,7 @@ public final class Chat implements ProtobufMessage, JidProvider { private final @NonNull Set pastParticipants; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public Chat(@NonNull Jid jid, @NonNull ConcurrentDoublyLinkedList historySyncMessages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean readOnly, boolean endOfHistoryTransfer, @NonNull ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, @Nullable String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, @NonNull List participants, byte @NonNull [] token, long tokenTimestampSeconds, byte @NonNull [] identityKey, int pinnedTimestampSeconds, @NonNull ChatMute mute, ChatWallpaper wallpaper, @NonNull MediaVisibility mediaVisibility, long tokenSenderTimestampSeconds, boolean suspended, boolean terminated, long foundationTimestampSeconds, Jid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, Jid parentGroupJid, String displayName, Jid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, Jid lidJid, @NonNull ConcurrentHashMap presences, @NonNull Set participantsPreKeys, @NonNull Set pastParticipants) { + public Chat(@NonNull Jid jid, @NonNull MessagesSet historySyncMessages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean readOnly, boolean endOfHistoryTransfer, @NonNull ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, @Nullable String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, @NonNull List participants, byte @NonNull [] token, long tokenTimestampSeconds, byte @NonNull [] identityKey, int pinnedTimestampSeconds, @NonNull ChatMute mute, ChatWallpaper wallpaper, @NonNull MediaVisibility mediaVisibility, long tokenSenderTimestampSeconds, boolean suspended, boolean terminated, long foundationTimestampSeconds, Jid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, Jid parentGroupJid, String displayName, Jid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, Jid lidJid, @NonNull ConcurrentHashMap presences, @NonNull Set participantsPreKeys, @NonNull Set pastParticipants) { this.jid = jid; this.historySyncMessages = historySyncMessages; this.newJid = newJid; @@ -201,7 +201,7 @@ public Chat(@NonNull Jid jid, @NonNull ConcurrentDoublyLinkedList historySyncMessages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean readOnly, boolean endOfHistoryTransfer, @NonNull ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, @Nullable String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, @NonNull List participants, byte @NonNull [] token, long tokenTimestampSeconds, byte @NonNull [] identityKey, int pinnedTimestampSeconds, @Nullable ChatMute mute, ChatWallpaper wallpaper, @Nullable MediaVisibility mediaVisibility, long tokenSenderTimestampSeconds, boolean suspended, boolean terminated, long foundationTimestampSeconds, Jid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, Jid parentGroupJid, String displayName, Jid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, Jid lidJid) { + public Chat(@NonNull Jid jid, @NonNull MessagesSet historySyncMessages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean readOnly, boolean endOfHistoryTransfer, @NonNull ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, @Nullable String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, @NonNull List participants, byte @NonNull [] token, long tokenTimestampSeconds, byte @NonNull [] identityKey, int pinnedTimestampSeconds, @Nullable ChatMute mute, ChatWallpaper wallpaper, @Nullable MediaVisibility mediaVisibility, long tokenSenderTimestampSeconds, boolean suspended, boolean terminated, long foundationTimestampSeconds, Jid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, Jid parentGroupJid, String displayName, Jid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, Jid lidJid) { this.jid = jid; this.historySyncMessages = historySyncMessages; this.newJid = newJid; @@ -636,11 +636,8 @@ public boolean addParticipant(@NonNull Jid jid, GroupRole role) { */ public boolean addParticipant(@NonNull GroupParticipant participant) { var result = participants.add(participant); - if(result) { - this.update = true; - } - - return result; + this.update = true; + return true; } /** diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java b/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java index bed837527..688de9b6b 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java @@ -27,8 +27,8 @@ public record GroupMetadata( @NonNull List participants, Optional ephemeralExpiration, - boolean community, - boolean openCommunity + boolean isCommunity, + boolean isOpenCommunity ) { } diff --git a/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java b/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java index bd0b5916f..1646bc645 100644 --- a/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java @@ -6,11 +6,11 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.base.ButtonActionLink; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.chat.ChatDisappear; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.jid.Jid; -import it.auties.whatsapp.model.button.base.ButtonActionLink; import it.auties.whatsapp.model.message.model.MessageContainer; import it.auties.whatsapp.model.message.model.MessageKey; import it.auties.whatsapp.model.message.model.MessageMetadataProvider; diff --git a/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java index 74d341a7e..8ee5156ca 100644 --- a/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java @@ -3,13 +3,24 @@ import it.auties.whatsapp.model.message.model.MessageContainer; import it.auties.whatsapp.model.newsletter.NewsletterReaction; import it.auties.whatsapp.util.Clock; +import it.auties.whatsapp.util.Json; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Optional; +import java.util.OptionalLong; -public record NewsletterMessageInfo(String id, long timestampSeconds, long views, Collection reactions, MessageContainer container) { +public record NewsletterMessageInfo(String id, OptionalLong timestampSeconds, OptionalLong views, Collection reactions, MessageContainer container) { public Optional timestamp() { - return Clock.parseSeconds(timestampSeconds); + return Clock.parseSeconds(timestampSeconds.orElse(0L)); + } + + /** + * Converts this message to a json. Useful when debugging. + * + * @return a non-null string + */ + public String toJson() { + return Json.writeValueAsString(this, true); } } diff --git a/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java b/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java index 4e3d887d2..cde0bf959 100644 --- a/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java @@ -56,13 +56,19 @@ static ButtonsMessage customBuilder(ButtonsMessageHeader header, String body, St .contextInfo(contextInfo) .buttons(buttons); switch (header) { - case ButtonsMessageHeaderText textMessage -> builder.headerText(textMessage); - case DocumentMessage documentMessage -> builder.headerDocument(documentMessage); - case ImageMessage imageMessage -> builder.headerImage(imageMessage); - case VideoOrGifMessage videoMessage -> builder.headerVideo(videoMessage); - case LocationMessage locationMessage -> builder.headerLocation(locationMessage); - case null -> {} + case ButtonsMessageHeaderText textMessage -> builder.headerText(textMessage) + .headerType(Type.TEXT); + case DocumentMessage documentMessage -> builder.headerDocument(documentMessage) + .headerType(Type.DOCUMENT); + case ImageMessage imageMessage -> builder.headerImage(imageMessage) + .headerType(Type.IMAGE); + case VideoOrGifMessage videoMessage -> builder.headerVideo(videoMessage) + .headerType(Type.VIDEO); + case LocationMessage locationMessage -> builder.headerLocation(locationMessage) + .headerType(Type.LOCATION); + case null -> builder.headerType(Type.UNKNOWN); } + return builder.build(); } diff --git a/src/main/java/it/auties/whatsapp/model/message/button/InteractiveResponseMessage.java b/src/main/java/it/auties/whatsapp/model/message/button/InteractiveResponseMessage.java index f5e22ef08..db929d079 100644 --- a/src/main/java/it/auties/whatsapp/model/message/button/InteractiveResponseMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/button/InteractiveResponseMessage.java @@ -3,8 +3,8 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.button.interactive.InteractiveBody; +import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageCategory; import it.auties.whatsapp.model.message.model.MessageType; diff --git a/src/main/java/it/auties/whatsapp/model/message/model/MessageKey.java b/src/main/java/it/auties/whatsapp/model/message/model/MessageKey.java index a7164a611..d75d601d3 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/MessageKey.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/MessageKey.java @@ -5,8 +5,8 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.BytesHelper; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; diff --git a/src/main/java/it/auties/whatsapp/model/message/model/MessageMetadataProvider.java b/src/main/java/it/auties/whatsapp/model/message/model/MessageMetadataProvider.java index 55c4c603d..37e9649e7 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/MessageMetadataProvider.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/MessageMetadataProvider.java @@ -2,8 +2,8 @@ import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; import java.util.Optional; diff --git a/src/main/java/it/auties/whatsapp/model/message/model/MessageReceipt.java b/src/main/java/it/auties/whatsapp/model/message/model/MessageReceipt.java index d24cb007e..0722e4ccb 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/MessageReceipt.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/MessageReceipt.java @@ -11,7 +11,10 @@ import org.checkerframework.checker.nullness.qual.Nullable; import java.time.ZonedDateTime; -import java.util.*; +import java.util.HashSet; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.Set; /** * A model that represents the receipt for a message diff --git a/src/main/java/it/auties/whatsapp/model/message/model/QuotedMessage.java b/src/main/java/it/auties/whatsapp/model/message/model/QuotedMessage.java index 80c3ddb24..e6d5d21c5 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/QuotedMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/QuotedMessage.java @@ -3,8 +3,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Objects; diff --git a/src/main/java/it/auties/whatsapp/model/message/payment/PaymentOrderMessage.java b/src/main/java/it/auties/whatsapp/model/message/payment/PaymentOrderMessage.java index f682d5b3c..bfacaa8df 100644 --- a/src/main/java/it/auties/whatsapp/model/message/payment/PaymentOrderMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/payment/PaymentOrderMessage.java @@ -5,8 +5,8 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageType; import it.auties.whatsapp.model.message.model.PaymentMessage; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java index af65c4411..9c80b8341 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/DocumentMessage.java @@ -4,10 +4,10 @@ import it.auties.protobuf.annotation.ProtobufBuilder; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplateTitle; import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplateTitle; import it.auties.whatsapp.model.info.ContextInfo; -import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; import it.auties.whatsapp.model.media.AttachmentType; import it.auties.whatsapp.model.message.button.ButtonsMessageHeader; import it.auties.whatsapp.model.message.model.MediaMessage; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/GroupInviteMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/GroupInviteMessage.java index 1eb8e1ac0..afc0c1449 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/GroupInviteMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/GroupInviteMessage.java @@ -5,8 +5,8 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageCategory; import it.auties.whatsapp.model.message.model.MessageType; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java index bc1d21c03..8311e484a 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/ImageMessage.java @@ -6,12 +6,12 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; +import it.auties.whatsapp.model.button.interactive.InteractiveLocationAnnotation; import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplateTitle; import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplateTitle; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.info.MessageInfo; -import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; -import it.auties.whatsapp.model.button.interactive.InteractiveLocationAnnotation; import it.auties.whatsapp.model.message.button.ButtonsMessageHeader; import it.auties.whatsapp.model.message.model.MediaMessage; import it.auties.whatsapp.model.message.model.MediaMessageType; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/PollCreationMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/PollCreationMessage.java index 100a98ef4..91bb514b0 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/PollCreationMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/PollCreationMessage.java @@ -7,10 +7,10 @@ import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.crypto.Sha256; -import it.auties.whatsapp.model.jid.Jid; -import it.auties.whatsapp.model.jid.JidProvider; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidProvider; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageCategory; import it.auties.whatsapp.model.message.model.MessageType; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/PollUpdateMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/PollUpdateMessage.java index 62178f8d2..c4b49cf4f 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/PollUpdateMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/PollUpdateMessage.java @@ -6,8 +6,8 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.poll.PollOption; import it.auties.whatsapp.model.poll.PollUpdateEncryptedMetadata; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/ProductMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/ProductMessage.java index 6ce26138d..c194f233b 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/ProductMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/ProductMessage.java @@ -3,8 +3,8 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.ButtonMessage; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageCategory; diff --git a/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java b/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java index 88220ab68..ac227c500 100644 --- a/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/standard/VideoOrGifMessage.java @@ -6,11 +6,11 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; +import it.auties.whatsapp.model.button.interactive.InteractiveLocationAnnotation; import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplateTitle; import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplateTitle; import it.auties.whatsapp.model.info.ContextInfo; -import it.auties.whatsapp.model.button.interactive.InteractiveHeaderAttachment; -import it.auties.whatsapp.model.button.interactive.InteractiveLocationAnnotation; import it.auties.whatsapp.model.message.button.ButtonsMessageHeader; import it.auties.whatsapp.model.message.model.MediaMessage; import it.auties.whatsapp.model.message.model.MediaMessageType; diff --git a/src/main/java/it/auties/whatsapp/model/mobile/CountryCode.java b/src/main/java/it/auties/whatsapp/model/mobile/CountryCode.java index 8441ff8fc..47907eec1 100644 --- a/src/main/java/it/auties/whatsapp/model/mobile/CountryCode.java +++ b/src/main/java/it/auties/whatsapp/model/mobile/CountryCode.java @@ -57,7 +57,6 @@ public enum CountryCode { DENMARK("45", 238), DJIBOUTI("253", 638), DOMINICA("1-767", 366), - DOMINICAN_REPUBLIC("1-809, 1-829, 1-849", 370), ECUADOR("593", 740), EGYPT("20", 602), EL_SALVADOR("503", 706), @@ -157,7 +156,6 @@ public enum CountryCode { PHILIPPINES("63", 515), POLAND("48", 260), PORTUGAL("351", 268), - PUERTO_RICO("1-787, 1-939", 330), QATAR("974", 427), REPUBLIC_OF_THE_CONGO("242", 630), ROMANIA("40", 226), diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/Newsletter.java b/src/main/java/it/auties/whatsapp/model/newsletter/Newsletter.java index 6f2cf0949..6efe1d889 100644 --- a/src/main/java/it/auties/whatsapp/model/newsletter/Newsletter.java +++ b/src/main/java/it/auties/whatsapp/model/newsletter/Newsletter.java @@ -1,13 +1,11 @@ package it.auties.whatsapp.model.newsletter; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; import it.auties.whatsapp.model.info.NewsletterMessageInfo; import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.jid.JidProvider; -import it.auties.whatsapp.util.ConcurrentDoublyLinkedList; +import it.auties.whatsapp.util.MessagesSet; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Collection; @@ -15,22 +13,51 @@ import java.util.Objects; import java.util.Optional; -public record Newsletter( - Jid jid, - NewsletterState state, - NewsletterMetadata metadata, - Optional viewerMetadata, - @JsonIgnore Collection messages -) implements JidProvider { +public final class Newsletter implements JidProvider { + private final Jid jid; + private final NewsletterState state; + private final NewsletterMetadata metadata; + private final NewsletterViewerMetadata viewerMetadata; + private final MessagesSet messages; + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public Newsletter( - @JsonProperty("id") Jid jid, - @JsonProperty("state") NewsletterState state, - @JsonProperty("thread_metadata") NewsletterMetadata metadata, - @JsonProperty("viewer_metadata") NewsletterViewerMetadata viewerMetadata, - @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, defaultImpl = ConcurrentDoublyLinkedList.class) Collection messages + Newsletter( + @JsonProperty("id") + Jid jid, + @JsonProperty("state") + NewsletterState state, + @JsonProperty("thread_metadata") + NewsletterMetadata metadata, + @JsonProperty("viewer_metadata") + NewsletterViewerMetadata viewerMetadata, + @JsonProperty("messages") + MessagesSet messages ) { - this(jid, state, metadata, Optional.ofNullable(viewerMetadata), Objects.requireNonNullElseGet(messages, ConcurrentDoublyLinkedList::new)); + this.jid = jid; + this.state = state; + this.metadata = metadata; + this.viewerMetadata = viewerMetadata; + this.messages = Objects.requireNonNullElseGet(messages, MessagesSet::new); + } + + public Newsletter(Jid jid, NewsletterState state, NewsletterMetadata metadata, NewsletterViewerMetadata viewerMetadata) { + this.jid = jid; + this.state = state; + this.metadata = metadata; + this.viewerMetadata = viewerMetadata; + this.messages = new MessagesSet<>(); + } + + public void addMessage(NewsletterMessageInfo message) { + this.messages.add(message); + } + + public void addMessages(Collection messages) { + this.messages.addAll(messages); + } + + public Collection messages() { + return Collections.unmodifiableCollection(messages); } @Override @@ -39,12 +66,29 @@ public Jid toJid() { return jid; } - public void addMessages(Collection messages) { - this.messages.addAll(messages); + public Jid jid() { + return jid; + } + + public Optional state() { + return Optional.ofNullable(state); + } + + public NewsletterMetadata metadata() { + return metadata; + } + + public Optional viewerMetadata() { + return Optional.ofNullable(viewerMetadata); } @Override - public Collection messages() { - return Collections.unmodifiableCollection(messages); + public boolean equals(Object obj) { + return obj instanceof Newsletter that && Objects.equals(this.jid(), that.jid()); + } + + @Override + public int hashCode() { + return Objects.hash(jid); } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterMetadata.java b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterMetadata.java index 257bae343..5b565addd 100644 --- a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterMetadata.java +++ b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterMetadata.java @@ -1,11 +1,13 @@ package it.auties.whatsapp.model.newsletter; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import it.auties.whatsapp.util.Clock; import java.time.ZonedDateTime; +import java.util.Objects; import java.util.Optional; -import java.util.OptionalInt; +import java.util.OptionalLong; public record NewsletterMetadata( NewsletterName name, @@ -14,11 +16,38 @@ public record NewsletterMetadata( Optional handle, Optional settings, String invite, - @JsonProperty("subscribers_count") OptionalInt subscribers, - String verification, - @JsonProperty("creation_time") long creationTimestampSeconds + OptionalLong subscribers, + boolean verification, + OptionalLong creationTimestampSeconds ) { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + NewsletterMetadata( + NewsletterName name, + NewsletterDescription description, + NewsletterPicture picture, + String handle, + NewsletterSettings settings, + String invite, + @JsonProperty("subscribers_count") + Long subscribers, + String verification, + @JsonProperty("creation_time") + Long creationTimestampSeconds + ) { + this( + name, + description, + Optional.ofNullable(picture), + Optional.ofNullable(handle), + Optional.ofNullable(settings), + invite, + subscribers == null ? OptionalLong.empty() : OptionalLong.of(subscribers), + Objects.equals(verification, "VERIFIED"), + creationTimestampSeconds == null ? OptionalLong.empty() : OptionalLong.of(creationTimestampSeconds) + ); + } + public Optional creationTimestamp() { - return Clock.parseSeconds(creationTimestampSeconds); + return Clock.parseSeconds(creationTimestampSeconds.orElse(0L)); } } diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterReactionSettings.java b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterReactionSettings.java index 5c3b778e5..9db8e1406 100644 --- a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterReactionSettings.java +++ b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterReactionSettings.java @@ -2,9 +2,22 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Arrays; import java.util.List; -public record NewsletterReactionSettings(String value, @JsonProperty("blocked_codes") List blockedCodes, - @JsonProperty("enabled_ts_sec") long enabledTimestampSeconds) { +public record NewsletterReactionSettings(Type value, @JsonProperty("blocked_codes") List blockedCodes, @JsonProperty("enabled_ts_sec") long enabledTimestampSeconds) { + public enum Type { + UNKNOWN, + ALL, + BASIC, + NONE, + BLOCKLIST; + public static Type of(String name) { + return Arrays.stream(values()) + .filter(entry -> entry.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(UNKNOWN); + } + } } diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterState.java b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterState.java index 9234b5c57..e6a57f24a 100644 --- a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterState.java +++ b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterState.java @@ -1,5 +1,29 @@ package it.auties.whatsapp.model.newsletter; -public record NewsletterState(String type) { +import com.fasterxml.jackson.annotation.JsonCreator; +import java.util.Objects; + +public final class NewsletterState { + private String type; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + + public NewsletterState(String type) { + this.type = type; + } + + public String type() { + return type; + } + + public NewsletterState setType(String type) { + this.type = type; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(type); + } } diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerMetadata.java b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerMetadata.java index 1ad6815a7..0dc73b6d6 100644 --- a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerMetadata.java +++ b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerMetadata.java @@ -1,5 +1,44 @@ package it.auties.whatsapp.model.newsletter; -public record NewsletterViewerMetadata(String mute, String role) { +import com.fasterxml.jackson.annotation.JsonCreator; +import java.util.Map; +import java.util.Objects; + +public final class NewsletterViewerMetadata { + private boolean mute; + private NewsletterViewerRole role; + + public NewsletterViewerMetadata(boolean mute, NewsletterViewerRole role) { + this.mute = mute; + this.role = role; + } + + @JsonCreator + NewsletterViewerMetadata(Map json) { + this(Objects.equals(json.get("mute"), "ON"), NewsletterViewerRole.of(json.get("role"))); + } + + public boolean mute() { + return mute; + } + + public NewsletterViewerRole role() { + return role; + } + + public NewsletterViewerMetadata setMute(boolean mute) { + this.mute = mute; + return this; + } + + public NewsletterViewerMetadata setRole(NewsletterViewerRole role) { + this.role = role; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(mute, role); + } } diff --git a/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerRole.java b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerRole.java new file mode 100644 index 000000000..cfcbf9dee --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/newsletter/NewsletterViewerRole.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.model.newsletter; + +import java.util.Arrays; + +public enum NewsletterViewerRole { + UNKNOWN, + OWNER, + SUBSCRIBER, + ADMIN, + GUEST; + + public static NewsletterViewerRole of(String name) { + return Arrays.stream(values()) + .filter(entry -> entry.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java b/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java index 987d80261..bc6434ca9 100644 --- a/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java +++ b/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.model.request; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; import java.util.List; import java.util.Map; diff --git a/src/main/java/it/auties/whatsapp/model/response/NewsletterLeaveResponse.java b/src/main/java/it/auties/whatsapp/model/response/NewsletterLeaveResponse.java new file mode 100644 index 000000000..a762daa51 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/response/NewsletterLeaveResponse.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.util.Json; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Optional; + +public record NewsletterLeaveResponse(@JsonProperty("id") Jid jid) { + public static Optional ofJson(@NonNull String json) { + return Json.readValue(json, JsonResponse.class).data(); + } + + private record JsonResponse(@JsonProperty("xwa2_notify_newsletter_on_leave") Optional data) { + + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/response/NewsletterMuteResponse.java b/src/main/java/it/auties/whatsapp/model/response/NewsletterMuteResponse.java new file mode 100644 index 000000000..ac9646199 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/response/NewsletterMuteResponse.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.model.response; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.util.Json; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public record NewsletterMuteResponse(@JsonProperty("id") Jid jid, @JsonProperty("mute") boolean mute) { + @JsonCreator + NewsletterMuteResponse(Map json) { + this(Jid.of(json.get("jid")), Objects.equals(json.get("mute"), "ON")); + } + + public static Optional ofJson(@NonNull String json) { + return Json.readValue(json, JsonResponse.class).data(); + } + + private record JsonResponse(@JsonProperty("xwa2_notify_newsletter_on_mute_change") Optional data) { + + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/response/NewsletterResponse.java b/src/main/java/it/auties/whatsapp/model/response/NewsletterResponse.java index 1360266cb..fe757be2b 100644 --- a/src/main/java/it/auties/whatsapp/model/response/NewsletterResponse.java +++ b/src/main/java/it/auties/whatsapp/model/response/NewsletterResponse.java @@ -1,13 +1,23 @@ package it.auties.whatsapp.model.response; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.util.Json; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; -public record NewsletterResponse(@JsonAlias({"xwa2_newsletter_update", "xwa2_newsletter_create", "xwa2_newsletter_subscribed", "xwa2_newsletters_directory_list"}) Newsletter newsletter) { +public record NewsletterResponse(Newsletter newsletter) { + @JsonCreator + NewsletterResponse(Map json) { + this(json.values() + .stream() + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Missing newsletter"))); + } + public static Optional ofJson(@NonNull String json) { return Json.readValue(json, JsonResponse.class).data(); } diff --git a/src/main/java/it/auties/whatsapp/model/response/SubscribedNewslettersResponse.java b/src/main/java/it/auties/whatsapp/model/response/SubscribedNewslettersResponse.java index bd29bc3fe..5d8f4d1a6 100644 --- a/src/main/java/it/auties/whatsapp/model/response/SubscribedNewslettersResponse.java +++ b/src/main/java/it/auties/whatsapp/model/response/SubscribedNewslettersResponse.java @@ -1,14 +1,24 @@ package it.auties.whatsapp.model.response; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonCreator; import it.auties.whatsapp.model.newsletter.Newsletter; import it.auties.whatsapp.util.Json; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.Optional; -public record SubscribedNewslettersResponse(@JsonAlias({"xwa2_newsletter_update", "xwa2_newsletter_create", "xwa2_newsletter_subscribed", "xwa2_newsletters_directory_list"}) List newsletters) { +public record SubscribedNewslettersResponse(List newsletters) { + @JsonCreator + SubscribedNewslettersResponse(Map> json) { + this(json.values() + .stream() + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Missing newsletters"))); + } + public static Optional ofJson(@NonNull String json) { return Json.readValue(json, JsonResponse.class).data(); } diff --git a/src/main/java/it/auties/whatsapp/model/setting/GlobalSettings.java b/src/main/java/it/auties/whatsapp/model/setting/GlobalSettings.java index ee651c901..35e243687 100644 --- a/src/main/java/it/auties/whatsapp/model/setting/GlobalSettings.java +++ b/src/main/java/it/auties/whatsapp/model/setting/GlobalSettings.java @@ -4,8 +4,8 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.media.MediaVisibility; import it.auties.whatsapp.model.chat.ChatWallpaper; +import it.auties.whatsapp.model.media.MediaVisibility; import it.auties.whatsapp.util.Clock; import java.time.ZonedDateTime; diff --git a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java index 208ec4af2..c6723a0ec 100644 --- a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java @@ -10,9 +10,9 @@ import it.auties.whatsapp.model.chat.ChatMute; import it.auties.whatsapp.model.companion.CompanionHashState; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageIndexInfo; import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.model.setting.EphemeralSettings; @@ -20,16 +20,7 @@ import it.auties.whatsapp.model.setting.PushNameSettings; import it.auties.whatsapp.model.setting.UnarchiveChatsSettings; import it.auties.whatsapp.model.sync.*; -import it.auties.whatsapp.model.sync.ActionDataSyncBuilder; -import it.auties.whatsapp.model.sync.ActionDataSyncSpec; -import it.auties.whatsapp.model.sync.ExternalBlobReferenceSpec; -import it.auties.whatsapp.model.sync.MutationSyncBuilder; -import it.auties.whatsapp.model.sync.MutationsSyncSpec; import it.auties.whatsapp.model.sync.PatchRequest.PatchEntry; -import it.auties.whatsapp.model.sync.PatchSyncBuilder; -import it.auties.whatsapp.model.sync.PatchSyncSpec; -import it.auties.whatsapp.model.sync.RecordSyncBuilder; -import it.auties.whatsapp.model.sync.SnapshotSyncSpec; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.Medias; import it.auties.whatsapp.util.Specification; diff --git a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java index ae4a60625..58ba91a4c 100644 --- a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java @@ -1,17 +1,25 @@ package it.auties.whatsapp.socket; +import it.auties.linkpreview.LinkPreview; +import it.auties.linkpreview.LinkPreviewMedia; +import it.auties.linkpreview.LinkPreviewResult; +import it.auties.whatsapp.api.TextPreviewSetting; import it.auties.whatsapp.crypto.*; import it.auties.whatsapp.model.action.ContactAction; import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec; +import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplate; +import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplate; import it.auties.whatsapp.model.chat.*; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.contact.ContactStatus; -import it.auties.whatsapp.model.info.MessageIndexInfo; -import it.auties.whatsapp.model.info.MessageInfo; -import it.auties.whatsapp.model.info.MessageInfoBuilder; +import it.auties.whatsapp.model.info.*; import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidProvider; import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.jid.JidType; +import it.auties.whatsapp.model.media.AttachmentType; +import it.auties.whatsapp.model.media.MediaFile; +import it.auties.whatsapp.model.media.MutableAttachmentProvider; import it.auties.whatsapp.model.message.button.*; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.message.model.reserved.LocalMediaMessage; @@ -20,8 +28,10 @@ import it.auties.whatsapp.model.message.server.ProtocolMessage; import it.auties.whatsapp.model.message.server.SenderKeyDistributionMessage; import it.auties.whatsapp.model.message.standard.*; +import it.auties.whatsapp.model.newsletter.NewsletterReaction; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.node.Node; +import it.auties.whatsapp.model.poll.*; import it.auties.whatsapp.model.request.MessageSendRequest; import it.auties.whatsapp.model.setting.EphemeralSettings; import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentitySpec; @@ -30,16 +40,15 @@ import it.auties.whatsapp.model.signal.message.SignalMessage; import it.auties.whatsapp.model.signal.message.SignalPreKeyMessage; import it.auties.whatsapp.model.signal.sender.SenderKeyName; -import it.auties.whatsapp.model.sync.HistorySync; +import it.auties.whatsapp.model.sync.*; import it.auties.whatsapp.model.sync.HistorySync.Type; -import it.auties.whatsapp.model.sync.HistorySyncNotification; -import it.auties.whatsapp.model.sync.HistorySyncSpec; -import it.auties.whatsapp.model.sync.PushName; import it.auties.whatsapp.util.*; +import org.checkerframework.checker.nullness.qual.NonNull; import java.io.ByteArrayOutputStream; import java.lang.System.Logger; import java.lang.System.Logger.Level; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -75,11 +84,242 @@ protected MessageHandler(SocketHandler socketHandler) { } protected CompletableFuture encode(MessageSendRequest request) { - return encodeMessage(request) + attributeMessage(request.info()); + return prepareOutgoingMessage(request.info()) + .thenComposeAsync(ignored -> encodeMessage(request)) .thenRunAsync(() -> attributeOutgoingMessage(request)) .exceptionallyAsync(throwable -> onEncodeError(request, throwable)); } + private CompletableFuture prepareOutgoingMessage(MessageInfo info) { + info.key().setChatJid(info.chatJid().withoutDevice()); + info.key().setSenderJid(info.senderJid() == null ? null : info.senderJid().withoutDevice()); + fixEphemeralMessage(info); + var content = info.message().content(); + return switch (content) { + case LocalMediaMessage mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage); + case ButtonMessage buttonMessage -> attributeButtonMessage(info, buttonMessage); + case TextMessage textMessage -> attributeTextMessage(textMessage); + case PollCreationMessage pollCreationMessage -> attributePollCreationMessage(info, pollCreationMessage); + case PollUpdateMessage pollUpdateMessage -> attributePollUpdateMessage(info, pollUpdateMessage); + case GroupInviteMessage groupInviteMessage -> attributeGroupInviteMessage(info, groupInviteMessage); + case null, default -> CompletableFuture.completedFuture(null); + }; + } + + // TODO: Fix this + private CompletableFuture attributeGroupInviteMessage(MessageInfo info, GroupInviteMessage groupInviteMessage) { + var url = "https://chat.whatsapp.com/%s".formatted(groupInviteMessage.code()); + var preview = LinkPreview.createPreview(URI.create(url)) + .stream() + .map(LinkPreviewResult::images) + .map(Collection::stream) + .map(Stream::findFirst) + .flatMap(Optional::stream) + .findFirst() + .map(LinkPreviewMedia::uri) + .orElse(null); + var parsedCaption = groupInviteMessage.caption() + .map(caption -> "%s: %s".formatted(caption, url)) + .orElse(url); + var replacement = new TextMessageBuilder() + .text(parsedCaption) + .description("WhatsApp Group Invite") + .title(groupInviteMessage.groupName()) + .previewType(TextMessage.PreviewType.NONE) + .thumbnail(Medias.download(preview).orElse(null)) + .matchedText(url) + .canonicalUrl(url) + .build(); + info.setMessage(MessageContainer.of(replacement)); + return CompletableFuture.completedFuture(null); + } + + private void fixEphemeralMessage(MessageInfo info) { + if (info.message().hasCategory(MessageCategory.SERVER)) { + return; + } + + var chat = info.chat().orElse(null); + if (chat != null && chat.isEphemeral()) { + info.message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .ifPresent(contextInfo -> createEphemeralContext(chat, contextInfo)); + info.setMessage(info.message().toEphemeral()); + return; + } + + if (info.message().type() != MessageType.EPHEMERAL) { + return; + } + + info.setMessage(info.message().unbox()); + } + + private void createEphemeralContext(Chat chat, ContextInfo contextInfo) { + var period = chat.ephemeralMessageDuration() + .period() + .toSeconds(); + contextInfo.setEphemeralExpiration((int) period); + } + + // TODO: Async + private CompletableFuture attributeTextMessage(TextMessage textMessage) { + if (socketHandler.store().textPreviewSetting() == TextPreviewSetting.DISABLED) { + return CompletableFuture.completedFuture(null); + } + + var match = LinkPreview.createPreview(textMessage.text()) + .orElse(null); + if (match == null) { + return CompletableFuture.completedFuture(null); + } + + var uri = match.result().uri().toString(); + if (socketHandler.store().textPreviewSetting() == TextPreviewSetting.ENABLED_WITH_INFERENCE && !match.text() + .equals(uri)) { + textMessage.setText(textMessage.text().replace(match.text(), uri)); + } + + var imageUri = match.result() + .images() + .stream() + .reduce(this::compareDimensions) + .map(LinkPreviewMedia::uri) + .orElse(null); + var videoUri = match.result() + .videos() + .stream() + .reduce(this::compareDimensions) + .map(LinkPreviewMedia::uri) + .orElse(null); + textMessage.setMatchedText(uri); + textMessage.setCanonicalUrl(Objects.requireNonNullElse(videoUri, match.result().uri()).toString()); + textMessage.setThumbnail(Medias.download(imageUri).orElse(null)); + textMessage.setDescription(match.result().siteDescription()); + textMessage.setTitle(match.result().title()); + textMessage.setPreviewType(videoUri != null ? TextMessage.PreviewType.VIDEO : TextMessage.PreviewType.NONE); + return CompletableFuture.completedFuture(null); + } + + private LinkPreviewMedia compareDimensions(LinkPreviewMedia first, LinkPreviewMedia second) { + return first.width() * first.height() > second.width() * second.height() ? first : second; + } + + private CompletableFuture attributeMediaMessage(Jid chatJid, LocalMediaMessage mediaMessage) { + var media = mediaMessage.decodedMedia() + .orElseThrow(() -> new IllegalArgumentException("Missing media to upload")); + var attachmentType = getAttachmentType(chatJid, mediaMessage); + var mediaConnection = socketHandler.store().mediaConnection(); + return Medias.upload(media, attachmentType, mediaConnection) + .thenAccept(upload -> attributeMediaMessage(mediaMessage, upload)); + } + + private AttachmentType getAttachmentType(Jid chatJid, LocalMediaMessage mediaMessage) { + if (!chatJid.hasServer(JidServer.NEWSLETTER)) { + return mediaMessage.attachmentType(); + } + + return switch (mediaMessage.mediaType()) { + case IMAGE -> AttachmentType.NEWSLETTER_IMAGE; + case DOCUMENT -> AttachmentType.NEWSLETTER_DOCUMENT; + case AUDIO -> AttachmentType.NEWSLETTER_AUDIO; + case VIDEO -> AttachmentType.NEWSLETTER_VIDEO; + case STICKER -> AttachmentType.NEWSLETTER_STICKER; + case NONE -> throw new IllegalArgumentException("Unexpected empty message"); + }; + } + + + private MutableAttachmentProvider attributeMediaMessage(MutableAttachmentProvider mediaMessage, MediaFile upload) { + if(mediaMessage instanceof LocalMediaMessage localMediaMessage) { + localMediaMessage.setHandle(upload.handle()); + } + + return mediaMessage.setMediaSha256(upload.fileSha256()) + .setMediaEncryptedSha256(upload.fileEncSha256()) + .setMediaKey(upload.mediaKey()) + .setMediaUrl(upload.url()) + .setMediaKeyTimestamp(upload.timestamp()) + .setMediaDirectPath(upload.directPath()) + .setMediaSize(upload.fileLength()); + } + + private CompletableFuture attributePollCreationMessage(MessageInfo info, PollCreationMessage pollCreationMessage) { + var pollEncryptionKey = pollCreationMessage.encryptionKey() + .orElseGet(KeyHelper::senderKey); + pollCreationMessage.setEncryptionKey(pollEncryptionKey); + info.setMessageSecret(pollEncryptionKey); + info.message() + .deviceInfo() + .ifPresent(deviceContextInfo -> deviceContextInfo.setMessageSecret(pollEncryptionKey)); + var metadata = new PollAdditionalMetadata(false); + info.setPollAdditionalMetadata(metadata); + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture attributePollUpdateMessage(MessageInfo info, PollUpdateMessage pollUpdateMessage) { + if (pollUpdateMessage.encryptedMetadata().isPresent()) { + return CompletableFuture.completedFuture(null); + } + + var iv = BytesHelper.random(12); + var me = socketHandler.store().jid(); + if(me.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + var additionalData = "%s\0%s".formatted(pollUpdateMessage.pollCreationMessageKey().id(), me.get().withoutDevice()); + var encryptedOptions = pollUpdateMessage.votes().stream().map(entry -> Sha256.calculate(entry.name())).toList(); + var pollUpdateEncryptedOptions = PollUpdateEncryptedOptionsSpec.encode(new PollUpdateEncryptedOptions(encryptedOptions)); + var originalPollInfo = socketHandler.store() + .findMessageByKey(pollUpdateMessage.pollCreationMessageKey()) + .orElseThrow(() -> new NoSuchElementException("Missing original poll message")); + var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content(); + var originalPollSender = originalPollInfo.senderJid().withoutDevice().toString().getBytes(StandardCharsets.UTF_8); + var modificationSenderJid = info.senderJid().withoutDevice(); + pollUpdateMessage.setVoter(modificationSenderJid); + var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8); + var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8); + var useSecretPayload = BytesHelper.concat( + pollUpdateMessage.pollCreationMessageKey().id().getBytes(StandardCharsets.UTF_8), + originalPollSender, + modificationSender, + secretName + ); + var encryptionKey = originalPollMessage.encryptionKey() + .orElseThrow(() -> new NoSuchElementException("Missing encryption key")); + var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32); + var pollUpdateEncryptedPayload = AesGcm.encrypt(iv, pollUpdateEncryptedOptions, useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8)); + var pollUpdateEncryptedMetadata = new PollUpdateEncryptedMetadata(pollUpdateEncryptedPayload, iv); + pollUpdateMessage.setEncryptedMetadata(pollUpdateEncryptedMetadata); + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture attributeButtonMessage(MessageInfo info, ButtonMessage buttonMessage) { + return switch (buttonMessage) { + case ButtonsMessage buttonsMessage when buttonsMessage.header().isPresent() + && buttonsMessage.header().get() instanceof LocalMediaMessage mediaMessage -> attributeMediaMessage(info.chatJid(), mediaMessage); + case TemplateMessage templateMessage when templateMessage.format().isPresent() -> { + var templateFormatter = templateMessage.format().get(); + yield switch (templateFormatter) { + case HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate + when highlyStructuredFourRowTemplate.title().isPresent() && highlyStructuredFourRowTemplate.title().get() instanceof LocalMediaMessage fourRowMedia -> + attributeMediaMessage(info.chatJid(), fourRowMedia); + case HydratedFourRowTemplate hydratedFourRowTemplate when hydratedFourRowTemplate.title().isPresent() && hydratedFourRowTemplate.title().get() instanceof LocalMediaMessage hydratedFourRowMedia -> + attributeMediaMessage(info.chatJid(), hydratedFourRowMedia); + case null, default -> CompletableFuture.completedFuture(null); + }; + } + case InteractiveMessage interactiveMessage + when interactiveMessage.header().isPresent() + && interactiveMessage.header().get().attachment().isPresent() + && interactiveMessage.header().get().attachment().get() instanceof LocalMediaMessage interactiveMedia -> attributeMediaMessage(info.chatJid(), interactiveMedia); + default -> CompletableFuture.completedFuture(null); + }; + } + private CompletableFuture encodeMessage(MessageSendRequest request) { return request.info().chatJid().hasServer(JidServer.NEWSLETTER) ? encodePlainMessage(request) : encodeE2EMessage(request); } @@ -430,18 +670,27 @@ private void parseSession(Node node) { builder.createOutgoing(registrationId, identity, signedKey, key); } - public CompletableFuture decode(Node node) { + public CompletableFuture decode(Node node, JidProvider chatOverride, boolean notify) { try { var businessName = getBusinessName(node); + if (node.hasNode("unavailable")) { + return decodeChatMessage(node, null, businessName, notify); + } + var encrypted = node.findNodes("enc"); - if (node.hasNode("unavailable") && !node.hasNode("enc")) { - return decodeMessage(node, null, businessName); + if(!encrypted.isEmpty()) { + var futures = encrypted.stream() + .map(message -> decodeChatMessage(node, message, businessName, notify)) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(futures); } - var futures = encrypted.stream() - .map(message -> decodeMessage(node, message, businessName)) - .toArray(CompletableFuture[]::new); - return CompletableFuture.allOf(futures); + if(node.hasNode("plaintext")) { + decodeNewsletterMessage(node, businessName, chatOverride, notify); + return CompletableFuture.completedFuture(null); + } + + return decodeChatMessage(node, null, businessName, notify); } catch (Throwable throwable) { socketHandler.handleFailure(MESSAGE, throwable); return CompletableFuture.failedFuture(throwable); @@ -494,10 +743,50 @@ private String getMediaType(MessageContainer container) { }; } - private CompletableFuture decodeMessage(Node infoNode, Node messageNode, String businessName) { + private void decodeNewsletterMessage(Node messageNode, String businessName, JidProvider chatOverride, boolean notify) { + try { + var newsletter = messageNode.attributes() + .getJid("from") + .flatMap(socketHandler.store()::findNewsletterByJid) + .or(() -> socketHandler.store().findNewsletterByJid(chatOverride)) + .orElseThrow(() -> new NoSuchElementException("Missing newsletter")); + var id = messageNode.attributes() + .getString("id"); + socketHandler.sendMessageAck(messageNode); + var receiptType = getReceiptType("newsletter", false); + socketHandler.sendReceipt(newsletter.jid(), null, List.of(id), receiptType); + var timestamp = messageNode.attributes() + .getOptionalLong("t"); + var viewsContainer = messageNode.findNode("views_count"); + var views = viewsContainer.isPresent() ? viewsContainer.get().attributes().getOptionalLong("count") : OptionalLong.empty(); + var reactions = messageNode.findNode("reactions") + .stream() + .map(node -> node.findNodes("reaction")) + .flatMap(Collection::stream) + .map(entry -> new NewsletterReaction(entry.attributes().getString("code"), entry.attributes().getLong("count"))) + .toList(); + var result = messageNode.findNode("plaintext") + .flatMap(Node::contentAsBytes) + .map(MessageContainerSpec::decode) + .map(messageContainer -> new NewsletterMessageInfo(id, timestamp, views, reactions, messageContainer)); + if(result.isEmpty()) { + return; + } + + newsletter.addMessage(result.get()); + if(!notify){ + return; + } + + socketHandler.onNewsletterMessage(result.get()); + } catch (Throwable throwable) { + socketHandler.handleFailure(MESSAGE, throwable); + } + } + + private CompletableFuture decodeChatMessage(Node infoNode, Node messageNode, String businessName, boolean notify) { try { lock.lock(); - var offline = infoNode.attributes().hasKey("offline"); var pushName = infoNode.attributes().getNullableString("notify"); var timestamp = infoNode.attributes().getLong("t"); var id = infoNode.attributes().getRequiredString("id"); @@ -534,6 +823,7 @@ private CompletableFuture decodeMessage(Node infoNode, Node messageNode, S return sendReceipt(infoNode, id, key.chatJid(), key.senderJid().orElse(null), key.fromMe()); } + if (messageNode == null) { logger.log(Level.WARNING, "Cannot decode message(id: %s, from: %s)".formatted(id, from)); return sendReceipt(infoNode, id, key.chatJid(), key.senderJid().orElse(null), key.fromMe()); @@ -557,8 +847,8 @@ private CompletableFuture decodeMessage(Node infoNode, Node messageNode, S .message(messageContainer) .build(); attributeMessageReceipt(info); - socketHandler.store().attribute(info); - saveMessage(info, offline); + attributeMessage(info); + saveMessage(info, notify); socketHandler.onReply(info); return sendReceipt(infoNode, id, key.chatJid(), key.senderJid().orElse(null), key.fromMe()); } catch (Throwable throwable) { @@ -641,7 +931,7 @@ private void attributeMessageReceipt(MessageInfo info) { info.setStatus(MessageStatus.READ); } - private void saveMessage(MessageInfo info, boolean offline) { + private void saveMessage(MessageInfo info, boolean notify) { if(info.message().content() instanceof SenderKeyDistributionMessage distributionMessage) { handleDistributionMessage(distributionMessage, info.senderJid()); } @@ -675,7 +965,10 @@ private void saveMessage(MessageInfo info, boolean offline) { if (!info.ignore() && !info.fromMe()) { chat.setUnreadMessagesCount(chat.unreadMessagesCount() + 1); } - socketHandler.onNewMessage(info, offline); + + if(notify) { + socketHandler.onNewMessage(info); + } } private void handleDistributionMessage(SenderKeyDistributionMessage distributionMessage, Jid from) { @@ -793,7 +1086,7 @@ private void handleHistorySync(HistorySync history) { private void handleInitialStatus(HistorySync history) { var store = socketHandler.store(); for (var messageInfo : history.statusV3Messages()) { - store.addStatus(messageInfo); + socketHandler.store().addStatus(messageInfo); } socketHandler.onStatus(); } @@ -895,12 +1188,16 @@ private void onForcedHistorySyncCompletion() { private void handleConversations(HistorySync history) { var store = socketHandler.store(); for (var chat : history.conversations()) { + for(var message : chat.messages()) { + attributeMessage(message.messageInfo()); + } + var pastParticipants = pastParticipantsQueue.remove(chat.jid()); if (pastParticipants != null) { chat.addPastParticipants(pastParticipants); } - store.addChat(chat); + socketHandler.store().addChat(chat); } } @@ -925,6 +1222,118 @@ private List toSingleList(List... all) { .toList(); } + protected MessageInfo attributeMessage(@NonNull MessageInfo info) { + var chat = socketHandler.store().findChatByJid(info.chatJid()) + .orElseGet(() -> socketHandler.store().addNewChat(info.chatJid())); + info.setChat(chat); + var me = socketHandler.store().jid().orElse(null); + if (info.fromMe() && me != null) { + info.key().setSenderJid(me.withoutDevice()); + } + + info.key() + .senderJid() + .ifPresent(senderJid -> attributeSender(info, senderJid)); + info.message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .ifPresent(this::attributeContext); + processMessage(info); + return info; + } + + private MessageKey attributeSender(MessageInfo info, Jid senderJid) { + var contact = socketHandler.store().findContactByJid(senderJid) + .orElseGet(() -> socketHandler.store().addContact(new Contact(senderJid))); + info.setSender(contact); + return info.key(); + } + + private void attributeContext(ContextInfo contextInfo) { + contextInfo.quotedMessageSenderJid().ifPresent(senderJid -> attributeContextSender(contextInfo, senderJid)); + contextInfo.quotedMessageChatJid().ifPresent(chatJid -> attributeContextChat(contextInfo, chatJid)); + } + + private void attributeContextChat(ContextInfo contextInfo, Jid chatJid) { + var chat = socketHandler.store().findChatByJid(chatJid) + .orElseGet(() -> socketHandler.store().addNewChat(chatJid)); + contextInfo.setQuotedMessageChat(chat); + } + + private void attributeContextSender(ContextInfo contextInfo, Jid senderJid) { + var contact = socketHandler.store().findContactByJid(senderJid) + .orElseGet(() -> socketHandler.store().addContact(new Contact(senderJid))); + contextInfo.setQuotedMessageSender(contact); + } + + private void processMessage(MessageInfo info) { + switch (info.message().content()) { + case PollCreationMessage pollCreationMessage -> handlePollCreation(info, pollCreationMessage); + case PollUpdateMessage pollUpdateMessage -> handlePollUpdate(info, pollUpdateMessage); + case ReactionMessage reactionMessage -> handleReactionMessage(info, reactionMessage); + default -> {} + } + } + + private void handlePollCreation(MessageInfo info, PollCreationMessage pollCreationMessage) { + if (pollCreationMessage.encryptionKey().isPresent()) { + return; + } + + info.message() + .deviceInfo() + .flatMap(DeviceContextInfo::messageSecret) + .or(info::messageSecret) + .ifPresent(pollCreationMessage::setEncryptionKey); + } + + private void handlePollUpdate(MessageInfo info, PollUpdateMessage pollUpdateMessage) { + var originalPollInfo = socketHandler.store().findMessageByKey(pollUpdateMessage.pollCreationMessageKey()) + .orElseThrow(() -> new NoSuchElementException("Missing original poll message")); + var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content(); + pollUpdateMessage.setPollCreationMessage(originalPollMessage); + var originalPollSender = originalPollInfo.senderJid() + .withoutDevice() + .toString() + .getBytes(StandardCharsets.UTF_8); + var modificationSenderJid = info.senderJid().withoutDevice(); + pollUpdateMessage.setVoter(modificationSenderJid); + var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8); + var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8); + var useSecretPayload = BytesHelper.concat( + originalPollInfo.id().getBytes(StandardCharsets.UTF_8), + originalPollSender, + modificationSender, + secretName + ); + var encryptionKey = originalPollMessage.encryptionKey() + .orElseThrow(() -> new NoSuchElementException("Missing encryption key")); + var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32); + var additionalData = "%s\0%s".formatted( + originalPollInfo.id(), + modificationSenderJid + ); + var metadata = pollUpdateMessage.encryptedMetadata() + .orElseThrow(() -> new NoSuchElementException("Missing encrypted metadata")); + var decrypted = AesGcm.decrypt(metadata.iv(), metadata.payload(), useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8)); + var pollVoteMessage = PollUpdateEncryptedOptionsSpec.decode(decrypted); + var selectedOptions = pollVoteMessage.selectedOptions() + .stream() + .map(sha256 -> originalPollMessage.getSelectableOption(HexFormat.of().formatHex(sha256))) + .flatMap(Optional::stream) + .toList(); + originalPollMessage.addSelectedOptions(modificationSenderJid, selectedOptions); + pollUpdateMessage.setVotes(selectedOptions); + var update = new PollUpdate(info.key(), pollVoteMessage, Clock.nowMilliseconds()); + info.pollUpdates().add(update); + } + + private void handleReactionMessage(MessageInfo info, ReactionMessage reactionMessage) { + info.setIgnore(true); + socketHandler.store().findMessageByKey(reactionMessage.key()) + .ifPresent(message -> message.reactions().add(reactionMessage)); + } + protected void dispose() { historyCache.clear(); if(executor != null && !executor.isShutdown()) { diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java index a8c4423b7..b24631eb3 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java @@ -20,11 +20,13 @@ import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.jid.JidProvider; import it.auties.whatsapp.model.jid.JidServer; -import it.auties.whatsapp.model.message.model.*; +import it.auties.whatsapp.model.message.model.MessageContainer; +import it.auties.whatsapp.model.message.model.MessageKey; +import it.auties.whatsapp.model.message.model.MessageKeyBuilder; +import it.auties.whatsapp.model.message.model.MessageStatus; import it.auties.whatsapp.model.message.server.ProtocolMessage; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.newsletter.Newsletter; -import it.auties.whatsapp.model.newsletter.NewsletterReaction; import it.auties.whatsapp.model.node.Attributes; import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.model.privacy.PrivacySettingEntry; @@ -184,15 +186,22 @@ public void onMessage(byte[] message) { .exceptionallyAsync(throwable -> handleFailure(LOGIN, throwable)); return; } - if (keys.readKey() == null) { + + var readKey = keys.readKey(); + if (readKey.isEmpty()) { return; } - var plainText = AesGcm.decrypt(keys.readCounter(true), message, keys.readKey()); - var decoder = new BinaryDecoder(); - var node = decoder.decode(plainText); - onNodeReceived(node); - store.resolvePendingRequest(node, false); - streamHandler.digest(node); + + try { + var plainText = AesGcm.decrypt(keys.readCounter(true), message, readKey.get()); + var decoder = new BinaryDecoder(); + var node = decoder.decode(plainText); + onNodeReceived(node); + store.resolvePendingRequest(node, false); + streamHandler.digest(node); + }catch (Throwable throwable) { + handleFailure(CRYPTOGRAPHY, throwable); + } } private void onNodeReceived(Node deciphered) { @@ -308,8 +317,8 @@ protected CompletableFuture pullInitialPatches() { return appStateHandler.pullInitial(); } - public void decodeMessage(Node node) { - messageHandler.decode(node); + public void decodeMessage(Node node, JidProvider chatOverride, boolean notify) { + messageHandler.decode(node, chatOverride, notify); } public CompletableFuture sendPeerMessage(Jid companion, ProtocolMessage message) { @@ -336,7 +345,6 @@ public CompletableFuture sendPeerMessage(Jid companion, ProtocolMessage me } public CompletableFuture sendMessage(MessageSendRequest request) { - store.attribute(request.info()); return messageHandler.encode(request); } @@ -443,7 +451,7 @@ public CompletableFuture> queryPicture(@NonNull JidProvider chat) var body = Node.of("picture", Map.of("query", "url", "type", "image")); if (chat.toJid().hasServer(JidServer.GROUP)) { return queryGroupMetadata(chat.toJid()) - .thenComposeAsync(result -> sendQuery("get", "w:profile:picture", Map.of(result.community() ? "parent_group_jid" : "target", chat.toJid()), body)) + .thenComposeAsync(result -> sendQuery("get", "w:profile:picture", Map.of(result.isCommunity() ? "parent_group_jid" : "target", chat.toJid()), body)) .thenApplyAsync(this::parseChatPicture); } @@ -497,36 +505,15 @@ public CompletableFuture queryNewsletterMessages(JidProvider newsletterJid var newsletter = store.findNewsletterByJid(newsletterJid) .orElseThrow(() -> new NoSuchElementException("Missing newsletter")); return sendQuery("get", "newsletter", Node.of("messages", Map.of("count", count, "type", "invite", "key", newsletter.metadata().invite()))) - .thenAcceptAsync(result -> onNewsletterMessages(result, newsletter)); + .thenAcceptAsync(result -> onNewsletterMessages(newsletter, result)); } - private void onNewsletterMessages(Node result, Newsletter newsletter) { - var results = result.findNode("messages") + private void onNewsletterMessages(Newsletter newsletter, Node result) { + result.findNode("messages") .stream() .map(messages -> messages.findNodes("message")) .flatMap(Collection::stream) - .map(this::parseNewsletterMessage) - .flatMap(Optional::stream) - .toList(); - newsletter.addMessages(results); - } - - private Optional parseNewsletterMessage(Node message) { - var id = message.attributes().getString("id"); - var timestamp = message.attributes().getLong("t"); - var views = message.findNode("views_count") - .map(viewCounter -> viewCounter.attributes().getLong("count")) - .orElseThrow(() -> new NoSuchElementException("Missing views")); - var reactions = message.findNode("reactions") - .stream() - .map(node -> node.findNodes("reaction")) - .flatMap(Collection::stream) - .map(entry -> new NewsletterReaction(entry.attributes().getString("code"), entry.attributes().getLong("count"))) - .toList(); - return message.findNode("plaintext") - .flatMap(Node::contentAsBytes) - .map(MessageContainerSpec::decode) - .map(messageContainer -> new NewsletterMessageInfo(id, timestamp, views, reactions, messageContainer)); + .forEach(messages -> decodeMessage(messages, newsletter, false)); } public CompletableFuture queryGroupMetadata(JidProvider group) { @@ -536,9 +523,11 @@ public CompletableFuture queryGroupMetadata(JidProvider group) { } protected GroupMetadata handleGroupMetadata(Node response) { - var metadata = response.findNode("group") + var metadata = Optional.of(response) + .filter(entry -> entry.hasDescription("group")) + .or(() -> response.findNode("group")) .map(this::parseGroupMetadata) - .orElseThrow(() -> new NoSuchElementException("Erroneous newsletters: %s".formatted(response))); + .orElseThrow(() -> new NoSuchElementException("Erroneous response: %s".formatted(response))); var chat = store.findChatByJid(metadata.jid()) .orElseGet(() -> store().addNewChat(metadata.jid())); if (chat != null) { @@ -693,12 +682,10 @@ protected void onUpdateChatPresence(ContactStatus status, Jid jid, Chat chat) { }); } - protected void onNewMessage(MessageInfo info, boolean offline) { + protected void onNewMessage(MessageInfo info) { callListenersAsync(listener -> { listener.onNewMessage(whatsapp, info); listener.onNewMessage(info); - listener.onNewMessage(whatsapp, info, offline); - listener.onNewMessage(info, offline); }); } @@ -798,6 +785,13 @@ protected void onNewsletters() { }); } + protected void onNewsletterMessage(NewsletterMessageInfo messageInfo) { + callListenersAsync(listener -> { + listener.onNewMessage(whatsapp, messageInfo); + listener.onNewMessage(messageInfo); + }); + } + protected void onStatus() { callListenersAsync(listener -> { listener.onStatus(whatsapp, store().status()); diff --git a/src/main/java/it/auties/whatsapp/socket/SocketRequest.java b/src/main/java/it/auties/whatsapp/socket/SocketRequest.java index af1d92bad..23fe91644 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketRequest.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketRequest.java @@ -114,10 +114,9 @@ private byte[] getPrologueData(@NonNull Store store) { private byte[] encryptMessage(Keys keys) { var encodedBody = body(); var body = getBody(encodedBody); - if (keys.writeKey() == null) { - return body; - } - return AesGcm.encrypt(keys.writeCounter(true), body, keys.writeKey()); + return keys.writeKey() + .map(bytes -> AesGcm.encrypt(keys.writeCounter(true), body, bytes)) + .orElse(body); } private byte[] getBody(Object encodedBody) { diff --git a/src/main/java/it/auties/whatsapp/socket/SocketSession.java b/src/main/java/it/auties/whatsapp/socket/SocketSession.java index 5fca77c55..a8fef3771 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketSession.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketSession.java @@ -7,6 +7,7 @@ import it.auties.whatsapp.util.Specification; import java.io.DataInputStream; +import java.io.EOFException; import java.io.IOException; import java.io.UncheckedIOException; import java.net.*; @@ -14,8 +15,14 @@ import java.net.http.HttpClient; import java.net.http.WebSocket; import java.nio.ByteBuffer; -import java.util.*; -import java.util.concurrent.*; +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; import static it.auties.whatsapp.util.Specification.Whatsapp.SOCKET_ENDPOINT; @@ -25,11 +32,13 @@ public abstract sealed class SocketSession permits SocketSession.WebSocketSessio final URI proxy; final Executor executor; final ReentrantLock outputLock; + final Collection inputParts; SocketListener listener; private SocketSession(URI proxy, Executor executor) { this.proxy = proxy; this.executor = executor; this.outputLock = new ReentrantLock(true); + this.inputParts = new ConcurrentLinkedDeque<>(); } abstract CompletableFuture connect(SocketListener listener); @@ -74,12 +83,10 @@ int decodeLength(ByteBuf buffer) { } public static final class WebSocketSession extends SocketSession implements WebSocket.Listener { - private final Collection inputParts; private WebSocket session; WebSocketSession(URI proxy, Executor executor) { super(proxy, executor); - this.inputParts = new ConcurrentLinkedDeque<>(); } @SuppressWarnings("resource") // Not needed @@ -193,7 +200,6 @@ static final class RawSocketSession extends SocketSession { Authenticator.setDefault(new ProxyAuthenticator()); } - // TODO: Explore AsynchronousSocketChannel private Socket socket; private boolean closed; @@ -223,17 +229,18 @@ CompletableFuture connect(SocketListener listener) { } @Override - @SuppressWarnings("UnusedReturnValue") void disconnect() { - if (!isOpen()) { + if(closed) { return; } try { socket.close(); - closeResources(); - } catch (IOException exception) { - throw new UncheckedIOException("Cannot close connection to host", exception); + this.closed = true; + this.socket = null; + listener.onClose(); + }catch (IOException exception) { + listener.onError(exception); } } @@ -254,7 +261,7 @@ public CompletableFuture sendBinary(byte[] bytes) { stream.write(bytes); stream.flush(); }catch (SocketException exception) { - closeResources(); + disconnect(); } catch (IOException exception) { throw new RequestException(exception); }finally { @@ -266,45 +273,27 @@ public CompletableFuture sendBinary(byte[] bytes) { private void readMessages() { try (var input = new DataInputStream(socket.getInputStream())) { while (isOpen()) { - var length = decodeLength(input); + var lengthBytes = new byte[3]; + input.readFully(lengthBytes); + var lengthBuffer = BytesHelper.newBuffer(lengthBytes); + var length = decodeLength(lengthBuffer); + lengthBuffer.release(); if (length < 0) { break; } + var message = new byte[length]; - if(isOpen()) { - input.readFully(message); - } + input.readFully(message); + System.out.println(message.length); listener.onMessage(message); } + } catch (EOFException ignored) { + } catch(Throwable throwable) { listener.onError(throwable); } finally { - closeResources(); + disconnect(); } } - - private int decodeLength(DataInputStream input) { - try { - var lengthBytes = new byte[3]; - input.readFully(lengthBytes); - return decodeLength(BytesHelper.newBuffer(lengthBytes)); - } catch (IOException exception) { - return -1; - } - } - - private void closeResources() { - if(closed) { - return; - } - - this.closed = true; - this.socket = null; - if (executor instanceof ExecutorService service) { - service.shutdownNow(); - } - - listener.onClose(); - } } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java index f4fee9876..6d44147e5 100644 --- a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java @@ -36,9 +36,7 @@ import it.auties.whatsapp.model.privacy.PrivacySettingValue; import it.auties.whatsapp.model.request.MessageSendRequest; import it.auties.whatsapp.model.request.SubscribedNewslettersRequest; -import it.auties.whatsapp.model.response.ContactStatusResponse; -import it.auties.whatsapp.model.response.NewsletterResponse; -import it.auties.whatsapp.model.response.SubscribedNewslettersResponse; +import it.auties.whatsapp.model.response.*; import it.auties.whatsapp.model.signal.auth.*; import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; @@ -66,7 +64,6 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -88,7 +85,6 @@ class StreamHandler { private final SocketHandler socketHandler; private final WebVerificationSupport webVerificationSupport; private final Map retries; - private final AtomicBoolean badMac; private final AtomicReference lastLinkCodeKey; private ScheduledExecutorService service; @@ -96,7 +92,6 @@ protected StreamHandler(SocketHandler socketHandler, WebVerificationSupport webV this.socketHandler = socketHandler; this.webVerificationSupport = webVerificationSupport; this.retries = new HashMap<>(); - this.badMac = new AtomicBoolean(); this.lastLinkCodeKey = new AtomicReference<>(); } @@ -110,20 +105,12 @@ protected void digest(@NonNull Node node) { case "receipt" -> digestReceipt(node); case "stream:error" -> digestError(node); case "success" -> digestSuccess(node); - case "message" -> socketHandler.decodeMessage(node); + case "message" -> socketHandler.decodeMessage(node, null, true); case "notification" -> digestNotification(node); case "presence", "chatstate" -> digestChatState(node); - case "xmlstreamend" -> digestStreamEnd(); } } - private void digestStreamEnd() { - if (socketHandler.state() != SocketState.CONNECTED || badMac.get()) { - return; - } - socketHandler.disconnect(DisconnectReason.DISCONNECTED); - } - private void digestFailure(Node node) { var reason = node.attributes().getInt("reason"); if (reason == 401 || reason == 403 || reason == 405) { @@ -352,7 +339,20 @@ private void digestNotification(Node node) { } private void handleNewsletter(Node node) { - // Received node Node[description=notification, attributes={t=1696615183, from=120363174593809831@newsletter, id=1485455203, type=newsletter}, content=[Node[description=live_updates, content=[Node[description=messages, attributes={t=1696615183}, content=[Node[description=message, attributes={server_id=138}, content=[Node[description=views_count, attributes={count=16206}], Node[description=reactions, content=[Node[description=reaction, attributes={code=❤️, count=62}], Node[description=reaction, attributes={code=👍, count=126}], Node[description=reaction, attributes={code=👏, count=1}], Node[description=reaction, attributes={code=😂, count=14}], Node[description=reaction, attributes={code=😍, count=1}], Node[description=reaction, attributes={code=😢, count=24}], Node[description=reaction, attributes={code=😮, count=10}], Node[description=reaction, attributes={code=🙏, count=18}]]]]], Node[description=message, attributes={server_id=139}, content=[Node[description=views_count, attributes={count=10869}], Node[description=reactions, content=[Node[description=reaction, attributes={code=⚡, count=1}], Node[description=reaction, attributes={code=❤️, count=64}], Node[description=reaction, attributes={code=👍, count=56}], Node[description=reaction, attributes={code=😂, count=10}], Node[description=reaction, attributes={code=😢, count=1}], Node[description=reaction, attributes={code=😮, count=29}], Node[description=reaction, attributes={code=🙏, count=13}]]]]], Node[description=message, attributes={server_id=140}, content=[Node[description=views_count, attributes={count=7509}], Node[description=reactions, content=[Node[description=reaction, attributes={code=⚡, count=1}], Node[description=reaction, attributes={code=❤️, count=30}], Node[description=reaction, attributes={code=👍, count=80}], Node[description=reaction, attributes={code=😂, count=17}], Node[description=reaction, attributes={code=😢, count=1}], Node[description=reaction, attributes={code=😮, count=40}], Node[description=reaction, attributes={code=🙏, count=4}]]]]]]]]]]] + var liveUpdates = node.findNode("live_updates"); + if(liveUpdates.isEmpty()) { + return; + } + + + var messages = liveUpdates.get().findNode("messages"); + if(messages.isEmpty()) { + return; + } + + messages.get() + .findNodes("message") + .forEach(entry -> socketHandler.decodeMessage(entry, null, false)); } private void handleMexNamespace(Node node) { @@ -363,28 +363,40 @@ private void handleMexNamespace(Node node) { } switch (update.attributes().getString("op_name")) { - case "NotificationNewsletterJoin" -> { - update.contentAsString() - .flatMap(NewsletterResponse::ofJson) - .map(NewsletterResponse::newsletter) - .ifPresent(result -> socketHandler.sendQuery("get", "newsletter", Node.of("messages", Map.of("jid", result.jid().withServer(JidServer.WHATSAPP), "count", 1, "type", "Jid")))); - } - case "NotificationNewsletterMuteChange" -> { - - } - case "NotificationNewsletterLeave" -> { - - } - case "NotificationNewsletterStateChange" -> { + case "NotificationNewsletterJoin" -> handleNewsletterJoin(update); + case "NotificationNewsletterMuteChange" -> handleNewsletterMute(update); + case "NotificationNewsletterLeave" -> handleNewsletterLeave(update); + case "NotificationNewsletterStateChange", "NotificationNewsletterAdminMetadataUpdate", "NotificationNewsletterUpdate" -> {} + } + } - } - case "NotificationNewsletterAdminMetadataUpdate" -> { + private void handleNewsletterJoin(Node update) { + var joinPayload = update.contentAsString() + .orElseThrow(() -> new NoSuchElementException("Missing join payload")); + var joinJson = NewsletterResponse.ofJson(joinPayload) + .orElseThrow(() -> new NoSuchElementException("Malformed join payload")); + socketHandler.store().addNewsletter(joinJson.newsletter()); + socketHandler.queryNewsletterMessages(joinJson.newsletter().jid(), DEFAULT_NEWSLETTER_MESSAGES); + } - } - case "NotificationNewsletterUpdate" -> { + private void handleNewsletterMute(Node update) { + var mutePayload = update.contentAsString() + .orElseThrow(() -> new NoSuchElementException("Missing mute payload")); + var muteJson = NewsletterMuteResponse.ofJson(mutePayload) + .orElseThrow(() -> new NoSuchElementException("Malformed mute payload")); + var newsletter = socketHandler.store() + .findNewsletterByJid(muteJson.jid()) + .orElseThrow(() -> new NoSuchElementException("Missing newsletter")); + newsletter.viewerMetadata() + .ifPresent(viewerMetadata -> viewerMetadata.setMute(muteJson.mute())); + } - } - } + private void handleNewsletterLeave(Node update) { + var leavePayload = update.contentAsString() + .orElseThrow(() -> new NoSuchElementException("Missing leave payload")); + var leaveJson = NewsletterLeaveResponse.ofJson(leavePayload) + .orElseThrow(() -> new NoSuchElementException("Malformed leave payload")); + socketHandler.store().removeNewsletter(leaveJson.jid()); } private void handleCompanionRegistration(Node node) { @@ -516,7 +528,7 @@ private void addMessageForGroupStubType(Chat chat, MessageInfo.StubType stubType .build(); socketHandler.store().attribute(message); chat.addNewMessage(message); - socketHandler.onNewMessage(message, false); + socketHandler.onNewMessage(message); if(participantJid == null){ return; } @@ -721,14 +733,13 @@ private void digestIb(Node node) { private void digestError(Node node) { if (node.hasNode("bad-mac")) { - badMac.set(true); socketHandler.handleFailure(CRYPTOGRAPHY, new RuntimeException("Detected a bad mac")); return; } var statusCode = node.attributes().getInt("code"); switch (statusCode) { - case 515, 503 -> socketHandler.disconnect(DisconnectReason.RECONNECTING); + case 503 -> socketHandler.disconnect(DisconnectReason.RECONNECTING); case 500 -> socketHandler.disconnect(DisconnectReason.LOGGED_OUT); case 401 -> handleStreamError(node); default -> node.children().forEach(error -> socketHandler.store().resolvePendingRequest(error, true)); @@ -755,36 +766,50 @@ private void digestSuccess(Node node) { sendPreKeys(); } - schedulePing(); createMediaConnection(0, null); var loggedInFuture = queryInitialInfo() .thenRunAsync(this::onInitialInfo) .exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable)); - queryNewsletters(); - - if (!socketHandler.keys().registered()) { - queryGroups(); + var registered = socketHandler.keys().registered(); + if (!registered) { + CompletableFuture.allOf(queryGroups(), queryNewsletters()) + .exceptionally(throwable -> socketHandler.handleFailure(LOGIN, throwable)) + .thenRun(this::onRegistration); return; } - var chatsFuture = socketHandler.store().serializer() + var attributionFuture = socketHandler.store() + .serializer() .attributeStore(socketHandler.store()) .exceptionallyAsync(exception -> socketHandler.handleFailure(MESSAGE, exception)); - CompletableFuture.allOf(loggedInFuture, chatsFuture) - .thenRunAsync(socketHandler::onChats); + CompletableFuture.allOf(loggedInFuture, attributionFuture) + .thenRunAsync(this::onAttribution); + } + + private void onRegistration() { + socketHandler.store().serialize(true); + socketHandler.keys().serialize(true); + } + + private void onAttribution() { + socketHandler.onChats(); + socketHandler.onNewsletters(); } - private void queryNewsletters() { + private CompletableFuture queryNewsletters() { var query = new SubscribedNewslettersRequest(new SubscribedNewslettersRequest.Variable()); - socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6388546374527196"), Json.writeValueAsBytes(query))) - .thenAcceptAsync(this::parseNewsletters); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6388546374527196"), Json.writeValueAsBytes(query))) + .thenAcceptAsync(this::parseNewsletters) + .exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable)); } private void parseNewsletters(Node result) { - result.findNode("result") + var newslettersPayload = result.findNode("result") .flatMap(Node::contentAsString) - .flatMap(SubscribedNewslettersResponse::ofJson) - .ifPresent(this::onNewsletters); + .orElseThrow(() -> new NoSuchElementException("Missing newsletter payload")); + var newslettersJson = SubscribedNewslettersResponse.ofJson(newslettersPayload) + .orElseThrow(() -> new NoSuchElementException("Malformed newsletters payload: " + newslettersPayload)); + onNewsletters(newslettersJson); } private void onNewsletters(SubscribedNewslettersResponse result) { @@ -815,8 +840,8 @@ private void scheduleNewsletterSubscription(Newsletter newsletter, long timeout) executor.execute(() -> subscribeToNewsletterUpdatesForever(newsletter)); } - private void queryGroups() { - socketHandler.sendQuery(JidServer.GROUP.toJid(), "get", "w:g2", Node.of("participating", Node.of("participants"), Node.of("description"))) + private CompletableFuture queryGroups() { + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "get", "w:g2", Node.of("participating", Node.of("participants"), Node.of("description"))) .thenAcceptAsync(this::onGroupsQuery); } @@ -886,16 +911,14 @@ private void schedulePing(){ } service = Executors.newSingleThreadScheduledExecutor(); - service.scheduleAtFixedRate(this::sendPing, PING_INTERVAL, PING_INTERVAL, TimeUnit.SECONDS); + service.scheduleAtFixedRate(this::sendPing, 0, PING_INTERVAL, TimeUnit.SECONDS); } private void onInitialInfo() { + schedulePing(); socketHandler.onLoggedIn(); if (!socketHandler.keys().registered()) { - if(socketHandler.store().clientType() == ClientType.WEB){ - socketHandler.keys().setRegistered(true); - } - + socketHandler.keys().setRegistered(socketHandler.keys().registered() || socketHandler.store().clientType() == ClientType.WEB); return; } @@ -915,19 +938,20 @@ private CompletableFuture queryRequiredInfo() { } private CompletableFuture queryRequiredMobileInfo() { - var requiredFuture = socketHandler.sendQuery("get", "w", Node.of("props", Map.of("protocol", "2", "hash", ""))) + return socketHandler.sendQuery("get", "w", Node.of("props", Map.of("protocol", "2", "hash", ""))) .thenAcceptAsync(this::parseProps) .thenComposeAsync(ignored -> checkBusinessStatus()) + .thenRunAsync(() -> { + socketHandler.sendQuery("get", "urn:xmpp:whatsapp:push", Node.of("config", Map.of("version", 1))) + .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); + socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", Node.of("clean", Map.of("timestamp", 0, "type", "account_sync"))) + .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); + if(socketHandler.store().business()){ + socketHandler.sendQuery("get", "fb:thrift_iq", Map.of("smax_id", 42), Node.of("linked_accounts")) + .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); + } + }) .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); - socketHandler.sendQuery("get", "urn:xmpp:whatsapp:push", Node.of("config", Map.of("version", 1))) - .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); - socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", Node.of("clean", Map.of("timestamp", 0, "type", "account_sync"))) - .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); - if(socketHandler.store().business()){ - socketHandler.sendQuery("get", "fb:thrift_iq", Map.of("smax_id", 42), Node.of("linked_accounts")) - .exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception)); - } - return requiredFuture; } private CompletableFuture queryRequiredWebInfo() { @@ -1063,9 +1087,9 @@ private void sendPing() { return; } - socketHandler.sendQueryWithNoResponse("get", "w:p", Node.of("ping")) - .exceptionallyAsync(throwable -> socketHandler.handleFailure(STREAM, throwable)); - socketHandler.onSocketEvent(SocketEvent.PING); + socketHandler.sendQuery("get", "w:p", Node.of("ping")) + .thenRun(() -> socketHandler.onSocketEvent(SocketEvent.PING)) + .exceptionallyAsync(throwable -> socketHandler.handleFailure(STREAM, throwable)); socketHandler.store().serialize(true); socketHandler.keys().serialize(true); } @@ -1295,7 +1319,7 @@ protected void dispose() { if(service != null){ service.shutdownNow(); } - badMac.set(false); + lastLinkCodeKey.set(null); } } diff --git a/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java b/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java deleted file mode 100644 index 4e3031df5..000000000 --- a/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java +++ /dev/null @@ -1,995 +0,0 @@ -package it.auties.whatsapp.util; - - -/* - * Written by Doug Lea with assistance from members of JCP JSR-166 - * Expert Group and released to the public domain, as explained at - * http://creativecommons.org/licenses/publicdomain - */ - -import com.fasterxml.jackson.annotation.JsonCreator; - -import java.util.*; -import java.util.concurrent.atomic.AtomicReference; - -/** - * A concurrent linked-list implementation of a {@link Deque} (double-ended - * queue). Concurrent insertion, removal, and access operations execute safely - * across multiple threads. Iterators are weakly consistent, returning - * elements reflecting the state of the deque at some point at or since the - * creation of the iterator. They do not throw {@link - * ConcurrentModificationException}, and may proceed concurrently with other - * operations. - * - *

- * This class and its iterators implement all of the optional methods - * of the {@link Collection} and {@link Iterator} interfaces. Like most other - * concurrent collection implementations, this class does not permit the use of - * null elements. because some null arguments and return values - * cannot be reliably distinguished from the absence of elements. Arbitrarily, - * the {@link Collection#remove} method is mapped to - * removeFirstOccurrence, and {@link Collection#add} is mapped to - * addLast. - * - *

- * Beware that, unlike in most collections, the size method is - * NOT a constant-time operation. Because of the asynchronous nature - * of these deques, determining the current number of elements requires a - * traversal of the elements. - * - *

- * This class is Serializable, but relies on default serialization - * mechanisms. Usually, it is a better idea for any serializable class using a - * ConcurrentLinkedDeque to instead serialize a snapshot of the - * elements obtained by method toArray. - * - * @author Doug Lea - * @param - * the type of elements held in this collection - */ -@SuppressWarnings("ALL") -public class ConcurrentDoublyLinkedList extends AbstractCollection - implements java.io.Serializable { - - /* - * This is an adaptation of an algorithm described in Paul Martin's "A - * Practical Lock-Free Doubly-Linked List". Sun Labs Tech report. The basic - * idea is to primarily rely on next-pointers to ensure consistency. - * Prev-pointers are in part optimistic, reconstructed using forward - * pointers as needed. The main forward list uses a variant of HM-list - * algorithm similar to the one used in ConcurrentSkipListMap class, but a - * little simpler. It is also basically similar to the approach in Edya - * Ladan-Mozes and Nir Shavit "An Optimistic Approach to Lock-Free FIFO - * Queues" in DISC04. - * - * Quoting a summary in Paul Martin's tech report: - * - * All cleanups work to maintain these invariants: (1) forward pointers are - * the ground truth. (2) forward pointers to dead nodes can be improved by - * swinging them further forward around the dead node. (2.1) forward - * pointers are still correct when pointing to dead nodes, and forward - * pointers from dead nodes are left as they were when the node was deleted. - * (2.2) multiple dead nodes may point forward to the same node. (3) - * backward pointers were correct when they were installed (3.1) backward - * pointers are correct when pointing to any node which points forward to - * them, but since more than one forward pointer may point to them, the live - * one is best. (4) backward pointers that are out of date due to deletion - * point to a deleted node, and need to point further back until they point - * to the live node that points to their source. (5) backward pointers that - * are out of date due to insertion point too far backwards, so shortening - * their scope (by searching forward) fixes them. (6) backward pointers from - * a dead node cannot be "improved" since there may be no live node pointing - * forward to their origin. (However, it does no harm to try to improve them - * while racing with a deletion.) - * - * - * Notation guide for local variables n, b, f : a node, its predecessor, and - * successor s : some other successor - */ - - // Minor convenience utilities - - /** - * Returns true if given reference is non-null and isn't a header, trailer, - * or marker. - * - * @param n - * (possibly null) node - * @return true if n exists as a user node - */ - private static boolean usable(Node n) { - return n != null && !n.isSpecial(); - } - - /** - * Throws NullPointerException if argument is null - * - * @param v - * the element - */ - private static void checkNullArg(Object v) { - if (v == null) - throw new NullPointerException(); - } - - /** - * Returns element unless it is null, in which case throws - * NoSuchElementException. - * - * @param v - * the element - * @return the element - */ - private E screenNullResult(E v) { - if (v == null) - throw new NoSuchElementException(); - return v; - } - - /** - * Creates an array list and fills it with elements of this list. Used by - * toArray. - * - * @return the arrayList - */ - private ArrayList toArrayList() { - ArrayList c = new ArrayList(); - for (Node n = header.forward(); n != null; n = n.forward()) - c.add(n.element); - return c; - } - - // Fields and constructors - - private static final long serialVersionUID = 876323262645176354L; - - /** - * List header. First usable node is at header.forward(). - */ - private final Node header; - - /** - * List trailer. Last usable node is at trailer.back(). - */ - private final Node trailer; - - /** - * Constructs an empty deque. - */ - @JsonCreator - public ConcurrentDoublyLinkedList() { - Node h = new Node(null, null, null); - Node t = new Node(null, null, h); - h.setNext(t); - header = h; - trailer = t; - } - - /** - * Constructs a deque containing the elements of the specified collection, - * in the order they are returned by the collection's iterator. - * - * @param c - * the collection whose elements are to be placed into this - * deque. - * @throws NullPointerException - * if c or any element within it is null - */ - public ConcurrentDoublyLinkedList(Collection c) { - this(); - addAll(c); - } - - /** - * Prepends the given element at the beginning of this deque. - * - * @param o - * the element to be inserted at the beginning of this deque. - * @throws NullPointerException - * if the specified element is null - */ - public void addFirst(E o) { - checkNullArg(o); - while (header.append(o) == null) - ; - } - - /** - * Appends the given element to the end of this deque. This is identical in - * function to the add method. - * - * @param o - * the element to be inserted at the end of this deque. - * @throws NullPointerException - * if the specified element is null - */ - public void addLast(E o) { - checkNullArg(o); - while (trailer.prepend(o) == null) - ; - } - - /** - * Prepends the given element at the beginning of this deque. - * - * @param o - * the element to be inserted at the beginning of this deque. - * @return true always - * @throws NullPointerException - * if the specified element is null - */ - public boolean offerFirst(E o) { - addFirst(o); - return true; - } - - /** - * Appends the given element to the end of this deque. (Identical in - * function to the add method; included only for consistency.) - * - * @param o - * the element to be inserted at the end of this deque. - * @return true always - * @throws NullPointerException - * if the specified element is null - */ - public boolean offerLast(E o) { - addLast(o); - return true; - } - - /** - * Retrieves, but does not remove, the first element of this deque, or - * returns null if this deque is empty. - * - * @return the first element of this queue, or null if empty. - */ - public E peekFirst() { - Node n = header.successor(); - return (n == null) ? null : n.element; - } - - /** - * Retrieves, but does not remove, the last element of this deque, or - * returns null if this deque is empty. - * - * @return the last element of this deque, or null if empty. - */ - public E peekLast() { - Node n = trailer.predecessor(); - return (n == null) ? null : n.element; - } - - /** - * Returns the first element in this deque. - * - * @return the first element in this deque. - * @throws NoSuchElementException - * if this deque is empty. - */ - public E getFirst() { - return screenNullResult(peekFirst()); - } - - /** - * Returns the last element in this deque. - * - * @return the last element in this deque. - * @throws NoSuchElementException - * if this deque is empty. - */ - public E getLast() { - return screenNullResult(peekLast()); - } - - /** - * Retrieves and removes the first element of this deque, or returns null if - * this deque is empty. - * - * @return the first element of this deque, or null if empty. - */ - public E pollFirst() { - for (;;) { - Node n = header.successor(); - if (!usable(n)) - return null; - if (n.delete()) - return n.element; - } - } - - /** - * Retrieves and removes the last element of this deque, or returns null if - * this deque is empty. - * - * @return the last element of this deque, or null if empty. - */ - public E pollLast() { - for (;;) { - Node n = trailer.predecessor(); - if (!usable(n)) - return null; - if (n.delete()) - return n.element; - } - } - - /** - * Removes and returns the first element from this deque. - * - * @return the first element from this deque. - * @throws NoSuchElementException - * if this deque is empty. - */ - public E removeFirst() { - return screenNullResult(pollFirst()); - } - - /** - * Removes and returns the last element from this deque. - * - * @return the last element from this deque. - * @throws NoSuchElementException - * if this deque is empty. - */ - public E removeLast() { - return screenNullResult(pollLast()); - } - - // *** Queue and stack methods *** - public boolean offer(E e) { - return offerLast(e); - } - - public boolean add(E e) { - return offerLast(e); - } - - public E poll() { - return pollFirst(); - } - - public E remove() { - return removeFirst(); - } - - public E peek() { - return peekFirst(); - } - - public E element() { - return getFirst(); - } - - public void push(E e) { - addFirst(e); - } - - public E pop() { - return removeFirst(); - } - - /** - * Removes the first element e such that o.equals(e), - * if such an element exists in this deque. If the deque does not contain - * the element, it is unchanged. - * - * @param o - * element to be removed from this deque, if present. - * @return true if the deque contained the specified element. - * @throws NullPointerException - * if the specified element is null - */ - public boolean removeFirstOccurrence(Object o) { - checkNullArg(o); - for (;;) { - Node n = header.forward(); - for (;;) { - if (n == null) - return false; - if (o.equals(n.element)) { - if (n.delete()) - return true; - else - break; // restart if interference - } - n = n.forward(); - } - } - } - - /** - * Removes the last element e such that o.equals(e), - * if such an element exists in this deque. If the deque does not contain - * the element, it is unchanged. - * - * @param o - * element to be removed from this deque, if present. - * @return true if the deque contained the specified element. - * @throws NullPointerException - * if the specified element is null - */ - public boolean removeLastOccurrence(Object o) { - checkNullArg(o); - for (;;) { - Node s = trailer; - for (;;) { - Node n = s.back(); - if (s.isDeleted() || (n != null && n.successor() != s)) - break; // restart if pred link is suspect. - if (n == null) - return false; - if (o.equals(n.element)) { - if (n.delete()) - return true; - else - break; // restart if interference - } - s = n; - } - } - } - - /** - * Returns true if this deque contains at least one element - * e such that o.equals(e). - * - * @param o - * element whose presence in this deque is to be tested. - * @return true if this deque contains the specified element. - */ - public boolean contains(Object o) { - if (o == null) - return false; - for (Node n = header.forward(); n != null; n = n.forward()) - if (o.equals(n.element)) - return true; - return false; - } - - /** - * Returns true if this collection contains no elements. - *

- * - * @return true if this collection contains no elements. - */ - public boolean isEmpty() { - return !usable(header.successor()); - } - - /** - * Returns the number of elements in this deque. If this deque contains more - * than Integer.MAX_VALUE elements, it returns - * Integer.MAX_VALUE. - * - *

- * Beware that, unlike in most collections, this method is NOT a - * constant-time operation. Because of the asynchronous nature of these - * deques, determining the current number of elements requires traversing - * them all to count them. Additionally, it is possible for the size to - * change during execution of this method, in which case the returned newsletters - * will be inaccurate. Thus, this method is typically not very useful in - * concurrent applications. - * - * @return the number of elements in this deque. - */ - public int size() { - long count = 0; - for (Node n = header.forward(); n != null; n = n.forward()) - ++count; - return (count >= Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) count; - } - - /** - * Removes the first element e such that o.equals(e), - * if such an element exists in this deque. If the deque does not contain - * the element, it is unchanged. - * - * @param o - * element to be removed from this deque, if present. - * @return true if the deque contained the specified element. - * @throws NullPointerException - * if the specified element is null - */ - public boolean remove(Object o) { - return removeFirstOccurrence(o); - } - - /** - * Appends all of the elements in the specified collection to the end of - * this deque, in the order that they are returned by the specified - * collection's iterator. The behavior of this operation is undefined if the - * specified collection is modified while the operation is in progress. - * (This implies that the behavior of this call is undefined if the - * specified Collection is this deque, and this deque is nonempty.) - * - * @param c - * the elements to be inserted into this deque. - * @return true if this deque changed as a newsletters of the call. - * @throws NullPointerException - * if c or any element within it is null - */ - public boolean addAll(Collection c) { - Iterator it = c.iterator(); - if (!it.hasNext()) - return false; - do { - addLast(it.next()); - } while (it.hasNext()); - return true; - } - - /** - * Removes all of the elements from this deque. - */ - public void clear() { - while (pollFirst() != null) - ; - } - - /** - * Returns an array containing all of the elements in this deque in the - * correct order. - * - * @return an array containing all of the elements in this deque in the - * correct order. - */ - public Object[] toArray() { - return toArrayList().toArray(); - } - - /** - * Returns an array containing all of the elements in this deque in the - * correct order; the runtime type of the returned array is that of the - * specified array. If the deque fits in the specified array, it is returned - * therein. Otherwise, a new array is allocated with the runtime type of the - * specified array and the size of this deque. - *

- * - * If the deque fits in the specified array with room to spare (i.e., the - * array has more elements than the deque), the element in the array - * immediately following the end of the collection is set to null. This is - * useful in determining the length of the deque only if the caller - * knows that the deque does not contain any null elements. - * - * @param a - * the array into which the elements of the deque are to be - * stored, if it is big enough; otherwise, a new array of the - * same runtime type is allocated for this purpose. - * @return an array containing the elements of the deque. - * @throws ArrayStoreException - * if the runtime type of a is not a supertype of the runtime - * type of every element in this deque. - * @throws NullPointerException - * if the specified array is null. - */ - public T[] toArray(T[] a) { - return toArrayList().toArray(a); - } - - /** - * Returns a weakly consistent iterator over the elements in this deque, in - * first-to-last order. The next method returns elements - * reflecting the state of the deque at some point at or since the creation - * of the iterator. The method does not throw - * {@link ConcurrentModificationException}, and may proceed concurrently - * with other operations. - * - * @return an iterator over the elements in this deque - */ - public Iterator iterator() { - return new CLDIterator(); - } - - /** - * Returns a weakly consistent iterator over the elements in this deque, in - * last-to-first order. The next method returns elements - * reflecting the state of the deque at some point at or since the creation - * of the iterator. The method does not throw - * {@link ConcurrentModificationException}, and may proceed concurrently - * with other operations. - * - * @return an iterator over the elements in this deque - */ - public Iterator descendingIterator() { - return new ReverseCLDIterator(); - } - - final class CLDIterator implements Iterator { - Node last; - - Node next = header.forward(); - - public boolean hasNext() { - return next != null; - } - - public E next() { - Node l = last = next; - if (l == null) - throw new NoSuchElementException(); - next = next.forward(); - return l.element; - } - - public void remove() { - Node l = last; - if (l == null) - throw new IllegalStateException(); - while (!l.delete() && !l.isDeleted()) - ; - } - } - - final class ReverseCLDIterator implements Iterator { - Node last; - - Node previous = trailer.predecessor(); - - public boolean hasNext() { - return previous != null && previous != header; - } - - public E next() { - Node l = last = previous; - if (l == null) - throw new NoSuchElementException(); - previous = previous.predecessor(); - return l.element; - } - - public void remove() { - Node l = last; - if (l == null) - throw new IllegalStateException(); - while (!l.delete() && !l.isDeleted()) - ; - } - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - - if(!(object instanceof ConcurrentDoublyLinkedList that)) { - return false; - } - - if(this.size() != that.size()) { - return false; - } - - var thisIterator = iterator(); - var thatIterator = that.iterator(); - while (thisIterator.hasNext()) { - if(!Objects.equals(thisIterator.next(), thatIterator.next())) { - return false; - } - } - - return true; - } - - @Override - public int hashCode() { - int hashCode = 0; - for (Node n = header.forward(); n != null; n = n.forward()) { - hashCode += 31 * Objects.hashCode(n.element); - } - - return hashCode; - } -} - - - -/** - * Linked Nodes. As a minor efficiency hack, this class opportunistically - * inherits from AtomicReference, with the atomic ref used as the "next" - * link. - *

- * Nodes are in doubly-linked lists. There are three kinds of special nodes, - * distinguished by: * The list header has a null prev link * The list - * trailer has a null next link * A deletion marker has a prev link pointing - * to itself. All three kinds of special nodes have null element fields. - *

- * Regular nodes have non-null element, next, and prev fields. To avoid - * visible inconsistencies when deletions overlap element replacement, - * replacements are done by replacing the node, not just setting the - * element. - *

- * Nodes can be traversed by read-only ConcurrentLinkedDeque class - * operations just by following raw next pointers, so long as they ignore - * any special nodes seen along the way. (This is automated in method - * forward.) However, traversal using prev pointers is not guaranteed to see - * all live nodes since a prev pointer of a deleted node can become - * unrecoverably stale. - */ -@SuppressWarnings("ALL") -class Node extends AtomicReference> { - private volatile Node prev; - - final E element; - - /** Creates a node with given contents */ - Node(E element, Node next, Node prev) { - super(next); - this.prev = prev; - this.element = element; - } - - /** Creates a marker node with given successor */ - Node(Node next) { - super(next); - this.prev = this; - this.element = null; - } - - /** - * Gets next link (which is actually the value held as atomic - * reference). - */ - Node getNext() { - return get(); - } - - /** - * Sets next link - * - * @param n - * the next node - */ - void setNext(Node n) { - set(n); - } - - /** - * compareAndSet next link - */ - private boolean casNext(Node cmp, Node val) { - return compareAndSet(cmp, val); - } - - /** - * Gets prev link - */ - Node getPrev() { - return prev; - } - - /** - * Sets prev link - * - * @param b - * the previous node - */ - void setPrev(Node b) { - prev = b; - } - - /** - * Returns true if this is a header, trailer, or marker node - */ - boolean isSpecial() { - return element == null; - } - - /** - * Returns true if this is a trailer node - */ - boolean isTrailer() { - return getNext() == null; - } - - /** - * Returns true if this is a header node - */ - boolean isHeader() { - return getPrev() == null; - } - - /** - * Returns true if this is a marker node - */ - boolean isMarker() { - return getPrev() == this; - } - - /** - * Returns true if this node is followed by a marker, meaning that it is - * deleted. - * - * @return true if this node is deleted - */ - boolean isDeleted() { - Node f = getNext(); - return f != null && f.isMarker(); - } - - /** - * Returns next node, ignoring deletion marker - */ - private Node nextNonmarker() { - Node f = getNext(); - return (f == null || !f.isMarker()) ? f : f.getNext(); - } - - /** - * Returns the next non-deleted node, swinging next pointer around any - * encountered deleted nodes, and also patching up successor''s prev - * link to point back to this. Returns null if this node is trailer so - * has no successor. - * - * @return successor, or null if no such - */ - Node successor() { - Node f = nextNonmarker(); - for (;;) { - if (f == null) - return null; - if (!f.isDeleted()) { - if (f.getPrev() != this && !isDeleted()) - f.setPrev(this); // relink f's prev - return f; - } - Node s = f.nextNonmarker(); - if (f == getNext()) - casNext(f, s); // unlink f - f = s; - } - } - - /** - * Returns the apparent predecessor of target by searching forward for - * it starting at this node, patching up pointers while traversing. Used - * by predecessor(). - * - * @return target's predecessor, or null if not found - */ - private Node findPredecessorOf(Node target) { - Node n = this; - for (;;) { - Node f = n.successor(); - if (f == target) - return n; - if (f == null) - return null; - n = f; - } - } - - /** - * Returns the previous non-deleted node, patching up pointers as - * needed. Returns null if this node is header so has no successor. May - * also return null if this node is deleted, so doesn't have a distinct - * predecessor. - * - * @return predecessor or null if not found - */ - Node predecessor() { - Node n = this; - for (;;) { - Node b = n.getPrev(); - if (b == null) - return n.findPredecessorOf(this); - Node s = b.getNext(); - if (s == this) - return b; - if (s == null || !s.isMarker()) { - Node p = b.findPredecessorOf(this); - if (p != null) - return p; - } - n = b; - } - } - - /** - * Returns the next node containing a nondeleted user element. Use for - * forward list traversal. - * - * @return successor, or null if no such - */ - Node forward() { - Node f = successor(); - return (f == null || f.isSpecial()) ? null : f; - } - - /** - * Returns previous node containing a nondeleted user element, if - * possible. Use for backward list traversal, but beware that if this - * method is called from a deleted node, it might not be able to - * determine a usable predecessor. - * - * @return predecessor, or null if no such could be found - */ - Node back() { - Node f = predecessor(); - return (f == null || f.isSpecial()) ? null : f; - } - - /** - * Tries to insert a node holding element as successor, failing if this - * node is deleted. - * - * @param element - * the element - * @return the new node, or null on failure. - */ - Node append(E element) { - for (;;) { - Node f = getNext(); - if (f == null || f.isMarker()) - return null; - Node x = new Node(element, f, this); - if (casNext(f, x)) { - f.setPrev(x); // optimistically link - return x; - } - } - } - - /** - * Tries to insert a node holding element as predecessor, failing if no - * live predecessor can be found to link to. - * - * @param element - * the element - * @return the new node, or null on failure. - */ - Node prepend(E element) { - for (;;) { - Node b = predecessor(); - if (b == null) - return null; - Node x = new Node(element, this, b); - if (b.casNext(this, x)) { - setPrev(x); // optimistically link - return x; - } - } - } - - /** - * Tries to mark this node as deleted, failing if already deleted or if - * this node is header or trailer - * - * @return true if successful - */ - boolean delete() { - Node b = getPrev(); - Node f = getNext(); - if (b != null && f != null && !f.isMarker() - && casNext(f, new Node(f))) { - if (b.casNext(this, f)) - f.setPrev(b); - return true; - } - return false; - } - - /** - * Tries to insert a node holding element to replace this node. failing - * if already deleted. - * - * @param newElement - * the new element - * @return the new node, or null on failure. - */ - Node replace(E newElement) { - for (;;) { - Node b = getPrev(); - Node f = getNext(); - if (b == null || f == null || f.isMarker()) - return null; - Node x = new Node(newElement, f, b); - if (casNext(f, new Node(x))) { - b.successor(); // to relink b - x.successor(); // to relink f - return x; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/util/Json.java b/src/main/java/it/auties/whatsapp/util/Json.java index c2a6dad6b..06b18fabe 100644 --- a/src/main/java/it/auties/whatsapp/util/Json.java +++ b/src/main/java/it/auties/whatsapp/util/Json.java @@ -1,13 +1,19 @@ package it.auties.whatsapp.util; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.Objects; +import java.util.Optional; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; @@ -18,15 +24,18 @@ import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS; import static java.lang.System.Logger.Level.ERROR; -@SuppressWarnings("deprecation") public final class Json { private static final ObjectMapper json; static { try { - json = new ObjectMapper().registerModule(new Jdk8Module().configureAbsentsAsNulls(true)) + var optionalModule = new SimpleModule(); + optionalModule.addDeserializer(Optional.class, new OptionalDeserializer()); + json = new ObjectMapper() + .registerModule(new Jdk8Module()) .registerModule(new JavaTimeModule()) .registerModule(new ParameterNamesModule()) + .registerModule(optionalModule) .setSerializationInclusion(NON_DEFAULT) .enable(FAIL_ON_EMPTY_BEANS) .enable(ACCEPT_SINGLE_VALUE_AS_ARRAY) @@ -77,4 +86,47 @@ public static T readValue(String value, TypeReference clazz) { throw new UncheckedIOException("Cannot read json", exception); } } + + private static class OptionalDeserializer extends StdDeserializer> implements ContextualDeserializer { + private final JavaType optionalType; + public OptionalDeserializer() { + super(Optional.class); + this.optionalType = null; + } + + private OptionalDeserializer(JavaType optionalType) { + super(Optional.class); + this.optionalType = optionalType; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) { + if(property == null) { + var optionalType = context.getContextualType(); + var valueType = optionalType.containedTypeOrUnknown(0); + return new OptionalDeserializer(valueType); + } + + var optionalType = property.getType(); + var valueType = optionalType.containedTypeOrUnknown(0); + return new OptionalDeserializer(valueType); + } + + @Override + public Optional deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException { + Objects.requireNonNull(optionalType, "Missing context"); + var node = jsonParser.getCodec().readTree(jsonParser); + var value = jsonParser.getCodec().treeToValue(node, optionalType.getRawClass()); + if (value == null) { + return Optional.empty(); + } + + return Optional.of(value); + } + + @Override + public Optional getNullValue(DeserializationContext context) { + return Optional.empty(); + } + } } diff --git a/src/main/java/it/auties/whatsapp/util/MessagesSet.java b/src/main/java/it/auties/whatsapp/util/MessagesSet.java new file mode 100644 index 000000000..de4f3d57c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/util/MessagesSet.java @@ -0,0 +1,348 @@ +package it.auties.whatsapp.util; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class MessagesSet extends AbstractQueue implements Deque { + private final AtomicReference> head; + private final AtomicReference> tail; + private final Set hashes; + public MessagesSet() { + this.head = new AtomicReference<>(null); + this.tail = new AtomicReference<>(null); + this.hashes = ConcurrentHashMap.newKeySet(); + } + + @Override + public void push(E e) { + add(e); + } + + @Override + public boolean offer(E e) { + return add(e); + } + + @Override + public boolean offerLast(E e) { + return add(e); + } + + @Override + public void addLast(E message) { + add(message); + } + + @Override + public boolean add(E e) { + var hash = Objects.hashCode(e); + if(hashes.contains(hash)) { + return false; + } + + var newNode = new Node<>(e); + var oldTail = tail.getAndSet(newNode); + if (oldTail == null) { + head.set(newNode); + } else { + oldTail.next = newNode; + newNode.prev = oldTail; + } + + hashes.add(hash); + return true; + } + + @Override + public boolean offerFirst(E e) { + addFirst(e); + return true; + } + + @Override + public void addFirst(E message) { + var hash = Objects.hashCode(message); + if(hashes.contains(hash)) { + return; + } + + var newNode = new Node<>(message); + var oldHead = head.getAndSet(newNode); + if (oldHead == null) { + tail.set(newNode); + } else { + oldHead.prev = newNode; + newNode.next = oldHead; + } + + hashes.add(hash); + } + + @Override + public E removeLast() { + return remove(); + } + + @Override + public boolean remove(Object o) { + var hash = Objects.hashCode(o); + if(!hashes.contains(hash)) { + return false; + } + + var node = head.get(); + while (node != null) { + if (node.item.equals(o)) { + removeNode(node); + hashes.remove(hash); + return true; + } + node = node.next; + } + + return false; + } + + @Override + public boolean removeAll(@NonNull Collection collection) { + var hashCodes = collection.stream() + .map(Objects::hashCode) + .collect(Collectors.toUnmodifiableSet()); + var node = head.get(); + while (node != null) { + var hash = Objects.hashCode(node.item); + if (hashCodes.contains(hash)) { + removeNode(node); + hashes.remove(hash); + return true; + } + node = node.next; + } + + return false; + } + + @Override + public E poll() { + return remove(); + } + + @Override + public E pollLast() { + return remove(); + } + + @Override + public E remove() { + var headItem = head.get(); + if (headItem == null) { + return null; + } + + var node = head.getAndSet(headItem.next); + if (node == tail.get()) { + tail.compareAndSet(node, node.prev); + } + + hashes.remove(Objects.hashCode(node.item)); + return node.item; + } + + private void removeNode(Node node) { + if (node == head.get()) { + head.compareAndSet(node, node.next); + } else if (node == tail.get()) { + tail.compareAndSet(node, node.prev); + } else { + node.prev.next = node.next; + node.next.prev = node.prev; + } + } + + @Override + public E pollFirst() { + return removeFirst(); + } + + @Override + public E pop() { + return removeFirst(); + } + + @Override + public E removeFirst() { + var node = head.getAndSet(head.get().next); + if (node == tail.get()) { + tail.compareAndSet(node, node.prev); + } + return node.item; + } + + @Override + public int size() { + return hashes.size(); + } + + @Override + public boolean isEmpty() { + return head.get() == null; + } + + @Override + public boolean contains(Object o) { + return hashes.contains(Objects.hashCode(o)); + } + + @Override + @NonNull + public Iterator iterator() { + return new Iterator<>() { + private Node nextNode = head.get(); + + @Override + public boolean hasNext() { + return nextNode != null; + } + + @Override + public E next() { + if (nextNode == null) { + throw new NoSuchElementException(); + } + + var item = nextNode.item; + nextNode = nextNode.next; + return item; + } + }; + } + + @NonNull + public Iterator descendingIterator() { + return new Iterator<>() { + private Node previousNode = tail.get(); + + @Override + public boolean hasNext() { + return previousNode != null; + } + + @Override + public E next() { + if (previousNode == null) { + throw new NoSuchElementException(); + } + + var item = previousNode.item; + previousNode = previousNode.prev; + return item; + } + }; + } + + + @Override + public E element() { + return peek(); + } + + @Override + public E peekFirst() { + return peek(); + } + + @Override + public E peek() { + var headItem = head.get(); + if(headItem == null) { + return null; + } + + return headItem.item; + } + + @Override + public E peekLast() { + var tailItem = tail.get(); + if(tailItem == null) { + return null; + } + + return tailItem.item; + } + + @Override + public E getFirst() { + var result = pollFirst(); + if(result == null) { + throw new NoSuchElementException(); + } + + return result; + } + + @Override + public E getLast() { + var result = pollLast(); + if(result == null) { + throw new NoSuchElementException(); + } + + return result; + } + + @Override + public boolean removeFirstOccurrence(Object o) { + var node = head.get(); + while (node != null) { + if (node.item.equals(o)) { + removeNode(node); + return true; + } + node = node.next; + } + return false; + } + + @Override + public boolean removeIf(Predicate filter) { + var node = tail.get(); + while (node != null) { + if (filter.test(node.item)) { + removeNode(node); + return true; + } + node = node.prev; + } + + return false; + } + + @Override + public boolean removeLastOccurrence(Object o) { + var node = tail.get(); + while (node != null) { + if (node.item.equals(o)) { + removeNode(node); + return true; + } + node = node.prev; + } + return false; + } + + private static class Node { + final E item; + Node next; + Node prev; + + Node(E item) { + this.item = item; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java b/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java index dfd74a49a..b738a2a53 100644 --- a/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java +++ b/src/main/java/it/auties/whatsapp/util/RegistrationHelper.java @@ -29,7 +29,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -60,6 +59,7 @@ private static CompletableFuture requestVerificationCode(Store store, Keys private static CompletableFuture checkRequestResponse(Store store, Keys keys, int statusCode, String body, VerificationCodeError lastError, VerificationCodeMethod method) { try { + System.out.println(body); if(statusCode != HttpURLConnection.HTTP_OK) { throw new RegistrationException(null, body); } @@ -91,15 +91,30 @@ private static CompletableFuture> requestVerificationCodeOpt return getRegistrationOptions(store, keys, lastError == VerificationCodeError.OLD_VERSION || lastError == VerificationCodeError.BAD_TOKEN, - Map.entry("mcc", countryCode.mcc()), - Map.entry("mnc", countryCode.mnc()), - Map.entry("sim_mcc", countryCode.mcc()), - Map.entry("sim_mnc", countryCode.mnc()), + Map.entry("mcc", padCountryCodeValue(String.valueOf(countryCode.mcc()))), + Map.entry("mnc", padCountryCodeValue(countryCode.mnc())), + Map.entry("sim_mcc", "000"), + Map.entry("sim_mnc", "000"), Map.entry("method", method.type()), - Map.entry("reason", lastError != null ? lastError.data() : "") + Map.entry("reason", ""), + Map.entry("hasav", 1) ); } + private static String padCountryCodeValue(String inputString) { + if (inputString.length() >= 3) { + return inputString; + } + + var stringBuilder = new StringBuilder(); + while (stringBuilder.length() < 3 - inputString.length()) { + stringBuilder.append('0'); + } + + stringBuilder.append(inputString); + return stringBuilder.toString(); + } + public static CompletableFuture sendVerificationCode(Store store, Keys keys, AsyncVerificationCodeSupplier handler, AsyncCaptchaCodeSupplier captchaHandler) { return handler.get() .thenComposeAsync(result -> sendVerificationCode(store, keys, result, captchaHandler, false)) @@ -172,7 +187,6 @@ private static CompletableFuture> sendRegistrationRequest(S .GET() .header("Content-Type", "application/x-www-form-urlencoded") .header("User-Agent", getUserAgent(store)) - .header("request_token", UUID.randomUUID().toString()) .build(); return client.sendAsync(request, BodyHandlers.ofString()); } @@ -214,12 +228,13 @@ private static CompletableFuture> getRegistrationOptions(Sto .thenApplyAsync(token -> getRegistrationOptions(store, keys, token, attributes)); } - // TODO: Add backup token, locale and language and expid private static Map getRegistrationOptions(Store store, Keys keys, String token, Entry[] attributes) { return Attributes.of(attributes) .put("cc", store.phoneNumber().orElseThrow().countryCode().prefix()) .put("in", store.phoneNumber().orElseThrow().numberWithoutPrefix()) - .put("rc", store.releaseChannel().index()) + .put("Rc", store.releaseChannel().index()) + .put("lg", "en") + .put("lc", "US") .put("authkey", Base64.getUrlEncoder().encodeToString(keys.noiseKeyPair().publicKey())) .put("e_regid", Base64.getUrlEncoder().encodeToString(keys.encodedRegistrationId())) .put("e_keytype", "BQ") @@ -227,6 +242,12 @@ private static Map getRegistrationOptions(Store store, Keys keys .put("e_skey_id", Base64.getUrlEncoder().encodeToString(keys.signedKeyPair().encodedId())) .put("e_skey_val", Base64.getUrlEncoder().encodeToString(keys.signedKeyPair().publicKey())) .put("e_skey_sig", Base64.getUrlEncoder().encodeToString(keys.signedKeyPair().signature())) + .put("fdid", keys.phoneId()) + .put("network_ratio_type", 1) + .put("expid", keys.deviceId()) + .put("simnum", 1) + .put("hasinrc", 1) + .put("pid", Math.floor(Math.random() * 1000)) .put("id", keys.recoveryToken()) .put("token", token) .toMap(); diff --git a/src/main/java/it/auties/whatsapp/util/Smile.java b/src/main/java/it/auties/whatsapp/util/Smile.java index 14d05f2ec..c7b70d83c 100644 --- a/src/main/java/it/auties/whatsapp/util/Smile.java +++ b/src/main/java/it/auties/whatsapp/util/Smile.java @@ -26,8 +26,7 @@ public final class Smile { static { try { - smile = SmileMapper.builder() - .build() + smile = new SmileMapper() .registerModule(new Jdk8Module()) .registerModule(new JavaTimeModule()) .registerModule(new ParameterNamesModule()) diff --git a/src/main/java/it/auties/whatsapp/util/Specification.java b/src/main/java/it/auties/whatsapp/util/Specification.java index fd5ac1d69..d98e1fc9e 100644 --- a/src/main/java/it/auties/whatsapp/util/Specification.java +++ b/src/main/java/it/auties/whatsapp/util/Specification.java @@ -20,7 +20,7 @@ public final static class Whatsapp { public static final int SOCKET_PORT = 443; public static final String WEB_UPDATE_URL = "https://web.whatsapp.com/check-update?version=2.2245.9&platform=web"; public static final String MOBILE_REGISTRATION_ENDPOINT = "https://v.whatsapp.net/v2"; - public static final Version DEFAULT_MOBILE_IOS_VERSION = Version.of("2.23.12.75"); + public static final Version DEFAULT_MOBILE_IOS_VERSION = Version.of("2.23.13.82"); private static final byte[] WHATSAPP_VERSION_HEADER = "WA".getBytes(StandardCharsets.UTF_8); private static final byte[] WEB_VERSION = new byte[]{6, BinaryTokens.DICTIONARY_VERSION}; public static final byte[] WEB_PROLOGUE = BytesHelper.concat(WHATSAPP_VERSION_HEADER, WEB_VERSION); diff --git a/src/test/java/it/auties/whatsapp/local/MobileTest.java b/src/test/java/it/auties/whatsapp/local/MobileTest.java index bafb9b1cb..7afe2d670 100644 --- a/src/test/java/it/auties/whatsapp/local/MobileTest.java +++ b/src/test/java/it/auties/whatsapp/local/MobileTest.java @@ -1,40 +1,37 @@ package it.auties.whatsapp.local; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.companion.CompanionDevice; import it.auties.whatsapp.model.mobile.VerificationCodeMethod; import it.auties.whatsapp.model.mobile.VerificationCodeResponse; import java.util.Scanner; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; public class MobileTest { public static void main(String[] args) { Whatsapp.mobileBuilder() - .newConnection() + .lastConnection() + .business(false) + .device(CompanionDevice.ios()) .unregistered() - .verificationCodeMethod(VerificationCodeMethod.CALL) .verificationCodeSupplier(MobileTest::onScanCode) - .verificationCaptchaSupplier(MobileTest::onCaptcha) - .register(18019091753L) + .verificationCodeMethod(VerificationCodeMethod.CALL) + .register(393495089819L) + .join() + .connect() .join() - .addLoggedInListener(api -> { - System.out.println("Connected"); - var call = api.startCall(Jid.of("393495089819")).join(); - System.out.println(call); - CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS) - .execute(() -> api.stopCall(call).join()); - }) + .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) + .addFeaturesListener(features -> System.out.printf("Received features: %s%n", features)) + .addNewChatMessageListener((api, message) -> System.out.println(message.toJson())) .addContactsListener((api, contacts) -> System.out.printf("Contacts: %s%n", contacts.size())) .addChatsListener(chats -> System.out.printf("Chats: %s%n", chats.size())) + .addNewslettersListener((newsletters) -> System.out.printf("Newsletters: %s%n", newsletters.size())) .addNodeReceivedListener(incoming -> System.out.printf("Received node %s%n", incoming)) .addNodeSentListener(outgoing -> System.out.printf("Sent node %s%n", outgoing)) - .addActionListener((action, info) -> System.out.printf("New action: %s, info: %s%n", action, info)) + .addActionListener ((action, info) -> System.out.printf("New action: %s, info: %s%n", action, info)) .addSettingListener(setting -> System.out.printf("New setting: %s%n", setting)) .addContactPresenceListener((chat, contact, status) -> System.out.printf("Status of %s changed in %s to %s%n", contact, chat.name(), status.name())) - .addAnyMessageStatusListener((chat, contact, info, status) -> System.out.printf("Message %s in chat %s now has status %s for %s %n", info.id(), info.chatName(), status, contact == null ? null : contact.name())) - .addChatMessagesSyncListener((chat, last) -> System.out.printf("%s now has %s messages: %s%n", chat.name(), chat.messages().size(), !last ? "waiting for more" : "done")) .addDisconnectedListener(reason -> System.out.printf("Disconnected: %s%n", reason)) .connect() .join(); diff --git a/src/test/java/it/auties/whatsapp/local/WebTest.java b/src/test/java/it/auties/whatsapp/local/WebTest.java index ef125066a..c99cc5c1c 100644 --- a/src/test/java/it/auties/whatsapp/local/WebTest.java +++ b/src/test/java/it/auties/whatsapp/local/WebTest.java @@ -14,7 +14,8 @@ public static void main(String[] args) { .unregistered(QrHandler.toTerminal()) .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) .addFeaturesListener(features -> System.out.printf("Received features: %s%n", features)) - .addNewMessageListener((api, message, offline) -> System.out.println(message.toJson())) + .addNewChatMessageListener((api, message) -> System.out.println(message.toJson())) + .addNewNewsletterMessageListener((api, message) -> System.out.println(message.toJson())) .addContactsListener((api, contacts) -> System.out.printf("Contacts: %s%n", contacts.size())) .addChatsListener(chats -> System.out.printf("Chats: %s%n", chats.size())) .addNewslettersListener((newsletters) -> System.out.printf("Newsletters: %s%n", newsletters.size())) diff --git a/src/test/java/it/auties/whatsapp/test/ButtonsTest.java b/src/test/java/it/auties/whatsapp/test/ButtonsTest.java index cceb7fb9d..c75e7da19 100644 --- a/src/test/java/it/auties/whatsapp/test/ButtonsTest.java +++ b/src/test/java/it/auties/whatsapp/test/ButtonsTest.java @@ -6,8 +6,8 @@ import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.controller.Keys; import it.auties.whatsapp.controller.Store; -import it.auties.whatsapp.model.GithubActions; import it.auties.whatsapp.listener.Listener; +import it.auties.whatsapp.model.GithubActions; import it.auties.whatsapp.model.button.base.Button; import it.auties.whatsapp.model.button.base.ButtonText; import it.auties.whatsapp.model.button.interactive.InteractiveButton; @@ -18,16 +18,18 @@ import it.auties.whatsapp.model.button.template.hydrated.*; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.info.MessageInfoBuilder; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.button.*; -import it.auties.whatsapp.model.message.model.*; +import it.auties.whatsapp.model.message.model.MessageContainerBuilder; +import it.auties.whatsapp.model.message.model.MessageKey; +import it.auties.whatsapp.model.message.model.MessageKeyBuilder; +import it.auties.whatsapp.model.message.model.MessageStatus; import it.auties.whatsapp.model.message.standard.TextMessage; import it.auties.whatsapp.model.node.Node; -import it.auties.whatsapp.util.Json; -import it.auties.whatsapp.utils.ConfigUtils; import it.auties.whatsapp.util.Smile; +import it.auties.whatsapp.utils.ConfigUtils; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.examples.ByteArrayHandler; import org.junit.jupiter.api.*; @@ -84,9 +86,7 @@ private void createApi() { .lastConnection() .historyLength(WebHistoryLength.zero()) .unregistered(QrHandler.toTerminal()) - .addListener(this) - .connect() - .join(); + .addListener(this); return; } log("Detected github actions environment"); @@ -153,67 +153,6 @@ public void testButtonsMessage() { log("Sent buttons"); } - @Test - @Order(1) - public void testButtonReplyMessage() { - if (skip) { - return; - } - log("Sending button reply..."); - var imageButtons = Json.readValue(""" - { - "buttonsResponseMessage":{ - "contextInfo":{ - "quotedMessageId":"0E7F901C06D16F2A", - "quotedMessageSenderJid":"393495089819@s.whatsapp.net", - "quotedMessageSender":{ - "jid":"393495089819@s.whatsapp.net", - "chosenName":"Alessandro Autiero", - "lastKnownPresence":"AVAILABLE", - "lastSeen":1681323820.337021700 - }, - "quotedMessage":{ - "buttonsMessage":{ - "headerText":"Header", - "body":"A nice body", - "footer":"A nice footer", - "buttons":[ - { - "id":"089c872c1759", - "text":{ - "content":"Button 0" - }, - "type":1 - }, - { - "id":"9043a40b60da", - "text":{ - "content":"Button 1" - }, - "type":1 - }, - { - "id":"d2a5a445a2de", - "text":{ - "content":"Button 2" - }, - "type":1 - } - ], - "headerType":2 - } - } - }, - "buttonId":"d2a5a445a2de", - "buttonText":"Button 2", - "responseType":1 - } - } - """, MessageContainer.class); - api.sendMessage(contact, imageButtons).join(); - log("Sent button reply"); - } - private List