From edb79e3ff59a9b8fbe0fdf691814de30c7c79a18 Mon Sep 17 00:00:00 2001 From: Alessandro Autiero Date: Tue, 3 Oct 2023 23:27:39 +0200 Subject: [PATCH] Working on better serialization --- pom.xml | 3 +- .../it/auties/whatsapp/api/ErrorHandler.java | 6 +- .../auties/whatsapp/api/WebHistoryLength.java | 28 +- .../java/it/auties/whatsapp/api/Whatsapp.java | 234 +++-- .../auties/whatsapp/binary/BinaryDecoder.java | 14 +- .../auties/whatsapp/binary/BinaryEncoder.java | 6 +- .../controller/ControllerSerializer.java | 4 +- .../DefaultControllerSerializer.java | 322 ++---- .../it/auties/whatsapp/controller/Keys.java | 36 +- .../it/auties/whatsapp/controller/Store.java | 112 +- .../whatsapp/controller/StoreBuilder.java | 25 +- .../auties/whatsapp/crypto/SessionCipher.java | 1 - .../it/auties/whatsapp/listener/Listener.java | 14 +- .../whatsapp/listener/OnContactPresence.java | 6 +- .../whatsapp/listener/OnLinkedDevices.java | 4 +- .../listener/OnWhatsappContactPresence.java | 6 +- .../listener/OnWhatsappLinkedDevices.java | 4 +- .../model/business/BusinessProfile.java | 4 +- .../interactive/InteractiveCollection.java | 4 +- .../it/auties/whatsapp/model/call/Call.java | 4 +- .../it/auties/whatsapp/model/chat/Chat.java | 93 +- .../whatsapp/model/chat/GroupMetadata.java | 8 +- .../whatsapp/model/chat/GroupParticipant.java | 8 +- .../model/chat/GroupPastParticipant.java | 4 +- .../model/chat/GroupPastParticipants.java | 4 +- .../model/companion/CompanionDevice.java | 8 +- .../whatsapp/model/contact/Contact.java | 16 +- .../whatsapp/model/contact/ContactCard.java | 17 +- .../whatsapp/model/info/ContextInfo.java | 20 +- .../whatsapp/model/info/MessageIndexInfo.java | 8 +- .../whatsapp/model/info/MessageInfo.java | 16 +- .../whatsapp/model/info/PaymentInfo.java | 4 +- .../whatsapp/model/info/ProductListInfo.java | 4 +- .../{contact/ContactJid.java => jid/Jid.java} | 80 +- .../JidProvider.java} | 7 +- .../JidServer.java} | 12 +- .../ContactJidType.java => jid/JidType.java} | 4 +- .../model/message/model/KeepInChat.java | 4 +- .../model/message/model/MessageKey.java | 20 +- .../model/MessageMetadataProvider.java | 6 +- .../model/message/model/MessageReceipt.java | 12 +- .../model/message/model/QuotedMessage.java | 6 +- .../message/payment/PaymentOrderMessage.java | 4 +- .../payment/RequestPaymentMessage.java | 4 +- .../message/server/DeviceSentMessage.java | 4 +- .../message/standard/GroupInviteMessage.java | 4 +- .../message/standard/PollCreationMessage.java | 12 +- .../message/standard/PollUpdateMessage.java | 8 +- .../message/standard/ProductMessage.java | 4 +- .../whatsapp/model/mobile/PhoneNumber.java | 6 +- .../whatsapp/model/node/Attributes.java | 10 +- .../it/auties/whatsapp/model/node/Node.java | 4 + .../model/privacy/PrivacySettingEntry.java | 6 +- .../model/request/MessageSendRequest.java | 4 +- .../model/request/UpdateChannelRequest.java | 4 +- .../model/response/ChannelResponse.java | 4 +- .../model/response/HasWhatsappResponse.java | 4 +- .../response/RecommendedChannelsResponse.java | 4 +- .../model/signal/auth/ClientPayload.java | 3 +- .../whatsapp/model/signal/auth/UserAgent.java | 6 +- .../model/sync/HistorySyncMessage.java | 10 + .../model/sync/HistorySyncNotification.java | 34 +- .../whatsapp/socket/AppStateHandler.java | 47 +- .../auties/whatsapp/socket/AuthHandler.java | 24 +- .../whatsapp/socket/MessageHandler.java | 170 +-- .../auties/whatsapp/socket/SocketHandler.java | 57 +- .../auties/whatsapp/socket/StreamHandler.java | 50 +- .../util/ConcurrentDoublyLinkedList.java | 992 ++++++++++++++++++ .../auties/whatsapp/util/FutureReference.java | 9 + .../auties/whatsapp/util/MetadataHelper.java | 11 +- .../java/it/auties/whatsapp/util}/Smile.java | 17 +- .../java/it/auties/whatsapp/util/Spec.java | 1 + src/main/java/module-info.java | 1 + src/test/java/it/auties/whatsapp/Test.java | 15 + .../auties/whatsapp/github/GithubSecrets.java | 2 +- .../it/auties/whatsapp/local/MobileTest.java | 4 +- .../it/auties/whatsapp/local/WebTest.java | 7 +- .../it/auties/whatsapp/test/ButtonsTest.java | 12 +- src/test/resources/RunMobileCITest.java | 14 +- src/test/resources/RunWebCITest.java | 14 +- 80 files changed, 1943 insertions(+), 841 deletions(-) rename src/main/java/it/auties/whatsapp/model/{contact/ContactJid.java => jid/Jid.java} (70%) rename src/main/java/it/auties/whatsapp/model/{contact/ContactJidProvider.java => jid/JidProvider.java} (61%) rename src/main/java/it/auties/whatsapp/model/{contact/ContactJidServer.java => jid/JidServer.java} (81%) rename src/main/java/it/auties/whatsapp/model/{contact/ContactJidType.java => jid/JidType.java} (93%) create mode 100644 src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java rename src/{test/java/it/auties/whatsapp/utils => main/java/it/auties/whatsapp/util}/Smile.java (85%) create mode 100644 src/test/java/it/auties/whatsapp/Test.java diff --git a/pom.xml b/pom.xml index ad964784..493e857e 100644 --- a/pom.xml +++ b/pom.xml @@ -197,7 +197,7 @@ true - + -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED @@ -207,6 +207,7 @@ -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED + -parameters true diff --git a/src/main/java/it/auties/whatsapp/api/ErrorHandler.java b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java index 58e1ce38..ab3650f0 100644 --- a/src/main/java/it/auties/whatsapp/api/ErrorHandler.java +++ b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java @@ -118,7 +118,11 @@ enum Location { /** * Called when an error occurs when serializing or deserializing a Whatsapp message */ - MESSAGE + MESSAGE, + /** + * Called when syncing messages after first QR scan + */ + HISTORY_SYNC } /** diff --git a/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java index e5651448..ffdc8ef0 100644 --- a/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java +++ b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java @@ -1,26 +1,46 @@ package it.auties.whatsapp.api; +import it.auties.whatsapp.util.Spec; + /** * The constants of this enumerated type describe the various chat history's codeLength that Whatsapp * can send on the first login attempt */ -public enum WebHistoryLength { +public record WebHistoryLength(int size) { + private static final WebHistoryLength ZERO = new WebHistoryLength(0); + private static final WebHistoryLength STANDARD = new WebHistoryLength(Spec.Whatsapp.DEFAULT_HISTORY_SIZE); + private static final WebHistoryLength EXTENDED = new WebHistoryLength(Integer.MAX_VALUE); + /** * Discards history * This will save a lot of system resources, but you won't have access to messages sent before the session creation */ - ZERO, + public static WebHistoryLength zero() { + return ZERO; + } + /** * This is the default setting for the web client * This is also the recommended setting */ - STANDARD, + public static WebHistoryLength standard() { + return STANDARD; + } /** * This will contain most of your messages * Unless you 100% know what you are doing don't use this * It consumes a lot of system resources */ - EXTENDED, + public static WebHistoryLength extended() { + return EXTENDED; + } + + /** + * Custom size + */ + public static WebHistoryLength custom(int size) { + return new WebHistoryLength(size); + } } diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index 898cf5b3..6c66199c 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -23,10 +23,14 @@ 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.*; +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; +import it.auties.whatsapp.model.jid.JidProvider; +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; @@ -48,11 +52,10 @@ 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.request.ContactStatusQuery; +import it.auties.whatsapp.model.request.MessageSendRequest; import it.auties.whatsapp.model.response.ContactStatusResponse; -import it.auties.whatsapp.model.response.ChannelResponse; import it.auties.whatsapp.model.response.HasWhatsappResponse; -import it.auties.whatsapp.model.response.RecommendedChannelsResponse; import it.auties.whatsapp.model.setting.LocaleSettings; import it.auties.whatsapp.model.setting.PushNameSettings; import it.auties.whatsapp.model.signal.auth.*; @@ -64,7 +67,6 @@ import it.auties.whatsapp.socket.SocketState; import it.auties.whatsapp.util.*; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; import org.jilt.Builder; import org.jilt.BuilderStyle; import org.jilt.Opt; @@ -79,7 +81,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; @@ -298,7 +303,7 @@ public CompletableFuture logout() { * @return the same instance wrapped in a completable future */ @SafeVarargs - public final CompletableFuture changePrivacySetting(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, @NonNull T @NonNull ... excluded) { + public final CompletableFuture changePrivacySetting(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, @NonNull T @NonNull ... excluded) { Validate.isTrue(type.isSupported(value), "Cannot change setting %s to %s: this toggle cannot be used because Whatsapp doesn't support it", value.name(), type.name()); var attributes = Attributes.of() @@ -306,7 +311,7 @@ public final CompletableFuture changePr .put("value", value.data()) .put("dhash", "none", () -> value == PrivacySettingValue.CONTACTS_EXCEPT) .toMap(); - var excludedJids = Arrays.stream(excluded).map(ContactJidProvider::toJid).toList(); + var excludedJids = Arrays.stream(excluded).map(JidProvider::toJid).toList(); var children = value != PrivacySettingValue.CONTACTS_EXCEPT ? null : excludedJids.stream() .map(entry -> Node.of("user", Map.of("jid", entry, "action", "add"))) .toList(); @@ -315,7 +320,7 @@ public final CompletableFuture changePr .thenApply(ignored -> this); } - private void onPrivacyFeatureChanged(PrivacySettingType type, PrivacySettingValue value, List excludedJids) { + private void onPrivacyFeatureChanged(PrivacySettingType type, PrivacySettingValue value, List excludedJids) { var newEntry = new PrivacySettingEntry(type, value, excludedJids); var oldEntry = store().findPrivacySetting(type); store().addPrivacySetting(type, newEntry); @@ -391,7 +396,7 @@ public CompletableFuture changeStatus(@NonNull String newStatus) { * @param jid the contact whose status the api should receive updates on * @return a CompletableFuture */ - public CompletableFuture subscribeToPresence(@NonNull T jid) { + public CompletableFuture subscribeToPresence(@NonNull T jid) { return socketHandler.subscribeToPresence(jid).thenApplyAsync(ignored -> jid); } @@ -449,7 +454,7 @@ public CompletableFuture sendReaction(@NonNull MessageMetadataProvi * @param message the message to send * @return a CompletableFuture */ - public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull String message) { + public CompletableFuture sendMessage(@NonNull JidProvider chat, @NonNull String message) { return sendMessage(chat, MessageContainer.of(message)); } @@ -460,7 +465,7 @@ public CompletableFuture sendMessage(@NonNull ContactJidProvider ch * @param message the message to send * @return a CompletableFuture */ - public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull Message message) { + public CompletableFuture sendMessage(@NonNull JidProvider chat, @NonNull Message message) { return sendMessage(chat, MessageContainer.of(message)); } @@ -471,7 +476,7 @@ public CompletableFuture sendMessage(@NonNull ContactJidProvider ch * @param message the message to send * @return a CompletableFuture */ - public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull MessageContainer message) { + public CompletableFuture sendMessage(@NonNull JidProvider chat, @NonNull MessageContainer message) { var key = new MessageKeyBuilder() .id(MessageKey.randomId()) .chatJid(chat.toJid()) @@ -484,7 +489,7 @@ public CompletableFuture sendMessage(@NonNull ContactJidProvider ch .key(key) .message(message) .timestampSeconds(Clock.nowSeconds()) - .broadcast(chat.toJid().hasServer(ContactJidServer.BROADCAST)) + .broadcast(chat.toJid().hasServer(JidServer.BROADCAST)) .build(); return sendMessage(info); } @@ -524,7 +529,7 @@ private CompletableFuture attributeMessageMetadata(MessageInfo info) { * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture markRead(@NonNull T chat) { + public CompletableFuture markRead(@NonNull T chat) { return mark(chat, true).thenComposeAsync(ignored -> markAllAsRead(chat)).thenApplyAsync(ignored -> chat); } @@ -700,7 +705,7 @@ private CompletableFuture attributeGroupInviteMessage(MessageInfo info, Gr return CompletableFuture.completedFuture(null); } - private CompletableFuture mark(@NonNull T chat, boolean read) { + private CompletableFuture mark(@NonNull T chat, boolean read) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat.toJid()) @@ -716,7 +721,7 @@ private CompletableFuture mark(@NonNull T chat return socketHandler.pushPatch(request).thenApplyAsync(ignored -> chat); } - private CompletableFuture markAllAsRead(ContactJidProvider chat) { + private CompletableFuture markAllAsRead(JidProvider chat) { var all = store() .findChatByJid(chat.toJid()) .stream() @@ -731,7 +736,7 @@ private LinkPreviewMedia compareDimensions(LinkPreviewMedia first, LinkPreviewMe return first.width() * first.height() > second.width() * second.height() ? first : second; } - private ActionMessageRangeSync createRange(ContactJidProvider chat, boolean allMessages) { + private ActionMessageRangeSync createRange(JidProvider chat, boolean allMessages) { var known = store().findChatByJid(chat.toJid()).orElseGet(() -> store().addNewChat(chat.toJid())); return new ActionMessageRangeSync(known, allMessages); } @@ -789,8 +794,8 @@ public CompletableFuture awaitReply(@NonNull String id) { * @param contact the contact to check * @return a CompletableFuture that wraps a non-null response */ - public CompletableFuture hasWhatsapp(@NonNull ContactJidProvider contact) { - return hasWhatsapp(new ContactJidProvider[]{contact}).thenApply(result -> result.get(contact.toJid())); + public CompletableFuture hasWhatsapp(@NonNull JidProvider contact) { + return hasWhatsapp(new JidProvider[]{contact}).thenApply(result -> result.get(contact.toJid())); } /** @@ -799,7 +804,7 @@ public CompletableFuture hasWhatsapp(@NonNull ContactJidPro * @param contacts the contacts to check * @return a CompletableFuture that wraps a non-null map */ - public CompletableFuture> hasWhatsapp(@NonNull ContactJidProvider... contacts) { + public CompletableFuture> hasWhatsapp(@NonNull JidProvider... contacts) { var contactNodes = Arrays.stream(contacts) .map(jid -> Node.of("user", Node.of("contact", jid.toJid().toPhoneNumber()))) .toArray(Node[]::new); @@ -807,7 +812,7 @@ public CompletableFuture> hasWhatsapp(@NonN .thenApplyAsync(this::parseHasWhatsappResponse); } - private Map parseHasWhatsappResponse(List nodes) { + private Map parseHasWhatsappResponse(List nodes) { return nodes.stream() .map(this::parseHasWhatsappResponse) .collect(Collectors.toMap(HasWhatsappResponse::contact, Function.identity())); @@ -830,7 +835,7 @@ private HasWhatsappResponse parseHasWhatsappResponse(Node node) { * * @return a CompletableFuture */ - public CompletableFuture> queryBlockList() { + public CompletableFuture> queryBlockList() { return socketHandler.queryBlockList(); } @@ -840,13 +845,13 @@ public CompletableFuture> queryBlockList() { * @param contactJid the non-null contact * @return a CompletableFuture */ - public CompletableFuture> queryName(@NonNull ContactJidProvider contactJid) { + public CompletableFuture> queryName(@NonNull JidProvider contactJid) { var contact = store().findContactByJid(contactJid); return contact.map(value -> CompletableFuture.completedFuture(value.chosenName())) .orElseGet(() -> queryNameFromServer(contactJid)); } - private CompletableFuture> queryNameFromServer(@NonNull ContactJidProvider contactJid) { + private CompletableFuture> queryNameFromServer(@NonNull JidProvider contactJid) { var query = new ContactStatusQuery(MessageKey.randomId(), List.of(new ContactStatusQuery.Variable(contactJid.toJid().user(), List.of("STATUS")))); return socketHandler.sendQuery("get", "w:mex", Node.of("query", Json.writeValueAsBytes(query))) .thenApplyAsync(this::parseNameResponse); @@ -865,7 +870,7 @@ private Optional parseNameResponse(Node result) { * @param chat the target contact * @return a CompletableFuture that wraps an optional contact status response */ - public CompletableFuture> queryAbout(@NonNull ContactJidProvider chat) { + public CompletableFuture> queryAbout(@NonNull JidProvider chat) { return socketHandler.queryAbout(chat); } @@ -875,7 +880,7 @@ public CompletableFuture> queryAbout(@NonNull Co * @param chat the chat of the chat to query * @return a CompletableFuture that wraps nullable jpg url hosted on Whatsapp's servers */ - public CompletableFuture> queryPicture(@NonNull ContactJidProvider chat) { + public CompletableFuture> queryPicture(@NonNull JidProvider chat) { return socketHandler.queryPicture(chat); } @@ -885,7 +890,7 @@ public CompletableFuture> queryPicture(@NonNull ContactJidProvider * @param chat the target group * @return a CompletableFuture */ - public CompletableFuture queryGroupMetadata(@NonNull ContactJidProvider chat) { + public CompletableFuture queryGroupMetadata(@NonNull JidProvider chat) { return socketHandler.queryGroupMetadata(chat.toJid()); } @@ -895,7 +900,7 @@ public CompletableFuture queryGroupMetadata(@NonNull ContactJidPr * @param contact the target contact * @return a CompletableFuture */ - public CompletableFuture> queryBusinessProfile(@NonNull ContactJidProvider contact) { + public CompletableFuture> queryBusinessProfile(@NonNull JidProvider contact) { return socketHandler.sendQuery("get", "w:biz", Node.of("business_profile", Map.of("v", 116), Node.of("profile", Map.of("jid", contact.toJid())))) .thenApplyAsync(this::getBusinessProfile); @@ -922,7 +927,7 @@ public CompletableFuture> queryBusinessCategories() { * @param chat the target group * @return a CompletableFuture */ - public CompletableFuture queryGroupInviteCode(@NonNull ContactJidProvider chat) { + public CompletableFuture queryGroupInviteCode(@NonNull JidProvider chat) { return socketHandler.sendQuery(chat.toJid(), "get", "w:g2", Node.of("invite")) .thenApplyAsync(Whatsapp::parseInviteCode); } @@ -940,7 +945,7 @@ private static String parseInviteCode(Node result) { * @param chat the target group * @return a CompletableFuture */ - public CompletableFuture revokeGroupInvite(@NonNull T chat) { + public CompletableFuture revokeGroupInvite(@NonNull T chat) { return socketHandler.sendQuery(chat.toJid(), "set", "w:g2", Node.of("invite")).thenApplyAsync(ignored -> chat); } @@ -951,7 +956,7 @@ public CompletableFuture revokeGroupInvite(@No * @return a CompletableFuture */ public CompletableFuture> acceptGroupInvite(@NonNull String inviteCode) { - return socketHandler.sendQuery(ContactJidServer.GROUP.toJid(), "set", "w:g2", Node.of("invite", Map.of("code", inviteCode))) + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", Node.of("invite", Map.of("code", inviteCode))) .thenApplyAsync(this::parseAcceptInvite); } @@ -980,7 +985,7 @@ public CompletableFuture changePresence(boolean available) { .thenApplyAsync(ignored -> available); } - private void updateSelfPresence(ContactJidProvider chatJid, ContactStatus presence) { + private void updateSelfPresence(JidProvider chatJid, ContactStatus presence) { if(chatJid == null){ store().setOnline(presence == ContactStatus.AVAILABLE); } @@ -1005,7 +1010,7 @@ private void updateSelfPresence(ContactJidProvider chatJid, ContactStatus presen * @param presence the new status * @return a CompletableFuture */ - public CompletableFuture changePresence(@NonNull T chatJid, @NonNull ContactStatus presence) { + public CompletableFuture changePresence(@NonNull T chatJid, @NonNull ContactStatus presence) { var knownPresence = store().findChatByJid(chatJid) .map(Chat::presences) .map(entry -> entry.get(jidOrThrowError().withoutDevice())) @@ -1039,7 +1044,7 @@ public CompletableFuture changePresence(@NonNu * @param contacts the target contacts * @return a CompletableFuture */ - public CompletableFuture> promote(@NonNull ContactJidProvider group, @NonNull ContactJidProvider @NonNull ... contacts) { + public CompletableFuture> promote(@NonNull JidProvider group, @NonNull JidProvider @NonNull ... contacts) { return executeActionOnGroupParticipant(group, GroupAction.PROMOTE, contacts); } @@ -1050,7 +1055,7 @@ public CompletableFuture> promote(@NonNull ContactJidProvider g * @param contacts the target contacts * @return a CompletableFuture */ - public CompletableFuture> demote(@NonNull ContactJidProvider group, @NonNull ContactJidProvider @NonNull ... contacts) { + public CompletableFuture> demote(@NonNull JidProvider group, @NonNull JidProvider @NonNull ... contacts) { return executeActionOnGroupParticipant(group, GroupAction.DEMOTE, contacts); } @@ -1061,7 +1066,7 @@ public CompletableFuture> demote(@NonNull ContactJidProvider gr * @param contacts the target contact/s * @return a CompletableFuture */ - public CompletableFuture> addGroupParticipant(@NonNull ContactJidProvider group, @NonNull ContactJidProvider @NonNull ... contacts) { + public CompletableFuture> addGroupParticipant(@NonNull JidProvider group, @NonNull JidProvider @NonNull ... contacts) { return executeActionOnGroupParticipant(group, GroupAction.ADD, contacts); } @@ -1072,13 +1077,13 @@ public CompletableFuture> addGroupParticipant(@NonNull ContactJ * @param contacts the target contact/s * @return a CompletableFuture */ - public CompletableFuture> removeGroupParticipant(@NonNull ContactJidProvider group, @NonNull ContactJidProvider @NonNull ... contacts) { + public CompletableFuture> removeGroupParticipant(@NonNull JidProvider group, @NonNull JidProvider @NonNull ... contacts) { return executeActionOnGroupParticipant(group, GroupAction.REMOVE, contacts); } - private CompletableFuture> executeActionOnGroupParticipant(ContactJidProvider group, GroupAction action, ContactJidProvider... jids) { + private CompletableFuture> executeActionOnGroupParticipant(JidProvider group, GroupAction action, JidProvider... jids) { var body = Arrays.stream(jids) - .map(ContactJidProvider::toJid) + .map(JidProvider::toJid) .map(jid -> Node.of("participant", Map.of("jid", checkGroupParticipantJid(jid, "Cannot execute action on yourself")))) .map(innerBody -> Node.of(action.data(), innerBody)) .toArray(Node[]::new); @@ -1086,12 +1091,12 @@ private CompletableFuture> executeActionOnGroupParticipant(Cont .thenApplyAsync(result -> parseGroupActionResponse(result, group, action)); } - private ContactJid checkGroupParticipantJid(ContactJid jid, String errorMessage) { + private Jid checkGroupParticipantJid(Jid jid, String errorMessage) { Validate.isTrue(!Objects.equals(jid.withoutDevice(), jidOrThrowError().withoutDevice()), errorMessage); return jid; } - private List parseGroupActionResponse(Node result, ContactJidProvider groupJid, GroupAction action) { + private List parseGroupActionResponse(Node result, JidProvider groupJid, GroupAction action) { var results = result.findNode(action.data()) .orElseThrow(() -> new NoSuchElementException("An erroneous group operation was executed")) .findNodes("participant") @@ -1110,7 +1115,7 @@ private List parseGroupActionResponse(Node result, ContactJidProvide return results; } - private void handleGroupAction(GroupAction action, Chat chat, ContactJid entry) { + private void handleGroupAction(GroupAction action, Chat chat, Jid entry) { switch (action) { case ADD -> chat.addParticipant(entry, GroupRole.USER); case REMOVE -> { @@ -1132,7 +1137,7 @@ private void handleGroupAction(GroupAction action, Chat chat, ContactJid entry) * @return a CompletableFuture * @throws IllegalArgumentException if the provided new name is empty or blank */ - public CompletableFuture changeGroupSubject(@NonNull T group, @NonNull String newName) { + public CompletableFuture changeGroupSubject(@NonNull T group, @NonNull String newName) { var body = Node.of("subject", newName.getBytes(StandardCharsets.UTF_8)); return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body).thenApplyAsync(ignored -> group); } @@ -1144,14 +1149,14 @@ public CompletableFuture changeGroupSubject(@N * @param description the new name for the group, can be null if you want to remove it * @return a CompletableFuture */ - public CompletableFuture changeGroupDescription(@NonNull T group, String description) { + public CompletableFuture changeGroupDescription(@NonNull T group, String description) { return socketHandler.queryGroupMetadata(group.toJid()) .thenApplyAsync(GroupMetadata::descriptionId) .thenComposeAsync(descriptionId -> changeGroupDescription(group, description, descriptionId.orElse(null))) .thenApplyAsync(ignored -> group); } - private CompletableFuture changeGroupDescription(ContactJidProvider group, String description, String descriptionId) { + private CompletableFuture changeGroupDescription(JidProvider group, String description, String descriptionId) { var descriptionNode = Optional.ofNullable(description) .map(content -> Node.of("body", content.getBytes(StandardCharsets.UTF_8))) .orElse(null); @@ -1165,7 +1170,7 @@ private CompletableFuture changeGroupDescription(ContactJidProvider group, .thenRunAsync(() -> onDescriptionSet(group, description)); } - private void onDescriptionSet(ContactJidProvider groupJid, String description) { + private void onDescriptionSet(JidProvider groupJid, String description) { if (groupJid instanceof Chat chat) { chat.setDescription(description); return; @@ -1183,7 +1188,7 @@ private void onDescriptionSet(ContactJidProvider groupJid, String description) { * @param policy the non-null policy * @return a future */ - public CompletableFuture changeGroupSetting(@NonNull T group, @NonNull GroupSetting setting, @NonNull GroupSettingPolicy policy) { + public CompletableFuture changeGroupSetting(@NonNull T group, @NonNull GroupSetting setting, @NonNull GroupSettingPolicy policy) { var body = Node.of(policy != GroupSettingPolicy.ANYONE ? setting.on() : setting.off()); return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body) .thenApplyAsync(ignored -> group); @@ -1195,7 +1200,7 @@ public CompletableFuture changeGroupSetting(@N * @param image the new image, can be null if you want to remove it * @return a CompletableFuture */ - public CompletableFuture changeProfilePicture(byte[] image) { + public CompletableFuture changeProfilePicture(byte[] image) { return changeGroupPicture(jidOrThrowError(), image); } @@ -1206,7 +1211,7 @@ public CompletableFuture changeProfilePicture(byte[] image) { * @param image the new image, can be null if you want to remove it * @return a CompletableFuture */ - public CompletableFuture changeGroupPicture(@NonNull T group, URI image) { + public CompletableFuture changeGroupPicture(@NonNull T group, URI image) { return changeGroupPicture(group, image == null ? null : Medias.download(image) .orElseThrow(() -> new IllegalArgumentException("Invalid uri: %s".formatted(image)))); } @@ -1218,7 +1223,7 @@ public CompletableFuture changeGroupPicture(@N * @param image the new image, can be null if you want to remove it * @return a CompletableFuture */ - public CompletableFuture changeGroupPicture(@NonNull T group, byte[] image) { + public CompletableFuture changeGroupPicture(@NonNull T group, byte[] image) { var profilePic = image != null ? Medias.getProfilePic(image) : null; var body = Node.of("picture", Map.of("type", "image"), profilePic); return socketHandler.sendQuery(group.toJid().withoutDevice(), "set", "w:profile:picture", body) @@ -1232,7 +1237,7 @@ public CompletableFuture changeGroupPicture(@N * @param contacts at least one contact to add to the group * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ContactJidProvider... contacts) { + public CompletableFuture createGroup(@NonNull String subject, @NonNull JidProvider... contacts) { return createGroup(subject, ChatEphemeralTimer.OFF, contacts); } @@ -1244,7 +1249,7 @@ public CompletableFuture createGroup(@NonNull String subject, @No * @param contacts at least one contact to add to the group * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull ContactJidProvider... contacts) { + public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, @NonNull JidProvider... contacts) { return createGroup(subject, timer, null, contacts); } @@ -1256,8 +1261,8 @@ public CompletableFuture createGroup(@NonNull String subject, @No * @param parentGroup the community to whom the new group will be linked * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, ContactJidProvider parentGroup) { - return createGroup(subject, timer, parentGroup, new ContactJidProvider[0]); + public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentGroup) { + return createGroup(subject, timer, parentGroup, new JidProvider[0]); } /** @@ -1269,7 +1274,7 @@ public CompletableFuture createGroup(@NonNull String subject, @No * @param contacts at least one contact to add to the group, not enforced if part of a community * @return a CompletableFuture */ - public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, ContactJidProvider parentCommunity, @NonNull ContactJidProvider... contacts) { + public CompletableFuture createGroup(@NonNull String subject, @NonNull ChatEphemeralTimer timer, JidProvider parentCommunity, @NonNull JidProvider... contacts) { Validate.isTrue(!subject.isBlank(), "The subject of a group cannot be blank"); var minimumMembersCount = parentCommunity == null ? 1 : 0; Validate.isTrue(contacts.length >= minimumMembersCount, "Expected at least %s members for this group", minimumMembersCount); @@ -1285,7 +1290,7 @@ public CompletableFuture createGroup(@NonNull String subject, @No .forEach(children::add); var key = HexFormat.of().formatHex(BytesHelper.random(12)); var body = Node.of("create", Map.of("subject", subject, "key", key), children); - return socketHandler.sendQuery(ContactJidServer.GROUP.toJid(), "set", "w:g2", body) + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", body) .thenApplyAsync(this::parseGroupResponse); } @@ -1323,13 +1328,13 @@ private String findErrorNode(Node result) { * @param group the target group * @throws IllegalArgumentException if the provided chat is not a group */ - public CompletableFuture leaveGroup(@NonNull T group) { + public CompletableFuture leaveGroup(@NonNull T group) { var body = Node.of("leave", Node.of("group", Map.of("id", group.toJid()))); - return socketHandler.sendQuery(ContactJidServer.GROUP.toJid(), "set", "w:g2", body) + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", body) .thenApplyAsync(ignored -> handleLeaveGroup(group)); } - private T handleLeaveGroup(T group) { + private T handleLeaveGroup(T group) { var chat = group instanceof Chat entry ? entry : store() .findChatByJid(group) .orElse(null); @@ -1352,7 +1357,7 @@ private T handleLeaveGroup(T group) { * @param groups the non-null groups to add * @return a CompletableFuture that wraps a map guaranteed to contain every group that was provided as input paired to whether the request was successful */ - public CompletableFuture> linkGroupsToCommunity(@NonNull ContactJidProvider community, @NonNull ContactJidProvider... groups){ + public CompletableFuture> linkGroupsToCommunity(@NonNull JidProvider community, @NonNull JidProvider... groups){ var body = Arrays.stream(groups) .map(entry -> Node.of("group", Map.of("jid", entry.toJid()))) .toArray(Node[]::new); @@ -1360,7 +1365,7 @@ public CompletableFuture> linkGroupsToCommunity(@NonNul .thenApplyAsync(result -> parseLinksResponse(result, groups)); } - private Map parseLinksResponse(Node result, @NonNull ContactJidProvider[] groups) { + private Map parseLinksResponse(Node result, @NonNull JidProvider[] groups) { var success = result.findNode("links") .stream() .map(entry -> entry.findNodes("link")) @@ -1372,7 +1377,7 @@ private Map parseLinksResponse(Node result, @NonNull Contac .flatMap(Optional::stream) .collect(Collectors.toUnmodifiableSet()); return Arrays.stream(groups) - .map(ContactJidProvider::toJid) + .map(JidProvider::toJid) .collect(Collectors.toUnmodifiableMap(Function.identity(), success::contains)); } @@ -1383,12 +1388,12 @@ private Map parseLinksResponse(Node result, @NonNull Contac * @param group the non-null group to unlink * @return a CompletableFuture that indicates whether the request was successful */ - public CompletableFuture unlinkGroupFromCommunity(@NonNull ContactJidProvider community, @NonNull ContactJidProvider group){ + public CompletableFuture unlinkGroupFromCommunity(@NonNull JidProvider community, @NonNull JidProvider group){ return socketHandler.sendQuery(community.toJid(), "set", "w:g2", Node.of("unlink", Map.of("unlink_type", "sub_group"), Node.of("group", Map.of("jid", group.toJid())))) .thenApplyAsync(result -> parseUnlinkResponse(result, group)); } - private boolean parseUnlinkResponse(Node result, @NonNull ContactJidProvider group) { + private boolean parseUnlinkResponse(Node result, @NonNull JidProvider group) { return result.findNode("unlink") .filter(entry -> entry.attributes().hasValue("unlink_type", "sub_group")) .flatMap(entry -> entry.findNode("group")) @@ -1402,7 +1407,7 @@ private boolean parseUnlinkResponse(Node result, @NonNull ContactJidProvider gro * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture mute(@NonNull T chat) { + public CompletableFuture mute(@NonNull T chat) { return mute(chat, ChatMute.muted()); } @@ -1413,7 +1418,7 @@ public CompletableFuture mute(@NonNull T chat) * @param mute the type of mute * @return a CompletableFuture */ - public CompletableFuture mute(@NonNull T chat, @NonNull ChatMute mute) { + public CompletableFuture mute(@NonNull T chat, @NonNull ChatMute mute) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat) @@ -1435,7 +1440,7 @@ public CompletableFuture mute(@NonNull T chat, * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture unmute(@NonNull T chat) { + public CompletableFuture unmute(@NonNull T chat) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat) @@ -1456,7 +1461,7 @@ public CompletableFuture unmute(@NonNull T cha * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture block(@NonNull T chat) { + public CompletableFuture block(@NonNull T chat) { var body = Node.of("item", Map.of("action", "block", "jid", chat.toJid())); return socketHandler.sendQuery("set", "blocklist", body).thenApplyAsync(ignored -> chat); } @@ -1467,7 +1472,7 @@ public CompletableFuture block(@NonNull T chat * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture unblock(@NonNull T chat) { + public CompletableFuture unblock(@NonNull T chat) { var body = Node.of("item", Map.of("action", "unblock", "jid", chat.toJid())); return socketHandler.sendQuery("set", "blocklist", body).thenApplyAsync(ignored -> chat); } @@ -1479,7 +1484,7 @@ public CompletableFuture unblock(@NonNull T ch * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture changeEphemeralTimer(@NonNull T chat, @NonNull ChatEphemeralTimer timer) { + public CompletableFuture changeEphemeralTimer(@NonNull T chat, @NonNull ChatEphemeralTimer timer) { return switch (chat.toJid().server()) { case USER, WHATSAPP -> { var message = new ProtocolMessageBuilder() @@ -1519,7 +1524,7 @@ public CompletableFuture markPlayed(@NonNull MessageInfo info) { * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture markUnread(@NonNull T chat) { + public CompletableFuture markUnread(@NonNull T chat) { return mark(chat, false); } @@ -1530,7 +1535,7 @@ public CompletableFuture markUnread(@NonNull T * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture pin(@NonNull T chat) { + public CompletableFuture pin(@NonNull T chat) { return pin(chat, true); } @@ -1540,11 +1545,11 @@ public CompletableFuture pin(@NonNull T chat) * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture unpin(@NonNull T chat) { + public CompletableFuture unpin(@NonNull T chat) { return pin(chat, false); } - private CompletableFuture pin(T chat, boolean pin) { + private CompletableFuture pin(T chat, boolean pin) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat) @@ -1589,7 +1594,7 @@ private String fromMeToFlag(MessageInfo info) { } private String participantToFlag(MessageInfo info) { - return info.chatJid().hasServer(ContactJidServer.GROUP) && !info.fromMe() ? info.senderJid().toString() : "0"; + return info.chatJid().hasServer(JidServer.GROUP) && !info.fromMe() ? info.senderJid().toString() : "0"; } private String booleanToInt(boolean keepStarredMessages) { @@ -1612,11 +1617,11 @@ public CompletableFuture unstar(@NonNull MessageInfo info) { * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture archive(@NonNull T chat) { + public CompletableFuture archive(@NonNull T chat) { return archive(chat, true); } - private CompletableFuture archive(T chat, boolean archive) { + private CompletableFuture archive(T chat, boolean archive) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat) @@ -1638,7 +1643,7 @@ private CompletableFuture archive(T chat, bool * @param chat the target chat * @return a CompletableFuture */ - public CompletableFuture unarchive(@NonNull T chat) { + public CompletableFuture unarchive(@NonNull T chat) { return archive(chat, false); } @@ -1656,7 +1661,7 @@ public CompletableFuture delete(@NonNull MessageInfo info, boolean .protocolType(ProtocolMessage.Type.REVOKE) .key(info.key()) .build(); - var sender = info.chatJid().hasServer(ContactJidServer.GROUP) ? jidOrThrowError() : null; + var sender = info.chatJid().hasServer(JidServer.GROUP) ? jidOrThrowError() : null; var key = new MessageKeyBuilder() .id(MessageKey.randomId()) .chatJid(info.chatJid()) @@ -1691,11 +1696,11 @@ public CompletableFuture delete(@NonNull MessageInfo info, boolean } private int getEditBit(@NonNull MessageInfo info) { - if(info.chatJid().hasServer(ContactJidServer.CHANNEL)) { + if(info.chatJid().hasServer(JidServer.CHANNEL)) { return 8; } - if(info.chatJid().hasServer(ContactJidServer.GROUP) && !info.fromMe()) { + if(info.chatJid().hasServer(JidServer.GROUP) && !info.fromMe()) { return 8; } @@ -1709,7 +1714,7 @@ private int getEditBit(@NonNull MessageInfo info) { * @param chat the non-null chat to delete * @return a CompletableFuture */ - public CompletableFuture delete(@NonNull T chat) { + public CompletableFuture delete(@NonNull T chat) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().removeChat(chat.toJid()); @@ -1732,7 +1737,7 @@ public CompletableFuture delete(@NonNull T cha * @param keepStarredMessages whether starred messages in this chat should be kept * @return a CompletableFuture */ - public CompletableFuture clear(@NonNull T chat, boolean keepStarredMessages) { + public CompletableFuture clear(@NonNull T chat, boolean keepStarredMessages) { if(store().clientType() == ClientType.MOBILE){ // TODO: Send notification to companions store().findChatByJid(chat.toJid()) @@ -1867,7 +1872,7 @@ public CompletableFuture> queryBusinessCatalog(int pr * @param productsLimit the maximum number of products to query * @return a CompletableFuture */ - public CompletableFuture> queryBusinessCatalog(@NonNull ContactJidProvider contact, int productsLimit) { + public CompletableFuture> queryBusinessCatalog(@NonNull JidProvider contact, int productsLimit) { return socketHandler.sendQuery("get", "w:biz:catalog", Node.of("product_catalog", Map.of("jid", contact, "allow_shop_source", "true"), Node.of("limit", String.valueOf(productsLimit) .getBytes(StandardCharsets.UTF_8)), Node.of("width", "100".getBytes(StandardCharsets.UTF_8)), Node.of("height", "100".getBytes(StandardCharsets.UTF_8)))) .thenApplyAsync(this::parseCatalog); @@ -1889,7 +1894,7 @@ private List parseCatalog(Node result) { * @param contact the business * @return a CompletableFuture */ - public CompletableFuture> queryBusinessCatalog(@NonNull ContactJidProvider contact) { + public CompletableFuture> queryBusinessCatalog(@NonNull JidProvider contact) { return queryBusinessCatalog(contact, 10); } @@ -1919,7 +1924,7 @@ public CompletableFuture queryBusinessCollections(int collectionsLimit) { * @param collectionsLimit the maximum number of collections to query * @return a CompletableFuture */ - public CompletableFuture> queryBusinessCollections(@NonNull ContactJidProvider contact, int collectionsLimit) { + public CompletableFuture> queryBusinessCollections(@NonNull JidProvider contact, int collectionsLimit) { return socketHandler.sendQuery("get", "w:biz:catalog", Map.of("smax_id", "35"), Node.of("collections", Map.of("biz_jid", contact), Node.of("collection_limit", String.valueOf(collectionsLimit) .getBytes(StandardCharsets.UTF_8)), Node.of("item_limit", String.valueOf(collectionsLimit) .getBytes(StandardCharsets.UTF_8)), Node.of("width", "100".getBytes(StandardCharsets.UTF_8)), Node.of("height", "100".getBytes(StandardCharsets.UTF_8)))) @@ -1942,7 +1947,7 @@ private List parseCollections(Node result) { * @param contact the business * @return a CompletableFuture */ - public CompletableFuture queryBusinessCollections(@NonNull ContactJidProvider contact) { + public CompletableFuture queryBusinessCollections(@NonNull JidProvider contact) { return queryBusinessCollections(contact, 50); } @@ -2043,7 +2048,7 @@ public CompletableFuture createCommunity(@NonNull String subject, 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(ContactJidServer.GROUP.toJid(), "set", "w:g2", entry).thenApplyAsync(node -> { + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", entry).thenApplyAsync(node -> { node.assertNode("group", () -> "Missing community response, something went wrong: " + findErrorNode(node)); return socketHandler.parseGroupMetadata(node); }); @@ -2057,9 +2062,9 @@ public CompletableFuture createCommunity(@NonNull String subject, * @param policy the non-null policy * @return a future */ - public CompletableFuture changeCommunitySetting(@NonNull T community, @NonNull CommunitySetting setting, @NonNull GroupSettingPolicy policy) { + public CompletableFuture changeCommunitySetting(@NonNull T community, @NonNull CommunitySetting setting, @NonNull GroupSettingPolicy policy) { var tag = policy == GroupSettingPolicy.ANYONE ? setting.on() : setting.off(); - return socketHandler.sendQuery(ContactJidServer.GROUP.toJid(), "set", "w:g2", Node.of(tag)) + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", Node.of(tag)) .thenApplyAsync(ignored -> community); } @@ -2081,7 +2086,7 @@ public CompletableFuture unlinkDevices(){ * @param companion the non-null companion to unlink * @return a future */ - public CompletableFuture unlinkDevice(@NonNull ContactJid companion){ + public CompletableFuture unlinkDevice(@NonNull Jid companion){ Validate.isTrue(companion.hasAgent(), "Expected companion, got jid without agent: %s", companion); return socketHandler.sendQuery("set", "md", Node.of("remove-companion-device", Map.of("jid", companion, "reason", "user_initiated"))) .thenRun(() -> store().removeLinkedCompanion(companion)) @@ -2157,7 +2162,7 @@ private CompletableFuture linkDevice(byte[] advIdentity, by .build(); var knownDevices = store().linkedDevices() .stream() - .map(ContactJid::device) + .map(Jid::device) .toList(); var keyIndexList = new KeyIndexListBuilder() .rawId(deviceIdentity.rawId()) @@ -2213,7 +2218,7 @@ private CompletableFuture handleCompanionPairing(Node resul .thenComposeAsync(encryptResult -> handleCompanionEncrypt(encryptResult, device, keyId)); } - private CompletableFuture awaitCompanionRegistration(ContactJid device) { + private CompletableFuture awaitCompanionRegistration(Jid device) { var future = new CompletableFuture(); OnLinkedDevices listener = data -> { if(data.contains(device)) { @@ -2226,7 +2231,7 @@ private CompletableFuture awaitCompanionRegistration(ContactJid device) { .thenRun(() -> removeListener(listener)); } - private CompletableFuture handleCompanionEncrypt(Node result, ContactJid companion, int keyId) { + private CompletableFuture handleCompanionEncrypt(Node result, Jid companion, int keyId) { store().addLinkedDevice(companion, keyId); socketHandler.parseSessions(result); return sendInitialSecurityMessage(companion) @@ -2240,7 +2245,7 @@ private CompletableFuture handleCompanionEncrypt(Node resul .thenApplyAsync(ignored -> CompanionLinkResult.SUCCESS); } - private CompletableFuture syncCompanionState(ContactJid companion) { + private CompletableFuture syncCompanionState(Jid companion) { var criticalUnblockLowRequest = createCriticalUnblockLowRequest(); var criticalBlockRequest = createCriticalBlockRequest(); return socketHandler.pushPatches(companion, List.of(criticalUnblockLowRequest, criticalBlockRequest)).thenComposeAsync(ignored -> { @@ -2304,7 +2309,7 @@ private PatchEntry createAndroidEntry() { } var osType = device.get().platform(); - if (osType != UserAgent.Platform.ANDROID && osType != UserAgent.Platform.SMB_ANDROID) { + if (osType != UserAgent.PlatformType.ANDROID && osType != UserAgent.PlatformType.SMB_ANDROID) { return null; } @@ -2335,7 +2340,7 @@ private PatchEntry createContactRequestEntry(Contact contact) { return PatchEntry.of(sync, Operation.SET, 2, contact.jid().toString()); } - private CompletableFuture sendRecentMessage(ContactJid jid) { + private CompletableFuture sendRecentMessage(Jid jid) { var pushNames = new HistorySyncBuilder() .conversations(List.of()) .syncType(HistorySync.Type.RECENT) @@ -2343,7 +2348,7 @@ private CompletableFuture sendRecentMessage(ContactJid jid) { return sendHistoryProtocolMessage(jid, pushNames, HistorySync.Type.PUSH_NAME); } - private CompletableFuture sendPushNamesMessage(ContactJid jid) { + private CompletableFuture sendPushNamesMessage(Jid jid) { var pushNamesData = store() .contacts() .stream() @@ -2357,7 +2362,7 @@ private CompletableFuture sendPushNamesMessage(ContactJid jid) { return sendHistoryProtocolMessage(jid, pushNames, HistorySync.Type.PUSH_NAME); } - private CompletableFuture sendInitialStatusMessage(ContactJid jid) { + private CompletableFuture sendInitialStatusMessage(Jid jid) { var initialStatus = new HistorySyncBuilder() .statusV3Messages(new ArrayList<>(store().status())) .syncType(HistorySync.Type.INITIAL_STATUS_V3) @@ -2365,7 +2370,7 @@ private CompletableFuture sendInitialStatusMessage(ContactJid jid) { return sendHistoryProtocolMessage(jid, initialStatus, HistorySync.Type.INITIAL_STATUS_V3); } - private CompletableFuture sendInitialBootstrapMessage(ContactJid jid) { + private CompletableFuture sendInitialBootstrapMessage(Jid jid) { var chats = store().chats() .stream() .toList(); @@ -2376,7 +2381,7 @@ private CompletableFuture sendInitialBootstrapMessage(ContactJid jid) { return sendHistoryProtocolMessage(jid, initialBootstrap, HistorySync.Type.INITIAL_BOOTSTRAP); } - private CompletableFuture sendInitialNullMessage(ContactJid jid) { + private CompletableFuture sendInitialNullMessage(Jid jid) { var pastParticipants = store().chats() .stream() .map(this::getPastParticipants) @@ -2400,7 +2405,7 @@ private GroupPastParticipants getPastParticipants(Chat chat) { .build(); } - private CompletableFuture sendAppStateKeysMessage(ContactJid companion) { + private CompletableFuture sendAppStateKeysMessage(Jid companion) { var preKeys = IntStream.range(0, 10) .mapToObj(index -> createAppKey(companion, index)) .toList(); @@ -2415,14 +2420,14 @@ private CompletableFuture sendAppStateKeysMessage(ContactJid companion) { return socketHandler.sendPeerMessage(companion, result); } - private AppStateSyncKey createAppKey(ContactJid jid, int index) { + private AppStateSyncKey createAppKey(Jid jid, int index) { return new AppStateSyncKeyBuilder() .keyId(new AppStateSyncKeyId(KeyHelper.appKeyId())) .keyData(createAppKeyData(jid, index)) .build(); } - private AppStateSyncKeyData createAppKeyData(ContactJid jid, int index) { + private AppStateSyncKeyData createAppKeyData(Jid jid, int index) { return new AppStateSyncKeyDataBuilder() .keyData(SignalKeyPair.random().publicKey()) .fingerprint(createAppKeyFingerprint(jid, index)) @@ -2430,7 +2435,7 @@ private AppStateSyncKeyData createAppKeyData(ContactJid jid, int index) { .build(); } - private AppStateSyncKeyFingerprint createAppKeyFingerprint(ContactJid jid, int index) { + private AppStateSyncKeyFingerprint createAppKeyFingerprint(Jid jid, int index) { return new AppStateSyncKeyFingerprintBuilder() .rawId(KeyHelper.senderKeyId()) .currentIndex(index) @@ -2438,7 +2443,7 @@ private AppStateSyncKeyFingerprint createAppKeyFingerprint(ContactJid jid, int i .build(); } - private CompletableFuture sendInitialSecurityMessage(ContactJid jid) { + private CompletableFuture sendInitialSecurityMessage(Jid jid) { var protocolMessage = new ProtocolMessageBuilder() .protocolType(ProtocolMessage.Type.INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC) .initialSecurityNotificationSettingSync(new InitialSecurityNotificationSettingSync(true)) @@ -2446,7 +2451,7 @@ private CompletableFuture sendInitialSecurityMessage(ContactJid jid) { return socketHandler.sendPeerMessage(jid, protocolMessage); } - private CompletableFuture sendHistoryProtocolMessage(ContactJid jid, HistorySync historySync, HistorySync.Type type) { + private CompletableFuture sendHistoryProtocolMessage(Jid jid, HistorySync historySync, HistorySync.Type type) { var syncBytes = HistorySyncSpec.encode(historySync); return Medias.upload(syncBytes, AttachmentType.HISTORY_SYNC, store().mediaConnection()) .thenApplyAsync(upload -> createHistoryProtocolMessage(upload, type)) @@ -2473,7 +2478,7 @@ private ProtocolMessage createHistoryProtocolMessage(MediaFile upload, HistorySy * * @return a future */ - public CompletableFuture> queryBusinessCertificate(@NonNull ContactJidProvider provider) { + public CompletableFuture> queryBusinessCertificate(@NonNull JidProvider provider) { return socketHandler.sendQuery("get", "w:biz", Node.of("verified_name", Map.of("jid", provider.toJid()))) .thenApplyAsync(this::parseCertificate); } @@ -2539,13 +2544,13 @@ private CompletableFuture set2fa(String code, String email) { * @param contact the non-null contact * @return a future */ - public CompletableFuture startCall(@NonNull ContactJidProvider contact) { + public CompletableFuture startCall(@NonNull JidProvider contact) { Validate.isTrue(store().clientType() == ClientType.MOBILE, "Calling is only available for the mobile api"); return socketHandler.querySessions(contact.toJid()) .thenComposeAsync(ignored -> sendCallMessage(contact)); } - private CompletableFuture sendCallMessage(ContactJidProvider provider) { + private CompletableFuture sendCallMessage(JidProvider provider) { var callId = MessageKey.randomId(); var audioStream = Node.of("audio", Map.of("rate", 8000, "enc", "opus")); var audioStreamTwo = Node.of("audio", Map.of("rate", 16000, "enc", "opus")); @@ -2561,14 +2566,14 @@ private CompletableFuture sendCallMessage(ContactJidProvider provider) { .thenApply(result -> onCallSent(provider, callId, result)); } - private Call onCallSent(ContactJidProvider provider, String callId, Node result) { + private Call onCallSent(JidProvider provider, String callId, Node result) { var call = new Call(provider.toJid(), jidOrThrowError(), callId, ZonedDateTime.now(), false, CallStatus.RINGING, false); store().addCall(call); socketHandler.onCall(call); return call; } - private Node createCallNode(ContactJidProvider provider) { + private Node createCallNode(JidProvider provider) { var call = new CallMessageBuilder() .key(SignalKeyPair.random().publicKey()) .build(); @@ -2581,7 +2586,6 @@ private Node createCallNode(ContactJidProvider provider) { Map.of("v", 2, "type", cipheredMessage.type(), "count", 0), cipheredMessage.message()); } - /** * Rejects an incoming call or stops an active call * Mobile API only @@ -3332,7 +3336,7 @@ public Whatsapp addLinkedDevicesListener(OnWhatsappCall onWhatsappCall) { return addListener(onWhatsappCall); } - private ContactJid jidOrThrowError() { + private Jid jidOrThrowError() { return store().jid() .orElseThrow(() -> new IllegalStateException("The session isn't connected")); } diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java b/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java index c552913e..9e8197f3 100644 --- a/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java +++ b/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java @@ -1,8 +1,8 @@ package it.auties.whatsapp.binary; import io.netty.buffer.ByteBuf; -import it.auties.whatsapp.model.contact.ContactJid; -import it.auties.whatsapp.model.contact.ContactJidServer; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.Validate; @@ -126,22 +126,22 @@ private String readHexString() { return readString(BinaryTokens.HEX, number >>> 7, 127 & number); } - private ContactJid readJidPair() { + private Jid readJidPair() { var read = read(true); if (read instanceof String encoded) { - return ContactJid.of(encoded, ContactJidServer.of(readString())); + return Jid.of(encoded, JidServer.of(readString())); } else if (read == null) { - return ContactJid.ofServer(ContactJidServer.of(readString())); + return Jid.ofServer(JidServer.of(readString())); } else { throw new RuntimeException("Invalid jid type"); } } - private ContactJid readCompanionJid() { + private Jid readCompanionJid() { var agent = buffer.readUnsignedByte(); var device = buffer.readUnsignedByte(); var user = readString(); - return ContactJid.ofDevice(user, device, agent); + return Jid.ofDevice(user, device, agent); } private int readSize(int token) { diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java b/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java index f7429519..b4cfbcbe 100644 --- a/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java +++ b/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.binary; import io.netty.buffer.ByteBuf; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.util.BytesHelper; @@ -215,7 +215,7 @@ private void write(Object input) { writeString(number.toString()); } else if (input instanceof byte[] bytes) { writeBytes(bytes); - } else if (input instanceof ContactJid jid) { + } else if (input instanceof Jid jid) { writeJid(jid); } else if (input instanceof Collection collection) { writeList(collection); @@ -241,7 +241,7 @@ private void writeBytes(byte[] bytes) { buffer.writeBytes(bytes); } - private void writeJid(ContactJid jid) { + private void writeJid(Jid jid) { if (jid.isCompanion()) { buffer.writeByte(COMPANION_JID.data()); buffer.writeByte(jid.agent()); diff --git a/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java index d0c9b951..0f110c0d 100644 --- a/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java @@ -36,7 +36,7 @@ public interface ControllerSerializer { * @param keys the non-null keys to serialize * @param async whether the operation should be executed asynchronously */ - void serializeKeys(Keys keys, boolean async); + CompletableFuture serializeKeys(Keys keys, boolean async); /** * Serializes the store @@ -44,7 +44,7 @@ public interface ControllerSerializer { * @param store the non-null store to serialize * @param async whether the operation should be executed asynchronously */ - void serializeStore(Store store, boolean async); + CompletableFuture serializeStore(Store store, boolean async); /** * Serializes the keys diff --git a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java index c7755473..ff0a52e7 100644 --- a/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java +++ b/src/main/java/it/auties/whatsapp/controller/DefaultControllerSerializer.java @@ -1,23 +1,16 @@ package it.auties.whatsapp.controller; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.smile.databind.SmileMapper; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import it.auties.whatsapp.api.ClientType; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.chat.ChatBuilder; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.util.Smile; import it.auties.whatsapp.util.Validate; import org.checkerframework.checker.nullness.qual.NonNull; -import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; -import java.lang.System.Logger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -27,22 +20,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Semaphore; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; -import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; -import static com.fasterxml.jackson.annotation.PropertyAccessor.*; -import static com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY; -import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS; -import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_ENUMS_USING_INDEX; -import static java.lang.System.Logger.Level.ERROR; -import static java.lang.System.Logger.Level.WARNING; - /** * The default serializer * It uses smile to serialize all the data locally @@ -51,10 +32,11 @@ 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 STORE_NAME = "store.smile"; + private static final String KEYS_NAME = "keys.smile"; private static final ControllerSerializer DEFAULT_SERIALIZER = new DefaultControllerSerializer(); private final Path baseDirectory; - private final Logger logger; private final Map> attributeStoreSerializers; private LinkedList cachedUuids; private LinkedList cachedPhoneNumbers; @@ -77,13 +59,12 @@ private DefaultControllerSerializer() { */ public DefaultControllerSerializer(@NonNull Path baseDirectory) { this.baseDirectory = baseDirectory; - this.logger = System.getLogger("DefaultSerializer"); this.attributeStoreSerializers = new ConcurrentHashMap<>(); try { Files.createDirectories(baseDirectory); Validate.isTrue(Files.isDirectory(baseDirectory), "Expected a directory as base path: %s", baseDirectory); } catch (IOException exception) { - logger.log(WARNING, "Cannot create base directory at %s: %s".formatted(baseDirectory, exception.getMessage())); + throw new UncheckedIOException(exception); } } @@ -93,7 +74,12 @@ public LinkedList listIds(@NonNull ClientType type) { return cachedUuids; } - try (var walker = Files.walk(getHome(type), 1).sorted(Comparator.comparing(this::getLastModifiedTime))) { + var directory = getHome(type); + if(Files.notExists(directory)) { + return new LinkedList<>(); + } + + try (var walker = Files.walk(directory, 1).sorted(Comparator.comparing(this::getLastModifiedTime))) { return cachedUuids = walker.map(this::parsePathAsId) .flatMap(Optional::stream) .collect(Collectors.toCollection(LinkedList::new)); @@ -108,7 +94,12 @@ public LinkedList listPhoneNumbers(@NonNull ClientType type) { return cachedPhoneNumbers; } - try (var walker = Files.walk(getHome(type), 1).sorted(Comparator.comparing(this::getLastModifiedTime))) { + var directory = getHome(type); + if(Files.notExists(directory)) { + return new LinkedList<>(); + } + + try (var walker = Files.walk(directory, 1).sorted(Comparator.comparing(this::getLastModifiedTime))) { return cachedPhoneNumbers = walker.map(this::parsePathAsPhoneNumber) .flatMap(Optional::stream) .collect(Collectors.toCollection(LinkedList::new)); @@ -143,18 +134,22 @@ private Optional parsePathAsPhoneNumber(Path file) { } @Override - public void serializeKeys(Keys keys, boolean async) { + public CompletableFuture serializeKeys(Keys keys, boolean async) { if (cachedUuids != null && !cachedUuids.contains(keys.uuid())) { cachedUuids.add(keys.uuid()); } - var path = getSessionFile(keys.clientType(), keys.uuid().toString(), "keys.smile"); - var preferences = SmileFile.of(path); - preferences.write(keys, async); + var outputFile = getSessionFile(keys.clientType(), keys.uuid().toString(), KEYS_NAME); + if (async) { + return CompletableFuture.runAsync(() -> writeFile(keys, KEYS_NAME, outputFile)); + } + + writeFile(keys, KEYS_NAME, outputFile); + return CompletableFuture.completedFuture(null); } @Override - public void serializeStore(Store store, boolean async) { + public CompletableFuture serializeStore(Store store, boolean async) { if (cachedUuids != null && !cachedUuids.contains(store.uuid())) { cachedUuids.add(store.uuid()); } @@ -166,28 +161,45 @@ public void serializeStore(Store store, boolean async) { var task = attributeStoreSerializers.get(store.uuid()); if (task != null && !task.isDone()) { - return; - } - var path = getSessionFile(store, "store.smile"); - var preferences = SmileFile.of(path); - preferences.write(store, async); - if (async) { - store.chats().forEach(chat -> serializeChat(store, chat)); - return; + return task; } - var futures = store.chats() + var storePath = getSessionFile(store, STORE_NAME); + var storeFuture = CompletableFuture.runAsync(() -> writeFile(store, STORE_NAME, storePath)); + var chatsFutures = store.chats() .stream() - .map(chat -> serializeChat(store, chat)) + .map(chat -> serializeChatAsync(store, chat)) .toArray(CompletableFuture[]::new); - CompletableFuture.allOf(futures).join(); + var chatsFuture = CompletableFuture.allOf(chatsFutures); + var result = CompletableFuture.allOf(storeFuture, chatsFuture); + if (async) { + return result; + } + + result.join(); + return CompletableFuture.completedFuture(null); } - private CompletableFuture serializeChat(Store store, Chat chat) { - var fileName = "%s%s.smile".formatted(CHAT_PREFIX, chat.jid().toString()); - var path = getSessionFile(store, fileName); - var preferences = SmileFile.of(path); - return preferences.write(chat, true); + private CompletableFuture serializeChatAsync(Store store, Chat chat) { + if(!store.hasUpdate(chat)) { + return CompletableFuture.completedFuture(null); + } + + var fileName = CHAT_PREFIX + chat.jid() + ".smile"; + var outputFile = getSessionFile(store, fileName); + return CompletableFuture.runAsync(() -> writeFile(chat, fileName, outputFile)); + } + + private void writeFile(Object object, String fileName, Path outputFile) { + try { + var tempFile = Files.createTempFile(fileName, ".tmp"); + try (var tempFileOutputStream = new GZIPOutputStream(Files.newOutputStream(tempFile))) { + Smile.writeValueAsBytes(tempFileOutputStream, object); + Files.move(tempFile, outputFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } + }catch (IOException exception) { + throw new UncheckedIOException("Cannot write file", exception); + } } @Override @@ -225,8 +237,11 @@ public Optional deserializeKeys(@NonNull ClientType type, long phoneNumber private Optional deserializeKeysFromId(ClientType type, String id) { var path = getSessionFile(type, id, "keys.smile"); - var preferences = SmileFile.of(path); - return preferences.read(Keys.class); + try (var input = new GZIPInputStream(Files.newInputStream(path))) { + return Optional.of(Smile.readValue(input, Keys.class)); + } catch (IOException exception) { + return Optional.empty(); + } } @Override @@ -264,8 +279,11 @@ public Optional deserializeStore(@NonNull ClientType type, long phoneNumb private Optional deserializeStoreFromId(ClientType type, String id) { var path = getSessionFile(type, id, "store.smile"); - var preferences = SmileFile.of(path); - return preferences.read(Store.class); + try (var input = new GZIPInputStream(Files.newInputStream(path))) { + return Optional.of(Smile.readValue(input, Store.class)); + } catch (IOException exception) { + return Optional.empty(); + } } @Override @@ -292,14 +310,18 @@ public synchronized CompletableFuture attributeStore(Store store) { @Override public void deleteSession(@NonNull Controller controller) { - var folderPath = getSessionDirectory(controller.clientType(), controller.uuid().toString()); - deleteDirectory(folderPath.toFile()); - var phoneNumber = controller.phoneNumber().orElse(null); - if (phoneNumber == null) { - return; - } - var linkedFolderPath = getSessionDirectory(controller.clientType(), phoneNumber.toString()); - deleteDirectory(linkedFolderPath.toFile()); + try { + var folderPath = getSessionDirectory(controller.clientType(), controller.uuid().toString()); + Files.deleteIfExists(folderPath); + var phoneNumber = controller.phoneNumber().orElse(null); + if (phoneNumber == null) { + return; + } + var linkedFolderPath = getSessionDirectory(controller.clientType(), phoneNumber.toString()); + Files.deleteIfExists(linkedFolderPath); + }catch (IOException exception) { + throw new UncheckedIOException("Cannot delete session", exception); + } } @Override @@ -315,184 +337,60 @@ private void linkToUuid(ClientType type, UUID uuid, String string) { var link = getSessionDirectory(type, string); Files.writeString(link, uuid.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException exception) { - logger.log(WARNING, "Cannot link %s to %s".formatted(string, uuid), exception); + throw new UncheckedIOException("Cannot link %s to %s".formatted(string, uuid), exception); } } - // Not using Java NIO api because of a bug - private void deleteDirectory(File directory) { - if (directory == null || !directory.exists()) { - return; - } - var files = directory.listFiles(); - if (files == null) { - if (directory.delete()) { - return; - } - - logger.log(WARNING, "Cannot delete folder %s".formatted(directory)); - return; - } - for (var file : files) { - if (file.isDirectory()) { - deleteDirectory(file); - continue; - } - if (file.delete()) { - continue; - } - logger.log(WARNING, "Cannot delete file %s".formatted(directory)); - } - if (directory.delete()) { - return; + private void deserializeChat(Store store, Path chatFile) { + try (var input = new GZIPInputStream(Files.newInputStream(chatFile))) { + store.addChat(Smile.readValue(input, Chat.class)); + } catch (IOException exception) { + store.addChat(rescueChat(chatFile)); } - logger.log(WARNING, "Cannot delete folder %s".formatted(directory)); } - private void deserializeChat(Store baseStore, Path entry) { - var chatPreferences = SmileFile.of(entry); - var chat = chatPreferences.read(Chat.class) - .orElseGet(() -> fixChat(entry)); - baseStore.addChatDirect(chat); - } + private Chat rescueChat(Path entry) { + try { + Files.deleteIfExists(entry); + } catch (IOException ignored) { - private Chat fixChat(Path entry) { + } var chatName = entry.getFileName().toString() .replaceFirst(CHAT_PREFIX, "") .replace(".smile", "") .replaceAll("~~", ":"); - logger.log(ERROR, "Chat at %s is corrupted, resetting it".formatted(chatName)); - try { - Files.deleteIfExists(entry); - } catch (IOException deleteException) { - logger.log(WARNING, "Cannot delete chat file"); - } return new ChatBuilder() - .jid(ContactJid.of(chatName)) + .jid(Jid.of(chatName)) .historySyncMessages(new ConcurrentLinkedDeque<>()) .build(); } private Path getHome(ClientType type) { - var directory = baseDirectory.resolve(type == ClientType.MOBILE ? "mobile" : "web"); - if (!Files.exists(directory)) { - try { - Files.createDirectories(directory); - } catch (IOException exception) { - throw new UncheckedIOException("Cannot create directory", exception); - } - } - - return directory; + return baseDirectory.resolve(type == ClientType.MOBILE ? "mobile" : "web"); } - private Path getSessionDirectory(ClientType clientType, String uuid) { - return getHome(clientType).resolve(uuid); + private Path getSessionDirectory(ClientType clientType, String path) { + return getHome(clientType).resolve(path); } private Path getSessionFile(Store store, String fileName) { - var fixedName = fileName.replaceAll(":", "~~"); - return getSessionFile(store.clientType(), store.uuid().toString(), fixedName); + try { + var fixedName = fileName.replaceAll(":", "~~"); + var result = getSessionFile(store.clientType(), store.uuid().toString(), fixedName); + Files.createDirectories(result.getParent()); + return result; + } catch (IOException exception) { + throw new UncheckedIOException("Cannot create directory", exception); + } } private Path getSessionFile(ClientType clientType, String uuid, String fileName) { - return getSessionDirectory(clientType, uuid).resolve(fileName); - } - - private record SmileFile(Path file, Semaphore semaphore) { - private final static ObjectMapper smile; - private final static ConcurrentHashMap instances; - private final static Logger logger; - - static { - instances = new ConcurrentHashMap<>(); - logger = System.getLogger("Smile"); - smile = SmileMapper.builder() - .build() - .registerModule(new Jdk8Module()) - .registerModule(new JavaTimeModule()) - .registerModule(new ParameterNamesModule()) - .setSerializationInclusion(NON_NULL) - .enable(WRITE_ENUMS_USING_INDEX) - .enable(FAIL_ON_EMPTY_BEANS) - .enable(ACCEPT_SINGLE_VALUE_AS_ARRAY) - .disable(FAIL_ON_UNKNOWN_PROPERTIES) - .setVisibility(ALL, NONE) - .setVisibility(CREATOR, ANY) - .setVisibility(FIELD, ANY); - } - - private SmileFile { - try { - Files.createDirectories(file.getParent()); - } catch (IOException exception) { - throw new UncheckedIOException("Cannot create smile file", exception); - } - } - - private static synchronized SmileFile of(@NonNull Path file) { - var knownInstance = instances.get(file); - if (knownInstance != null) { - return knownInstance; - } - - var instance = new SmileFile(file, new Semaphore(1)); - instances.put(file, instance); - return instance; - } - - private Optional read(Class clazz) { - return read(new TypeReference<>() { - @Override - public Class getType() { - return clazz; - } - }); - } - - private Optional read(TypeReference reference) { - if (Files.notExists(file)) { - return Optional.empty(); - } - - try (var input = new GZIPInputStream(Files.newInputStream(file))) { - return Optional.of(smile.readValue(input, reference)); - } catch (IOException exception) { - return Optional.empty(); - } - } - - private CompletableFuture write(Object input, boolean async) { - if (!async) { - writeSync(input); - return CompletableFuture.completedFuture(null); - } - - return CompletableFuture.runAsync(() -> writeSync(input)).exceptionallyAsync(throwable -> { - logger.log(ERROR, "Cannot serialize smile file", throwable); - return null; - }); - } - - private void writeSync(Object input) { - try { - if (input == null) { - return; - } - - semaphore.acquire(); - var tempFile = Files.createTempFile(file.getFileName().toString(), ".tmp"); - try (var tempFileOutputStream = new GZIPOutputStream(Files.newOutputStream(tempFile))) { - smile.writeValue(tempFileOutputStream, input); - Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } - } catch (IOException exception) { - throw new UncheckedIOException("Cannot complete file write", exception); - } catch (InterruptedException exception) { - throw new RuntimeException("Cannot acquire lock", exception); - } finally { - semaphore.release(); - } + try { + var result = getSessionDirectory(clientType, uuid).resolve(fileName); + Files.createDirectories(result.getParent()); + return result; + } catch (IOException exception) { + throw new UncheckedIOException("Cannot create directory", exception); } } } diff --git a/src/main/java/it/auties/whatsapp/controller/Keys.java b/src/main/java/it/auties/whatsapp/controller/Keys.java index 454b5aa3..80481566 100644 --- a/src/main/java/it/auties/whatsapp/controller/Keys.java +++ b/src/main/java/it/auties/whatsapp/controller/Keys.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import it.auties.whatsapp.api.ClientType; import it.auties.whatsapp.model.companion.CompanionHashState; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentity; import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentityHMAC; @@ -123,7 +123,7 @@ public final class Keys extends Controller { * App state keys */ @NonNull - private final Map> appStateKeys; + private final Map> appStateKeys; /** * Sessions map @@ -135,10 +135,10 @@ public final class Keys extends Controller { * Hash state */ @NonNull - private final Map> hashStates; + private final Map> hashStates; @NonNull - private final Map> groupsPreKeys; + private final Map> groupsPreKeys; /** * Whether the client was registered @@ -176,7 +176,7 @@ public final class Keys extends Controller { private byte[] writeKey, readKey; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - Keys(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, int registrationId, @NonNull SignalKeyPair noiseKeyPair, @NonNull SignalKeyPair ephemeralKeyPair, @NonNull SignalKeyPair identityKeyPair, @NonNull SignalKeyPair companionKeyPair, SignalSignedKeyPair signedKeyPair, byte @Nullable [] signedKeyIndex, @Nullable Long signedKeyIndexTimestamp, @NonNull List preKeys, byte @NonNull [] prologue, @NonNull String phoneId, @NonNull String deviceId, @NonNull String recoveryToken, @Nullable SignedDeviceIdentity companionIdentity, @NonNull Map senderKeys, @NonNull Map> appStateKeys, @NonNull Map sessions, @NonNull Map> hashStates, @NonNull Map> groupsPreKeys, boolean registered, boolean businessCertificate, boolean initialAppSync) { + Keys(@NonNull UUID uuid, PhoneNumber phoneNumber, @NonNull ControllerSerializer serializer, @NonNull ClientType clientType, @Nullable List alias, int registrationId, @NonNull SignalKeyPair noiseKeyPair, @NonNull SignalKeyPair ephemeralKeyPair, @NonNull SignalKeyPair identityKeyPair, @NonNull SignalKeyPair companionKeyPair, SignalSignedKeyPair signedKeyPair, byte @Nullable [] signedKeyIndex, @Nullable Long signedKeyIndexTimestamp, @NonNull List preKeys, byte @NonNull [] prologue, @NonNull String phoneId, @NonNull String deviceId, @NonNull String recoveryToken, @Nullable SignedDeviceIdentity companionIdentity, @NonNull Map senderKeys, @NonNull Map> appStateKeys, @NonNull Map sessions, @NonNull Map> hashStates, @NonNull Map> groupsPreKeys, boolean registered, boolean businessCertificate, boolean initialAppSync) { super(uuid, phoneNumber, serializer, clientType, alias); this.registrationId = registrationId; this.noiseKeyPair = noiseKeyPair; @@ -292,7 +292,7 @@ public Optional findPreKeyById(Integer id) { * @param id the non-null id to search * @return a non-null Optional app state dataSync key */ - public Optional findAppKeyById(@NonNull ContactJid jid, byte[] id) { + public Optional findAppKeyById(@NonNull Jid jid, byte[] id) { return Objects.requireNonNull(appStateKeys.get(jid), "Missing keys") .stream() .filter(preKey -> preKey.keyId() != null && Arrays.equals(preKey.keyId().keyId(), id)) @@ -306,7 +306,7 @@ public Optional findAppKeyById(@NonNull ContactJid jid, byte[] * @param patchType the non-null name to search * @return a non-null hash state */ - public Optional findHashStateByName(@NonNull ContactJid device, @NonNull PatchType patchType) { + public Optional findHashStateByName(@NonNull Jid device, @NonNull PatchType patchType) { return Optional.ofNullable(hashStates.get(device)) .map(entry -> entry.get(patchType)); } @@ -351,7 +351,7 @@ public Keys putSession(@NonNull SessionAddress address, @NonNull Session record) * @param state the non-null hash state * @return this */ - public Keys putState(@NonNull ContactJid device, @NonNull CompanionHashState state) { + public Keys putState(@NonNull Jid device, @NonNull CompanionHashState state) { var oldData = Objects.requireNonNullElseGet(hashStates.get(device), HashMap::new); oldData.put(state.name(), state); hashStates.put(device, oldData); @@ -365,7 +365,7 @@ public Keys putState(@NonNull ContactJid device, @NonNull CompanionHashState sta * @param keys the keys to add * @return this */ - public Keys addAppKeys(@NonNull ContactJid jid, @NonNull Collection keys) { + public Keys addAppKeys(@NonNull Jid jid, @NonNull Collection keys) { appStateKeys.put(jid, new LinkedList<>(keys)); return this; } @@ -375,7 +375,7 @@ public Keys addAppKeys(@NonNull ContactJid jid, @NonNull Collection getAppKeys(@NonNull ContactJid jid) { + public LinkedList getAppKeys(@NonNull Jid jid) { return Objects.requireNonNullElseGet(appStateKeys.get(jid), LinkedList::new); } @@ -460,19 +460,19 @@ public Collection preKeys() { return Collections.unmodifiableList(preKeys); } - public void addRecipientWithPreKeys(@NonNull ContactJid group, @NonNull ContactJid recipient) { + public void addRecipientWithPreKeys(@NonNull Jid group, @NonNull Jid recipient) { var preKeys = groupsPreKeys.get(group); if (preKeys != null) { preKeys.add(recipient); return; } - var newPreKeys = new ArrayList(); + var newPreKeys = new ArrayList(); newPreKeys.add(recipient); groupsPreKeys.put(group, newPreKeys); } - public void addRecipientsWithPreKeys(@NonNull ContactJid group, @NonNull Collection recipients) { + public void addRecipientsWithPreKeys(@NonNull Jid group, @NonNull Collection recipients) { var preKeys = groupsPreKeys.get(group); if (preKeys != null) { preKeys.addAll(recipients); @@ -483,7 +483,7 @@ public void addRecipientsWithPreKeys(@NonNull ContactJid group, @NonNull Collect groupsPreKeys.put(group, newPreKeys); } - public boolean hasGroupKeys(@NonNull ContactJid group, @NonNull ContactJid recipient) { + public boolean hasGroupKeys(@NonNull Jid group, @NonNull Jid recipient) { var preKeys = groupsPreKeys.get(group); return preKeys != null && preKeys.contains(recipient); } @@ -550,7 +550,7 @@ public String recoveryToken() { return this.senderKeys; } - public @NonNull Map> appStateKeys() { + public @NonNull Map> appStateKeys() { return this.appStateKeys; } @@ -558,11 +558,11 @@ public String recoveryToken() { return this.sessions; } - public @NonNull Map> hashStates() { + public @NonNull Map> hashStates() { return this.hashStates; } - public @NonNull Map> groupsPreKeys() { + public @NonNull Map> groupsPreKeys() { return this.groupsPreKeys; } diff --git a/src/main/java/it/auties/whatsapp/controller/Store.java b/src/main/java/it/auties/whatsapp/controller/Store.java index b8bb8bbc..02f1a816 100644 --- a/src/main/java/it/auties/whatsapp/controller/Store.java +++ b/src/main/java/it/auties/whatsapp/controller/Store.java @@ -16,9 +16,9 @@ import it.auties.whatsapp.model.chat.ChatEphemeralTimer; import it.auties.whatsapp.model.companion.CompanionDevice; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.contact.ContactJid; -import it.auties.whatsapp.model.contact.ContactJidProvider; -import it.auties.whatsapp.model.contact.ContactJidServer; +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.info.ContextInfo; import it.auties.whatsapp.model.info.DeviceContextInfo; import it.auties.whatsapp.model.info.MessageInfo; @@ -36,7 +36,7 @@ import it.auties.whatsapp.model.privacy.PrivacySettingEntry; import it.auties.whatsapp.model.privacy.PrivacySettingType; import it.auties.whatsapp.model.request.Request; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.auth.UserAgent.ReleaseChannel; import it.auties.whatsapp.model.signal.auth.Version; import it.auties.whatsapp.model.sync.HistorySyncMessage; @@ -149,7 +149,7 @@ public final class Store extends Controller { * The value is the device's companion jid */ @NonNull - private LinkedHashMap linkedDevicesKeys; + private LinkedHashMap linkedDevicesKeys; /** * The profile picture of the user linked to this account. This field will be null while the user @@ -170,13 +170,13 @@ public final class Store extends Controller { * The user linked to this account. This field will be null while the user hasn't logged in yet. */ @Nullable - private ContactJid jid; + private Jid jid; /** * The lid user linked to this account. This field will be null while the user hasn't logged in yet. */ @Nullable - private ContactJid lid; + private Jid lid; /** * The non-null map of properties received by whatsapp @@ -189,19 +189,26 @@ public final class Store extends Controller { */ @NonNull @JsonIgnore - private final ConcurrentHashMap chats; + private final ConcurrentHashMap chats; + + /** + * A map of chats and their last known unique id + * Useful for serialization + */ + @NonNull + private final ConcurrentHashMap chatsUpdateIds; /** * The non-null map of contacts */ @NonNull - private final ConcurrentHashMap contacts; + private final ConcurrentHashMap contacts; /** * The non-null list of status messages */ @NonNull - private final ConcurrentHashMap> status; + private final ConcurrentHashMap> status; /** * The non-null map of privacy settings @@ -320,7 +327,7 @@ public final class Store extends Controller { * The os of the associated device, available only for the web api */ @Nullable - private Platform companionDeviceOs; + private PlatformType companionDeviceOs; /** * Whether the mac of every app state request should be checked @@ -331,7 +338,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 ContactJid jid, @Nullable ContactJid lid, @NonNull ConcurrentHashMap properties, @NonNull ConcurrentHashMap chats, @NonNull ConcurrentHashMap contacts, @NonNull ConcurrentHashMap> status, @NonNull ConcurrentHashMap privacySettings, @NonNull ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, @NonNull ConcurrentHashMap requests, @NonNull ConcurrentHashMap> replyHandlers, @NonNull KeySetView listeners, @NonNull String tag, long initializationTimeStamp, @Nullable MediaConnection mediaConnection, @NonNull CountDownLatch mediaConnectionLatch, @NonNull ChatEphemeralTimer newChatsEphemeralTimer, @NonNull TextPreviewSetting textPreviewSetting, @NonNull WebHistoryLength historyLength, boolean autodetectListeners, boolean automaticPresenceUpdates, @NonNull ReleaseChannel releaseChannel, @Nullable CompanionDevice device, @Nullable Platform 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 chatsUpdateIds, @NonNull ConcurrentHashMap contacts, @NonNull ConcurrentHashMap> status, @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); @@ -357,20 +364,20 @@ public final class Store extends Controller { this.jid = jid; this.lid = lid; this.properties = properties; - this.chats = chats; + this.chats = new ConcurrentHashMap<>(); + this.chatsUpdateIds = chatsUpdateIds; this.contacts = contacts; this.status = status; this.privacySettings = privacySettings; this.calls = calls; this.unarchiveChats = unarchiveChats; this.twentyFourHourFormat = twentyFourHourFormat; - this.requests = requests; - this.replyHandlers = replyHandlers; - this.listeners = listeners; - this.tag = tag; + this.requests = new ConcurrentHashMap<>(); + this.replyHandlers = new ConcurrentHashMap<>(); + this.listeners = ConcurrentHashMap.newKeySet(); + this.tag = HexFormat.of().formatHex(BytesHelper.random(1)); this.initializationTimeStamp = initializationTimeStamp; - this.mediaConnection = mediaConnection; - this.mediaConnectionLatch = mediaConnectionLatch; + this.mediaConnectionLatch = new CountDownLatch(1); this.newChatsEphemeralTimer = newChatsEphemeralTimer; this.textPreviewSetting = textPreviewSetting; this.historyLength = historyLength; @@ -380,7 +387,6 @@ public final class Store extends Controller { this.device = device; this.companionDeviceOs = companionDeviceOs; this.checkPatchMacs = checkPatchMacs; - serializer.linkMetadata(this); } /** @@ -399,7 +405,7 @@ public static StoreBuilder builder() { * @param jid the jid to search * @return a non-null optional */ - public Optional findContactByJid(ContactJidProvider jid) { + public Optional findContactByJid(JidProvider jid) { if (jid == null) { return Optional.empty(); } @@ -462,7 +468,7 @@ public Optional findMessageByKey(MessageKey key) { * @param id the jid to search * @return a non-null optional */ - public Optional findMessageById(ContactJidProvider provider, String id) { + public Optional findMessageById(JidProvider provider, String id) { if (provider == null || id == null) { return Optional.empty(); } @@ -486,7 +492,7 @@ public Optional findMessageById(ContactJidProvider provider, String * @param jid the jid to search * @return a non-null optional */ - public Optional findChatByJid(ContactJidProvider jid) { + public Optional findChatByJid(JidProvider jid) { if (jid == null) { return Optional.empty(); } @@ -579,7 +585,7 @@ public Collection status() { * @param jid the sender of the status * @return a non-null immutable list */ - public Collection findStatusBySender(ContactJidProvider jid) { + public Collection findStatusBySender(JidProvider jid) { return Optional.ofNullable(status.get(jid.toJid())) .map(Collections::unmodifiableCollection) .orElseGet(Set::of); @@ -663,9 +669,8 @@ public boolean resolvePendingReply(@NonNull MessageInfo response) { * @param chatJid the chat to add * @return the input chat */ - public Chat addNewChat(@NonNull ContactJid chatJid) { + public Chat addNewChat(@NonNull Jid chatJid) { var chat = new ChatBuilder() - .historySyncMessages(new ConcurrentLinkedDeque<>()) .jid(chatJid) .build(); addChat(chat); @@ -680,7 +685,7 @@ public Chat addNewChat(@NonNull ContactJid chatJid) { */ public Optional addChat(@NonNull Chat chat) { chat.messages().forEach(this::attribute); - if (chat.hasName() && chat.jid().hasServer(ContactJidServer.WHATSAPP)) { + if (chat.hasName() && chat.jid().hasServer(JidServer.WHATSAPP)) { var contact = findContactByJid(chat.jid()) .orElseGet(() -> addContact(new Contact(chat.jid()))); contact.setFullName(chat.name()); @@ -725,18 +730,18 @@ public Optional addChatDirect(Chat chat) { * @param chatJid the chat to remove * @return the chat that was deleted wrapped by an optional */ - public Optional removeChat(@NonNull ContactJid chatJid) { + public Optional removeChat(@NonNull Jid chatJid) { return Optional.ofNullable(chats.remove(chatJid)); } /** * Adds a contact in memory * - * @param contactJid the contact to add + * @param jid the contact to add * @return the input contact */ - public Contact addContact(@NonNull ContactJid contactJid) { - return addContact(new Contact(contactJid)); + public Contact addContact(@NonNull Jid jid) { + return addContact(new Contact(jid)); } /** @@ -784,7 +789,7 @@ public MessageInfo attribute(@NonNull MessageInfo info) { return info; } - private MessageKey attributeSender(MessageInfo info, ContactJid senderJid) { + private MessageKey attributeSender(MessageInfo info, Jid senderJid) { var contact = findContactByJid(senderJid) .orElseGet(() -> addContact(new Contact(senderJid))); info.setSender(contact); @@ -796,13 +801,13 @@ private void attributeContext(ContextInfo contextInfo) { contextInfo.quotedMessageChatJid().ifPresent(chatJid -> attributeContextChat(contextInfo, chatJid)); } - private void attributeContextChat(ContextInfo contextInfo, ContactJid chatJid) { + private void attributeContextChat(ContextInfo contextInfo, Jid chatJid) { var chat = findChatByJid(chatJid) .orElseGet(() -> addNewChat(chatJid)); contextInfo.setQuotedMessageChat(chat); } - private void attributeContextSender(ContextInfo contextInfo, ContactJid senderJid) { + private void attributeContextSender(ContextInfo contextInfo, Jid senderJid) { var contact = findContactByJid(senderJid) .orElseGet(() -> addContact(new Contact(senderJid))); contextInfo.setQuotedMessageSender(contact); @@ -1059,7 +1064,7 @@ public PrivacySettingEntry addPrivacySetting(@NonNull PrivacySettingType type, @ * * @return an unmodifiable map */ - public Map linkedDevicesKeys() { + public Map linkedDevicesKeys() { return Collections.unmodifiableMap(linkedDevicesKeys); } @@ -1069,7 +1074,7 @@ public Map linkedDevicesKeys() { * * @return an unmodifiable list */ - public Collection linkedDevices() { + public Collection linkedDevices() { return Collections.unmodifiableCollection(linkedDevicesKeys.keySet()); } @@ -1081,7 +1086,7 @@ public Collection linkedDevices() { * @param keyId the id of its key * @return the nullable old key */ - public Optional addLinkedDevice(@NonNull ContactJid companion, int keyId) { + public Optional addLinkedDevice(@NonNull Jid companion, int keyId) { return Optional.ofNullable(linkedDevicesKeys.put(companion, keyId)); } @@ -1092,7 +1097,7 @@ public Optional addLinkedDevice(@NonNull ContactJid companion, int keyI * @param companion a non-null companion * @return the nullable old key */ - public Optional removeLinkedCompanion(@NonNull ContactJid companion) { + public Optional removeLinkedCompanion(@NonNull Jid companion) { return Optional.ofNullable(linkedDevicesKeys.remove(companion)); } @@ -1186,7 +1191,7 @@ public Optional proxy() { * * @return a non-null optional */ - public Optional companionDeviceOs() { + public Optional companionDeviceOs() { return Optional.ofNullable(companionDeviceOs); } @@ -1283,6 +1288,23 @@ public Optional findCallById(String callId) { return callId == null ? Optional.empty() : Optional.ofNullable(calls.get(callId)); } + /** + * Checks whether the provided chat was updated since this method was last invoked + * + * @param chat the non-null chat to check + * @return a boolean + */ + public boolean hasUpdate(@NonNull Chat chat) { + var lastId = chatsUpdateIds.getOrDefault(chat.jid(), -1); + var newId = chat.updateId(); + if(lastId == newId) { + return false; + } + + chatsUpdateIds.put(chat.jid(), newId); + return true; + } + public String tag() { return tag; } @@ -1316,11 +1338,11 @@ public Optional about() { return Optional.ofNullable(this.about); } - public Optional jid() { + public Optional jid() { return Optional.ofNullable(this.jid); } - public Optional lid() { + public Optional lid() { return Optional.ofNullable(this.lid); } @@ -1428,7 +1450,7 @@ public Store setDeviceHash(String deviceHash) { return this; } - public Store setLinkedDevicesKeys(LinkedHashMap linkedDevicesKeys) { + public Store setLinkedDevicesKeys(LinkedHashMap linkedDevicesKeys) { this.linkedDevicesKeys = linkedDevicesKeys; return this; } @@ -1443,12 +1465,12 @@ public Store setAbout(String about) { return this; } - public Store setJid(ContactJid jid) { + public Store setJid(Jid jid) { this.jid = jid; return this; } - public Store setLid(ContactJid lid) { + public Store setLid(Jid lid) { this.lid = lid; return this; } @@ -1499,7 +1521,7 @@ public Store setDevice(CompanionDevice device) { return this; } - public Store setCompanionDeviceOs(Platform companionDeviceOs) { + public Store setCompanionDeviceOs(PlatformType companionDeviceOs) { this.companionDeviceOs = companionDeviceOs; return this; } diff --git a/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java b/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java index e5f6b383..971d8820 100644 --- a/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java +++ b/src/main/java/it/auties/whatsapp/controller/StoreBuilder.java @@ -7,15 +7,18 @@ import it.auties.whatsapp.model.chat.ChatEphemeralTimer; import it.auties.whatsapp.model.companion.CompanionDevice; import it.auties.whatsapp.model.mobile.PhoneNumber; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.UserAgent; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.auth.UserAgent.ReleaseChannel; import it.auties.whatsapp.model.signal.auth.Version; -import it.auties.whatsapp.util.*; +import it.auties.whatsapp.util.Clock; +import it.auties.whatsapp.util.FutureReference; +import it.auties.whatsapp.util.MetadataHelper; +import it.auties.whatsapp.util.Spec; import java.net.URI; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CountDownLatch; public class StoreBuilder { private UUID uuid; @@ -202,7 +205,7 @@ public Store build() { } var serializer = Objects.requireNonNullElseGet(this.serializer, DefaultControllerSerializer::instance); - return new Store( + var result = new Store( Objects.requireNonNull(uuid, "Uuid is required if the StoreBuilder can't find a serialized session"), phoneNumber, serializer, @@ -235,16 +238,10 @@ public Store build() { new ConcurrentHashMap<>(), false, false, - new ConcurrentHashMap<>(), - new ConcurrentHashMap<>(), - ConcurrentHashMap.newKeySet(), - HexFormat.of().formatHex(BytesHelper.random(1)), Clock.nowSeconds(), - null, - new CountDownLatch(1), ChatEphemeralTimer.OFF, Objects.requireNonNullElse(textPreviewSetting, TextPreviewSetting.ENABLED_WITH_INFERENCE), - Objects.requireNonNullElse(historyLength, WebHistoryLength.STANDARD), + Objects.requireNonNullElse(historyLength, WebHistoryLength.standard()), autodetectListeners, automaticPresenceUpdates, Objects.requireNonNullElse(releaseChannel, ReleaseChannel.RELEASE), @@ -252,12 +249,14 @@ public Store build() { null, checkPatchMacs ); + serializer.linkMetadata(result); + return result; }); } - private Platform getPlatform(ClientType clientType) { + private UserAgent.PlatformType getPlatform(ClientType clientType) { return switch (clientType) { - case WEB -> Platform.WEB; + case WEB -> PlatformType.WEB; case MOBILE -> business ? device.businessPlatform() : device.platform(); }; } diff --git a/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java index 7e3f645a..30a4fdd7 100644 --- a/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java +++ b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java @@ -189,7 +189,6 @@ private Session loadSession() { } private Session loadSession(Supplier> defaultSupplier) { - System.out.printf("Data for %s: %s%n", address, Json.writeValueAsString(keys.findSessionByAddress(address).orElse(null), true)); return keys.findSessionByAddress(address) .or(defaultSupplier) .orElseThrow(() -> new NoSuchElementException("Missing session for: %s".formatted(address))); diff --git a/src/main/java/it/auties/whatsapp/listener/Listener.java b/src/main/java/it/auties/whatsapp/listener/Listener.java index 3a4e559b..390bda0a 100644 --- a/src/main/java/it/auties/whatsapp/listener/Listener.java +++ b/src/main/java/it/auties/whatsapp/listener/Listener.java @@ -8,7 +8,7 @@ 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.contact.ContactJid; +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; @@ -203,20 +203,20 @@ default void onContacts(Collection contacts) { * * @param whatsapp an instance to the calling api * @param chat the chat that this update regards - * @param contactJid the contact that this update regards + * @param jid the contact that this update regards * @param status the new status of the contact */ - default void onContactPresence(Whatsapp whatsapp, Chat chat, ContactJid contactJid, ContactStatus status) { + default void onContactPresence(Whatsapp whatsapp, Chat chat, Jid jid, ContactStatus status) { } /** * Called when the socket receives an update regarding the presence of a contact * * @param chat the chat that this update regards - * @param contactJid the contact that this update regards + * @param jid the contact that this update regards * @param status the new status of the contact */ - default void onContactPresence(Chat chat, ContactJid contactJid, ContactStatus status) { + default void onContactPresence(Chat chat, Jid jid, ContactStatus status) { } /** @@ -643,7 +643,7 @@ default void onPrivacySettingChanged(PrivacySettingEntry oldPrivacyEntry, Privac * @param whatsapp an instance to the calling api * @param devices the non-null devices */ - default void onLinkedDevices(Whatsapp whatsapp, Collection devices){ + default void onLinkedDevices(Whatsapp whatsapp, Collection devices){ } @@ -652,7 +652,7 @@ default void onLinkedDevices(Whatsapp whatsapp, Collection devices){ * * @param devices the non-null devices */ - default void onLinkedDevices(Collection devices){ + default void onLinkedDevices(Collection devices){ } diff --git a/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java index e18ce87e..2fd3e66e 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java +++ b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.listener; import it.auties.whatsapp.model.chat.Chat; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; public interface OnContactPresence extends Listener { @@ -9,9 +9,9 @@ public interface OnContactPresence extends Listener { * Called when the socket receives an update regarding the presence of a contact * * @param chat the chat that this update regards - * @param contactJid the contact that this update regards + * @param jid the contact that this update regards * @param status the new status of the contact */ @Override - void onContactPresence(Chat chat, ContactJid contactJid, ContactStatus status); + void onContactPresence(Chat chat, Jid jid, ContactStatus status); } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnLinkedDevices.java b/src/main/java/it/auties/whatsapp/listener/OnLinkedDevices.java index 6b6893a9..b6aba0e2 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnLinkedDevices.java +++ b/src/main/java/it/auties/whatsapp/listener/OnLinkedDevices.java @@ -1,6 +1,6 @@ package it.auties.whatsapp.listener; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import java.util.Collection; @@ -11,5 +11,5 @@ public interface OnLinkedDevices extends Listener { * @param devices the non-null devices */ @Override - void onLinkedDevices(Collection devices); + void onLinkedDevices(Collection devices); } diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java index 4e8d532e..6a914c5f 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java @@ -2,7 +2,7 @@ import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.model.chat.Chat; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; public interface OnWhatsappContactPresence extends Listener { @@ -11,9 +11,9 @@ public interface OnWhatsappContactPresence extends Listener { * * @param whatsapp an instance to the calling api * @param chat the chat that this update regards - * @param contactJid the contact that this update regards + * @param jid the contact that this update regards * @param status the new status of the contact */ @Override - void onContactPresence(Whatsapp whatsapp, Chat chat, ContactJid contactJid, ContactStatus status); + void onContactPresence(Whatsapp whatsapp, Chat chat, Jid jid, ContactStatus status); } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java index 00a70f30..6e11dcfc 100644 --- a/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.listener; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import java.util.Collection; @@ -13,5 +13,5 @@ public interface OnWhatsappLinkedDevices extends Listener { * @param devices the non-null devices */ @Override - void onLinkedDevices(Whatsapp whatsapp, Collection devices); + void onLinkedDevices(Whatsapp whatsapp, Collection devices); } diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java b/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java index 4cc4cfb6..e20cc5ef 100644 --- a/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java @@ -1,6 +1,6 @@ package it.auties.whatsapp.model.business; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.node.Node; import org.checkerframework.checker.nullness.qual.NonNull; @@ -15,7 +15,7 @@ */ public record BusinessProfile( @NonNull - ContactJid jid, + Jid jid, Optional description, Optional address, Optional email, diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveCollection.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveCollection.java index aaa66d9c..d30d2d32 100644 --- a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveCollection.java +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveCollection.java @@ -3,7 +3,7 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.button.InteractiveMessageContent; import org.checkerframework.checker.nullness.qual.NonNull; @@ -14,7 +14,7 @@ public record InteractiveCollection( @ProtobufProperty(index = 1, type = ProtobufType.STRING) @NonNull - ContactJid business, + Jid business, @ProtobufProperty(index = 2, type = ProtobufType.STRING) @NonNull String id, diff --git a/src/main/java/it/auties/whatsapp/model/call/Call.java b/src/main/java/it/auties/whatsapp/model/call/Call.java index b0b4dd5c..575776ef 100644 --- a/src/main/java/it/auties/whatsapp/model/call/Call.java +++ b/src/main/java/it/auties/whatsapp/model/call/Call.java @@ -1,10 +1,10 @@ package it.auties.whatsapp.model.call; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.time.ZonedDateTime; -public record Call(@NonNull ContactJid chat, @NonNull ContactJid caller, @NonNull String id, @NonNull ZonedDateTime time, +public record Call(@NonNull Jid chat, @NonNull Jid caller, @NonNull String id, @NonNull ZonedDateTime time, boolean video, @NonNull CallStatus status, boolean offline) { } \ 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 b0086660..e43bed59 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/Chat.java +++ b/src/main/java/it/auties/whatsapp/model/chat/Chat.java @@ -8,15 +8,16 @@ import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.contact.ContactJid; -import it.auties.whatsapp.model.contact.ContactJidProvider; -import it.auties.whatsapp.model.contact.ContactJidType; import it.auties.whatsapp.model.contact.ContactStatus; 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.jid.JidType; import it.auties.whatsapp.model.media.MediaVisibility; 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 org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -24,7 +25,6 @@ import java.time.ZonedDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; import java.util.function.Function; import java.util.function.Predicate; @@ -34,21 +34,21 @@ * effect on WhatsappWeb's servers */ @ProtobufMessageName("Conversation") -public final class Chat implements ProtobufMessage, ContactJidProvider { +public final class Chat implements ProtobufMessage, JidProvider { @NonNull private final UUID uuid; @ProtobufProperty(index = 1, type = ProtobufType.STRING) - private final @NonNull ContactJid jid; + private final @NonNull Jid jid; @ProtobufProperty(index = 2, type = ProtobufType.OBJECT, repeated = true) - private final @NonNull ConcurrentLinkedDeque historySyncMessages; + private final @NonNull ConcurrentDoublyLinkedList historySyncMessages; @ProtobufProperty(index = 3, type = ProtobufType.STRING) - private final ContactJid newJid; + private final Jid newJid; @ProtobufProperty(index = 4, type = ProtobufType.STRING) - private final ContactJid oldJid; + private final Jid oldJid; @ProtobufProperty(index = 6, type = ProtobufType.UINT32) private int unreadMessagesCount; @@ -122,7 +122,7 @@ public final class Chat implements ProtobufMessage, ContactJidProvider { private long foundationTimestampSeconds; @ProtobufProperty(index = 32, type = ProtobufType.STRING) - private ContactJid founder; + private Jid founder; @ProtobufProperty(index = 33, type = ProtobufType.STRING) private String description; @@ -136,28 +136,28 @@ public final class Chat implements ProtobufMessage, ContactJidProvider { private boolean defaultSubGroup; @ProtobufProperty(index = 37, type = ProtobufType.STRING) - private final ContactJid parentGroupJid; + private final Jid parentGroupJid; @ProtobufProperty(index = 38, type = ProtobufType.STRING) private String displayName; @ProtobufProperty(index = 39, type = ProtobufType.STRING) - private ContactJid phoneJid; + private Jid phoneJid; @ProtobufProperty(index = 40, type = ProtobufType.BOOL) private boolean shareOwnPhoneNumber; @ProtobufProperty(index = 41, type = ProtobufType.BOOL) private boolean pnhDuplicateLidThread; @ProtobufProperty(index = 42, type = ProtobufType.STRING) - private ContactJid lidJid; + private Jid lidJid; - private final @NonNull ConcurrentHashMap presences; + private final @NonNull ConcurrentHashMap presences; - private final @NonNull Set participantsPreKeys; + private final @NonNull Set participantsPreKeys; private final @NonNull Set pastParticipants; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public Chat(@NonNull UUID uuid, @NonNull ContactJid jid, @NonNull ConcurrentLinkedDeque historySyncMessages, ContactJid newJid, ContactJid 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, ContactJid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, ContactJid parentGroupJid, String displayName, ContactJid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, ContactJid lidJid, @NonNull ConcurrentHashMap presences, @NonNull Set participantsPreKeys, @NonNull Set pastParticipants) { + public Chat(@NonNull UUID uuid, @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) { this.uuid = uuid; this.jid = jid; this.historySyncMessages = historySyncMessages; @@ -203,7 +203,7 @@ public Chat(@NonNull UUID uuid, @NonNull ContactJid jid, @NonNull ConcurrentLink this.pastParticipants = pastParticipants; } - public Chat(@NonNull ContactJid jid, @NonNull ConcurrentLinkedDeque historySyncMessages, ContactJid newJid, ContactJid 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, ContactJid founder, String description, boolean support, boolean parentGroup, boolean defaultSubGroup, ContactJid parentGroupJid, String displayName, ContactJid phoneJid, boolean shareOwnPhoneNumber, boolean pnhDuplicateLidThread, ContactJid lidJid) { + 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) { this.uuid = UUID.randomUUID(); this.jid = jid; this.historySyncMessages = historySyncMessages; @@ -272,7 +272,7 @@ public String name() { * @return true if this chat is a group */ public boolean isGroup() { - return jid.type() == ContactJidType.GROUP; + return jid.type() == JidType.GROUP; } /** @@ -611,7 +611,7 @@ public void addParticipants(Collection participants) { * @param role the role of the participant * @return whether the participant was added */ - public boolean addParticipant(@NonNull ContactJid jid, GroupRole role) { + public boolean addParticipant(@NonNull Jid jid, GroupRole role) { return addParticipant(new GroupParticipant(jid, role)); } @@ -631,19 +631,19 @@ public boolean addParticipant(@NonNull GroupParticipant participant) { * @param jid the non-null jid of the participant * @return whether the participant was removed */ - public boolean removeParticipant(@NonNull ContactJid jid) { + public boolean removeParticipant(@NonNull Jid jid) { return participants.removeIf(entry -> Objects.equals(entry.jid(), jid)); } /** * Finds a participant by jid - * This method only works if {@link Whatsapp#queryGroupMetadata(ContactJidProvider)} has been called before on this chat. + * This method only works if {@link Whatsapp#queryGroupMetadata(JidProvider)} has been called before on this chat. * By default, all groups that have been used in the last two weeks wil be synced automatically * * @param jid the non-null jid of the participant * @return the participant, if present */ - public Optional findParticipant(@NonNull ContactJid jid) { + public Optional findParticipant(@NonNull Jid jid) { return participants.stream() .filter(entry -> Objects.equals(entry.jid(), jid)) .findFirst(); @@ -680,7 +680,7 @@ public boolean addPastParticipants(List pastParticipants) * @param jid the non-null jid of the past participant * @return whether the participant was removed */ - public boolean removePastParticipant(@NonNull ContactJid jid) { + public boolean removePastParticipant(@NonNull Jid jid) { return pastParticipants.removeIf(entry -> Objects.equals(entry.jid(), jid)); } @@ -690,18 +690,18 @@ public boolean removePastParticipant(@NonNull ContactJid jid) { * @param jid the non-null jid of the past participant * @return the past participant, if present */ - public Optional findPastParticipant(@NonNull ContactJid jid) { + public Optional findPastParticipant(@NonNull Jid jid) { return pastParticipants.stream() .filter(entry -> Objects.equals(entry.jid(), jid)) .findFirst(); } - public Set participantsPreKeys() { + public Set participantsPreKeys() { return Collections.unmodifiableSet(participantsPreKeys); } - public void addParticipantsPreKeys(Collection contactJids) { - participantsPreKeys.addAll(contactJids); + public void addParticipantsPreKeys(Collection jids) { + participantsPreKeys.addAll(jids); } public void clearParticipantsPreKeys() { @@ -726,7 +726,7 @@ public boolean equals(Object other) { */ @Override @NonNull - public ContactJid toJid() { + public Jid toJid() { return jid(); } @@ -740,23 +740,36 @@ public int hashCode() { return Objects.hash(jid()); } + /** + * Generates a unique identifier using all the properties in this object + * This is not a hash code as the actual hash code needs to follow the equality contract + * + * @return an unsigned int + */ + public int updateId() { + int result = Objects.hash(uuid, jid, historySyncMessages, newJid, oldJid, unreadMessagesCount, readOnly, endOfHistoryTransfer, ephemeralMessageDuration, ephemeralMessagesToggleTimeSeconds, endOfHistoryTransferType, timestampSeconds, name, notSpam, archived, disappearInitiator, markedAsUnread, participants, tokenTimestampSeconds, pinnedTimestampSeconds, mute, wallpaper, mediaVisibility, tokenSenderTimestampSeconds, suspended, terminated, foundationTimestampSeconds, founder, description, support, parentGroup, defaultSubGroup, parentGroupJid, displayName, phoneJid, shareOwnPhoneNumber, pnhDuplicateLidThread, lidJid, presences, participantsPreKeys, pastParticipants); + result = 31 * result + Arrays.hashCode(token); + result = 31 * result + Arrays.hashCode(identityKey); + return result; + } + public @NonNull UUID uuid() { return uuid; } - public @NonNull ContactJid jid() { + public @NonNull Jid jid() { return jid; } - public @NonNull ConcurrentLinkedDeque historySyncMessages() { + public @NonNull Collection historySyncMessages() { return historySyncMessages; } - public Optional newJid() { + public Optional newJid() { return Optional.ofNullable(newJid); } - public Optional oldJid() { + public Optional oldJid() { return Optional.ofNullable(oldJid); } @@ -852,7 +865,7 @@ public long foundationTimestampSeconds() { return foundationTimestampSeconds; } - public Optional founder() { + public Optional founder() { return Optional.ofNullable(founder); } @@ -872,7 +885,7 @@ public boolean defaultSubGroup() { return defaultSubGroup; } - public Optional parentGroupJid() { + public Optional parentGroupJid() { return Optional.ofNullable(parentGroupJid); } @@ -880,7 +893,7 @@ public Optional displayName() { return displayName.describeConstable(); } - public Optional phoneJid() { + public Optional phoneJid() { return Optional.ofNullable(phoneJid); } @@ -892,11 +905,11 @@ public boolean pnhDuplicateLidThread() { return pnhDuplicateLidThread; } - public Optional lidJid() { + public Optional lidJid() { return Optional.ofNullable(lidJid); } - public @NonNull ConcurrentHashMap presences() { + public @NonNull ConcurrentHashMap presences() { return presences; } @@ -1027,7 +1040,7 @@ public Chat setFoundationTimestampSeconds(long foundationTimestampSeconds) { return this; } - public Chat setFounder(ContactJid founder) { + public Chat setFounder(Jid founder) { this.founder = founder; return this; } @@ -1057,7 +1070,7 @@ public Chat setDisplayName(String displayName) { return this; } - public Chat setPhoneJid(ContactJid phoneJid) { + public Chat setPhoneJid(Jid phoneJid) { this.phoneJid = phoneJid; return this; } @@ -1072,7 +1085,7 @@ public Chat setPnhDuplicateLidThread(boolean pnhDuplicateLidThread) { return this; } - public Chat setLidJid(ContactJid lidJid) { + public Chat setLidJid(Jid lidJid) { this.lidJid = lidJid; return this; } 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 2414fcc0..bed83752 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java @@ -1,6 +1,6 @@ package it.auties.whatsapp.model.chat; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.time.ZonedDateTime; @@ -13,13 +13,13 @@ */ public record GroupMetadata( @NonNull - ContactJid jid, + Jid jid, @NonNull String subject, - Optional subjectAuthor, + Optional subjectAuthor, Optional subjectTimestamp, Optional foundationTimestamp, - Optional founder, + Optional founder, Optional description, Optional descriptionId, @NonNull diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java b/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java index 29de5cd1..bf3814b8 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java @@ -5,7 +5,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import java.util.Objects; @@ -15,18 +15,18 @@ @ProtobufMessageName("GroupParticipant") public final class GroupParticipant implements ProtobufMessage { @ProtobufProperty(index = 1, type = ProtobufType.STRING) - private final ContactJid jid; + private final Jid jid; @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) private GroupRole role; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public GroupParticipant(ContactJid jid, GroupRole role) { + public GroupParticipant(Jid jid, GroupRole role) { this.jid = jid; this.role = Objects.requireNonNullElse(role, GroupRole.USER); } - public ContactJid jid() { + public Jid jid() { return jid; } diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java index 8a3d14d0..c168b081 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java @@ -6,7 +6,7 @@ import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Clock; import java.time.ZonedDateTime; @@ -19,7 +19,7 @@ @ProtobufMessageName("PastParticipant") public record GroupPastParticipant( @ProtobufProperty(index = 1, type = ProtobufType.STRING) - ContactJid jid, + Jid jid, @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) Reason reason, @ProtobufProperty(index = 3, type = ProtobufType.UINT64) diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java index 5e5a2476..d56e0d00 100644 --- a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java @@ -4,7 +4,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; @@ -16,7 +16,7 @@ public record GroupPastParticipants( @ProtobufProperty(index = 1, type = ProtobufType.STRING) @NonNull - ContactJid groupJid, + Jid groupJid, @ProtobufProperty(index = 2, type = ProtobufType.OBJECT, repeated = true) @NonNull List pastParticipants diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java index bd1bb6c3..a37b3ff6 100644 --- a/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java @@ -1,6 +1,6 @@ package it.auties.whatsapp.model.companion; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.auth.Version; import org.checkerframework.checker.nullness.qual.NonNull; @@ -12,9 +12,9 @@ * @param platform the non-null os of the device * @param osVersion the non-null os version of the device */ -public record CompanionDevice(@NonNull String model, @NonNull String manufacturer, @NonNull Platform platform, @NonNull Platform businessPlatform, @NonNull Version osVersion) { - private static final CompanionDevice IPHONE_7 = new CompanionDevice("iPhone 7", "Apple", Platform.IOS, Platform.SMB_IOS, Version.of("15.3.1")); - private static final CompanionDevice SAMSUNG_GALAXY_S9 = new CompanionDevice("star2lte", "Samsung", Platform.ANDROID, Platform.SMB_ANDROID, Version.of("8.0.0")); +public record CompanionDevice(@NonNull String model, @NonNull String manufacturer, @NonNull PlatformType platform, @NonNull PlatformType businessPlatform, @NonNull Version osVersion) { + private static final CompanionDevice IPHONE_7 = new CompanionDevice("iPhone 7", "Apple", PlatformType.IOS, PlatformType.SMB_IOS, Version.of("15.3.1")); + private static final CompanionDevice SAMSUNG_GALAXY_S9 = new CompanionDevice("star2lte", "Samsung", PlatformType.ANDROID, PlatformType.SMB_ANDROID, Version.of("8.0.0")); /** * Returns an Iphone 7 diff --git a/src/main/java/it/auties/whatsapp/model/contact/Contact.java b/src/main/java/it/auties/whatsapp/model/contact/Contact.java index 7021b532..ed730730 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/Contact.java +++ b/src/main/java/it/auties/whatsapp/model/contact/Contact.java @@ -3,6 +3,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; 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.jid.JidProvider; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -14,12 +16,12 @@ * A model class that represents a Contact. This class is only a model, this means that changing its * values will have no real effect on WhatsappWeb's servers. */ -public final class Contact implements ContactJidProvider { +public final class Contact implements JidProvider { /** * The non-null unique jid used to identify this contact */ @NonNull - private final ContactJid jid; + private final Jid jid; /** * The nullable name specified by this contact when he created a Whatsapp account. Theoretically, @@ -48,7 +50,7 @@ public final class Contact implements ContactJidProvider { * composing, recording or paused in a group this field will not be affected. Instead, * {@link Chat#presences()} should be used. By default, Whatsapp will not send updates about a * contact's status unless they send a message or are in the recent contacts. To force Whatsapp to - * send updates, use {@link Whatsapp#subscribeToPresence(ContactJidProvider)}. + * send updates, use {@link Whatsapp#subscribeToPresence(JidProvider)}. */ @NonNull private ContactStatus lastKnownPresence; @@ -65,13 +67,13 @@ public final class Contact implements ContactJidProvider { */ private boolean blocked; - public Contact(@NonNull ContactJid jid) { + public Contact(@NonNull Jid jid) { this.jid = jid; this.lastKnownPresence = ContactStatus.UNAVAILABLE; } @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public Contact(@NonNull ContactJid jid, @Nullable String chosenName, @Nullable String fullName, @Nullable String shortName, @NonNull ContactStatus lastKnownPresence, @Nullable ZonedDateTime lastSeen, boolean blocked) { + public Contact(@NonNull Jid jid, @Nullable String chosenName, @Nullable String fullName, @Nullable String shortName, @NonNull ContactStatus lastKnownPresence, @Nullable ZonedDateTime lastSeen, boolean blocked) { this.jid = jid; this.chosenName = chosenName; this.fullName = fullName; @@ -81,7 +83,7 @@ public Contact(@NonNull ContactJid jid, @Nullable String chosenName, @Nullable S this.blocked = blocked; } - public ContactJid jid() { + public Jid jid() { return this.jid; } @@ -165,7 +167,7 @@ public boolean equals(Object other) { } @Override - public @NonNull ContactJid toJid() { + public @NonNull Jid toJid() { return jid(); } } diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactCard.java b/src/main/java/it/auties/whatsapp/model/contact/ContactCard.java index d544e1cf..b70667fa 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/ContactCard.java +++ b/src/main/java/it/auties/whatsapp/model/contact/ContactCard.java @@ -6,6 +6,7 @@ import ezvcard.property.SimpleProperty; import ezvcard.property.Telephone; import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.*; @@ -19,7 +20,7 @@ public record ContactCard( Optional version, Optional name, @NonNull - Map> phoneNumbers, + Map> phoneNumbers, Optional businessName ) { private static final String BUSINESS_NAME_PROPERTY = "X-WA-BIZ-NAME"; @@ -62,30 +63,30 @@ private static String getPhoneType(Telephone entry) { return entry.getParameters().getType(); } - private static List getPhoneValue(Telephone entry) { - return List.of(ContactJid.of(entry.getParameter(PHONE_NUMBER_PROPERTY))); + private static List getPhoneValue(Telephone entry) { + return List.of(Jid.of(entry.getParameter(PHONE_NUMBER_PROPERTY))); } - private static List joinPhoneNumbers(List first, List second) { + private static List joinPhoneNumbers(List first, List second) { return Stream.of(first, second).flatMap(Collection::stream).toList(); } - public List getPhoneNumber(@NonNull ContactJid contact) { + public List getPhoneNumber(@NonNull Jid contact) { return Objects.requireNonNullElseGet(phoneNumbers.get(DEFAULT_NUMBER_TYPE), List::of); } - private void addPhoneNumber(VCard vcard, String type, ContactJid contact) { + private void addPhoneNumber(VCard vcard, String type, Jid contact) { var telephone = new Telephone(contact.toPhoneNumber()); telephone.getParameters().setType(type); telephone.getParameters().put(PHONE_NUMBER_PROPERTY, contact.user()); vcard.addTelephoneNumber(telephone); } - public void addPhoneNumber(@NonNull ContactJid contact) { + public void addPhoneNumber(@NonNull Jid contact) { addPhoneNumber(DEFAULT_NUMBER_TYPE, contact); } - public void addPhoneNumber(@NonNull String category, @NonNull ContactJid contact) { + public void addPhoneNumber(@NonNull String category, @NonNull Jid contact) { var oldValue = phoneNumbers.get(category); if(oldValue == null){ phoneNumbers.put(category, List.of(contact)); 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 38f19e0d..bd0b5916 100644 --- a/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java @@ -9,7 +9,7 @@ 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.contact.ContactJid; +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; @@ -38,7 +38,7 @@ public final class ContextInfo implements Info, ProtobufMessage { */ @ProtobufProperty(index = 2, type = ProtobufType.STRING) @Nullable - private final ContactJid quotedMessageSenderJid; + private final Jid quotedMessageSenderJid; /** * The message container that this ContextualMessage quotes @@ -52,14 +52,14 @@ public final class ContextInfo implements Info, ProtobufMessage { */ @ProtobufProperty(index = 4, type = ProtobufType.STRING) @Nullable - private final ContactJid quotedMessageChatJid; + private final Jid quotedMessageChatJid; /** * A list of the contacts' jids mentioned in this ContextualMessage */ @ProtobufProperty(index = 15, type = ProtobufType.STRING, repeated = true) @NonNull - private final List mentions; + private final List mentions; /** * Conversation source @@ -180,7 +180,7 @@ public final class ContextInfo implements Info, ProtobufMessage { */ @ProtobufProperty(index = 35, type = ProtobufType.STRING) @Nullable - private final ContactJid parentGroup; + private final Jid parentGroup; /** * Trust banner type @@ -209,7 +209,7 @@ public final class ContextInfo implements Info, ProtobufMessage { private Chat quotedMessageChat; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public ContextInfo(@Nullable String quotedMessageId, @Nullable ContactJid quotedMessageSenderJid, @Nullable MessageContainer quotedMessage, @Nullable ContactJid quotedMessageChatJid, @NonNull List mentions, @Nullable String conversionSource, byte @Nullable [] conversionData, int conversionDelaySeconds, int forwardingScore, boolean forwarded, @Nullable AdReplyInfo quotedAd, @Nullable MessageKey placeholderKey, int ephemeralExpiration, long ephemeralSettingTimestamp, byte @Nullable [] ephemeralSharedSecret, @Nullable ExternalAdReplyInfo externalAdReply, @Nullable String entryPointConversionSource, @Nullable String entryPointConversionApp, int entryPointConversionDelaySeconds, @Nullable ChatDisappear disappearingMode, @Nullable ButtonActionLink actionLink, @Nullable String groupSubject, @Nullable ContactJid parentGroup, @Nullable String trustBannerType, int trustBannerAction) { + public ContextInfo(@Nullable String quotedMessageId, @Nullable Jid quotedMessageSenderJid, @Nullable MessageContainer quotedMessage, @Nullable Jid quotedMessageChatJid, @NonNull List mentions, @Nullable String conversionSource, byte @Nullable [] conversionData, int conversionDelaySeconds, int forwardingScore, boolean forwarded, @Nullable AdReplyInfo quotedAd, @Nullable MessageKey placeholderKey, int ephemeralExpiration, long ephemeralSettingTimestamp, byte @Nullable [] ephemeralSharedSecret, @Nullable ExternalAdReplyInfo externalAdReply, @Nullable String entryPointConversionSource, @Nullable String entryPointConversionApp, int entryPointConversionDelaySeconds, @Nullable ChatDisappear disappearingMode, @Nullable ButtonActionLink actionLink, @Nullable String groupSubject, @Nullable Jid parentGroup, @Nullable String trustBannerType, int trustBannerAction) { this.quotedMessageId = quotedMessageId; this.quotedMessageSenderJid = quotedMessageSenderJid; this.quotedMessage = quotedMessage; @@ -272,7 +272,7 @@ public ContextInfo setQuotedMessageSender(Contact quotedMessageSender) { * * @return an optional */ - public Optional quotedMessageChatJid() { + public Optional quotedMessageChatJid() { return Optional.ofNullable(quotedMessageChatJid).or(this::quotedMessageSenderJid); } @@ -281,7 +281,7 @@ public Optional quotedMessageChatJid() { * * @return an optional */ - public Optional quotedMessageSenderJid() { + public Optional quotedMessageSenderJid() { return Optional.ofNullable(quotedMessageSenderJid); } @@ -329,7 +329,7 @@ public ContextInfo setQuotedMessageChat(Chat quotedMessageChat) { return this; } - public List mentions() { + public List mentions() { return mentions; } @@ -411,7 +411,7 @@ public Optional groupSubject() { return Optional.ofNullable(groupSubject); } - public Optional parentGroup() { + public Optional parentGroup() { return Optional.ofNullable(parentGroup); } diff --git a/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java b/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java index 8fe53a80..b47b5d80 100644 --- a/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.model.info; import com.fasterxml.jackson.core.type.TypeReference; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Json; import org.checkerframework.checker.nullness.qual.NonNull; @@ -17,7 +17,7 @@ * @param messageId the nullable id of the message regarding the chane * @param fromMe whether the change regards yourself */ -public record MessageIndexInfo(@NonNull String type, Optional chatJid, Optional messageId, boolean fromMe) implements Info { +public record MessageIndexInfo(@NonNull String type, Optional chatJid, Optional messageId, boolean fromMe) implements Info { /** * Constructs a new message index info * @@ -27,7 +27,7 @@ public record MessageIndexInfo(@NonNull String type, Optional chatJi * @param fromMe whether the change regards yourself * @return a non-null message index info */ - public static MessageIndexInfo of(@NonNull String type, ContactJid chatJid, String messageId, boolean fromMe) { + public static MessageIndexInfo of(@NonNull String type, Jid chatJid, String messageId, boolean fromMe) { return new MessageIndexInfo(type, Optional.ofNullable(chatJid), Optional.ofNullable(messageId), fromMe); } @@ -40,7 +40,7 @@ public static MessageIndexInfo of(@NonNull String type, ContactJid chatJid, Stri public static MessageIndexInfo ofJson(@NonNull String json) { var array = Json.readValue(json, new TypeReference>() {}); var type = getProperty(array, 0).orElseThrow(() -> new NoSuchElementException("Cannot parse MessageSync: missing type")); - var chatJid = getProperty(array, 1).map(ContactJid::of); + var chatJid = getProperty(array, 1).map(Jid::of); var messageId = getProperty(array, 2); var fromMe = getProperty(array, 3).map(Boolean::parseBoolean).orElse(false); return new MessageIndexInfo(type, chatJid, messageId, fromMe); diff --git a/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java index 9c933b1a..cc7b710f 100644 --- a/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java @@ -11,7 +11,7 @@ import it.auties.whatsapp.model.business.BusinessPrivacyStatus; import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.media.MediaData; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.message.standard.LiveLocationMessage; @@ -46,7 +46,7 @@ public final class MessageInfo implements Info, MessageMetadataProvider, Protobu @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) private @NonNull MessageStatus status; @ProtobufProperty(index = 5, type = ProtobufType.STRING) - private final ContactJid senderJid; + private final Jid senderJid; @ProtobufProperty(index = 6, type = ProtobufType.UINT64) private final long messageC2STimestamp; @ProtobufProperty(index = 16, type = ProtobufType.BOOL) @@ -120,7 +120,7 @@ public final class MessageInfo implements Info, MessageMetadataProvider, Protobu @ProtobufProperty(index = 50, type = ProtobufType.OBJECT) private final KeepInChat keepInChat; @ProtobufProperty(index = 51, type = ProtobufType.STRING) - private final ContactJid originalSender; + private final Jid originalSender; @ProtobufProperty(index = 52, type = ProtobufType.UINT64) private long revokeTimestampSeconds; @@ -132,7 +132,7 @@ public final class MessageInfo implements Info, MessageMetadataProvider, Protobu private Contact sender; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public MessageInfo(@NonNull MessageKey key, @NonNull MessageContainer message, long timestampSeconds, @NonNull MessageStatus status, ContactJid senderJid, long messageC2STimestamp, boolean ignore, boolean starred, boolean broadcast, String pushName, byte[] mediaCiphertextSha256, boolean multicast, boolean urlText, boolean urlNumber, StubType stubType, boolean clearMedia, List stubParameters, int duration, List labels, PaymentInfo paymentInfo, LiveLocationMessage finalLiveLocation, PaymentInfo quotedPaymentInfo, long ephemeralStartTimestamp, int ephemeralDuration, boolean enableEphemeral, boolean ephemeralOutOfSync, BusinessPrivacyStatus businessPrivacyStatus, String businessVerifiedName, MediaData mediaData, PhotoChange photoChange, @NonNull MessageReceipt receipt, List reactions, MediaData quotedStickerData, byte[] futureProofData, PublicServiceAnnouncementStatus psaStatus, List pollUpdates, PollAdditionalMetadata pollAdditionalMetadata, String agentId, boolean statusAlreadyViewed, byte[] messageSecret, KeepInChat keepInChat, ContactJid originalSender, long revokeTimestampSeconds, @Nullable Chat chat, @Nullable Contact sender) { + public MessageInfo(@NonNull MessageKey key, @NonNull MessageContainer message, long timestampSeconds, @NonNull MessageStatus status, Jid senderJid, long messageC2STimestamp, boolean ignore, boolean starred, boolean broadcast, String pushName, byte[] mediaCiphertextSha256, boolean multicast, boolean urlText, boolean urlNumber, StubType stubType, boolean clearMedia, List stubParameters, int duration, List labels, PaymentInfo paymentInfo, LiveLocationMessage finalLiveLocation, PaymentInfo quotedPaymentInfo, long ephemeralStartTimestamp, int ephemeralDuration, boolean enableEphemeral, boolean ephemeralOutOfSync, BusinessPrivacyStatus businessPrivacyStatus, String businessVerifiedName, MediaData mediaData, PhotoChange photoChange, @NonNull MessageReceipt receipt, List reactions, MediaData quotedStickerData, byte[] futureProofData, PublicServiceAnnouncementStatus psaStatus, List pollUpdates, PollAdditionalMetadata pollAdditionalMetadata, String agentId, boolean statusAlreadyViewed, byte[] messageSecret, KeepInChat keepInChat, Jid originalSender, long revokeTimestampSeconds, @Nullable Chat chat, @Nullable Contact sender) { this.key = key; this.message = message; this.timestampSeconds = timestampSeconds; @@ -181,7 +181,7 @@ public MessageInfo(@NonNull MessageKey key, @NonNull MessageContainer message, l } - public MessageInfo(@NonNull MessageKey key, @NonNull MessageContainer message, long timestampSeconds, @NonNull MessageStatus status, ContactJid senderJid, long messageC2STimestamp, boolean ignore, boolean starred, boolean broadcast, String pushName, byte[] mediaCiphertextSha256, boolean multicast, boolean urlText, boolean urlNumber, StubType stubType, boolean clearMedia, List stubParameters, int duration, List labels, PaymentInfo paymentInfo, LiveLocationMessage finalLiveLocation, PaymentInfo quotedPaymentInfo, long ephemeralStartTimestamp, int ephemeralDuration, boolean enableEphemeral, boolean ephemeralOutOfSync, BusinessPrivacyStatus businessPrivacyStatus, String businessVerifiedName, MediaData mediaData, PhotoChange photoChange, @NonNull MessageReceipt receipt, List reactions, MediaData quotedStickerData, byte[] futureProofData, PublicServiceAnnouncementStatus psaStatus, List pollUpdates, PollAdditionalMetadata pollAdditionalMetadata, String agentId, boolean statusAlreadyViewed, byte[] messageSecret, KeepInChat keepInChat, ContactJid originalSender, long revokeTimestampSeconds) { + public MessageInfo(@NonNull MessageKey key, @NonNull MessageContainer message, long timestampSeconds, @NonNull MessageStatus status, Jid senderJid, long messageC2STimestamp, boolean ignore, boolean starred, boolean broadcast, String pushName, byte[] mediaCiphertextSha256, boolean multicast, boolean urlText, boolean urlNumber, StubType stubType, boolean clearMedia, List stubParameters, int duration, List labels, PaymentInfo paymentInfo, LiveLocationMessage finalLiveLocation, PaymentInfo quotedPaymentInfo, long ephemeralStartTimestamp, int ephemeralDuration, boolean enableEphemeral, boolean ephemeralOutOfSync, BusinessPrivacyStatus businessPrivacyStatus, String businessVerifiedName, MediaData mediaData, PhotoChange photoChange, @NonNull MessageReceipt receipt, List reactions, MediaData quotedStickerData, byte[] futureProofData, PublicServiceAnnouncementStatus psaStatus, List pollUpdates, PollAdditionalMetadata pollAdditionalMetadata, String agentId, boolean statusAlreadyViewed, byte[] messageSecret, KeepInChat keepInChat, Jid originalSender, long revokeTimestampSeconds) { this.key = key; this.message = Objects.requireNonNullElseGet(message, MessageContainer::empty); this.timestampSeconds = timestampSeconds; @@ -254,7 +254,7 @@ public String chatName() { * * @return a non-null ContactJid */ - public ContactJid chatJid() { + public Jid chatJid() { return key.chatJid(); } @@ -332,7 +332,7 @@ public String id() { * @return a non-null ContactJid */ @Override - public ContactJid senderJid() { + public Jid senderJid() { return requireNonNullElseGet(senderJid, () -> key.senderJid().orElseGet(key::chatJid)); } @@ -517,7 +517,7 @@ public Optional keepInChat() { return Optional.ofNullable(keepInChat); } - public Optional originalSender() { + public Optional originalSender() { return Optional.ofNullable(originalSender); } diff --git a/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java b/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java index a668e87a..0d4f8d7c 100644 --- a/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java @@ -6,7 +6,7 @@ import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.MessageKey; import it.auties.whatsapp.model.payment.PaymentMoney; import it.auties.whatsapp.util.Clock; @@ -29,7 +29,7 @@ public record PaymentInfo( long amount1000, @ProtobufProperty(index = 3, type = ProtobufType.STRING) @NonNull - ContactJid receiverJid, + Jid receiverJid, @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) @NonNull Status status, diff --git a/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java b/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java index 05e346b8..49e3e24f 100644 --- a/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java +++ b/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java @@ -4,7 +4,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.product.ProductListHeaderImage; import it.auties.whatsapp.model.product.ProductSection; import org.checkerframework.checker.nullness.qual.NonNull; @@ -23,7 +23,7 @@ public record ProductListInfo( ProductListHeaderImage headerImage, @ProtobufProperty(index = 3, type = ProtobufType.STRING) @NonNull - ContactJid seller + Jid seller ) implements Info, ProtobufMessage { } diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactJid.java b/src/main/java/it/auties/whatsapp/model/jid/Jid.java similarity index 70% rename from src/main/java/it/auties/whatsapp/model/contact/ContactJid.java rename to src/main/java/it/auties/whatsapp/model/jid/Jid.java index 2159776c..f80e766a 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/ContactJid.java +++ b/src/main/java/it/auties/whatsapp/model/jid/Jid.java @@ -1,4 +1,4 @@ -package it.auties.whatsapp.model.contact; +package it.auties.whatsapp.model.jid; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -13,11 +13,11 @@ * A model class that represents a jid. This class is only a model, this means that changing its * values will have no real effect on WhatsappWeb's servers. */ -public record ContactJid(String user, @NonNull ContactJidServer server, int device, int agent) implements ContactJidProvider { +public record Jid(String user, @NonNull JidServer server, int device, int agent) implements JidProvider { /** * Default constructor */ - public ContactJid(String user, @NonNull ContactJidServer server, int device, int agent){ + public Jid(String user, @NonNull JidServer server, int device, int agent){ this.user = user != null && user.startsWith("+") ? user.substring(1) : user; this.server = server; this.device = device; @@ -29,13 +29,13 @@ public ContactJid(String user, @NonNull ContactJidServer server, int device, int * @param server the non-null custom server * @return a non-null contact jid */ - public static ContactJid ofServer(@NonNull ContactJidServer server) { + public static Jid ofServer(@NonNull JidServer server) { return of(null, server); } @ProtobufConverter // Reserved for protobuf - public static ContactJid ofProtobuf(@Nullable String input) { - return input == null ? null : ContactJid.of(input); + public static Jid ofProtobuf(@Nullable String input) { + return input == null ? null : Jid.of(input); } /** @@ -45,10 +45,10 @@ public static ContactJid ofProtobuf(@Nullable String input) { * @param server the non-null custom server * @return a non-null contact jid */ - public static ContactJid of(String jid, @NonNull ContactJidServer server) { + public static Jid of(String jid, @NonNull JidServer server) { var complexUser = withoutServer(jid); if (complexUser == null) { - return new ContactJid(null, server, 0, 0); + return new Jid(null, server, 0, 0); } if (complexUser.contains(":")) { var simpleUser = complexUser.split(":", 2); @@ -57,16 +57,16 @@ public static ContactJid of(String jid, @NonNull ContactJidServer server) { if (user.contains("_")) { var simpleUserAgent = user.split("_", 2); var agent = tryParseAgent(simpleUserAgent[1]); - return new ContactJid(simpleUserAgent[0], server, device, agent); + return new Jid(simpleUserAgent[0], server, device, agent); } - return new ContactJid(user, server, device, 0); + return new Jid(user, server, device, 0); } if (!complexUser.contains("_")) { - return new ContactJid(complexUser, server, 0, 0); + return new Jid(complexUser, server, 0, 0); } var simpleUserAgent = complexUser.split("_", 2); var agent = tryParseAgent(simpleUserAgent[1]); - return new ContactJid(simpleUserAgent[0], server, 0, agent); + return new Jid(simpleUserAgent[0], server, 0, agent); } /** @@ -79,7 +79,7 @@ public static String withoutServer(String jid) { if (jid == null) { return null; } - for (var server : ContactJidServer.values()) { + for (var server : JidServer.values()) { jid = jid.replace("@%s".formatted(server), ""); } return jid; @@ -101,8 +101,8 @@ private static int tryParseAgent(String string) { * @param device the device jid * @return a non-null contact jid */ - public static ContactJid ofDevice(String jid, int device, int agent) { - return new ContactJid(withoutServer(jid), ContactJidServer.WHATSAPP, device, agent); + public static Jid ofDevice(String jid, int device, int agent) { + return new Jid(withoutServer(jid), JidServer.WHATSAPP, device, agent); } /** @@ -112,8 +112,8 @@ public static ContactJid ofDevice(String jid, int device, int agent) { * @param device the device jid * @return a non-null contact jid */ - public static ContactJid ofDevice(String jid, int device) { - return new ContactJid(withoutServer(jid), ContactJidServer.WHATSAPP, device, 0); + public static Jid ofDevice(String jid, int device) { + return new Jid(withoutServer(jid), JidServer.WHATSAPP, device, 0); } @ProtobufConverter @@ -128,8 +128,8 @@ public String toValue() { * @return a non-null contact jid */ @JsonCreator - public static ContactJid of(@NonNull String jid) { - return of(jid, ContactJidServer.of(jid)); + public static Jid of(@NonNull String jid) { + return of(jid, JidServer.of(jid)); } /** @@ -138,8 +138,8 @@ public static ContactJid of(@NonNull String jid) { * @param jid the non-null jid of the user * @return a non-null contact jid */ - public static ContactJid of(long jid) { - return of(String.valueOf(jid), ContactJidServer.WHATSAPP); + public static Jid of(long jid) { + return of(String.valueOf(jid), JidServer.WHATSAPP); } /** @@ -147,20 +147,20 @@ public static ContactJid of(long jid) { * * @return a non null type */ - public ContactJidType type() { - return isCompanion() ? ContactJidType.COMPANION : switch (server()) { - case WHATSAPP -> Objects.equals(user(), "16505361212") ? ContactJidType.OFFICIAL_SURVEY_ACCOUNT : ContactJidType.USER; - case LID -> ContactJidType.LID; - case BROADCAST -> Objects.equals(user(), "status") ? ContactJidType.STATUS : ContactJidType.BROADCAST; - case GROUP -> ContactJidType.GROUP; - case GROUP_CALL -> ContactJidType.GROUP_CALL; - case CHANNEL -> ContactJidType.CHANNEL; + public JidType type() { + return isCompanion() ? JidType.COMPANION : switch (server()) { + case WHATSAPP -> Objects.equals(user(), "16505361212") ? JidType.OFFICIAL_SURVEY_ACCOUNT : JidType.USER; + case LID -> JidType.LID; + case BROADCAST -> Objects.equals(user(), "status") ? JidType.STATUS : JidType.BROADCAST; + case GROUP -> JidType.GROUP; + case GROUP_CALL -> JidType.GROUP_CALL; + case CHANNEL -> JidType.CHANNEL; case USER -> switch (user()) { - case "server" -> ContactJidType.SERVER; - case "0" -> ContactJidType.ANNOUNCEMENT; - case "16508638904" -> ContactJidType.IAS; - case "16505361212" -> ContactJidType.OFFICIAL_BUSINESS_ACCOUNT; - default -> ContactJidType.UNKNOWN; + case "server" -> JidType.SERVER; + case "0" -> JidType.ANNOUNCEMENT; + case "16508638904" -> JidType.IAS; + case "16505361212" -> JidType.OFFICIAL_BUSINESS_ACCOUNT; + default -> JidType.UNKNOWN; }; }; } @@ -180,7 +180,7 @@ public boolean isCompanion() { * @param server the server to check against * @return a boolean */ - public boolean hasServer(ContactJidServer server) { + public boolean hasServer(JidServer server) { return server() == server; } @@ -190,7 +190,7 @@ public boolean hasServer(ContactJidServer server) { * @param server the server to check against * @return a boolean */ - public boolean isServerJid(ContactJidServer server) { + public boolean isServerJid(JidServer server) { return user() == null && server() == server; } @@ -200,8 +200,8 @@ public boolean isServerJid(ContactJidServer server) { * @param server the new server * @return a non-null jid */ - public ContactJid withServer(@NonNull ContactJidServer server) { - return new ContactJid(user(), server, device, agent); + public Jid withServer(@NonNull JidServer server) { + return new Jid(user(), server, device, agent); } /** @@ -209,7 +209,7 @@ public ContactJid withServer(@NonNull ContactJidServer server) { * * @return a non-null jid */ - public ContactJid withoutDevice() { + public Jid withoutDevice() { return of(user(), server()); } @@ -253,7 +253,7 @@ public SessionAddress toSignalAddress() { */ @Override @NonNull - public ContactJid toJid() { + public Jid toJid() { return this; } diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactJidProvider.java b/src/main/java/it/auties/whatsapp/model/jid/JidProvider.java similarity index 61% rename from src/main/java/it/auties/whatsapp/model/contact/ContactJidProvider.java rename to src/main/java/it/auties/whatsapp/model/jid/JidProvider.java index 41d06df7..329b577a 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/ContactJidProvider.java +++ b/src/main/java/it/auties/whatsapp/model/jid/JidProvider.java @@ -1,17 +1,18 @@ -package it.auties.whatsapp.model.contact; +package it.auties.whatsapp.model.jid; import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.contact.Contact; import org.checkerframework.checker.nullness.qual.NonNull; /** * Utility interface to make providing a jid easier */ -public sealed interface ContactJidProvider permits Chat, Contact, ContactJid { +public sealed interface JidProvider permits Chat, Contact, Jid { /** * Returns this object as a jid * * @return a non-null jid */ @NonNull - ContactJid toJid(); + Jid toJid(); } diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactJidServer.java b/src/main/java/it/auties/whatsapp/model/jid/JidServer.java similarity index 81% rename from src/main/java/it/auties/whatsapp/model/contact/ContactJidServer.java rename to src/main/java/it/auties/whatsapp/model/jid/JidServer.java index 01d489ec..a6c1322a 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/ContactJidServer.java +++ b/src/main/java/it/auties/whatsapp/model/jid/JidServer.java @@ -1,4 +1,4 @@ -package it.auties.whatsapp.model.contact; +package it.auties.whatsapp.model.jid; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; @@ -9,7 +9,7 @@ * The constants of this enumerated type describe the various servers that a jid might be linked * to */ -public enum ContactJidServer { +public enum JidServer { /** * User */ @@ -41,12 +41,12 @@ public enum ContactJidServer { private final String address; - ContactJidServer(String address) { + JidServer(String address) { this.address = address; } @JsonCreator - public static ContactJidServer of(String address) { + public static JidServer of(String address) { return Arrays.stream(values()) .filter(entry -> address != null && address.endsWith(entry.address())) .findFirst() @@ -57,8 +57,8 @@ public String address() { return address; } - public ContactJid toJid() { - return ContactJid.ofServer(this); + public Jid toJid() { + return Jid.ofServer(this); } @Override diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactJidType.java b/src/main/java/it/auties/whatsapp/model/jid/JidType.java similarity index 93% rename from src/main/java/it/auties/whatsapp/model/contact/ContactJidType.java rename to src/main/java/it/auties/whatsapp/model/jid/JidType.java index c3a83feb..a79aae2f 100644 --- a/src/main/java/it/auties/whatsapp/model/contact/ContactJidType.java +++ b/src/main/java/it/auties/whatsapp/model/jid/JidType.java @@ -1,9 +1,9 @@ -package it.auties.whatsapp.model.contact; +package it.auties.whatsapp.model.jid; /** * The constants of this enumerated type describe the various types of jids currently supported */ -public enum ContactJidType { +public enum JidType { /** * Represents a device connected using the multi device beta */ diff --git a/src/main/java/it/auties/whatsapp/model/message/model/KeepInChat.java b/src/main/java/it/auties/whatsapp/model/message/model/KeepInChat.java index aa200936..12f3a067 100644 --- a/src/main/java/it/auties/whatsapp/model/message/model/KeepInChat.java +++ b/src/main/java/it/auties/whatsapp/model/message/model/KeepInChat.java @@ -4,7 +4,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Clock; import java.time.ZonedDateTime; @@ -23,7 +23,7 @@ public record KeepInChat( @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) MessageKey key, @ProtobufProperty(index = 4, type = ProtobufType.STRING) - ContactJid deviceJid, + Jid deviceJid, @ProtobufProperty(index = 5, type = ProtobufType.INT64) long clientTimestampInMilliseconds, @ProtobufProperty(index = 6, type = ProtobufType.INT64) 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 6b2dad94..a7164a61 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,7 +5,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.util.BytesHelper; import org.checkerframework.checker.nullness.qual.NonNull; @@ -23,7 +23,7 @@ @ProtobufMessageName("MessageKey") public final class MessageKey implements ProtobufMessage { @ProtobufProperty(index = 1, type = ProtobufType.STRING) - private @NonNull ContactJid chatJid; + private @NonNull Jid chatJid; @ProtobufProperty(index = 2, type = ProtobufType.BOOL) private final boolean fromMe; @@ -32,21 +32,21 @@ public final class MessageKey implements ProtobufMessage { private final @NonNull String id; @ProtobufProperty(index = 4, type = ProtobufType.STRING) - private @Nullable ContactJid senderJid; + private @Nullable Jid senderJid; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public MessageKey(@NonNull ContactJid chatJid, boolean fromMe, @Nullable String id, @Nullable ContactJid senderJid) { + public MessageKey(@NonNull Jid chatJid, boolean fromMe, @Nullable String id, @Nullable Jid senderJid) { this.chatJid = chatJid; this.fromMe = fromMe; this.id = Objects.requireNonNull(id, MessageKey::randomId); this.senderJid = senderJid; } - public MessageKey(@NonNull ContactJid chatJid, boolean fromMe) { + public MessageKey(@NonNull Jid chatJid, boolean fromMe) { this(chatJid, fromMe, null); } - public MessageKey(@NonNull ContactJid chatJid, boolean fromMe, @Nullable ContactJid senderJid) { + public MessageKey(@NonNull Jid chatJid, boolean fromMe, @Nullable Jid senderJid) { this(chatJid, fromMe, randomId(), senderJid); } @@ -61,11 +61,11 @@ public static String randomId() { .toUpperCase(Locale.ROOT); } - public @NonNull ContactJid chatJid() { + public @NonNull Jid chatJid() { return chatJid; } - public MessageKey setChatJid(ContactJid chatJid) { + public MessageKey setChatJid(Jid chatJid) { this.chatJid = chatJid; return this; } @@ -78,11 +78,11 @@ public boolean fromMe() { return id; } - public Optional senderJid() { + public Optional senderJid() { return Optional.ofNullable(senderJid); } - public MessageKey setSenderJid(ContactJid senderJid) { + public MessageKey setSenderJid(Jid senderJid) { this.senderJid = senderJid; return this; } 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 950a1591..55c4c603 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,7 +2,7 @@ import it.auties.whatsapp.model.chat.Chat; import it.auties.whatsapp.model.contact.Contact; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; import java.util.Optional; @@ -24,7 +24,7 @@ public sealed interface MessageMetadataProvider permits MessageInfo, QuotedMessa * * @return a jid */ - ContactJid chatJid(); + Jid chatJid(); /** * Returns the chat of the message @@ -38,7 +38,7 @@ public sealed interface MessageMetadataProvider permits MessageInfo, QuotedMessa * * @return a jid */ - ContactJid senderJid(); + Jid senderJid(); /** * Returns the sender of the message 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 97938db5..d24cb007 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 @@ -5,7 +5,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufMessage; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Clock; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -29,10 +29,10 @@ public final class MessageReceipt implements ProtobufMessage { private Long playedTimestampSeconds; @ProtobufProperty(index = 5, type = ProtobufType.STRING, repeated = true) @NonNull - private final Set deliveredJids; + private final Set deliveredJids; @ProtobufProperty(index = 6, type = ProtobufType.STRING, repeated = true) @NonNull - private final Set readJids; + private final Set readJids; public MessageReceipt() { this.deliveredJids = new HashSet<>(); @@ -40,7 +40,7 @@ public MessageReceipt() { } @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public MessageReceipt(@Nullable Long deliveredTimestampSeconds, @Nullable Long readTimestampSeconds, @Nullable Long playedTimestampSeconds, @NonNull Set deliveredJids, @NonNull Set readJids) { + public MessageReceipt(@Nullable Long deliveredTimestampSeconds, @Nullable Long readTimestampSeconds, @Nullable Long playedTimestampSeconds, @NonNull Set deliveredJids, @NonNull Set readJids) { this.deliveredTimestampSeconds = deliveredTimestampSeconds; this.readTimestampSeconds = readTimestampSeconds; this.playedTimestampSeconds = playedTimestampSeconds; @@ -87,11 +87,11 @@ public Optional playedTimestamp() { return Clock.parseSeconds(playedTimestampSeconds); } - public Set deliveredJids() { + public Set deliveredJids() { return deliveredJids; } - public Set readJids() { + public Set readJids() { return readJids; } 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 8e48844b..80c3ddb2 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,7 +3,7 @@ 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.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; import org.checkerframework.checker.nullness.qual.NonNull; @@ -63,7 +63,7 @@ public static Optional of(@NonNull ContextInfo contextInfo) { } @Override - public ContactJid chatJid() { + public Jid chatJid() { return chat.jid(); } @@ -73,7 +73,7 @@ public ContactJid chatJid() { * @return a jid */ @Override - public ContactJid senderJid() { + public Jid senderJid() { return Objects.requireNonNullElseGet(sender.jid(), this::chatJid); } 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 76e5c2c6..f682d5b3 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,7 +5,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageType; @@ -39,7 +39,7 @@ public record PaymentOrderMessage( Optional title, @ProtobufProperty(index = 8, type = ProtobufType.STRING) @NonNull - ContactJid sellerId, + Jid sellerId, @ProtobufProperty(index = 9, type = ProtobufType.STRING) @NonNull String token, diff --git a/src/main/java/it/auties/whatsapp/model/message/payment/RequestPaymentMessage.java b/src/main/java/it/auties/whatsapp/model/message/payment/RequestPaymentMessage.java index 5211111d..a135826b 100644 --- a/src/main/java/it/auties/whatsapp/model/message/payment/RequestPaymentMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/payment/RequestPaymentMessage.java @@ -3,7 +3,7 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.MessageContainer; import it.auties.whatsapp.model.message.model.MessageType; import it.auties.whatsapp.model.message.model.PaymentMessage; @@ -27,7 +27,7 @@ public record RequestPaymentMessage( @ProtobufProperty(index = 2, type = ProtobufType.UINT64) long amount1000, @ProtobufProperty(index = 3, type = ProtobufType.STRING) - ContactJid requestFrom, + Jid requestFrom, @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) Optional noteMessage, @ProtobufProperty(index = 5, type = ProtobufType.UINT64) diff --git a/src/main/java/it/auties/whatsapp/model/message/server/DeviceSentMessage.java b/src/main/java/it/auties/whatsapp/model/message/server/DeviceSentMessage.java index 133643f7..9371fcba 100644 --- a/src/main/java/it/auties/whatsapp/model/message/server/DeviceSentMessage.java +++ b/src/main/java/it/auties/whatsapp/model/message/server/DeviceSentMessage.java @@ -3,7 +3,7 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.message.model.MessageContainer; import it.auties.whatsapp.model.message.model.MessageType; import it.auties.whatsapp.model.message.model.ServerMessage; @@ -19,7 +19,7 @@ public record DeviceSentMessage( @ProtobufProperty(index = 1, type = ProtobufType.STRING) @NonNull - ContactJid destinationJid, + Jid destinationJid, @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) @NonNull MessageContainer message, 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 4a2d1234..1eb8e1ac 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,7 +5,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufEnum; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.message.model.ContextualMessage; import it.auties.whatsapp.model.message.model.MessageCategory; @@ -24,7 +24,7 @@ public record GroupInviteMessage( @ProtobufProperty(index = 1, type = ProtobufType.STRING) @NonNull - ContactJid group, + Jid group, @ProtobufProperty(index = 2, type = ProtobufType.STRING) @NonNull String code, 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 82024d5e..0d53dc87 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,8 +7,8 @@ import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.crypto.Sha256; -import it.auties.whatsapp.model.contact.ContactJid; -import it.auties.whatsapp.model.contact.ContactJidProvider; +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.message.model.ContextualMessage; @@ -46,10 +46,10 @@ public final class PollCreationMessage implements ContextualMessage { private final Map selectableOptionsMap; - private final Map> selectedOptionsMap; + private final Map> selectedOptionsMap; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public PollCreationMessage(byte @Nullable [] encryptionKey, @NonNull String title, List selectableOptions, int selectableOptionsCount, ContextInfo contextInfo, Map selectableOptionsMap, Map> selectedOptionsMap) { + public PollCreationMessage(byte @Nullable [] encryptionKey, @NonNull String title, List selectableOptions, int selectableOptionsCount, ContextInfo contextInfo, Map selectableOptionsMap, Map> selectedOptionsMap) { this.encryptionKey = encryptionKey; this.title = title; this.selectableOptions = selectableOptions; @@ -101,7 +101,7 @@ static PollCreationMessage simpleBuilder(@NonNull String title, @NonNull List getSelectedOptions(@NonNull ContactJidProvider voter) { + public Collection getSelectedOptions(@NonNull JidProvider voter) { var results = selectedOptionsMap.get(voter.toJid()); if(results == null) { return List.of(); @@ -110,7 +110,7 @@ public Collection getSelectedOptions(@NonNull ContactJidProvider vot return Collections.unmodifiableCollection(results); } - public void addSelectedOptions(ContactJidProvider voter, Collection voted) { + public void addSelectedOptions(JidProvider voter, Collection voted) { selectedOptionsMap.put(voter.toJid(), voted); } 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 a5dc8031..ea299886 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,7 +6,7 @@ import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.poll.PollOption; @@ -29,7 +29,7 @@ @ProtobufMessageName("Message.PollUpdateMessage") public final class PollUpdateMessage implements Message, EncryptedMessage { @Nullable - private ContactJid voter; + private Jid voter; @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) @NonNull @@ -91,11 +91,11 @@ public PollUpdateMessage setEncryptedMetadata(PollUpdateEncryptedMetadata encryp return this; } - public Optional voter() { + public Optional voter() { return Optional.ofNullable(voter); } - public PollUpdateMessage setVoter(ContactJid voter) { + public PollUpdateMessage setVoter(Jid voter) { this.voter = voter; return this; } 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 23fd410c..6ce26138 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,7 +3,7 @@ import it.auties.protobuf.annotation.ProtobufMessageName; import it.auties.protobuf.annotation.ProtobufProperty; import it.auties.protobuf.model.ProtobufType; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.ContextInfo; import it.auties.whatsapp.model.message.model.ButtonMessage; import it.auties.whatsapp.model.message.model.ContextualMessage; @@ -25,7 +25,7 @@ public record ProductMessage( Product product, @ProtobufProperty(index = 2, type = ProtobufType.STRING) @NonNull - ContactJid businessOwnerJid, + Jid businessOwnerJid, @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) @NonNull ProductCatalog catalog, diff --git a/src/main/java/it/auties/whatsapp/model/mobile/PhoneNumber.java b/src/main/java/it/auties/whatsapp/model/mobile/PhoneNumber.java index 8e6c9fa2..715d3af1 100644 --- a/src/main/java/it/auties/whatsapp/model/mobile/PhoneNumber.java +++ b/src/main/java/it/auties/whatsapp/model/mobile/PhoneNumber.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.Optional; @@ -41,8 +41,8 @@ public String prefix() { return countryCode.prefix(); } - public ContactJid toJid(){ - return ContactJid.of(toString()); + public Jid toJid(){ + return Jid.of(toString()); } @Override diff --git a/src/main/java/it/auties/whatsapp/model/node/Attributes.java b/src/main/java/it/auties/whatsapp/model/node/Attributes.java index 157724c8..84d73c53 100644 --- a/src/main/java/it/auties/whatsapp/model/node/Attributes.java +++ b/src/main/java/it/auties/whatsapp/model/node/Attributes.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.*; @@ -304,15 +304,15 @@ private boolean parseBool(Object value) { * @param key the non-null key * @return a non-null optional */ - public Optional getJid(@NonNull String key) { + public Optional getJid(@NonNull String key) { return get(key, Object.class).map(this::parseJid); } - private ContactJid parseJid(Object value) { - if (value instanceof ContactJid jid) { + private Jid parseJid(Object value) { + if (value instanceof Jid jid) { return jid; } else if (value instanceof String encodedJid) { - return ContactJid.of(encodedJid); + return Jid.of(encodedJid); } else { throw new IllegalStateException("Unexpected value: " + value); } diff --git a/src/main/java/it/auties/whatsapp/model/node/Node.java b/src/main/java/it/auties/whatsapp/model/node/Node.java index 7eb47215..d77cfbda 100644 --- a/src/main/java/it/auties/whatsapp/model/node/Node.java +++ b/src/main/java/it/auties/whatsapp/model/node/Node.java @@ -163,6 +163,10 @@ private static Collection getNodesOrThrow(Collection entries) { } var results = (Collection) entries; + if(results.isEmpty()) { + return null; + } + return results.stream() .filter(Objects::nonNull) .toList(); diff --git a/src/main/java/it/auties/whatsapp/model/privacy/PrivacySettingEntry.java b/src/main/java/it/auties/whatsapp/model/privacy/PrivacySettingEntry.java index acbf21a9..161bcb93 100644 --- a/src/main/java/it/auties/whatsapp/model/privacy/PrivacySettingEntry.java +++ b/src/main/java/it/auties/whatsapp/model/privacy/PrivacySettingEntry.java @@ -1,6 +1,6 @@ package it.auties.whatsapp.model.privacy; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; @@ -12,11 +12,11 @@ * @param value the non-null value * @param excluded the non-null list of excluded contacts if {@link PrivacySettingEntry#value} == {@link PrivacySettingValue#CONTACTS_EXCEPT} */ -public record PrivacySettingEntry(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, List excluded) { +public record PrivacySettingEntry(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, List excluded) { /** * Canonical constructor */ - public PrivacySettingEntry(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, List excluded) { + public PrivacySettingEntry(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, List excluded) { this.type = type; this.value = value; this.excluded = excluded == null ? List.of() : excluded; 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 4765f537..987d8026 100644 --- a/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java +++ b/src/main/java/it/auties/whatsapp/model/request/MessageSendRequest.java @@ -1,12 +1,12 @@ package it.auties.whatsapp.model.request; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.info.MessageInfo; import java.util.List; import java.util.Map; -public record MessageSendRequest(MessageInfo info, List recipients, boolean force, boolean peer, Map additionalAttributes) { +public record MessageSendRequest(MessageInfo info, List recipients, boolean force, boolean peer, Map additionalAttributes) { public MessageSendRequest(MessageInfo info) { this(info, null, false, false, null); } diff --git a/src/main/java/it/auties/whatsapp/model/request/UpdateChannelRequest.java b/src/main/java/it/auties/whatsapp/model/request/UpdateChannelRequest.java index 831f57bb..48190752 100644 --- a/src/main/java/it/auties/whatsapp/model/request/UpdateChannelRequest.java +++ b/src/main/java/it/auties/whatsapp/model/request/UpdateChannelRequest.java @@ -1,10 +1,10 @@ package it.auties.whatsapp.model.request; import com.fasterxml.jackson.annotation.JsonProperty; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; public record UpdateChannelRequest(Variable variables) { - public record Variable(@JsonProperty("newsletter_id") ContactJid jid, UpdatePayload updates) { + public record Variable(@JsonProperty("newsletter_id") Jid jid, UpdatePayload updates) { } diff --git a/src/main/java/it/auties/whatsapp/model/response/ChannelResponse.java b/src/main/java/it/auties/whatsapp/model/response/ChannelResponse.java index 5e73e88f..0df791f7 100644 --- a/src/main/java/it/auties/whatsapp/model/response/ChannelResponse.java +++ b/src/main/java/it/auties/whatsapp/model/response/ChannelResponse.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Clock; import it.auties.whatsapp.util.Json; import org.checkerframework.checker.nullness.qual.NonNull; @@ -11,7 +11,7 @@ import java.util.List; import java.util.Optional; -public record ChannelResponse(@JsonProperty("id") ContactJid jid, ChannelState state, +public record ChannelResponse(@JsonProperty("id") Jid jid, ChannelState state, @JsonProperty("thread_metadata") ChannelMetadata metadata, @JsonProperty("viewer_metadata") ChannelViewerMetadata viewerMetadata diff --git a/src/main/java/it/auties/whatsapp/model/response/HasWhatsappResponse.java b/src/main/java/it/auties/whatsapp/model/response/HasWhatsappResponse.java index 82055cc9..64d0463e 100644 --- a/src/main/java/it/auties/whatsapp/model/response/HasWhatsappResponse.java +++ b/src/main/java/it/auties/whatsapp/model/response/HasWhatsappResponse.java @@ -1,8 +1,8 @@ package it.auties.whatsapp.model.response; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import org.checkerframework.checker.nullness.qual.NonNull; -public record HasWhatsappResponse(@NonNull ContactJid contact, boolean hasWhatsapp) implements ResponseWrapper { +public record HasWhatsappResponse(@NonNull Jid contact, boolean hasWhatsapp) implements ResponseWrapper { } diff --git a/src/main/java/it/auties/whatsapp/model/response/RecommendedChannelsResponse.java b/src/main/java/it/auties/whatsapp/model/response/RecommendedChannelsResponse.java index 92369be2..0f209626 100644 --- a/src/main/java/it/auties/whatsapp/model/response/RecommendedChannelsResponse.java +++ b/src/main/java/it/auties/whatsapp/model/response/RecommendedChannelsResponse.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.model.response; import com.fasterxml.jackson.annotation.JsonProperty; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.util.Clock; import it.auties.whatsapp.util.Json; import org.checkerframework.checker.nullness.qual.NonNull; @@ -26,7 +26,7 @@ private record JsonData(@JsonProperty("xwa2_newsletters_directory_list") Optiona } - public record RecommendedChannel(@JsonProperty("id") ContactJid jid, + public record RecommendedChannel(@JsonProperty("id") Jid jid, @JsonProperty("thread_metadata") ChannelMetadata metadata) { } diff --git a/src/main/java/it/auties/whatsapp/model/signal/auth/ClientPayload.java b/src/main/java/it/auties/whatsapp/model/signal/auth/ClientPayload.java index b1bd5665..658525f2 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/auth/ClientPayload.java +++ b/src/main/java/it/auties/whatsapp/model/signal/auth/ClientPayload.java @@ -32,7 +32,8 @@ public record ClientPayload(@ProtobufProperty(index = 1, type = UINT64) Long use @ProtobufProperty(index = 24, type = INT32) Integer lc, @ProtobufProperty(index = 30, type = OBJECT) ClientPayloadIOSAppExtension iosAppExtension, @ProtobufProperty(index = 31, type = UINT64) Long fbAppId, - @ProtobufProperty(index = 32, type = BYTES) byte[] fbDeviceId) implements ProtobufMessage { + @ProtobufProperty(index = 32, type = BYTES) byte[] fbDeviceId, + @ProtobufProperty(index = 33, type = BOOL) Boolean pull) implements ProtobufMessage { public enum ClientPayloadConnectType implements ProtobufEnum { diff --git a/src/main/java/it/auties/whatsapp/model/signal/auth/UserAgent.java b/src/main/java/it/auties/whatsapp/model/signal/auth/UserAgent.java index 18693f54..8d2b3ae7 100644 --- a/src/main/java/it/auties/whatsapp/model/signal/auth/UserAgent.java +++ b/src/main/java/it/auties/whatsapp/model/signal/auth/UserAgent.java @@ -10,7 +10,7 @@ import static it.auties.protobuf.model.ProtobufType.STRING; @ProtobufMessageName("ClientPayload.UserAgent") -public record UserAgent(@ProtobufProperty(index = 1, type = OBJECT) Platform platform, +public record UserAgent(@ProtobufProperty(index = 1, type = OBJECT) PlatformType platform, @ProtobufProperty(index = 2, type = OBJECT) Version appVersion, @ProtobufProperty(index = 3, type = STRING) String mcc, @ProtobufProperty(index = 4, type = STRING) String mnc, @@ -25,7 +25,7 @@ public record UserAgent(@ProtobufProperty(index = 1, type = OBJECT) Platform pla @ProtobufProperty(index = 13, type = STRING) String deviceBoard) implements ProtobufMessage { @ProtobufMessageName("ClientPayload.UserAgent.Platform") - public enum Platform implements ProtobufEnum { + public enum PlatformType implements ProtobufEnum { UNKNOWN(999), ANDROID(0), IOS(1), @@ -57,7 +57,7 @@ public enum Platform implements ProtobufEnum { MILAN(27), CAPI(28); - Platform(@ProtobufEnumIndex int index) { + PlatformType(@ProtobufEnumIndex int index) { this.index = index; } diff --git a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncMessage.java b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncMessage.java index f707058c..e01296a9 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncMessage.java +++ b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncMessage.java @@ -7,6 +7,8 @@ import it.auties.whatsapp.model.info.MessageInfo; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.Objects; + @ProtobufMessageName("HistorySyncMsg") public record HistorySyncMessage( @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) @@ -15,5 +17,13 @@ public record HistorySyncMessage( @ProtobufProperty(index = 2, type = ProtobufType.UINT64) long messageOrderId ) implements ProtobufMessage { + @Override + public boolean equals(Object obj) { + return obj instanceof HistorySyncMessage that && Objects.equals(messageInfo, that.messageInfo()); + } + @Override + public int hashCode() { + return Objects.hashCode(messageInfo); + } } \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java index c8d6cce9..d7ff84bd 100644 --- a/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java +++ b/src/main/java/it/auties/whatsapp/model/sync/HistorySyncNotification.java @@ -31,9 +31,17 @@ public final class HistorySyncNotification implements MutableAttachmentProvider< private final Integer chunkOrder; @ProtobufProperty(index = 8, type = STRING) private final String originalMessageId; + @ProtobufProperty(index = 9, type = UINT32) + private final int progress; + @ProtobufProperty(index = 10, type = INT64) + private final long oldestMsgInChunkTimestampSec; + @ProtobufProperty(index = 11, type = BYTES) + private final byte[] initialHistBootstrapInlinePayload; + @ProtobufProperty(index = 12, type = STRING) + private final String peerDataRequestSessionId; @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) - public HistorySyncNotification(byte[] mediaSha256, long mediaSize, byte[] mediaKey, byte[] mediaEncryptedSha256, String mediaDirectPath, HistorySync.Type syncType, Integer chunkOrder, String originalMessageId) { + public HistorySyncNotification(byte[] mediaSha256, Long mediaSize, byte[] mediaKey, byte[] mediaEncryptedSha256, String mediaDirectPath, HistorySync.Type syncType, Integer chunkOrder, String originalMessageId, int progress, long oldestMsgInChunkTimestampSec, byte[] initialHistBootstrapInlinePayload, String peerDataRequestSessionId) { this.mediaSha256 = mediaSha256; this.mediaSize = mediaSize; this.mediaKey = mediaKey; @@ -42,6 +50,10 @@ public HistorySyncNotification(byte[] mediaSha256, long mediaSize, byte[] mediaK this.syncType = syncType; this.chunkOrder = chunkOrder; this.originalMessageId = originalMessageId; + this.progress = progress; + this.oldestMsgInChunkTimestampSec = oldestMsgInChunkTimestampSec; + this.initialHistBootstrapInlinePayload = initialHistBootstrapInlinePayload; + this.peerDataRequestSessionId = peerDataRequestSessionId; } @Override @@ -126,6 +138,23 @@ public Optional originalMessageId() { return Optional.ofNullable(originalMessageId); } + + public int progress() { + return progress; + } + + public long oldestMsgInChunkTimestampSec() { + return oldestMsgInChunkTimestampSec; + } + + public Optional initialHistBootstrapInlinePayload() { + return Optional.ofNullable(initialHistBootstrapInlinePayload); + } + + public Optional peerDataRequestSessionId() { + return Optional.ofNullable(peerDataRequestSessionId); + } + @ProtobufMessageName("Message.HistorySyncNotification.HistorySyncType") public enum Type implements ProtobufEnum { @@ -134,7 +163,8 @@ public enum Type implements ProtobufEnum { FULL(2), RECENT(3), PUSH_NAME(4), - NON_BLOCKING_DATA(5); + NON_BLOCKING_DATA(5), + ON_DEMAND(6); Type(@ProtobufEnumIndex int index) { this.index = index; diff --git a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java index 5ad4872c..96c2d5e0 100644 --- a/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/AppStateHandler.java @@ -10,7 +10,7 @@ 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.contact.ContactJid; +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.node.Attributes; @@ -20,7 +20,16 @@ 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.Spec; @@ -61,7 +70,7 @@ private synchronized ExecutorService getOrCreateAppService(){ return executor; } - protected CompletableFuture push(@NonNull ContactJid jid, @NonNull List patches) { + protected CompletableFuture push(@NonNull Jid jid, @NonNull List patches) { return runPushTask(() -> { var clientType = socketHandler.store().clientType(); var pullOperation = switch (clientType){ @@ -94,7 +103,7 @@ private CompletableFuture runPushTask(Supplier> task) return future; } - private CompletableFuture sendPush(ContactJid jid, List patches, boolean readPatches) { + private CompletableFuture sendPush(Jid jid, List patches, boolean readPatches) { var requests = patches.stream() .map(entry -> createPushRequest(jid, entry)) .toList(); @@ -110,7 +119,7 @@ private CompletableFuture sendPush(ContactJid jid, List patc .thenRunAsync(() -> onPush(jid, requests, readPatches)); } - private PushRequest createPushRequest(ContactJid jid, PatchRequest request) { + private PushRequest createPushRequest(Jid jid, PatchRequest request) { var oldState = socketHandler.keys() .findHashStateByName(jid, request.type()) .orElseGet(() -> new CompanionHashState(request.type())); @@ -181,7 +190,7 @@ private Node createPushRequestNode(PushRequest request, boolean mobile) { Node.of("patch", PatchSyncSpec.encode(request.sync()))); } - private void onPush(ContactJid jid, List requests, boolean readPatches) { + private void onPush(Jid jid, List requests, boolean readPatches) { requests.forEach(request -> { socketHandler.keys().putState(jid, request.newState()); if (!readPatches) { @@ -262,7 +271,7 @@ private Void onPullError(boolean initial, Throwable exception) { return socketHandler.handleFailure(PULL_APP_STATE, exception); } - private CompletableFuture pullUninterruptedly(ContactJid jid, Set patchTypes) { + private CompletableFuture pullUninterruptedly(Jid jid, Set patchTypes) { var tempStates = new HashMap(); var nodes = getPullNodes(jid, patchTypes, tempStates); return socketHandler.sendQuery("set", "w:sync:app:state", Node.of("sync", nodes)) @@ -272,11 +281,11 @@ private CompletableFuture pullUninterruptedly(ContactJid jid, Set handlePullResult(ContactJid jid, Set remaining) { + private CompletableFuture handlePullResult(Jid jid, Set remaining) { return remaining.isEmpty() ? CompletableFuture.completedFuture(true) : pullUninterruptedly(jid, remaining); } - private List getPullNodes(ContactJid jid, Set patchTypes, Map tempStates) { + private List getPullNodes(Jid jid, Set patchTypes, Map tempStates) { return patchTypes.stream() .map(name -> createStateWithVersion(jid, name)) .peek(state -> tempStates.put(state.name(), state)) @@ -284,13 +293,13 @@ private List getPullNodes(ContactJid jid, Set patchTypes, Map

new CompanionHashState(name)); } - private Set decodeSyncs(ContactJid jid, Map tempStates, List records) { + private Set decodeSyncs(Jid jid, Map tempStates, List records) { return records.stream() .map(record -> { var chunk = decodeSync(jid, record, tempStates); @@ -302,7 +311,7 @@ private Set decodeSyncs(ContactJid jid, Map tempStates) { + private PatchChunk decodeSync(Jid jid, SnapshotSyncRecord record, Map tempStates) { try { var results = new ArrayList(); if (record.hasSnapshot()) { @@ -484,7 +493,7 @@ private void deleteMessage(MessageInfo message, Chat chat) { socketHandler.onMessageDeleted(message, false); } - private SyncRecord decodePatches(ContactJid jid, PatchType name, List patches, CompanionHashState state) { + private SyncRecord decodePatches(Jid jid, PatchType name, List patches, CompanionHashState state) { var newState = state.copy(); var results = patches.stream() .map(patch -> decodePatch(jid, name, newState, patch)) @@ -494,7 +503,7 @@ private SyncRecord decodePatches(ContactJid jid, PatchType name, List return new SyncRecord(newState, results); } - private MutationsRecord decodePatch(ContactJid jid, PatchType patchType, CompanionHashState newState, PatchSync patch) { + private MutationsRecord decodePatch(Jid jid, PatchType patchType, CompanionHashState newState, PatchSync patch) { if (patch.hasExternalMutations()) { Medias.download(patch.externalMutations()) .join() @@ -517,12 +526,12 @@ private void handleExternalMutation(PatchSync patch, byte[] blob) { patch.mutations().addAll(mutationsSync.mutations()); } - private Optional calculateSnapshotMac(ContactJid jid, PatchType name, CompanionHashState newState, PatchSync patch) { + private Optional calculateSnapshotMac(Jid jid, PatchType name, CompanionHashState newState, PatchSync patch) { return getMutationKeys(jid, patch.keyId()) .map(mutationKeys -> generateSnapshotMac(newState.hash(), newState.version(), name, mutationKeys.snapshotMacKey())); } - private Optional calculatePatchMac(ContactJid jid, PatchSync patch, PatchType patchType) { + private Optional calculatePatchMac(Jid jid, PatchSync patch, PatchType patchType) { return getMutationKeys(jid, patch.keyId()) .map(mutationKeys -> generatePatchMac(patch.snapshotMac(), getSyncMutationMac(patch), patch.encodedVersion(), patchType, mutationKeys.patchMacKey())); } @@ -535,7 +544,7 @@ private byte[][] getSyncMutationMac(PatchSync patch) { .toArray(byte[][]::new); } - private Optional decodeSnapshot(ContactJid jid, PatchType name, SnapshotSync snapshot) { + private Optional decodeSnapshot(Jid jid, PatchType name, SnapshotSync snapshot) { var mutationKeys = getMutationKeys(jid, snapshot.keyId()); if (mutationKeys.isEmpty()) { return Optional.empty(); @@ -549,7 +558,7 @@ private Optional decodeSnapshot(ContactJid jid, PatchType name, Snap return Optional.of(new SyncRecord(newState, mutations.records())); } - private Optional getMutationKeys(ContactJid jid, KeyId snapshot) { + private Optional getMutationKeys(Jid jid, KeyId snapshot) { return socketHandler.keys() .findAppKeyById(jid, snapshot.id()) .map(AppStateSyncKey::keyData) @@ -557,7 +566,7 @@ private Optional getMutationKeys(ContactJid jid, KeyId snapshot) { .map(MutationKeys::of); } - private MutationsRecord decodeMutations(ContactJid jid, List syncs, CompanionHashState state) { + private MutationsRecord decodeMutations(Jid jid, List syncs, CompanionHashState state) { var generator = new LTHash(state); var mutations = syncs.stream() .map(mutation -> decodeMutation(jid, mutation.operation(), mutation.record(), generator)) @@ -566,7 +575,7 @@ private MutationsRecord decodeMutations(ContactJid jid, List return new MutationsRecord(generator.finish(), mutations); } - private Optional decodeMutation(ContactJid jid, RecordSync.Operation operation, RecordSync sync, LTHash generator) { + private Optional decodeMutation(Jid jid, RecordSync.Operation operation, RecordSync sync, LTHash generator) { var mutationKeys = getMutationKeys(jid, sync.keyId()); if (mutationKeys.isEmpty()) { return Optional.empty(); diff --git a/src/main/java/it/auties/whatsapp/socket/AuthHandler.java b/src/main/java/it/auties/whatsapp/socket/AuthHandler.java index c18bb5df..a280ca16 100644 --- a/src/main/java/it/auties/whatsapp/socket/AuthHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/AuthHandler.java @@ -3,13 +3,12 @@ import it.auties.curve25519.Curve25519; import it.auties.protobuf.exception.ProtobufDeserializationException; import it.auties.whatsapp.api.ClientType; -import it.auties.whatsapp.api.WebHistoryLength; import it.auties.whatsapp.model.mobile.CountryCode; import it.auties.whatsapp.model.mobile.PhoneNumber; import it.auties.whatsapp.model.request.Request; import it.auties.whatsapp.model.signal.auth.*; import it.auties.whatsapp.model.signal.auth.DNSSource.ResolutionMethod; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.sync.HistorySyncConfigBuilder; import it.auties.whatsapp.util.BytesHelper; import it.auties.whatsapp.util.Spec; @@ -78,8 +77,12 @@ private boolean onHandshakeSent(SocketHandshake handshake) { } private WebInfo createWebInfo() { + if(socketHandler.store().historyLength().size() > Spec.Whatsapp.DEFAULT_HISTORY_SIZE) { + return null; + } + return new WebInfoBuilder() - .webSubPlatform(socketHandler.store().historyLength() == WebHistoryLength.EXTENDED ? WebInfo.Platform.WIN_STORE : WebInfo.Platform.WEB_BROWSER) + .webSubPlatform(WebInfo.Platform.WEB_BROWSER) .build(); } @@ -127,7 +130,7 @@ private String getDeviceMnc(boolean mobile) { .orElse("000"); } - private Platform getUserAgentPlatform(boolean mobile) { + private PlatformType getUserAgentPlatform(boolean mobile) { if(mobile) { var device = socketHandler.store() .device() @@ -135,11 +138,11 @@ private Platform getUserAgentPlatform(boolean mobile) { return socketHandler.store().business() ? device.businessPlatform() : device.platform(); } - if(socketHandler.store().historyLength() == WebHistoryLength.EXTENDED) { - return Platform.WINDOWS; + if(socketHandler.store().historyLength().size() > Spec.Whatsapp.DEFAULT_HISTORY_SIZE) { + return PlatformType.WINDOWS; } - return Platform.WEB; + return PlatformType.WEB; } private ClientPayload createUserClientPayload() { @@ -170,6 +173,7 @@ private ClientPayload createUserClientPayload() { yield builder.webInfo(createWebInfo()) .username(Long.parseLong(jid.get().user())) .passive(true) + .pull(true) .device(jid.get().device()) .build(); } @@ -177,6 +181,7 @@ private ClientPayload createUserClientPayload() { yield builder.webInfo(createWebInfo()) .regData(createRegisterData()) .passive(false) + .pull(false) .build(); } }; @@ -210,12 +215,13 @@ private CompanionRegistrationData createRegisterData() { private CompanionProperties createCompanionProps() { return switch (socketHandler.store().clientType()) { case WEB -> { + var syncSize = socketHandler.store().historyLength().size(); + var fullSync = syncSize > Spec.Whatsapp.DEFAULT_HISTORY_SIZE; var config = new HistorySyncConfigBuilder() .inlineInitialPayloadInE2EeMsg(true) .supportBotUserAgentChatHistory(true) - .storageQuotaMb(59206) + .storageQuotaMb(syncSize) .build(); - var fullSync = socketHandler.store().historyLength() == WebHistoryLength.EXTENDED; yield new CompanionPropertiesBuilder() .os(socketHandler.store().name()) .platformType(fullSync ? CompanionProperties.PlatformType.DESKTOP : CompanionProperties.PlatformType.CHROME) diff --git a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java index 05f18725..ebae3969 100644 --- a/src/main/java/it/auties/whatsapp/socket/MessageHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/MessageHandler.java @@ -1,6 +1,5 @@ package it.auties.whatsapp.socket; -import it.auties.whatsapp.api.WebHistoryLength; import it.auties.whatsapp.crypto.*; import it.auties.whatsapp.model.action.ContactAction; import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec; @@ -9,6 +8,9 @@ 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.jid.JidType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.message.button.*; import it.auties.whatsapp.model.message.model.*; import it.auties.whatsapp.model.message.payment.PaymentOrderMessage; @@ -26,29 +28,34 @@ 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.*; +import it.auties.whatsapp.model.sync.HistorySync; 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 java.lang.System.Logger; import java.lang.System.Logger.Level; import java.nio.charset.StandardCharsets; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.Stream; -import static it.auties.whatsapp.api.ErrorHandler.Location.MESSAGE; -import static it.auties.whatsapp.api.ErrorHandler.Location.UNKNOWN; +import static it.auties.whatsapp.api.ErrorHandler.Location.*; import static it.auties.whatsapp.util.Spec.Signal.*; class MessageHandler { private static final int HISTORY_SYNC_TIMEOUT = 25; private final SocketHandler socketHandler; - private final Map> pastParticipantsQueue; - private final Set historyCache; + private final Map> pastParticipantsQueue; + private final Set historyCache; private final Logger logger; private final EnumSet historySyncTypes; private final ReentrantLock lock; @@ -73,12 +80,10 @@ protected synchronized CompletableFuture encode(MessageSendRequest request } private CompletableFuture encodeMessage(MessageSendRequest request) { - return request.info().chatJid().hasServer(ContactJidServer.CHANNEL) ? encodePlainMessage(request) : encodeE2EMessage(request); + return request.info().chatJid().hasServer(JidServer.CHANNEL) ? encodePlainMessage(request) : encodeE2EMessage(request); } private CompletableFuture encodePlainMessage(MessageSendRequest request) { - //// Media - //Sent Binary Message: Node[description=message, attributes={media_id=wa_channel_image/flat/7B47F3C0F34315B5EB3BA9A273F5649E, to=120363180154968187@s.whatsapp.net, id=3EB003DD89821A2EB4DC94, type=media}, content=[Node[description=plaintext, attributes={mediatype=image}, content=ABC]]]] var message = request.info().message(); var messageAttributes = Attributes.ofNullable(request.additionalAttributes()) .put("mediatype", getMediaType(message), Objects::nonNull) @@ -94,7 +99,7 @@ private CompletableFuture encodePlainMessage(MessageSendRequest request) { var type = message.content().type() == MessageType.TEXT ? "text" : "media"; var attributes = Attributes.of() .put("id", request.info().id()) - .put("to", request.info().chatJid().withServer(ContactJidServer.WHATSAPP)) + .put("to", request.info().chatJid().withServer(JidServer.WHATSAPP)) .put("type", type) .toMap(); return socketHandler.send(Node.of("message", attributes, messageNode)) @@ -181,7 +186,7 @@ private CompletableFuture encodeConversation(MessageSendRequest request) { .thenComposeAsync(socketHandler::send); } - private List getRecipients(MessageSendRequest request, ContactJid sender) { + private List getRecipients(MessageSendRequest request, Jid sender) { if(request.peer()){ return List.of(request.info().chatJid()); } @@ -194,8 +199,8 @@ private List getRecipients(MessageSendRequest request, ContactJid se } private boolean isConversation(MessageInfo info) { - return info.chatJid().hasServer(ContactJidServer.WHATSAPP) - || info.chatJid().hasServer(ContactJidServer.USER); + return info.chatJid().hasServer(JidServer.WHATSAPP) + || info.chatJid().hasServer(JidServer.USER); } private Node createEncodedMessageNode(MessageSendRequest request, List preKeys, Node descriptor) { @@ -245,7 +250,7 @@ private boolean hasPreKeyMessage(List participants) { .anyMatch(PKMSG::equals); } - private CompletableFuture> createConversationNodes(MessageSendRequest request, List contacts, byte[] message, byte[] deviceMessage) { + private CompletableFuture> createConversationNodes(MessageSendRequest request, List contacts, byte[] message, byte[] deviceMessage) { var jid = socketHandler.store() .jid() .orElse(null); @@ -262,7 +267,7 @@ private CompletableFuture> createConversationNodes(MessageSendRequest return companions.thenCombineAsync(others, (first, second) -> toSingleList(first, second)); } - private CompletableFuture> createGroupNodes(MessageSendRequest request, byte[] distributionMessage, List participants, boolean force) { + private CompletableFuture> createGroupNodes(MessageSendRequest request, byte[] distributionMessage, List participants, boolean force) { var missingParticipants = participants.stream() .filter(participant -> force || !socketHandler.keys().hasGroupKeys(request.info().chatJid(), participant)) .toList(); @@ -279,7 +284,7 @@ private CompletableFuture> createGroupNodes(MessageSendRequest reques }); } - protected CompletableFuture querySessions(List contacts, boolean force) { + protected CompletableFuture querySessions(List contacts, boolean force) { var missingSessions = contacts.stream() .filter(contact -> force || !socketHandler.keys().hasSession(contact.toSignalAddress())) .map(contact -> Node.of("user", Map.of("jid", contact))) @@ -292,20 +297,20 @@ private CompletableFuture querySession(List children){ .thenAcceptAsync(this::parseSessions); } - private List createMessageNodes(MessageSendRequest request, List contacts, byte[] message) { + private List createMessageNodes(MessageSendRequest request, List contacts, byte[] message) { return contacts.stream() .map(contact -> createMessageNode(request, contact, message, false)) .toList(); } - private Node createMessageNode(MessageSendRequest request, ContactJid contact, byte[] message, boolean peer) { + private Node createMessageNode(MessageSendRequest request, Jid contact, byte[] message, boolean peer) { var cipher = new SessionCipher(contact.toSignalAddress(), socketHandler.keys()); var encrypted = cipher.encrypt(message); var messageNode = createMessageNode(request, encrypted); return peer ? messageNode : Node.of("to", Map.of("jid", contact), messageNode); } - private CompletableFuture> getGroupDevices(GroupMetadata metadata) { + private CompletableFuture> getGroupDevices(GroupMetadata metadata) { var jids = metadata.participants() .stream() .map(GroupParticipant::jid) @@ -313,12 +318,12 @@ private CompletableFuture> getGroupDevices(GroupMetadata metada return getDevices(jids, false); } - protected CompletableFuture> getDevices(List contacts, boolean excludeSelf) { + protected CompletableFuture> getDevices(List contacts, boolean excludeSelf) { return queryDevices(contacts, excludeSelf) .thenApplyAsync(missingDevices -> excludeSelf ? toSingleList(contacts, missingDevices) : missingDevices); } - private CompletableFuture> queryDevices(List contacts, boolean excludeSelf) { + private CompletableFuture> queryDevices(List contacts, boolean excludeSelf) { var contactNodes = contacts.stream() .map(contact -> Node.of("user", Map.of("jid", contact))) .toList(); @@ -330,7 +335,7 @@ private CompletableFuture> queryDevices(List contac .thenApplyAsync(result -> parseDevices(result, excludeSelf)); } - private List parseDevices(Node node, boolean excludeSelf) { + private List parseDevices(Node node, boolean excludeSelf) { return node.children() .stream() .map(child -> child.findNode("list")) @@ -342,7 +347,7 @@ private List parseDevices(Node node, boolean excludeSelf) { .toList(); } - private List parseDevice(Node wrapper, boolean excludeSelf) { + private List parseDevice(Node wrapper, boolean excludeSelf) { var jid = wrapper.attributes() .getJid("jid") .orElseThrow(() -> new NoSuchElementException("Missing jid for sync device")); @@ -354,11 +359,11 @@ private List parseDevice(Node wrapper, boolean excludeSelf) { .stream() .map(child -> parseDeviceId(child, jid, excludeSelf)) .flatMap(Optional::stream) - .map(id -> ContactJid.ofDevice(jid.user(), id)) + .map(id -> Jid.ofDevice(jid.user(), id)) .toList(); } - private Optional parseDeviceId(Node child, ContactJid jid, boolean excludeSelf) { + private Optional parseDeviceId(Node child, Jid jid, boolean excludeSelf) { var self = socketHandler.store() .jid() .orElse(null); @@ -484,13 +489,13 @@ private CompletableFuture decodeMessage(Node infoNode, Node messageNode, S .id(MessageKey.randomId()); var receiver = socketHandler.store() .jid() - .map(ContactJid::withoutDevice) + .map(Jid::withoutDevice) .orElse(null); if(receiver == null){ return CompletableFuture.completedFuture(null); // This means that the session got disconnected while processing } - if (from.hasServer(ContactJidServer.WHATSAPP) || from.hasServer(ContactJidServer.USER)) { + if (from.hasServer(JidServer.WHATSAPP) || from.hasServer(JidServer.USER)) { keyBuilder.chatJid(recipient); keyBuilder.senderJid(from); keyBuilder.fromMe(Objects.equals(from.withoutDevice(), receiver)); @@ -521,7 +526,7 @@ private CompletableFuture decodeMessage(Node infoNode, Node messageNode, S var messageContainer = BytesHelper.bytesToMessage(decodedMessage.message()).unbox(); var info = messageBuilder.key(key) - .broadcast(key.chatJid().hasServer(ContactJidServer.BROADCAST)) + .broadcast(key.chatJid().hasServer(JidServer.BROADCAST)) .pushName(pushName) .status(MessageStatus.DELIVERED) .businessVerifiedName(businessName) @@ -541,7 +546,7 @@ private CompletableFuture decodeMessage(Node infoNode, Node messageNode, S } } - private CompletableFuture sendReceipt(Node infoNode, String id, ContactJid chatJid, ContactJid senderJid, boolean fromMe) { + private CompletableFuture sendReceipt(Node infoNode, String id, Jid chatJid, Jid senderJid, boolean fromMe) { var participant = fromMe && senderJid == null ? chatJid : senderJid; var category = infoNode.attributes().getString("category"); var receiptType = getReceiptType(category, fromMe); @@ -565,7 +570,7 @@ private String getReceiptType(String category, boolean fromMe) { return null; } - private MessageDecodeResult decodeMessageBytes(String type, byte[] encodedMessage, ContactJid from, ContactJid participant) { + private MessageDecodeResult decodeMessageBytes(String type, byte[] encodedMessage, Jid from, Jid participant) { try { if (encodedMessage == null) { return new MessageDecodeResult(null, new IllegalArgumentException("Missing encoded message")); @@ -578,14 +583,14 @@ private MessageDecodeResult decodeMessageBytes(String type, byte[] encodedMessag yield signalGroup.decrypt(encodedMessage); } case PKMSG -> { - var user = from.hasServer(ContactJidServer.WHATSAPP) ? from : participant; + var user = from.hasServer(JidServer.WHATSAPP) ? from : participant; Objects.requireNonNull(user, "Cannot decipher pkmsg without user"); var session = new SessionCipher(user.toSignalAddress(), socketHandler.keys()); var preKey = SignalPreKeyMessage.ofSerialized(encodedMessage); yield session.decrypt(preKey); } case MSG -> { - var user = from.hasServer(ContactJidServer.WHATSAPP) ? from : participant; + var user = from.hasServer(JidServer.WHATSAPP) ? from : participant; Objects.requireNonNull(user, "Cannot decipher msg without user"); var session = new SessionCipher(user.toSignalAddress(), socketHandler.keys()); var signalMessage = SignalMessage.ofSerialized(encodedMessage); @@ -602,7 +607,7 @@ private MessageDecodeResult decodeMessageBytes(String type, byte[] encodedMessag private void attributeMessageReceipt(MessageInfo info) { var self = socketHandler.store() .jid() - .map(ContactJid::withoutDevice) + .map(Jid::withoutDevice) .orElse(null); if (!info.fromMe() || (self != null && !info.chatJid().equals(self))) { return; @@ -617,7 +622,7 @@ private void saveMessage(MessageInfo info, boolean offline) { if(info.message().content() instanceof SenderKeyDistributionMessage distributionMessage) { handleDistributionMessage(distributionMessage, info.senderJid()); } - if (info.chatJid().type() == ContactJidType.STATUS) { + if (info.chatJid().type() == JidType.STATUS) { socketHandler.store().addStatus(info); socketHandler.onNewStatus(info); return; @@ -650,7 +655,7 @@ private void saveMessage(MessageInfo info, boolean offline) { socketHandler.onNewMessage(info, offline); } - private void handleDistributionMessage(SenderKeyDistributionMessage distributionMessage, ContactJid from) { + private void handleDistributionMessage(SenderKeyDistributionMessage distributionMessage, Jid from) { var groupName = new SenderKeyName(distributionMessage.groupId(), from.toSignalAddress()); var builder = new GroupBuilder(socketHandler.keys()); var message = SignalDistributionMessage.ofSerialized(distributionMessage.data()); @@ -701,11 +706,12 @@ private void onHistorySyncNotification(MessageInfo info, ProtocolMessage protoco downloadHistorySync(protocolMessage) .thenAcceptAsync(history -> onHistoryNotification(info, history)) - .exceptionallyAsync(throwable -> socketHandler.handleFailure(MESSAGE, throwable)); + .exceptionallyAsync(throwable -> socketHandler.handleFailure(HISTORY_SYNC, throwable)) + .thenRunAsync(() -> socketHandler.sendReceipt(info.chatJid(), null, List.of(info.id()), "hist_sync")); } private boolean isZeroHistorySyncComplete() { - return socketHandler.store().historyLength() == WebHistoryLength.ZERO + return socketHandler.store().historyLength().size() == 0 && historySyncTypes.contains(Type.INITIAL_STATUS_V3) && historySyncTypes.contains(Type.PUSH_NAME) && historySyncTypes.contains(Type.INITIAL_BOOTSTRAP) @@ -725,26 +731,20 @@ private CompletableFuture downloadHistorySync(ProtocolMessage proto } private CompletableFuture downloadHistorySyncNotification(HistorySyncNotification notification) { - return Medias.download(notification) - .thenApplyAsync(entry -> entry.orElseThrow(() -> new NoSuchElementException("Cannot download history sync"))) - .thenApplyAsync(result -> HistorySyncSpec.decode(BytesHelper.decompress(result))); + return notification.initialHistBootstrapInlinePayload() + .map(result -> CompletableFuture.completedFuture(HistorySyncSpec.decode(BytesHelper.decompress(result)))) + .orElseGet(() -> Medias.download(notification) + .thenApplyAsync(entry -> entry.orElseThrow(() -> new NoSuchElementException("Cannot download history sync"))) + .thenApplyAsync(result -> HistorySyncSpec.decode(BytesHelper.decompress(result)))); } private void onHistoryNotification(MessageInfo info, HistorySync history) { handleHistorySync(history); - if (history.progress() != null) { - scheduleTimeoutSync(history); - socketHandler.onHistorySyncProgress(history.progress(), history.syncType() == Type.RECENT); + if (history.progress() == null) { + return; } - socketHandler.sendReceipt(info.chatJid(), null, List.of(info.id()), "hist_sync"); - } - private void scheduleTimeoutSync(HistorySync history) { - var executor = CompletableFuture.delayedExecutor(HISTORY_SYNC_TIMEOUT, TimeUnit.SECONDS); - if(historySyncTask != null){ - historySyncTask.cancel(true); - } - this.historySyncTask = CompletableFuture.runAsync(() -> handleChatsSync(history, true), executor); + socketHandler.onHistorySyncProgress(history.progress(), history.syncType() == Type.RECENT); } private void onMessageDeleted(MessageInfo info, MessageInfo message) { @@ -759,7 +759,7 @@ private void handleHistorySync(HistorySync history) { case INITIAL_STATUS_V3 -> handleInitialStatus(history); case PUSH_NAME -> handlePushNames(history); case INITIAL_BOOTSTRAP -> handleInitialBootstrap(history); - case RECENT, FULL -> handleChatsSync(history, false); + case RECENT, FULL -> handleChatsSync(history); case NON_BLOCKING_DATA -> handleNonBlockingData(history); } }finally { @@ -783,7 +783,7 @@ private void handlePushNames(HistorySync history) { } private void handNewPushName(PushName pushName) { - var jid = ContactJid.of(pushName.id()); + var jid = Jid.of(pushName.id()); var contact = socketHandler.store() .findContactByJid(jid) .orElseGet(() -> createNewContact(jid)); @@ -792,41 +792,83 @@ private void handNewPushName(PushName pushName) { socketHandler.onAction(action, MessageIndexInfo.of("contact", jid, null, true)); } - private Contact createNewContact(ContactJid jid) { + private Contact createNewContact(Jid jid) { var contact = socketHandler.store().addContact(jid); socketHandler.onNewContact(contact); return contact; } private void handleInitialBootstrap(HistorySync history) { - if(socketHandler.store().historyLength() != WebHistoryLength.ZERO){ - historyCache.addAll(history.conversations()); + if(socketHandler.store().historyLength().size() != 0){ + var jids = history.conversations() + .stream() + .map(Chat::jid) + .toList(); + historyCache.addAll(jids); } handleConversations(history); socketHandler.onChats(); } - private void handleChatsSync(HistorySync history, boolean forceDone) { - if(socketHandler.store().historyLength() == WebHistoryLength.ZERO){ + private void handleChatsSync(HistorySync history) { + if(socketHandler.store().historyLength().size() == 0){ return; } handleConversations(history); - for (var cached : historyCache) { + handleConversationsNotifications(history); + scheduleHistorySyncTimeout(); + } + + private void handleConversationsNotifications(HistorySync history) { + var toRemove = new HashSet(); + for (var cachedJid : historyCache) { var chat = socketHandler.store() - .findChatByJid(cached.jid()) - .orElse(cached); - var done = forceDone || !history.conversations().contains(cached); + .findChatByJid(cachedJid) + .orElse(null); + if(chat == null) { + continue; + } + + var done = !history.conversations().contains(chat); if(done){ chat.setEndOfHistoryTransfer(true); chat.setEndOfHistoryTransferType(Chat.EndOfHistoryTransferType.COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY); + toRemove.add(cachedJid); } + socketHandler.onChatRecentMessages(chat, done); } - historyCache.removeIf(entry -> !history.conversations().contains(entry)); + + historyCache.removeAll(toRemove); + } + + private void scheduleHistorySyncTimeout() { + var executor = CompletableFuture.delayedExecutor(HISTORY_SYNC_TIMEOUT, TimeUnit.SECONDS); + if(historySyncTask != null){ + historySyncTask.cancel(true); + } + + this.historySyncTask = CompletableFuture.runAsync(this::onForcedHistorySyncCompletion, executor); } + private void onForcedHistorySyncCompletion() { + for (var cachedJid : historyCache) { + var chat = socketHandler.store() + .findChatByJid(cachedJid) + .orElse(null); + if(chat == null) { + continue; + } + + socketHandler.onChatRecentMessages(chat, true); + } + + historyCache.clear(); + } + + private void handleConversations(HistorySync history) { var store = socketHandler.store(); for (var chat : history.conversations()) { diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java index 50daab21..ceb6c112 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java @@ -15,6 +15,9 @@ 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.jid.Jid; +import it.auties.whatsapp.model.jid.JidProvider; +import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.message.model.MessageContainer; import it.auties.whatsapp.model.message.model.MessageKey; import it.auties.whatsapp.model.message.model.MessageKeyBuilder; @@ -319,7 +322,7 @@ public CompletableFuture pushPatch(PatchRequest request) { return appStateHandler.push(jid, List.of(request)); } - public CompletableFuture pushPatches(ContactJid jid, List requests) { + public CompletableFuture pushPatches(Jid jid, List requests) { return appStateHandler.push(jid, requests); } @@ -335,7 +338,7 @@ public void decodeMessage(Node node) { messageHandler.decode(node); } - public CompletableFuture sendPeerMessage(ContactJid companion, ProtocolMessage message) { + public CompletableFuture sendPeerMessage(Jid companion, ProtocolMessage message) { if (message == null) { return CompletableFuture.completedFuture(null); } @@ -365,10 +368,10 @@ public CompletableFuture sendMessage(MessageSendRequest request) { @SuppressWarnings("UnusedReturnValue") public CompletableFuture sendQueryWithNoResponse(String method, String category, Node... body) { - return sendQueryWithNoResponse(null, ContactJidServer.WHATSAPP.toJid(), method, category, null, body); + return sendQueryWithNoResponse(null, JidServer.WHATSAPP.toJid(), method, category, null, body); } - public CompletableFuture sendQueryWithNoResponse(String id, ContactJid to, String method, String category, Map metadata, Node... body) { + public CompletableFuture sendQueryWithNoResponse(String id, Jid to, String method, String category, Map metadata, Node... body) { var attributes = Attributes.ofNullable(metadata) .put("id", id, Objects::nonNull) .put("type", method) @@ -396,7 +399,7 @@ private void onNodeSent(Node node) { }); } - public CompletableFuture> queryAbout(@NonNull ContactJidProvider chat) { + public CompletableFuture> queryAbout(@NonNull JidProvider chat) { var query = Node.of("status"); var body = Node.of("user", Map.of("jid", chat.toJid())); return sendInteractiveQuery(query, body).thenApplyAsync(this::parseStatus); @@ -420,7 +423,7 @@ private Optional parseStatus(List responses) { } public CompletableFuture sendQuery(String method, String category, Node... body) { - return sendQuery(null, ContactJidServer.WHATSAPP.toJid(), method, category, null, body); + return sendQuery(null, JidServer.WHATSAPP.toJid(), method, category, null, body); } private List parseQueryResult(Node result) { @@ -433,7 +436,7 @@ private List parseQueryResult(Node result) { .toList(); } - public CompletableFuture sendQuery(String id, ContactJid to, String method, String category, Map metadata, Node... body) { + public CompletableFuture sendQuery(String id, Jid to, String method, String category, Map metadata, Node... body) { var attributes = Attributes.ofNullable(metadata) .put("xmlns", category, Objects::nonNull) .put("id", id, Objects::nonNull) @@ -462,9 +465,9 @@ public CompletableFuture send(Node node, Function filter) { return result; } - public CompletableFuture> queryPicture(@NonNull ContactJidProvider chat) { + public CompletableFuture> queryPicture(@NonNull JidProvider chat) { var body = Node.of("picture", Map.of("query", "url", "type", "image")); - if (chat.toJid().hasServer(ContactJidServer.GROUP)) { + 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)) .thenApplyAsync(this::parseChatPicture); @@ -475,7 +478,7 @@ public CompletableFuture> queryPicture(@NonNull ContactJidProvider } public CompletableFuture sendQuery(String method, String category, Map metadata, Node... body) { - return sendQuery(null, ContactJidServer.WHATSAPP.toJid(), method, category, metadata, body); + return sendQuery(null, JidServer.WHATSAPP.toJid(), method, category, metadata, body); } private Optional parseChatPicture(Node result) { @@ -484,12 +487,12 @@ private Optional parseChatPicture(Node result) { .map(URI::create); } - public CompletableFuture> queryBlockList() { + public CompletableFuture> queryBlockList() { return sendQuery("get", "blocklist", (Node) null) .thenApplyAsync(this::parseBlockList); } - private List parseBlockList(Node result) { + private List parseBlockList(Node result) { return result.findNode("list") .orElseThrow(() -> new NoSuchElementException("Missing block list in response")) .findNodes("item") @@ -499,12 +502,12 @@ private List parseBlockList(Node result) { .toList(); } - public CompletableFuture subscribeToPresence(ContactJidProvider jid) { + public CompletableFuture subscribeToPresence(JidProvider jid) { var node = Node.of("presence", Map.of("to", jid.toJid(), "type", "subscribe")); return sendWithNoResponse(node); } - public CompletableFuture queryGroupMetadata(ContactJidProvider group) { + public CompletableFuture queryGroupMetadata(JidProvider group) { var body = Node.of("query", Map.of("request", "interactive")); return sendQuery(group.toJid(), "get", "w:g2", body) .thenApplyAsync(this::handleGroupMetadata); @@ -529,7 +532,7 @@ protected GroupMetadata handleGroupMetadata(Node response) { public GroupMetadata parseGroupMetadata(@NonNull Node node) { var groupId = node.attributes() .getOptionalString("id") - .map(id -> ContactJid.of(id, ContactJidServer.GROUP)) + .map(id -> Jid.of(id, JidServer.GROUP)) .orElseThrow(() -> new NoSuchElementException("Missing group jid")); var subject = node.attributes().getString("subject"); var subjectAuthor = node.attributes().getJid("s_o"); @@ -574,11 +577,11 @@ private GroupParticipant parseGroupParticipant(Node node) { return new GroupParticipant(id, role); } - public CompletableFuture sendQuery(ContactJid to, String method, String category, Node... body) { + public CompletableFuture sendQuery(Jid to, String method, String category, Node... body) { return sendQuery(null, to, method, category, null, body); } - public CompletableFuture sendReceipt(ContactJid jid, ContactJid participant, List messages, String type) { + public CompletableFuture sendReceipt(Jid jid, Jid participant, List messages, String type) { if (messages.isEmpty()) { return CompletableFuture.completedFuture(null); } @@ -588,7 +591,7 @@ public CompletableFuture sendReceipt(ContactJid jid, ContactJid participan .put("t", Clock.nowMilliseconds(), () -> Objects.equals(type, "read") || Objects.equals(type, "read-self")) .put("to", jid) .put("type", type, Objects::nonNull); - if (Objects.equals(type, "sender") && jid.hasServer(ContactJidServer.WHATSAPP)) { + if (Objects.equals(type, "sender") && jid.hasServer(JidServer.WHATSAPP)) { attributes.put("recipient", jid); attributes.put("to", participant); } else { @@ -650,8 +653,8 @@ protected void onMessageStatus(MessageStatus status, Contact participant, Messag }); } - protected void onUpdateChatPresence(ContactStatus status, ContactJid contactJid, Chat chat) { - var contact = store.findContactByJid(contactJid); + protected void onUpdateChatPresence(ContactStatus status, Jid jid, Chat chat) { + var contact = store.findContactByJid(jid); if (contact.isPresent()) { contact.get().setLastKnownPresence(status); if (status == contact.get().lastKnownPresence()) { @@ -661,10 +664,10 @@ protected void onUpdateChatPresence(ContactStatus status, ContactJid contactJid, contact.get().setLastSeen(ZonedDateTime.now()); } - chat.presences().put(contactJid, status); + chat.presences().put(jid, status); callListenersAsync(listener -> { - listener.onContactPresence(whatsapp, chat, contactJid, status); - listener.onContactPresence(chat, contactJid, status); + listener.onContactPresence(whatsapp, chat, jid, status); + listener.onContactPresence(chat, jid, status); }); } @@ -889,7 +892,7 @@ protected void onNewContact(Contact contact) { }); } - protected void onDevices(LinkedHashMap devices) { + protected void onDevices(LinkedHashMap devices) { callListenersAsync(listener -> { listener.onLinkedDevices(whatsapp, devices.keySet()); listener.onLinkedDevices(devices.keySet()); @@ -910,8 +913,8 @@ public void onPrivacySettingChanged(PrivacySettingEntry oldEntry, PrivacySetting }); } - protected void querySessionsForcefully(ContactJid contactJid) { - messageHandler.querySessions(List.of(contactJid), true); + protected void querySessionsForcefully(Jid jid) { + messageHandler.querySessions(List.of(jid), true); } private void dispose() { @@ -946,7 +949,7 @@ protected T handleFailure(Location location, Throwable throwable) { return null; } - public CompletableFuture querySessions(@NonNull ContactJid jid) { + public CompletableFuture querySessions(@NonNull Jid jid) { return messageHandler.getDevices(List.of(jid), true) .thenCompose(values -> messageHandler.querySessions(values, false)); } diff --git a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java index 67649886..535edff0 100644 --- a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java @@ -19,6 +19,9 @@ import it.auties.whatsapp.model.contact.*; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.info.MessageInfoBuilder; +import it.auties.whatsapp.model.jid.JidType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidServer; import it.auties.whatsapp.model.media.MediaConnection; import it.auties.whatsapp.model.message.model.MessageKey; import it.auties.whatsapp.model.message.model.MessageKeyBuilder; @@ -33,7 +36,11 @@ import it.auties.whatsapp.model.response.ChannelResponse; import it.auties.whatsapp.model.response.ContactStatusResponse; import it.auties.whatsapp.model.signal.auth.*; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentityBuilder; +import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentityHMACSpec; +import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentitySpec; +import it.auties.whatsapp.model.signal.auth.DeviceIdentitySpec; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; import it.auties.whatsapp.model.signal.keypair.SignalPreKeyPair; import it.auties.whatsapp.model.sync.PatchType; @@ -152,7 +159,7 @@ private ContactStatus getUpdateType(Node node) { .orElse(ContactStatus.AVAILABLE); } - private void updateContactPresence(ContactJid chatJid, ContactStatus status, ContactJid contact) { + private void updateContactPresence(Jid chatJid, ContactStatus status, Jid contact) { socketHandler.store() .findChatByJid(chatJid) .ifPresent(chat -> socketHandler.onUpdateChatPresence(status, contact, chat)); @@ -161,7 +168,7 @@ private void updateContactPresence(ContactJid chatJid, ContactStatus status, Con private void digestReceipt(Node node) { var chat = node.attributes() .getJid("from") - .filter(jid -> jid.type() != ContactJidType.STATUS) + .filter(jid -> jid.type() != JidType.STATUS) .flatMap(socketHandler.store()::findChatByJid) .orElse(null); getReceiptsMessageIds(node) @@ -311,7 +318,7 @@ private void digestCallAck(Node node) { .forEach(to -> sendRelay(callCreator, callId, to)); } - private void sendRelay(ContactJid callCreator, String callId, ContactJid to) { + private void sendRelay(Jid callCreator, String callId, Jid to) { var values = new byte[][]{ new byte[]{-105, 99, -47, -29, 13, -106}, new byte[]{-99, -16, -53, 62, 13, -106}, @@ -352,7 +359,7 @@ private void handleMexNamespace(Node node) { case "NotificationNewsletterJoin" -> { update.contentAsString() .flatMap(ChannelResponse::ofJson) - .ifPresent(result -> socketHandler.sendQuery("get", "newsletter", Node.of("messages", Map.of("jid", result.jid().withServer(ContactJidServer.WHATSAPP), "count", 1, "type", "Jid")))); + .ifPresent(result -> socketHandler.sendQuery("get", "newsletter", Node.of("messages", Map.of("jid", result.jid().withServer(JidServer.WHATSAPP), "count", 1, "type", "Jid")))); } case "NotificationNewsletterMuteChange" -> { @@ -509,7 +516,7 @@ private void addMessageForGroupStubType(Chat chat, MessageInfo.StubType stubType handleGroupStubType(chat, stubType, participantJid); } - private void handleGroupStubType(Chat chat, MessageInfo.StubType stubType, ContactJid participantJid) { + private void handleGroupStubType(Chat chat, MessageInfo.StubType stubType, Jid participantJid) { switch (stubType){ case GROUP_PARTICIPANT_ADD -> chat.addParticipant(participantJid, GroupRole.USER); case GROUP_PARTICIPANT_REMOVE, GROUP_PARTICIPANT_LEAVE -> chat.removeParticipant(participantJid); @@ -542,7 +549,7 @@ private void handleEncryptNotification(Node node) { var chat = node.attributes() .getJid("from") .orElseThrow(() -> new NoSuchElementException("Missing chat in notification")); - if (!chat.isServerJid(ContactJidServer.WHATSAPP)) { + if (!chat.isServerJid(JidServer.WHATSAPP)) { return; } var keysSize = node.findNode("count") @@ -639,7 +646,7 @@ private CompletableFuture addPrivacySetting(Node node, boolean update) { return CompletableFuture.completedFuture(null); } - private List getUpdatedBlockedList(Node node, PrivacySettingEntry privacyEntry, PrivacySettingValue privacyValue) { + private List getUpdatedBlockedList(Node node, PrivacySettingEntry privacyEntry, PrivacySettingValue privacyValue) { if(privacyValue != PrivacySettingValue.CONTACTS_EXCEPT){ return List.of(); } @@ -659,7 +666,7 @@ private List getUpdatedBlockedList(Node node, PrivacySettingEntry pr return newValues; } - private CompletableFuture> queryPrivacyExcludedContacts(PrivacySettingType type, PrivacySettingValue value) { + private CompletableFuture> queryPrivacyExcludedContacts(PrivacySettingType type, PrivacySettingValue value) { if(value != PrivacySettingValue.CONTACTS_EXCEPT){ return CompletableFuture.completedFuture(List.of()); } @@ -668,7 +675,7 @@ private CompletableFuture> queryPrivacyExcludedContacts(Privacy .thenApplyAsync(this::parsePrivacyExcludedContacts); } - private List parsePrivacyExcludedContacts(Node result) { + private List parsePrivacyExcludedContacts(Node result) { return result.findNode("privacy") .orElseThrow(() -> new NoSuchElementException("Missing privacy in result: %s".formatted(result))) .findNode("list") @@ -758,7 +765,7 @@ private void digestSuccess(Node node) { } private void queryGroups() { - socketHandler.sendQuery(ContactJidServer.GROUP.toJid(), "get", "w:g2", Node.of("participating", Node.of("participants"), Node.of("description"))) + socketHandler.sendQuery(JidServer.GROUP.toJid(), "get", "w:g2", Node.of("participating", Node.of("participants"), Node.of("description"))) .thenAcceptAsync(this::onGroupsQuery); } @@ -978,7 +985,7 @@ private void handleUserPictureChange(URI newPicture, boolean update) { socketHandler.onUserPictureChange(newPicture, oldStatus); } - private void markBlocked(ContactJid entry) { + private void markBlocked(Jid entry) { socketHandler.store().findContactByJid(entry).orElseGet(() -> { var contact = socketHandler.store().addContact(entry); socketHandler.onNewContact(contact); @@ -1011,9 +1018,12 @@ private void sendPing() { if (socketHandler.state() != SocketState.CONNECTED) { return; } + socketHandler.sendQueryWithNoResponse("get", "w:p", Node.of("ping")) .exceptionallyAsync(throwable -> socketHandler.handleFailure(STREAM, throwable)); socketHandler.onSocketEvent(SocketEvent.PING); + socketHandler.store().serialize(true); + socketHandler.keys().serialize(true); } private void createMediaConnection(int tries, Throwable error) { @@ -1124,7 +1134,7 @@ private void onAskedPairingCode(PairingCodeHandler codeHandler, String code) { codeHandler.accept(code); } - private ContactJid getPhoneNumberAsJid() { + private Jid getPhoneNumberAsJid() { return socketHandler.store() .phoneNumber() .map(PhoneNumber::toJid) @@ -1201,7 +1211,7 @@ private void sendConfirmNode(Node node, Node content) { var attributes = Attributes.of() .put("id", node.id()) .put("type", "result") - .put("to", ContactJidServer.WHATSAPP.toJid()) + .put("to", JidServer.WHATSAPP.toJid()) .toMap(); var request = Node.of("iq", attributes, content); socketHandler.sendWithNoResponse(request); @@ -1227,13 +1237,13 @@ private void saveCompanion(Node container) { socketHandler.store().addContact(me); } - private Platform getCompanionOs(String name) { + private UserAgent.PlatformType getCompanionOs(String name) { return switch (name.toLowerCase()) { - case "smba" -> Platform.SMB_ANDROID; - case "smbi" -> Platform.SMB_IOS; - case "android" -> UserAgent.Platform.ANDROID; - case "iphone", "ipad", "ios" -> Platform.IOS; - default -> UserAgent.Platform.UNKNOWN; + case "smba" -> UserAgent.PlatformType.SMB_ANDROID; + case "smbi" -> UserAgent.PlatformType.SMB_IOS; + case "android" -> PlatformType.ANDROID; + case "iphone", "ipad", "ios" -> UserAgent.PlatformType.IOS; + default -> UserAgent.PlatformType.UNKNOWN; }; } diff --git a/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java b/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java new file mode 100644 index 00000000..60c9e04d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/util/ConcurrentDoublyLinkedList.java @@ -0,0 +1,992 @@ +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 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 + */ + +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. + */ + 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 result + * 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 result 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. + */ + +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/FutureReference.java b/src/main/java/it/auties/whatsapp/util/FutureReference.java index cef4f3b1..f3d67120 100644 --- a/src/main/java/it/auties/whatsapp/util/FutureReference.java +++ b/src/main/java/it/auties/whatsapp/util/FutureReference.java @@ -1,8 +1,11 @@ package it.auties.whatsapp.util; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; @@ -11,6 +14,11 @@ public class FutureReference { private T value; private CompletableFuture future; + @JsonCreator + public FutureReference(@Nullable T initialValue) { + this.value = Objects.requireNonNull(initialValue, "Missing value"); + } + public FutureReference(@Nullable T initialValue, Supplier> defaultValue) { this.value = initialValue; if(initialValue == null) { @@ -18,6 +26,7 @@ public FutureReference(@Nullable T initialValue, Supplier> } } + @JsonValue public T value() { if(future != null) { this.value = future.join(); diff --git a/src/main/java/it/auties/whatsapp/util/MetadataHelper.java b/src/main/java/it/auties/whatsapp/util/MetadataHelper.java index 8adf94c8..cb84ece9 100644 --- a/src/main/java/it/auties/whatsapp/util/MetadataHelper.java +++ b/src/main/java/it/auties/whatsapp/util/MetadataHelper.java @@ -2,7 +2,8 @@ import it.auties.whatsapp.crypto.MD5; import it.auties.whatsapp.model.response.WebVersionResponse; -import it.auties.whatsapp.model.signal.auth.UserAgent.Platform; +import it.auties.whatsapp.model.signal.auth.UserAgent; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; import it.auties.whatsapp.model.signal.auth.Version; import it.auties.whatsapp.util.Spec.Whatsapp; import net.dongliu.apk.parser.ByteArrayApkFile; @@ -60,11 +61,11 @@ public static void setAndroidCache(@NonNull Path path) { } } - public static CompletableFuture getVersion(Platform platform) { + public static CompletableFuture getVersion(UserAgent.PlatformType platform) { return getVersion(platform, true); } - public static CompletableFuture getVersion(Platform platform, boolean useJarCache) { + public static CompletableFuture getVersion(UserAgent.PlatformType platform, boolean useJarCache) { return switch (platform) { case WEB, WINDOWS, MACOS -> getWebVersion(); case ANDROID -> getAndroidData(platform.isBusiness(), useJarCache) @@ -97,7 +98,7 @@ private static CompletableFuture getWebVersion() { } } - public static CompletableFuture getToken(long phoneNumber, Platform platform, boolean useJarCache) { + public static CompletableFuture getToken(long phoneNumber, PlatformType platform, boolean useJarCache) { return switch (platform) { case ANDROID -> getAndroidToken(String.valueOf(phoneNumber), platform.isBusiness(), useJarCache); case IOS -> getIosToken(phoneNumber, platform, useJarCache); @@ -105,7 +106,7 @@ public static CompletableFuture getToken(long phoneNumber, Platform plat }; } - private static CompletableFuture getIosToken(long phoneNumber, Platform platform, boolean useJarCache) { + private static CompletableFuture getIosToken(long phoneNumber, UserAgent.PlatformType platform, boolean useJarCache) { return getVersion(platform, useJarCache) .thenApply(version -> getIosToken(phoneNumber, version)); } diff --git a/src/test/java/it/auties/whatsapp/utils/Smile.java b/src/main/java/it/auties/whatsapp/util/Smile.java similarity index 85% rename from src/test/java/it/auties/whatsapp/utils/Smile.java rename to src/main/java/it/auties/whatsapp/util/Smile.java index 66e052e8..14d05f2e 100644 --- a/src/test/java/it/auties/whatsapp/utils/Smile.java +++ b/src/main/java/it/auties/whatsapp/util/Smile.java @@ -1,10 +1,11 @@ -package it.auties.whatsapp.utils; +package it.auties.whatsapp.util; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.smile.databind.SmileMapper; 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.InputStream; @@ -12,7 +13,7 @@ import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; -import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_DEFAULT; +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; import static com.fasterxml.jackson.annotation.PropertyAccessor.*; import static com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; @@ -25,17 +26,19 @@ public final class Smile { static { try { - smile = new SmileMapper() + smile = SmileMapper.builder() + .build() .registerModule(new Jdk8Module()) .registerModule(new JavaTimeModule()) - .setSerializationInclusion(NON_DEFAULT) + .registerModule(new ParameterNamesModule()) + .setSerializationInclusion(NON_NULL) .enable(WRITE_ENUMS_USING_INDEX) .enable(FAIL_ON_EMPTY_BEANS) .enable(ACCEPT_SINGLE_VALUE_AS_ARRAY) .disable(FAIL_ON_UNKNOWN_PROPERTIES) - .setVisibility(ALL, ANY) - .setVisibility(GETTER, NONE) - .setVisibility(IS_GETTER, NONE); + .setVisibility(ALL, NONE) + .setVisibility(CREATOR, ANY) + .setVisibility(FIELD, ANY); } catch (Throwable throwable) { var logger = System.getLogger("Smile"); logger.log(ERROR, "An exception occurred while initializing smile", throwable); diff --git a/src/main/java/it/auties/whatsapp/util/Spec.java b/src/main/java/it/auties/whatsapp/util/Spec.java index fbc09d8a..9204e88c 100644 --- a/src/main/java/it/auties/whatsapp/util/Spec.java +++ b/src/main/java/it/auties/whatsapp/util/Spec.java @@ -41,6 +41,7 @@ public final static class Whatsapp { public static final String MOBILE_IOS_STATIC = "0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM"; public static final int COMPANION_PAIRING_TIMEOUT = 10; public static CompanionDevice DEFAULT_MOBILE_DEVICE = CompanionDevice.android(); + public static int DEFAULT_HISTORY_SIZE = 59206; } public final static class Signal { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2252c5ac..e992da60 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -65,4 +65,5 @@ exports it.auties.whatsapp.model.call; exports it.auties.whatsapp.model.node; exports it.auties.whatsapp.model.button.template.highlyStructured; + exports it.auties.whatsapp.model.jid; } \ No newline at end of file diff --git a/src/test/java/it/auties/whatsapp/Test.java b/src/test/java/it/auties/whatsapp/Test.java new file mode 100644 index 00000000..e2fa9b28 --- /dev/null +++ b/src/test/java/it/auties/whatsapp/Test.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp; + +import it.auties.whatsapp.util.ConcurrentDoublyLinkedList; + +import java.util.Objects; + +public class Test { + public static void main(String[] args) { + var data = new ConcurrentDoublyLinkedList<>(); + data.add("abc"); + System.out.println(Objects.hashCode(data)); + data.add("def"); + System.out.println(Objects.hashCode(data)); + } +} diff --git a/src/test/java/it/auties/whatsapp/github/GithubSecrets.java b/src/test/java/it/auties/whatsapp/github/GithubSecrets.java index c746ddf8..4d7799d7 100644 --- a/src/test/java/it/auties/whatsapp/github/GithubSecrets.java +++ b/src/test/java/it/auties/whatsapp/github/GithubSecrets.java @@ -6,7 +6,7 @@ import it.auties.whatsapp.api.Whatsapp; import it.auties.whatsapp.util.Json; import it.auties.whatsapp.utils.ConfigUtils; -import it.auties.whatsapp.utils.Smile; +import it.auties.whatsapp.util.Smile; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.examples.ByteArrayHandler; diff --git a/src/test/java/it/auties/whatsapp/local/MobileTest.java b/src/test/java/it/auties/whatsapp/local/MobileTest.java index 8a43009f..16fe5d03 100644 --- a/src/test/java/it/auties/whatsapp/local/MobileTest.java +++ b/src/test/java/it/auties/whatsapp/local/MobileTest.java @@ -1,7 +1,7 @@ package it.auties.whatsapp.local; import it.auties.whatsapp.api.Whatsapp; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.mobile.VerificationCodeMethod; import it.auties.whatsapp.model.mobile.VerificationCodeResponse; import org.junit.jupiter.api.Test; @@ -23,7 +23,7 @@ public void run() { .join() .addLoggedInListener(api -> { System.out.println("Connected"); - var call = api.startCall(ContactJid.of("393495089819")).join(); + var call = api.startCall(Jid.of("393495089819")).join(); System.out.println(call); CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS) .execute(() -> api.stopCall(call).join()); diff --git a/src/test/java/it/auties/whatsapp/local/WebTest.java b/src/test/java/it/auties/whatsapp/local/WebTest.java index 1176b997..66d6e0e6 100644 --- a/src/test/java/it/auties/whatsapp/local/WebTest.java +++ b/src/test/java/it/auties/whatsapp/local/WebTest.java @@ -3,6 +3,7 @@ import it.auties.whatsapp.api.QrHandler; import it.auties.whatsapp.api.WebHistoryLength; import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.MessageInfo; import org.junit.jupiter.api.Test; // Just used for testing locally @@ -10,8 +11,8 @@ public class WebTest { @Test public void run() { var whatsapp = Whatsapp.webBuilder() - .newConnection() - .historyLength(WebHistoryLength.ZERO) + .lastConnection() + .historyLength(WebHistoryLength.extended()) .unregistered(QrHandler.toTerminal()) .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) .addFeaturesListener(features -> System.out.printf("Received features: %s%n", features)) @@ -24,7 +25,7 @@ public void run() { .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((api, chat, last) -> System.out.printf("%s now has %s messages: %s%n", chat.name(), chat.messages().size(), !last ? "waiting for more" : "done")) + .addChatMessagesSyncListener((api, chat, last) -> System.out.printf("%s now has %s messages: %s(oldest message: %s)%n", chat.name(), chat.messages().size(), !last ? "waiting for more" : "done", chat.oldestMessage().flatMap(MessageInfo::timestamp).orElse(null))) .addDisconnectedListener(reason -> System.out.printf("Disconnected: %s%n", reason)) .connect() .join(); diff --git a/src/test/java/it/auties/whatsapp/test/ButtonsTest.java b/src/test/java/it/auties/whatsapp/test/ButtonsTest.java index 96a972f7..376066c1 100644 --- a/src/test/java/it/auties/whatsapp/test/ButtonsTest.java +++ b/src/test/java/it/auties/whatsapp/test/ButtonsTest.java @@ -18,7 +18,7 @@ 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.contact.ContactJid; +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.message.button.*; @@ -27,7 +27,7 @@ import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.util.Json; import it.auties.whatsapp.utils.ConfigUtils; -import it.auties.whatsapp.utils.Smile; +import it.auties.whatsapp.util.Smile; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.examples.ByteArrayHandler; import org.junit.jupiter.api.*; @@ -53,7 +53,7 @@ public class ButtonsTest implements Listener { private static Whatsapp api; private static CompletableFuture future; private static CountDownLatch latch; - private static ContactJid contact; + private static Jid contact; private static boolean skip; static { @@ -82,7 +82,7 @@ private void createApi() { } api = Whatsapp.webBuilder() .lastConnection() - .historyLength(WebHistoryLength.ZERO) + .historyLength(WebHistoryLength.zero()) .unregistered(QrHandler.toTerminal()) .addListener(this) .connect() @@ -111,13 +111,13 @@ private T loadGithubParameter(String parameter, Class type) { private void loadConfig() throws IOException { if (GithubActions.isActionsEnvironment()) { log("Loading environment variables..."); - contact = ContactJid.of(System.getenv(GithubActions.CONTACT_NAME)); + contact = Jid.of(System.getenv(GithubActions.CONTACT_NAME)); log("Loaded environment variables..."); return; } log("Loading configuration file..."); var props = ConfigUtils.loadConfiguration(); - contact = ContactJid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); + contact = Jid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); log("Loaded configuration file"); } diff --git a/src/test/resources/RunMobileCITest.java b/src/test/resources/RunMobileCITest.java index 452302da..34e8d689 100644 --- a/src/test/resources/RunMobileCITest.java +++ b/src/test/resources/RunMobileCITest.java @@ -14,7 +14,7 @@ import it.auties.whatsapp.model.chat.*; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.contact.ContactCard; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.message.button.*; @@ -59,8 +59,8 @@ public class RunMobileCITest implements Listener { private static Whatsapp api; private static CompletableFuture future; private static CountDownLatch latch; - private static ContactJid contact; - private static ContactJid group; + private static Jid contact; + private static Jid group; private static boolean skip; static { @@ -121,13 +121,13 @@ private T loadGithubParameter(String parameter, Class type) { private void loadConfig() throws IOException { if (GithubActions.isActionsEnvironment()) { log("Loading environment variables..."); - contact = ContactJid.of(System.getenv(GithubActions.CONTACT_NAME)); + contact = Jid.of(System.getenv(GithubActions.CONTACT_NAME)); log("Loaded environment variables..."); return; } log("Loading configuration file..."); var props = ConfigUtils.loadConfiguration(); - contact = ContactJid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); + contact = Jid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); log("Loaded configuration file"); } @@ -142,7 +142,7 @@ public void testHasWhatsapp() { return; } - var response = api.hasWhatsapp(contact, ContactJid.of("123456789")).join(); + var response = api.hasWhatsapp(contact, Jid.of("123456789")).join(); log("Has whatsapp? %s", response); } @@ -819,7 +819,7 @@ public void testInteractiveMessage() { } log("Sending interactive messages.."); var collectionMessage = InteractiveCollection.builder() - .business(ContactJid.of("15086146312@s.whatsapp.net")) + .business(Jid.of("15086146312@s.whatsapp.net")) .id("15086146312") .version(3) .build(); diff --git a/src/test/resources/RunWebCITest.java b/src/test/resources/RunWebCITest.java index 1b517d06..7476f0fd 100644 --- a/src/test/resources/RunWebCITest.java +++ b/src/test/resources/RunWebCITest.java @@ -15,7 +15,7 @@ import it.auties.whatsapp.model.chat.*; import it.auties.whatsapp.model.contact.Contact; import it.auties.whatsapp.model.contact.ContactCard; -import it.auties.whatsapp.model.contact.ContactJid; +import it.auties.whatsapp.model.jid.Jid; import it.auties.whatsapp.model.contact.ContactStatus; import it.auties.whatsapp.model.info.MessageInfo; import it.auties.whatsapp.model.message.button.*; @@ -58,8 +58,8 @@ public class RunWebCITest implements Listener { private static Whatsapp api; private static CompletableFuture future; private static CountDownLatch latch; - private static ContactJid contact; - private static ContactJid group; + private static Jid contact; + private static Jid group; private static boolean skip; static { @@ -111,13 +111,13 @@ private T loadGithubParameter(String parameter, Class type) { private void loadConfig() throws IOException { if (GithubActions.isActionsEnvironment()) { log("Loading environment variables..."); - contact = ContactJid.of(System.getenv(GithubActions.CONTACT_NAME)); + contact = Jid.of(System.getenv(GithubActions.CONTACT_NAME)); log("Loaded environment variables..."); return; } log("Loading configuration file..."); var props = ConfigUtils.loadConfiguration(); - contact = ContactJid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); + contact = Jid.of(Objects.requireNonNull(props.getProperty("contact"), "Missing contact property in config")); log("Loaded configuration file"); } @@ -132,7 +132,7 @@ public void testHasWhatsapp() { return; } - var response = api.hasWhatsapp(contact, ContactJid.of("123456789")).join(); + var response = api.hasWhatsapp(contact, Jid.of("123456789")).join(); log("Has whatsapp? %s", response); } @@ -833,7 +833,7 @@ public void testInteractiveMessage() { } log("Sending interactive messages.."); var collectionMessage = InteractiveCollection.builder() - .business(ContactJid.of("15086146312@s.whatsapp.net")) + .business(Jid.of("15086146312@s.whatsapp.net")) .id("15086146312") .version(3) .build();