diff --git a/README.md b/README.md index 44801010..d51a0123 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,10 @@ Remember to handle them as your application will terminate without doing anythin Please do not open redundant issues on GitHub because of this. ### How to create a connection +
+ Detailed Walkthrough + + To create a new connection, start by creating a builder with the api you need: - Web ```java @@ -181,47 +185,47 @@ You can now customize the API with these options: There are also platform specific options: 1. Web - - historyLength: The amount of messages to sync from the companion device - ```java - .historyLength(WebHistoryLength.THREE_MONTHS) - ``` -2. Mobile - - device: the device you want to fake (only Android supports business accounts for now, IOS supports only normal accounts): - ```java - .device(CompanionDevice.android()) - ``` - - business: whether you want to create a business account or a standard one + - historyLength: The amount of messages to sync from the companion device ```java - .business(true) - ``` - - businessCategory: the category of your business account - ```java - .businessCategory(new BusinessCategory(id, name)) - ``` - - businessEmail: the email of your business account - ```java - .businessEmail("email@domanin.com") - ``` - - businessWebsite: the website of your business account - ```java - .businessWebsite("https://google.com") - ``` - - businessDescription: the description of your business account - ```java - .businessDescription("A nice description") - ``` - - businessLatitude: the latitude of your business account - ```java - .businessLatitude(37.386051) - ``` - - businessLongitude: the longitude of your business account - ```java - .businessLongitude(-122.083855) - ``` - - businessAddress: the address of your business account - ```java - .businessAddress("1600 Amphitheatre Pkwy, Mountain View") + .historyLength(WebHistoryLength.THREE_MONTHS) ``` +2. Mobile + - device: the device you want to fake: + ```java + .device(CompanionDevice.android(false)) // Standard Android + .device(CompanionDevice.android(true)) //Business android + .device(CompanionDevice.ios(false)) // Standard iOS + .device(CompanionDevice.ios(true)) // Business iOS + .device(CompanionDevice.kaiOs()) // Standard KaiOS + ``` + - businessCategory: the category of your business account + ```java + .businessCategory(new BusinessCategory(id, name)) + ``` + - businessEmail: the email of your business account + ```java + .businessEmail("email@domanin.com") + ``` + - businessWebsite: the website of your business account + ```java + .businessWebsite("https://google.com") + ``` + - businessDescription: the description of your business account + ```java + .businessDescription("A nice description") + ``` + - businessLatitude: the latitude of your business account + ```java + .businessLatitude(37.386051) + ``` + - businessLongitude: the longitude of your business account + ```java + .businessLongitude(-122.083855) + ``` + - businessAddress: the address of your business account + ```java + .businessAddress("1600 Amphitheatre Pkwy, Mountain View") + ``` > **_IMPORTANT:_** All options are serialized: there is no need to specify them again when deserializing an existing session @@ -231,7 +235,7 @@ Finally select the registration status of your session: .registered() ``` - Creates a new unregistered session: this means that the QR code wasn't scanned / the OTP wasn't sent to the companion's phone via SMS/Call/OTP - + If you are using the Web API, you can either register via QR code: ```java .unregistered(QrHandler.toTerminal()) @@ -259,6 +263,67 @@ Now you can connect to your session: ``` to connect to Whatsapp. Remember to handle the result using, for example, `join` to await the connection's result. +
+ +
+ Web QR Pairing Example + + ```java + Whatsapp.webBuilder() // Use the Web api + .lastConnection() // Deserialize the last connection, or create a new one if it doesn't exist + .unregistered(QrHandler.toTerminal()) // Print the QR to the terminal + .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) // Print a message when connected + .addDisconnectedListener(reason -> System.out.printf("Disconnected: %s%n", reason)) // Print a message when disconnected + .addNewChatMessageListener(message -> System.out.printf("New message: %s%n", message.toJson())) // Print a message when a new chat message arrives + .connect() // Connect to Whatsapp asynchronously + .join(); // Await the result + ``` +
+ +
+ Web Pairing Code Example + + ```java + System.out.println("Enter the phone number(include the country code prefix, but no +, spaces or parenthesis):") + var scanner = new Scanner(System.in); + var phoneNumber = scanner.nextLong(); + Whatsapp.webBuilder() // Use the Web api + .lastConnection() // Deserialize the last connection, or create a new one if it doesn't exist + .unregistered(phoneNumber, PairingCodeHandler.toTerminal()) // Print the pairing code to the terminal + .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) // Print a message when connected + .addDisconnectedListener(reason -> System.out.printf("Disconnected: %s%n", reason)) // Print a message when disconnected + .addNewChatMessageListener(message -> System.out.printf("New message: %s%n", message.toJson())) // Print a message when a new chat message arrives + .connect() // Connect to Whatsapp asynchronously + .join(); // Await the result + ``` +
+ +
+ Mobile Example + + ```java + System.out.println("Enter the phone number(include the country code prefix, but no +, spaces or parenthesis):") + var scanner = new Scanner(System.in); + var phoneNumber = scanner.nextLong(); + Whatsapp.mobileBuilder() // Use the Mobile api + .lastConnection() // Deserialize the last connection, or create a new one if it doesn't exist + .device(CompanionDevice.ios(false)) // Use a non-business iOS account + .unregistered() // If the connection was just created, it needs to be registered + .verificationCodeMethod(VerificationCodeMethod.SMS) // If the connection was just created, send an SMS OTP + .verificationCodeSupplier(() -> { // Called when the OTP needs to be sent to Whatsapp + System.out.println("Enter OTP: "); + var scanner = new Scanner(System.in); + return scanner.nextLine(); + }) + .register(phoneNumber) // Register the phone number asynchronously, if necessary + .join() // Await the result + .addLoggedInListener(api -> System.out.printf("Connected: %s%n", api.store().privacySettings())) // Print a message when connected + .addDisconnectedListener(reason -> System.out.printf("Disconnected: %s%n", reason)) // Print a message when disconnected + .addNewChatMessageListener(message -> System.out.printf("New message: %s%n", message.toJson())) // Print a message when a new chat message arrives + .connect() // Connect to Whatsapp asynchronously + .join(); // Await the result + ``` +
### How to close a connection @@ -662,7 +727,7 @@ All types of messages supported by Whatsapp are supported by this library: - Video ```java - var video = VideoMessage.simpleVideoBuilder() // Create a new video message builder + var video = new VideoMessageSimpleBuilder() // Create a new video message builder .media(urlMedia) // Set the video of this message .caption("A nice video") // Set the caption of this message .width(100) // Set the width of the video @@ -674,14 +739,14 @@ All types of messages supported by Whatsapp are supported by this library: - GIF(Video) ```java - var gif = VideoMessage.simpleGifBuilder() // Create a new gif message builder + var gif = new GifMessageSimpleBuilder() // Create a new gif message builder .media(urlMedia) // Set the gif of this message - .caption("A nice video") // Set the caption of this message + .caption("A nice gif") // Set the caption of this message .gifAttribution(VideoMessageAttribution.TENOR) // Set the source of the gif .build(); // Create the message api.sendMessage(chat, gif); ``` - > **_IMPORTANT:_** Whatsapp doesn't support conventional gifs. Instead, videos can be played as gifs if particular attributes are set. This is the reason why the gif builder is under the VideoMessage class. Sending a conventional gif will result in an exception if detected or in undefined behaviour. + > **_IMPORTANT:_** Whatsapp doesn't support conventional gifs. Instead, videos can be played as gifs if particular attributes are set. Sending a conventional gif will result in an exception if detected or in undefined behaviour. - Document @@ -770,26 +835,26 @@ Whatsapp starts sending updates regarding the presence of a contact only when: To force Whatsapp to send these updates use: ``` java -api.subscribeToUserPresence(contact); +api.subscribeToPresence(contact); ``` Then, after the subscribeToUserPresence's future is completed, query again the presence of that contact. ### Query data about a group, or a contact -##### Text status +##### About ``` java -var status = api.queryStatus(contact) // A completable future +var status = api.queryAbout(contact) // A completable future .join() // Wait for the future to complete - .map(ContactStatusResponse::status) // Map the response to its status + .flatMap(ContactAboutResponse::about) // Map the response to its status .orElse(null); // If no status is available yield null ``` ##### Profile picture or chat picture ``` java -var status = api.queryPicture(contact) // A completable future +var picture = api.queryPicture(contact) // A completable future .join() // Wait for the future to complete .orElse(null); // If no picture is available yield null ``` @@ -815,25 +880,25 @@ var starredMessages = chat.starredMessages(); // All the starred messages in a c ##### Mute a chat ``` java -var future = api.mute(chat); +var future = api.muteChat(chat); ``` ##### Unmute a chat ``` java -var future = api.mute(chat); +var future = api.unmuteChat(chat); ``` ##### Archive a chat ``` java -var future = api.archive(chat); +var future = api.archiveChat(chat); ``` ##### Unarchive a chat ``` java -var future = api.unarchive(chat); +var future = api.unarchiveChat(chat); ``` ##### Change ephemeral message status in a chat @@ -845,43 +910,39 @@ var future = api.changeEphemeralTimer(chat, ChatEphemeralTimer.ONE_WEEK); ##### Mark a chat as read ``` java -var future = api.markAsRead(chat); +var future = api.markChatRead(chat); ``` ##### Mark a chat as unread ``` java -var future = api.markAsUnread(chat); +var future = api.markChatUnread(chat); ``` ##### Pin a chat ``` java -var future = api.pin(chat); +var future = api.pinChat(chat); ``` ##### Unpin a chat ``` java -var future = api.unpin(chat); +var future = api.unpinChat(chat); ``` ##### Clear a chat ``` java -var future = api.clear(chat); +var future = api.clearChat(chat, false); ``` -> **_IMPORTANT:_** This method is experimental and may not work - ##### Delete a chat ``` java -var future = api.delete(chat); +var future = api.deleteChat(chat); ``` -> **_IMPORTANT:_** This method is experimental and may not work - ### Change the state of a participant of a group ##### Add a contact to a group @@ -922,16 +983,10 @@ var future = api.changeGroupSubject(group, newName); var future = api.changeGroupDescription(group, newDescription); ``` -##### Change who can send messages in a group - -``` java -var future = api.changeWhoCanSendMessages(group, GroupPolicy.ANYONE); -``` - -##### Change who can edit the metadata/settings in a group +##### Change a setting in a group ``` java -var future = api.changeWhoCanEditInfo(group, GroupPolicy.ANYONE); +var future = api.changeGroupSetting(group, GroupSetting.EDIT_GROUP_INFO, GroupPolicy.ANYONE); ``` ##### Change or remove the picture of a group @@ -963,10 +1018,10 @@ var future = api.queryGroupInviteCode(group); ##### Revoke a group's invite code ``` java -var future = api.revokeGroupInviteCode(group); +var future = api.revokeGroupInvite(group); ``` -##### Query a group's invite code +##### Accept a group invite ``` java var future = api.acceptGroupInvite(inviteCode); @@ -974,19 +1029,19 @@ var future = api.acceptGroupInvite(inviteCode); ### Companions (Mobile api only) -### Link a companion +##### Link a companion ``` java var future = api.linkCompanion(qrCode); ``` -### Unlink a companion +##### Unlink a companion ``` java var future = api.unlinkCompanion(companionJid); ``` -### Unlink all companions +##### Unlink all companions ``` java var future = api.unlinkCompanions(); @@ -994,18 +1049,41 @@ var future = api.unlinkCompanions(); ### 2FA (Mobile api only) -### Enable 2FA +##### Enable 2FA ``` java var future = api.enable2fa("000000", "mail@domain.com"); ``` -### Disable 2FA +##### Disable 2FA ``` java var future = api.disable2fa(); ``` +### Calls (Mobile api only) + +##### Start a call + +``` java +var future = api.startCall(contact); +``` + +> **_IMPORTANT:_** Currently there is no audio/video support + +##### Stop or reject a call + +``` java +var future = api.stopCall(contact); +``` + +### Communities + +> **_IMPORTANT:_** Fully supported, but not documented here. Check the Javadocs. + +### Newsletters + +> **_IMPORTANT:_** Fully supported, but not documented here. Check the Javadocs. Some methods may not be listed here, all contributions are welcomed to this documentation! Some methods may not be supported on the mobile api, please report them, so I can fix them. diff --git a/src/main/java/it/auties/whatsapp/api/AsyncCaptchaCodeSupplier.java b/src/main/java/it/auties/whatsapp/api/AsyncCaptchaCodeSupplier.java deleted file mode 100644 index 6fe531d1..00000000 --- a/src/main/java/it/auties/whatsapp/api/AsyncCaptchaCodeSupplier.java +++ /dev/null @@ -1,21 +0,0 @@ -package it.auties.whatsapp.api; - -import it.auties.whatsapp.model.response.VerificationCodeResponse; - -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; - -/** - * An interface to represent a supplier that returns a code wrapped in a CompletableFuture - */ -public interface AsyncCaptchaCodeSupplier extends Function> { - /** - * Creates an asynchronous supplier from a synchronous one - * - * @param supplier a non-null supplier - * @return a non-null async supplier - */ - static AsyncCaptchaCodeSupplier of(Function supplier) { - return (response) -> CompletableFuture.completedFuture(supplier.apply(response)); - } -} diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java index 93d4b4cb..f4fa5c41 100644 --- a/src/main/java/it/auties/whatsapp/api/Whatsapp.java +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -725,16 +725,16 @@ public CompletableFuture> queryName(JidProvider contactJid) { } private CompletableFuture> queryNameFromServer(JidProvider contactJid) { - var query = new ContactStatusRequest(ChatMessageKey.randomId(), List.of(new ContactStatusRequest.Variable(contactJid.toJid().user(), List.of("STATUS")))); - return socketHandler.sendQuery("get", "w:mex", Node.of("query", Json.writeValueAsBytes(query))) + var query = new UserChosenNameRequest(List.of(new UserChosenNameRequest.Variable(contactJid.toJid().user()))); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6556393721124826"), Json.writeValueAsBytes(query))) .thenApplyAsync(this::parseNameResponse); } private Optional parseNameResponse(Node result) { return result.findNode("result") .flatMap(Node::contentAsString) - .flatMap(ContactStatusResponse::ofJson) - .flatMap(ContactStatusResponse::status); + .flatMap(UserChosenNameResponse::ofJson) + .flatMap(UserChosenNameResponse::name); } /** @@ -743,7 +743,7 @@ private Optional parseNameResponse(Node result) { * @param chat the target contact * @return a CompletableFuture that wraps an optional contact status newsletters */ - public CompletableFuture> queryAbout(JidProvider chat) { + public CompletableFuture> queryAbout(JidProvider chat) { return socketHandler.queryAbout(chat); } diff --git a/src/main/java/it/auties/whatsapp/model/request/ContactStatusRequest.java b/src/main/java/it/auties/whatsapp/model/request/ContactStatusRequest.java deleted file mode 100644 index dc4fa50d..00000000 --- a/src/main/java/it/auties/whatsapp/model/request/ContactStatusRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.auties.whatsapp.model.request; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -public record ContactStatusRequest(String queryId, List variables) { - public record Variable(@JsonProperty("user_id") String userId, List updates) { - - } -} diff --git a/src/main/java/it/auties/whatsapp/model/response/ContactStatusResponse.java b/src/main/java/it/auties/whatsapp/model/response/ContactStatusResponse.java deleted file mode 100644 index 88754e02..00000000 --- a/src/main/java/it/auties/whatsapp/model/response/ContactStatusResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package it.auties.whatsapp.model.response; - -import com.fasterxml.jackson.core.type.TypeReference; -import it.auties.whatsapp.model.node.Node; -import it.auties.whatsapp.util.Clock; -import it.auties.whatsapp.util.Json; - -import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -public record ContactStatusResponse(Optional status, Optional timestamp) { - public static ContactStatusResponse ofNode(Node source) { - return new ContactStatusResponse( - source.contentAsString(), - Clock.parseSeconds(source.attributes().getLong("t")) - ); - } - - @SuppressWarnings("unchecked") - public static Optional ofJson(String json) { - try { - var parsedJson = Json.readValue(json, new TypeReference>() {}); - var data = (Map) parsedJson.get("data"); - var updates = (List) data.get("xwa2_users_updates_since"); - var latestUpdate = (Map) updates.get(0); - var updatesData = (List) latestUpdate.get("updates"); - var latestUpdateData = (Map) updatesData.get(0); - return Optional.of(new ContactStatusResponse(Optional.ofNullable((String) latestUpdateData.get("text")), Optional.empty())); - } catch (Throwable throwable) { - return Optional.empty(); - } - } -} diff --git a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java index 8146eb4c..02f21271 100644 --- a/src/main/java/it/auties/whatsapp/socket/SocketHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/SocketHandler.java @@ -29,7 +29,7 @@ import it.auties.whatsapp.model.node.Node; import it.auties.whatsapp.model.privacy.PrivacySettingEntry; import it.auties.whatsapp.model.request.MessageSendRequest; -import it.auties.whatsapp.model.response.ContactStatusResponse; +import it.auties.whatsapp.model.response.ContactAboutResponse; import it.auties.whatsapp.model.setting.Setting; import it.auties.whatsapp.model.signal.auth.ClientHelloBuilder; import it.auties.whatsapp.model.signal.auth.HandshakeMessageBuilder; @@ -377,10 +377,11 @@ private void onNodeSent(Node node) { }); } - public CompletableFuture> queryAbout(JidProvider chat) { + public CompletableFuture> queryAbout(JidProvider chat) { var query = Node.of("status"); var body = Node.of("user", Map.of("jid", chat.toJid())); - return sendInteractiveQuery(query, body).thenApplyAsync(this::parseStatus); + return sendInteractiveQuery(query, body) + .thenApplyAsync(this::parseAbout); } public CompletableFuture> sendInteractiveQuery(Node queryNode, Node... queryBody) { @@ -392,12 +393,12 @@ public CompletableFuture> sendInteractiveQuery(Node queryNode, Node.. return sendQuery("get", "usync", sync).thenApplyAsync(this::parseQueryResult); } - private Optional parseStatus(List responses) { + private Optional parseAbout(List responses) { return responses.stream() .map(entry -> entry.findNode("status")) .flatMap(Optional::stream) .findFirst() - .map(ContactStatusResponse::ofNode); + .map(ContactAboutResponse::ofNode); } public CompletableFuture sendQuery(String method, String category, Node... body) { diff --git a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java index a2c8b0cd..45449902 100644 --- a/src/main/java/it/auties/whatsapp/socket/StreamHandler.java +++ b/src/main/java/it/auties/whatsapp/socket/StreamHandler.java @@ -1103,12 +1103,12 @@ private CompletableFuture updateUserAbout(boolean update) { .thenAcceptAsync(result -> parseNewAbout(result.orElse(null), update)); } - private void parseNewAbout(ContactStatusResponse result, boolean update) { + private void parseNewAbout(ContactAboutResponse result, boolean update) { if (result == null) { return; } - result.status().ifPresent(about -> { + result.about().ifPresent(about -> { socketHandler.store().setAbout(about); if (!update) { return;