diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 000000000..4ae6d55fa --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,29 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Java CI with Maven + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + - name: Build with Maven + run: mvn -B package --file pom.xml + env: + GITHUB_ACTIONS: true + WHATSAPP_STORE: ${{ secrets.WHATSAPP_STORE }} + WHATSAPP_KEYS: ${{ secrets.WHATSAPP_KEYS }} + WHATSAPP_CONTACT: ${{ secrets.WHATSAPP_CONTACT }} + GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..de4b15720 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +.test/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/CobaltStreamline.iml b/.idea/CobaltStreamline.iml new file mode 100644 index 000000000..6c0de7d44 --- /dev/null +++ b/.idea/CobaltStreamline.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..35a631fea --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..bed2401b8 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 000000000..712ab9d98 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..52d397754 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 000000000..2b63946d5 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..6d3a56651 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..ab140bb7c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Alessandro Autiero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..d173b8f97 --- /dev/null +++ b/README.md @@ -0,0 +1,1086 @@ +# Cobalt | Private Repo + +Whatsapp4j has been renamed to Cobalt to comply with an official request coming from Whatsapp. +The repository's history was cleared to comply with this request, but keep in mind that the project has been actively developed for over two years. +To be clear, this library is not affiliated with Whatsapp LLC in any way. +This is a personal project that I maintain in my free time + +### What is Cobalt + +Cobalt is a library built to interact with Whatsapp. +It can be used with: +1. Whatsapp Web (Companion) +2. Whatsapp Mobile (Personal and Business) + +### Donations + +If you like my work, you can become a sponsor here on GitHub or tip me through: +- [Paypal](https://www.paypal.me/AutiesDevelopment). +- ERC20 address: 0xA7842cDb100fb91718961153149C86e4F4030a76 +- TRC20 address: THiutwmP7GFEz28tLB3k5ivoyTnxrteKoH + +I can also work on sponsored features and/or projects! + +### Java version + +This library was built for [Java 21](https://openjdk.java.net/projects/jdk/21/), the latest LTS. + +### Breaking changes policy + +Until the library doesn't reach release 1.0, there will be major breaking changes between each release. +This is needed to finalize the design of the API. +After this milestone, breaking changes will be present only in major releases. + +### Optimizing memory usage + +If the machine you are hosting this library on has memory constraints, please look into how to tune a JVM. +The easiest thing you can do is use the -Xmx argument to specify the maximum size, in bytes, of the memory allocation pool. +I have written this disclaimer because many new devs tend to get confused by Java's opportunistic memory allocation. + +### Can this library get my device banned? + +While there is no risk in using this library with your main account, keep in mind that Whatsapp has anti-spam measures for their web client. +If you add a participant from a brand-new number to a group, it will most likely get you banned. +If you compile the library yourself, don't run the CI on a brand-new number, or it will get banned for spamming too many requests(the CI has to test that all the library works). +In short, if you use this library without a malicious intent, you will never get banned. + +### How to install + +#### Maven + +```xml + + com.github.auties00 + cobalt + 0.0.2 + +``` + +#### Gradle + +1. Groovy DSL + ```groovy + implementation 'com.github.auties00:cobalt:0.0.2' + ``` + +2. Kotlin DSL + ```kotlin + implementation("com.github.auties00:cobalt:0.0.2") + ``` + +### Javadocs & Documentation + +Javadocs for Whatsapp4j are available [here](https://www.javadoc.io/doc/com.github.auties00/whatsappweb4j/latest/whatsapp4j/index.html). +The documentation for this project reaches most of the publicly available APIs(i.e. public members in exported packages), but sometimes the Javadoc may be incomplete +or some methods could be absent from the project's README. If you find any of the latter, know that even small contributions are welcomed! + +### How to contribute + +As of today, no additional configuration or artifact building is needed to edit this project. +I recommend using the latest version of IntelliJ, though any other IDE should work. +If you are not familiar with git, follow these short tutorials in order: + +1. [Fork this project](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) +2. [Clone the new repo](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) +3. [Create a new branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches#creating-a-branch) +4. Once you have implemented the new + feature, [create a new merge request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request) + +If you are trying to implement a feature that is present on WhatsappWeb's WebClient, for example audio or video calls, +consider using [WhatsappWeb4jRequestAnalyzer](https://github.com/Auties00/whatsappweb4j-request-analyzer), a tool I built for this exact purpose. + +### Disclaimer about async operations +This library heavily depends on async operations using the CompletableFuture construct. +Remember to handle them as your application will terminate without doing anything if the main thread is not executing any task. +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 + Whatsapp.webBuilder() + ``` +- Mobile + ```java + Whatsapp.mobileBuilder() + ``` +If you want to use a custom serializer, specify it: + ```java + .serializer(new CustomControllerSerializer()) + ``` +Now select the type of connection that you need: +- Create a fresh connection + ```java + .newConnection(someUuid) + ``` +- Retrieve a connection by id if available, otherwise create a new one + ```java + .newConnection(someUuid) + ``` +- Retrieve a connection by phone number if available, otherwise create a new one + ```java + .newConnection(phoneNumber) + ``` +- Retrieve a connection by an alias if available, otherwise create a new one + ```java + .newConnection(alias) + ``` +- Retrieve a connection by id if available, otherwise returns an empty Optional + ```java + .newOptionalConnection(someUuid) + ``` +- Retrieve the first connection that was serialized if available, otherwise create a new one + ```java + .firstConnection() + ``` +- Retrieve the first connection that was serialized if available, otherwise returns an empty Optional + ```java + .firstOptionalConnection() + ``` +- Retrieve the last connection that was serialized if available, otherwise create a new one + ```java + .lastConnection() + ``` +- Retrieve the last connection that was serialized if available, otherwise returns an empty Optional + ```java + .lastOptionalConnection() + ``` +You can now customize the API with these options: +- name - The device's name for Whatsapp Web, the push name for Whatsapp's Mobile + ```java + .name("Some Custom Name :)") + ``` +- version - The version of Whatsapp to use + ```java + .version(new Version("x.xx.xx")) + ``` +- autodetectListeners - Whether listeners annotated with `@RegisterListener` should automatically be registered + ```java + .autodetectListeners(true) + ``` +- cacheDetectedListeners - Whether the listeners that were automatically registered should be cached + ```java + .cacheDetectedListeners(true) + ``` +- textPreviewSetting - Whether a media preview should be generated for text messages containing links + ```java + .textPreviewSetting(TextPreviewSetting.ENABLED_WITH_INFERENCE) + ``` +- checkPatchMacs - Whether patch macs coming from app state pulls should be validated + ```java + .checkPatchMacs(checkPatchMacs) + ``` +- proxy - The proxy to use for the socket connection + ```java + .proxy(someProxy) + ``` + +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: + ```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 + +Finally select the registration status of your session: +- Creates a new registered session: this means that the QR code was already scanned / the OTP was already sent to Whatsapp + ```java + .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()) + ``` + or with a pairing code(new feature): + ```java + .unregistered(yourPhoneNumberWithCountryCode, PairingCodeHandler.toTerminal()) + ``` + Otherwise, if you are using the mobile API, you can decide if you want to receive an SMS, a call or an OTP: + ```java + .verificationCodeMethod(VerificationCodeMethod.SMS) + ``` + Then provide a supplier for that verification method: + ```java + .verificationCodeSupplier(() -> yourAsyncOrSyncLogic()) + ``` + Finally, register: + ```java + .register(yourPhoneNumberWithCountryCode) + ``` + +Now you can connect to your session: + ```java + .connect() + ``` +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 + +There are three ways to close a connection: + +1. Disconnect + + ```java + api.disconnect(); + ``` + > **_IMPORTANT:_** The session remains valid for future uses + +2. Reconnect + + ```java + api.reconnect(); + ``` + > **_IMPORTANT:_** The session remains valid for future uses + +3. Log out + + ```java + api.logout(); + ``` + > **_IMPORTANT:_** The session doesn't remain valid for future uses + +### What is a listener and how to register it + +Listeners are crucial to handle events related to Whatsapp and implement logic for your application. +Listeners can be used either as: + +1. Standalone concrete implementation + + If your application is complex enough, + it's preferable to divide your listeners' logic across multiple specialized classes. + To create a new concrete listener, declare a class or record that implements the Listener interface: + + ```java + import it.auties.whatsapp.listener.Listener; + + public class MyListener implements Listener { + @Override + public void onLoggedIn() { + System.out.println("Hello :)"); + } + } + ``` + + Remember to manually register this listener: + + ```java + api.addListener(new MyListener()); + ``` + + Or to register it automatically using the `@RegisterListener` annotation: + + ```java + import it.auties.whatsapp.listener.RegisterListener; + import it.auties.whatsapp.listener.Listener; + + @RegisterListener // Automatically registers this listener + public class MyListener implements Listener { + @Override + public void onLoggedIn() { + System.out.println("Hello :)"); + } + } + ``` + + Listeners often need access to the Whatsapp instance that registered them to, for example, send messages. + If your listener is marked with @RegisterListener and a single argument constructor that takes a Whatsapp instance as a parameter exists, + the latter can be injected automatically, regardless of if your implementation uses a class or a record. + Records, though, are usually more elegant: + + ```java + import it.auties.whatsapp.listener.RegisterListener; + import it.auties.whatsapp.api.Whatsapp; + import it.auties.whatsapp.listener.Listener; + + @RegisterListener // Automatically registers this listener + public record MyListener(Whatsapp api) implements Listener { // A non-null whatsapp instance is injected + @Override + public void onLoggedIn() { + System.out.println("Hello :)"); + } + } + ``` + + > **_IMPORTANT:_** Only non-abstract classes that provide a no arguments constructor or + > a single parameter constructor of type Whatsapp can be registered automatically + + > **_IMPORTANT:_** In some environments @RegisterListener might not work. + > Before opening an issue, try to disable `cacheDetectedListeners`. + +2. Functional interface + + If your application is very simple or only requires this library in small operations, + it's preferable to add a listener using a lambda instead of using full-fledged classes. + To declare a new functional listener, call the method add followed by the name of the listener that you want to implement without the on suffix: + ```java + api.addDisconnectedListener(reason -> System.out.println("Goodbye: " + reason)); + ``` + + All lambda listeners can access the instance of `Whatsapp` that called them: + ```java + api.addDisconnectedListener((whatsapp, reason) -> System.out.println("Goodbye: " + reason)); + ``` + + This is extremely useful if you want to implement a functionality for your application in a compact manner: + ```java + Whatsapp.newConnection() + .addLoggedInListener(() -> System.out.println("Connected")) + .addNewMessageListener((whatsapp, info) -> whatsapp.sendMessage(info.chatJid(), "Automatic answer", info)) + .connect() + .join(); + ``` + +### How to handle serialization + +In the original version of WhatsappWeb, chats, contacts and messages could be queried at any from Whatsapp's servers. +The multi-device implementation, instead, sends all of this information progressively when the connection is initialized for the first time and doesn't allow any subsequent queries to access the latter. +In practice, this means that this data needs to be serialized somewhere. +The same is true for the mobile api. + +By default, this library serializes data regarding a session at `$HOME/.whatsapp4j/[web|mobile]/`. +The data is stored in gzipped .smile files to reduce disk usage. + +If your application needs to serialize data in a different way, for example in a database create a custom implementation of ControllerSerializer. +Then make sure to specify your implementation in the `Whatsapp` builder. +This is explained in the "How to create a connection" section. + +### How to handle session disconnects + +When the session is closed, the onDisconnect method in any listener is invoked. +These are the three reasons that can cause a disconnect: + +1. DISCONNECTED + + A normal disconnection. + This doesn't indicate any error being thrown. + +2. RECONNECT + + The client is being disconnected but only to reopen the connection. + This always happens when the QR is first scanned for example. + +3. LOGGED_OUT + + The client was logged out by itself or by its companion. + By default, no error is thrown if this happens, though this behaviour can be changed easily: + ```java + import it.auties.whatsapp.api.DisconnectReason; + import it.auties.whatsapp.listener.Listener; + + class ThrowOnLogOut implements Listener { + @Override + public void onDisconnected(DisconnectReason reason) { + if (reason != SocketEvent.LOGGED_OUT) { + return; + } + + throw new RuntimeException("Hey, I was logged off :/"); + } + } + ``` + +### How to query chats, contacts, messages and status + +Access the store associated with a connection by calling the store method: +```java +var store = api.store(); +``` + +> **_IMPORTANT:_** When your program first starts up, these fields will be empty. For each type of data, an event is +> fired and listenable using a WhatsappListener + +You can access all the chats that are in memory: + +```java +var chats = store.chats(); +``` + +Or the contacts: + +```java +var contacts = store.contacts(); +``` + +Or even the status: + +```java +var status = store.status(); +``` + +Data can also be easily queried by using these methods: + +- Chats + - Query a chat by its jid + ```java + var chat = store.findChatByJid(jid); + ``` + - Query a chat by its name + ```java + var chat = store.findChatByName(name); + ``` + - Query a chat by a message inside it + ```java + var chat = store.findChatByMessage(message); + ``` + - Query all chats that match a name + ```java + var chats = store.findChatsByName(name); + ``` +- Contacts + - Query a contact by its jid + ```java + var chat = store.findContactByJid(jid); + ``` + - Query a contact by its name + ```java + var contact = store.findContactByName(name); + ``` + - Query all contacts that match a name + ```java + var contacts = store.findContactsByName(name); + ``` +- Media status + - Query status by sender + ```java + var chat = store.findStatusBySender(contact); + ``` + +### How to query other data + +To access information about the companion device: +```java +var companion = store.jid(); +``` +This object is a jid like any other, but it has the device field filled to distinguish it from the main one. +Instead, if you only need the phone number: +```java +var phoneNumber = store.jid().toPhoneNumber(); +``` +All the settings and metadata about the companion is available inside the Store class +```java +var store = api.store(); +``` +Explore of the available methods! + +### How to query cryptographic data + +Access keys store associated with a connection by calling the keys method: +```java +var keys = api.keys(); +``` +There are several methods to access and query cryptographic data, but as it's only necessary for advanced users, +please check the javadocs if this is what you need. + +### How to send messages + +To send a message, start by finding the chat where the message should be sent. Here is an example: + +```java +var chat = api.store() + .findChatByName("My Awesome Friend") + .orElseThrow(() -> new NoSuchElementException("Hey, you don't exist")); +``` + +All types of messages supported by Whatsapp are supported by this library: +> **_IMPORTANT:_** Buttons are not documented here because they are unstable. +> If you are interested you can try to use them, but they are not guaranteed to work. +> There are some examples in the tests directory. + +- Text + + ```java + api.sendMessage(chat, "This is a text message!"); + ``` + +- Complex text + + ```java + var message = new TextMessageBuilder() // Create a new text message + .text("Check this video out: https://www.youtube.com/watch?v=dQw4w9WgXcQ") // Set the text of the message + .canonicalUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ") // Set the url of the message + .matchedText("https://www.youtube.com/watch?v=dQw4w9WgXcQ") // Set the matched text for the url in the message + .title("A nice suprise") // Set the title of the url + .description("Check me out") // Set the description of the url + .build(); // Create the message + api.sendMessage(chat, message); + ``` + +- Location + + ```java + var location = new LocationMessageBuilder() // Create a new location message + .caption("Look at this!") // Set the caption of the message, that is the text below the file + .latitude(38.9193) // Set the longitude of the location to share + .longitude(1183.1389) // Set the latitude of the location to share + .build(); // Create the message + api.sendMessage(chat, location); + ``` + +- Live location + + ```java + var location = new LiveLocationMessageBuilder() // Create a new live location message + .caption("Look at this!") // Set the caption of the message, that is the text below the file. Not available if this message is live + .latitude(38.9193) // Set the longitude of the location to share + .longitude(1183.1389) // Set the latitude of the location to share + .accuracy(10) // Set the accuracy of the location in meters + .speed(12) // Set the speed of the device sharing the location in meter per endTimeStamp + .build(); // Create the message + api.sendMessage(chat, location); + ``` + > **_IMPORTANT:_** Live location updates are not supported by Whatsapp multi-device. No ETA has been given for a fix. + +- Group invite + ```java + var group = api.store() + .findChatByName("Programmers") + .filter(Chat::isGroup) + .orElseThrow(() -> new NoSuchElementException("Hey, you don't exist")); + var inviteCode = api.queryGroupInviteCode(group).join(); + var groupInvite = new GroupInviteMessageBuilder() // Create a new group invite message + .caption("Come join my group of fellow programmers") // Set the caption of this message + .name(group.name()) // Set the name of the group + .groupJid(group.jid())) // Set the jid of the group + .inviteExpiration(ZonedDateTime.now().plusDays(3).toEpochSecond()) // Set the expiration of this invite + .inviteCode(inviteCode) // Set the code of the group + .build(); // Create the message + api.sendMessage(chat, groupInvite); + ``` + +- Contact + ```java + var vcard = new ContactCardBuilder() // Create a new vcard + .name("A nice friend") // Set the name of the contact + .phoneNumber(contact) // Set the phone number of the contact + .build(); // Create the vcard + var contactMessage = new ContactMessageBuilder() // Create a new contact message + .name("A nice friend") // Set the display name of the contact + .vcard(vcard) // Set the vcard(https://en.wikipedia.org/wiki/VCard) of the contact + .build(); // Create the message + api.sendMessage(chat, contactMessage); + ``` + +- Contact array + + ```java + var contactsMessage = new ContactsArrayMessageBuilder() // Create a new contacts array message + .name("A nice friend") // Set the display name of the first contact that this message contains + .contacts(List.of(jack,lucy,jeff)) // Set a list of contact messages that this message wraps + .build(); // Create the message + api.sendMessage(chat, contactsMessage); + ``` + +- Media + + > **_IMPORTANT:_** + > + > The thumbnail for videos and gifs is generated automatically only if ffmpeg is installed on the host machine. + > + > The length of videos, gifs and audios in seconds is computed automatically only if ffprobe is installed on the host machine. + + To send a media, start by reading the content inside a byte array. + You might want to read it from a file: + + ```java + var media = Files.readAllBytes(Path.of("somewhere")); + ``` + + Or from a URL: + + ```java + var media = new URL(url).openStream().readAllBytes(); + ``` + + All medias supported by Whatsapp are supported by this library: + + - Image + + ```java + var image = new ImageMessageSimpleBuilder() // Create a new image message builder + .media(media) // Set the image of this message + .caption("A nice image") // Set the caption of this message + .build(); // Create the message + api.sendMessage(chat, image); + ``` + + - Audio or voice + + ```java + var audio = new AudioMessageSimpleBuilder() // Create a new audio message builder + .media(urlMedia) // Set the audio of this message + .voiceMessage(false) // Set whether this message is a voice message + .build(); // Create the message + api.sendMessage(chat, audio); + ``` + + - Video + + ```java + 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 + .height(100) // Set the height of the video + .build(); // Create the message + api.sendMessage(chat, video); + ``` + + - GIF(Video) + + ```java + var gif = new GifMessageSimpleBuilder() // Create a new gif message builder + .media(urlMedia) // Set the gif 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. Sending a conventional gif will result in an exception if detected or in undefined behaviour. + + - Document + + ```java + var document = new DocumentMessageSimpleBuilder() // Create a new document message builder + .media(urlMedia) // Set the document of this message + .title("A nice pdf") // Set the title of the document + .fileName("pdf-test.pdf") // Set the name of the document + .pageCount(1) // Set the number of pages of the document + .build(); // Create the message + api.sendMessage(chat, document); + ``` +- Reaction + + - Send a reaction + + ```java + var someMessage = ...; // The message to react to + api.sendReaction(someMessage, Emoji.RED_HEART); // Use the Emoji class for a list of all Emojis + ``` + + - Remove a reaction + + ```java + var someMessage = ...; // The message to react to + api.removeReaction(someMessage); // Use the Emoji class for a list of all Emojis + ``` + +### How to wait for replies + +If you want to wait for a single reply, use: +``` java +var response = api.awaitReply(info).join(); +``` + +You can also register a listener, but in many cases the async/await paradigm is easier to use then callback based listeners. + +### How to delete messages + +``` java +var result = api.delete(someMessage, everyone); // Deletes a message for yourself or everyone +``` + +### How to change your status + +To change the status of the client: + +``` java +api.changePresence(true); // online +api.changePresence(false); // offline +``` + +If you want to change the status of your companion, start by choosing the right presence: +These are the allowed values: + +- AVAILABLE +- UNAVAILABLE +- COMPOSING +- RECORDING + +Then, execute this method: + +``` java +api.changePresence(chat, presence); +``` + +> **_IMPORTANT:_** The changePresence method returns a CompletableFuture: remember to handle this async construct if +> needed + +### How to query the last known presence for a contact + +To query the last known status of a Contact, use the following snippet: + +``` java +var lastKnownPresenceOptional = contact.lastKnownPresence(); +``` + +If the returned value is an empty Optional, the last status of the contact is unknown. + +Whatsapp starts sending updates regarding the presence of a contact only when: + +- A message was recently exchanged between you and said contact +- A new message arrives from said contact +- You send a message to said contact + +To force Whatsapp to send these updates use: + +``` java +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 + +##### About + +``` java +var status = api.queryAbout(contact) // A completable future + .join() // Wait for the future to complete + .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 picture = api.queryPicture(contact) // A completable future + .join() // Wait for the future to complete + .orElse(null); // If no picture is available yield null +``` + +##### Group's Metadata + +``` java +var metadata = api.queryGroupMetadata(group); // A completable future + .join(); // Wait for the future to complete +``` + +### Search messages + +``` java +var messages = chat.messages(); // All the messages in a chat +var firstMessage = chat.firstMessage(); // First message in a chat chronologically +var lastMessage = chat.lastMessage(); // Last message in a chat chronologically +var starredMessages = chat.starredMessages(); // All the starred messages in a chat +``` + +### Change the state of a chat + +##### Mute a chat + +``` java +var future = api.muteChat(chat); +``` + +##### Unmute a chat + +``` java +var future = api.unmuteChat(chat); +``` + +##### Archive a chat + +``` java +var future = api.archiveChat(chat); +``` + +##### Unarchive a chat + +``` java +var future = api.unarchiveChat(chat); +``` + +##### Change ephemeral message status in a chat + +``` java +var future = api.changeEphemeralTimer(chat, ChatEphemeralTimer.ONE_WEEK); +``` + +##### Mark a chat as read + +``` java +var future = api.markChatRead(chat); +``` + +##### Mark a chat as unread + +``` java +var future = api.markChatUnread(chat); +``` + +##### Pin a chat + +``` java +var future = api.pinChat(chat); +``` + +##### Unpin a chat + +``` java +var future = api.unpinChat(chat); +``` + +##### Clear a chat + +``` java +var future = api.clearChat(chat, false); +``` + +##### Delete a chat + +``` java +var future = api.deleteChat(chat); +``` + +### Change the state of a participant of a group + +##### Add a contact to a group + +``` java +var future = api.addGroupParticipant(group, contact); +``` + +##### Remove a contact from a group + +``` java +var future = api.removeGroupParticipant(group, contact); +``` + +##### Promote a contact to admin in a group + +``` java +var future = api.promoteGroupParticipant(group, contact); +``` + +##### Demote a contact to user in a group + +``` java +var future = api.demoteGroupParticipant(group, contact); +``` + +### Change the metadata or settings of a group + +##### Change group's name/subject + +``` java +var future = api.changeGroupSubject(group, newName); +``` + +##### Change or remove group's description + +``` java +var future = api.changeGroupDescription(group, newDescription); +``` + +##### Change a setting in a group + +``` java +var future = api.changeGroupSetting(group, GroupSetting.EDIT_GROUP_INFO, GroupPolicy.ANYONE); +``` + +##### Change or remove the picture of a group + +``` java +var future = api.changeGroupPicture(group, img); +``` + +### Other group related methods + +##### Create a group + +``` java +var future = api.createGroup("A nice name :)", friend, friend2); +``` + +##### Leave a group + +``` java +var future = api.leaveGroup(group); +``` + +##### Query a group's invite code + +``` java +var future = api.queryGroupInviteCode(group); +``` + +##### Revoke a group's invite code + +``` java +var future = api.revokeGroupInvite(group); +``` + +##### Accept a group invite + +``` java +var future = api.acceptGroupInvite(inviteCode); +``` + +### Companions (Mobile api only) + +##### Link a companion + +``` java +var future = api.linkCompanion(qrCode); +``` + +##### Unlink a companion + +``` java +var future = api.unlinkCompanion(companionJid); +``` + +##### Unlink all companions + +``` java +var future = api.unlinkCompanions(); +``` + +### 2FA (Mobile api only) + +##### Enable 2FA + +``` java +var future = api.enable2fa("000000", "mail@domain.com"); +``` + +##### 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. +Ideally I'd like all of them to work. diff --git a/ci/WHATSAPP_KEYS.gpg b/ci/WHATSAPP_KEYS.gpg new file mode 100644 index 000000000..d71226773 --- /dev/null +++ b/ci/WHATSAPP_KEYS.gpg @@ -0,0 +1,3926 @@ +-----BEGIN PGP MESSAGE----- +Version: BCPG v1.70 + +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 +nqJtWKb9rJyeLsfKWgLOFlk= +=+T/C +-----END PGP MESSAGE----- +q/7j +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 +=Rv0g +-----END PGP MESSAGE----- +JQTlHyuq+lyX/65X1Ec8KvzQtGPDGk +KPl73RPa+Rlibr1q/zxml5lNem/ipyDC2WOpgK50NzKlxxGeQyV0bSbWOSrAWIDn +9WKS4Id3YZbFYHPFmemwVoZUBA3VHuj1FAIUoyfvDYUyFDlV9JtfuaKlKUB3Ypq3 +2COtTKeQIo21CT4A2Wv5Z4ly30xuqiq/qZvP6Aj5PVdS6hMSysUDHb2cTly5ifdz +K65EqV3+jL+wnJVquWTKpS0Jf4fOaTW1ioajfXLF3FZCUtqoyXIoruSb+uTRzyzs ++2Nk8FSgjbGrsdIavUKVhoEqMVaOIUDdhaoBJYLWEL+nl6TWRvYeOLfX4NxTFJ2f +qfBaKKMDsdFDhOsgvdUe8oT3jEn3rnIXsrrggOFym5FbiRvR/+fpKBQr7c49Oxnm +fDX2WOV0YBj6784SrhNNpY+vHUzTxrcZxAYUnRmXa3m90UJmlLwoqqbBtsV9z2uP +RkOK1BgkbfgBPDlrsGErZWetd4izNqb0d8nKpnCgVH18H2uIcd8dUSmyqWiuvh6v +hVHfCuQ9KgNltgCyvHA3mureRW7JL3VC+oJA5y56r5hHvco6zlLDLh5aKfGHW3su +gjp20CPbdnLQbSdyoW5ddGT2BsDCs6jC26IFaJXkFXXgJZUuf2Gff99pkYA43Zpy +N8G/5b9j/QCaJyv8KgTtU7c30afiTj5EvNNYKUkxKp6p1se65/zQyaP9uouEk2NU +n9lvHyV/Tr0CL9LhD7rpuav/I7Z5VH6/yNJiew/L5UWao3ys+ThAxhF6ap5i42LZ +l14/5WD6CTHDO5fRjRaHt/Zmqnjeez36B6cawznO8E+jUdh0mwHdAmavUGxcubdF +ZWeUA+gkaF7DMI03EkuyfO8qtgV1DjhS0yx1gM9EhzqjaCPQxleUGD97pI4ASDiF +PoL4vSiXeAKkNsm/g5T40lGfrM0yd3b7/xAM5KJedxv2GAklSCBYbIq8a7n4uCcn +QFDkoiLxcbXlZmaQXYQ7D9yHCxIzvU33V5qF7ZBZ3AgV+faHXvAF5i0TaBw2q1UJ +3gSYoW6hkamp8iNhdKNxRTmzJ0lzxv4MFR1+DvxB4I8NGbLMaI+wVDwXn3LXnPmI +8IVc9VbEoGoNxjQF3NAJ3F8TdTi1IJyiCOcdgLD/RFfSA9pGASIPfIDcehwFoUyr +ON9eZvrqoRwhB4lTKWKuwqZi1xUUUA/qu1sUMS2VN8TXiaa/hsrxZa5gE5hMICJg +0ELYYPHKKmEWMrWtf8/o0QLNqAWOlALuoo/iNjUK1TUBp93IZuoLypMfdAXoutr4 +XdW79gJLEysoNiD6MEUsS+NWoXib1nSHqE0kqt1oFPfmEmhDXU1uFAmuDKjfnohI +eFQAc1ur1zqIPdlXzY8NcNGdNtByJKELCJmbZQZej/ZHdVjswEl4JgGzK24y4oK4 +RjeHjWLEUaLScHDwbOrC2zfuL9RpKnDlwGkEKqXCrIQoeLGUq5jtM/HCObjGpQxq +HM6Fnl0Hop8/LjeZ4bTDi/vdYTcVBepXHQO6Z0KmqBGWiFy8QhSNdMow3agjGCdf +8YE/fDjHhhqZKxhL700lQcP3uPvBU7jhBeyTqwirTYoWieieTTeE/CIR3s+bxtAY +bItofXRNqxuqKbm1GBi78/yToRSj8EaaK6kYFBCPYX2DyK+rcCM8exBWlaVbS53A ++bWE2w8K8HV6eFzBtqbFV45HQJWy23YUtiL8JLBuWrzrxOPogDpmx2xp34idjLTj +U0cR9IsSn5qTviGbHgGEiSts+A1c/8l87RvRBsFssjW6o4Ickfr9mb0YujPY27us +MB0qqdpQBce2cVe9nVvO+Kbm/+9lrshIwHyrlU93ozRBNalXSMl8WHasOs5LngFg +Rh9gY5SJqiuO0+kokBp0hZtQJJ4S3l6qdLLlED2W4Hw+VVWaX9Vp9Y/FpZmi6Gam +UsE68+erM2Qi4Gh/JQ8RikARPayKcBEkhgR1K3erSuJphkiDFcFmHoWpD6ouvxul +U/WLEAffQ5IVzAQAyeeARKyp6uHuvUIT4g9rJTZdpXy8nRHSlkWRWQlf6y36pEKT +/2E+Z0f7OJa8aE/+Pfj7mN/CRYZZ65Rt7rsuOJxH6x9U7mWRrE0IpuTwwk3vbah1 +kF8jhlWr6LX46wUe68umnK3nRsLK73th36DVkojJTXCmP9uYhnQdFIQQOxgRoKbE ++GIFwZfqLaoaRL09yM0QpWBQxOspYAskM3tjxm+p+jzi8zKyF20SRX+qDdN4H4Hj +puGKZTBSHpXqEpqmth0D+3q57mZkkzBmvRjoQY6zJ+KVCGpQa1licF/gSdcV+qOg +WHfZrGWFuXDEZspI7oJSfnGoDiUBUtC20Aiui8PeEO0ynKScO+liG5cHtthmTLuB +tpMHOzi7+EVewsf2wXu1DYAm8v2ES3Lt2oIOXXk33cNTWklmVUFEevrevJCz9nep +IvFchD4e3sagGT2OZR1QAZ1AaUy+2yeFetoaZMnV290mC7QIm6eejFyWgR5eR5Xl +UDXVc9hy9X3XqpKnStFLiolpatCuLHbiG95QHzcGoMLD2pqdAEGCT833modvyceo +Pvxm92DFl2UZlMo/7YB3DrYDbB9EDdeblAh20IJWsLgCGxH6RFdk3/HYgh0CRTjC +cX0ka7nniM15KBJWQeu5E9CUQOuy3mkSLg+IskDTASZ578JfI9swzW/7kDa1XOXc +dkk8brJTcbxWAmIfEhiy1CD22UJyzevtoVDPYNfWLsoyIQxcgBrkMG2m2NVTUJSA +MF9J9oVW6D8XF5eHfmx38Ng4Byv+VpctQnFU5RV35kjP7uXXUS0ILUoHDEOxxnsC +ru826wzPxY6q3vh/PTTH7iTh80KSr9xXOmH2H1pDDyyoIH8fZeovSt/HK/K8mKw4 +a1F89brgTdZrj8ELwURBvuDas7KNHX0gMaJsvpAdiykT7AR4kM1tNT3syMr07zh7 +b8MEM4biFDS+TC8+U5bAGZn00VaSbu2vuncnEa6VbElKqpkcT6NxDlh+74mWyQHC +55zSQCicv6NYPtaQox0F03qlSxd27kOOnc9xBKPuKxdBf01kK9HMHiDvJaNDO+/A +MWPb/Nsif0Z8nOZ39WpPAmUGyDW0aM7ZA9lMcbJ4NHFYZw3PUqfUoOn+pKA0/gXN +eULoYc49dowaD7VcVxZxZw9ALahyQi635aEvSqmTTENcBzPrkAYtpbcqqvO9OG/u +SdvaoYIgwsdmz8KfjDWQ5yveGIdGFw5BQE/DTn2B3V80K+geUzC8Khwr+6zIJEcQ +G9fqdsClhPT5eySwVwxMsu1psG4cBf5vEPAhHWSiTBBcXuGJX5lp/CfVF87JyWYs +rvIdgg8ZTzsxBI8cErWmwfnAhlTb3FVO+zCaqsRsl2s/PQ37Yq38UjByMyHAW3by +QNuCrE/EFfVvR4Um2vp/IGpF0/1uhgKkUhaneY3iy1BwuLnRYQzlbLykrz4eva0Y +elCYH9+VqoNTOeb/OxBnu7aWZslnWvDNwCjhAuEUQubEOJuzceGDT8ENWiVVmrag +FQA4qnR2Hz9uQfsmGdC2DXA2IxG0zJfUHRUIcjJWh3JyshyhDz7EED0VkpbOFkuV +AJaaEDo+p+KEUrkyuE1vjwouzSG1b/v7IwbZ6gk2MgnbwleTuwGUtobtdmDg12qj +emfOPatIrl6cNhcV9NoJY9D0fRda5ZcuFcH2+AtsUGp+sUiqFtBUiluCZFw/Tdvb +I7KPFftNugnSZ/2PM+UPPw22QoOi6yJ2pj2noa6nGn2ODM+TQLrJn5WEifWz9ix9 +l2XXse80vaPDyyF82nW4zVgILo7Pa9iH6J9NoCBUKPSSom04vRNOJQpiNRjJmYLL +i2JJ+UgNolumHq8n/szp1oCdB4mpnCP+0jny/hCuxzUHdCS0eHkMt36WiEXMGf29 +V9XLVpW0xcxoumMjAlITeBDmBtVmjpNcI/+MIb2eFkpvtAp35wv2kJxoS0VFnfT6 +sO1g0eRzOph3ZM+AigEutkZf/udKwatmtBucoGQjV/KLnFYnl5gXwR/ueOkaV8gM +5Xdg3Bt3cNISyRBc8iIX5n8GexN0eelqy6R8sclafmf70JS9Is9iY4L/rCBWxD2a +fFMQifLqzvmYL/S2Nz55b7a5G7hhIEu9iq18YS3DQZNhdQ5LzDktGBmyRkmxTemm +kJGfyZ8zXr0mKrD1ues/x/fdEVQkCGMuRI/lKOdHH5YocivQU1NUNmTFk+lz0JUA +MiBJAqi97DK0Jv6NQitkc/zVL7AIy0vsTgSP8oU2MckekIyplYGhwhDTgY5heq/l +cJVBQFP2v7+QR9H6r5dJ3gq3/K5RK1vIki/C3HmYhl/bj0287UNU2fKz9550sKdu +IqD1sTrXuwftdE6nxexovpfIHnSsjS5rgcm9boUfssClAP0g98QJM/hoKWrVkf1A +n0EE7Ue3oy0hHSsEXx0u1AhMBcX0CFy9D/3pp1JGOMS3QTAizaN/IBxuomwXW09Y +kKtXcNObRHTqJwzj+QgpFhZovd5haZOPMwFK9gmbjGcBDJKzsV0KFvfHytvksM5x +yWtYDfmAWiw0JmgP7Vws9mAWu8bcO1J5GQY1wiNEWFsRcmvGwjeuFRKnk2HO4Dhq +0jJ0WelCPc5+eNKQTz3xK/RwwUy254PUkZYx9TYB0psPMLegh9rXBD5KE5ldFGaG +xXYx0RfnxAHkeCEltYbA+oQeyHOjwEvD4D2Z0chmPj4F2h6R8D1hIu48GXWTGKod +LoJg4ABd2mYLqJV0tA69WukG02pdepClOnkMrYJ8hWx0ZCPYSni8T8oa+LHbpTNf +uouLHx5PUkssB7TcU2+QtpRLrZbTXCJwj7353qNY6XZ7Py5opXecup9sc/mk1Wgx +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 +l0mWLKDvK6uE+FiEpsKchO0mua2UXw== +=4+/x +-----END PGP MESSAGE----- diff --git a/ci/WHATSAPP_STORE.gpg b/ci/WHATSAPP_STORE.gpg new file mode 100644 index 000000000..286d35931 --- /dev/null +++ b/ci/WHATSAPP_STORE.gpg @@ -0,0 +1,142 @@ +-----BEGIN PGP MESSAGE----- +Version: BCPG v1.70 + +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 +3g== +=/CEJ +-----END PGP MESSAGE----- diff --git a/cobalt.iml b/cobalt.iml new file mode 100644 index 000000000..339339347 --- /dev/null +++ b/cobalt.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100755 index 000000000..8d937f4c1 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 000000000..f80fbad3e --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..a01098bfa --- /dev/null +++ b/pom.xml @@ -0,0 +1,404 @@ + + + 4.0.0 + com.github.auties00 + cobalt + 0.0.3 + ${project.groupId}:${project.artifactId} + Standalone fully-featured Whatsapp Web API for Java and Kotlin + https://github.com/Auties00/Cobalt + + + Alessandro Autiero + alautiero@gmail.com + + + + + MIT License + https://www.opensource.org/licenses/mit-license.php + repo + + + + https://github.com/Auties00/Cobalt/tree/master + scm:git:https://github.com/Auties00/Cobalt.git + scm:ssh:https://github.com/Auties00/Cobalt.git + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + sign + + + performRelease + true + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven.source.plugin.version} + + + attach-sources + + jar + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven.javadoc.plugin.version} + + + attach-javadocs + + jar + + + + + ${java.version} + true + private + true + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven.gpg.plugin.version} + + + sign-artifacts + verify + + sign + + + ${gpg.keyname} + ${gpg.passphrase} + + --pinentry-mode + loopback + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + ${maven.nexus.plugin.version} + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + + + + uber-jar + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + jar + + + UTF-8 + 21 + 1.2 + 3.0.0-M9 + 3.0.1 + 3.11.0 + 3.2.1 + 3.5.0 + 1.6.13 + 1.70 + 1.0.2 + 3.5.1 + 3.0.4 + 5.10.0-M1 + 5.13.0 + 2.15.2 + 1.1 + 4.8.160 + 1.2 + 5.1.4 + 2.3 + 0.12.1 + 2.2 + 8.13.13 + 2.0.27 + 5.2.3 + 4.1.93.Final + 2.0.7 + 2.20.0 + 2.15.0 + 2.6.10 + + + + + + + org.codehaus.mojo + versions-maven-plugin + ${maven.versions.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${java.version} + ${java.version} + ${java.version} + + + com.github.auties00 + protobuf-serialization-plugin + ${protoc.version} + + + cc.jilt + jilt + ${jilt.version} + + + + -parameters + + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + + + + + + + + + + com.google.zxing + javase + ${zxing.version} + + + com.github.auties00 + qr-terminal + ${qr.terminal.version} + + + + + org.bouncycastle + bcprov-jdk15on + ${bouncy.castle.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bouncy.castle.version} + + + com.github.auties00 + curve25519 + ${curve25519.version} + + + + + com.github.auties00 + protobuf-base + ${protoc.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + + + io.github.classgraph + classgraph + ${classgraph.version} + + + + + com.googlecode.libphonenumber + libphonenumber + ${libphonenumber.version} + + + + + net.dongliu + apk-parser + ${apk.parser.version} + + + + + com.googlecode.plist + dd-plist + 1.27 + + + + + com.github.auties00 + link-preview + ${linkpreview.version} + true + + + org.apache.pdfbox + pdfbox + ${pdfbox.version} + true + + + org.apache.poi + poi-ooxml + ${poi.version} + true + + + org.apache.poi + poi-scratchpad + ${poi.version} + true + + + com.googlecode.ez-vcard + ez-vcard + ${vcard.version} + true + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + com.goterl + lazysodium-java + ${lazysodium.version} + test + + + net.java.dev.jna + jna + ${jna.version} + test + + + org.bouncycastle + bcmail-jdk15on + ${bouncy.castle.version} + test + + + org.bouncycastle + bcpg-jdk15on + ${bouncy.castle.version} + test + + + com.github.javafaker + javafaker + ${faker.version} + test + + + diff --git a/proto/extractor/.gitignore b/proto/extractor/.gitignore new file mode 100644 index 000000000..28f1ba756 --- /dev/null +++ b/proto/extractor/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store \ No newline at end of file diff --git a/proto/extractor/README.md b/proto/extractor/README.md new file mode 100644 index 000000000..fc9e60329 --- /dev/null +++ b/proto/extractor/README.md @@ -0,0 +1,8 @@ +# Proto Extract + +Derived initially from `whatseow`'s proto extract, this version generates a predictable diff friendly protobuf. It also does not rely on a hardcoded set of modules to look for but finds all proto modules on its own and extracts the proto from there. + +## Usage +1. Install dependencies with `yarn` (or `npm install`) +2. `yarn start` +3. The script will update `../WAProto/WAProto.proto` (except if something is broken) diff --git a/proto/extractor/index.js b/proto/extractor/index.js new file mode 100644 index 000000000..3637eb27c --- /dev/null +++ b/proto/extractor/index.js @@ -0,0 +1,382 @@ +const request = require('request-promise-native') +const acorn = require('acorn') +const walk = require('acorn-walk') +const fs = require('fs/promises') + +const addPrefix = (lines, prefix) => lines.map(line => prefix + line) + +const extractAllExpressions = (node) => { + const expressions = [node] + const exp = node.expression + if(exp) { + expressions.push(exp) + } + + if(node.expression?.expressions?.length) { + for(const exp of node.expression?.expressions) { + expressions.push(...extractAllExpressions(exp)) + } + } + + return expressions +} + +async function findAppModules() { + const ua = { + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0', + 'Sec-Fetch-Dest': 'script', + 'Sec-Fetch-Mode': 'no-cors', + 'Sec-Fetch-Site': 'same-origin', + 'Referer': 'https://web.whatsapp.com/', + 'Accept': '*/*', + 'Accept-Language': 'Accept-Language: en-US,en;q=0.5', + } + } + const baseURL = 'https://web.whatsapp.com' + const serviceworker = await request.get(`${baseURL}/serviceworker.js`, ua) + + const versions = [...serviceworker.matchAll(/assets-manifest-([\d\.]+).json/g)].map(r => r[1]) + const version = versions[0] + + let bootstrapQRURL = '' + if(version) { + const asset = await request.get(`${baseURL}/assets-manifest-${version}.json`, ua) + const hashFiles = JSON.parse(asset) + const files = Object.keys(hashFiles) + const app = files.find(f => /^app\./.test(f)) + bootstrapQRURL = `${baseURL}/${app}` + } else { + const index = await request.get(baseURL, ua) + const bootstrapQRID = index.match(/src="\/app.([0-9a-z]{10,}).js"/)[1] + bootstrapQRURL = baseURL + '/app.' + bootstrapQRID + '.js' + } + + console.error('Found source JS URL:', bootstrapQRURL) + + const qrData = await request.get(bootstrapQRURL, ua) + const waVersion = qrData.match(/(?:appVersion:|VERSION_STR=)"(\d\.\d+\.\d+)"/)[1] + console.log('Current version:', waVersion) + // This one list of types is so long that it's split into two JavaScript declarations. + // The module finder below can't handle it, so just patch it manually here. + const patchedQrData = qrData.replace('t.ActionLinkSpec=void 0,t.TemplateButtonSpec', 't.ActionLinkSpec=t.TemplateButtonSpec') + //const patchedQrData = qrData.replace("Spec=void 0,t.", "Spec=t.") + const qrModules = acorn.parse(patchedQrData).body[0].expression.arguments[0].elements[1].properties + + const result = qrModules.filter(m => { + const hasProto = !!m.value.body.body.find(b => { + const expressions = extractAllExpressions(b) + return expressions?.find(e => e?.left?.property?.name === 'internalSpec') + }) + if(hasProto) { + return true + } + }) + + return result +} + +(async() => { + const unspecName = name => name.endsWith('Spec') ? name.slice(0, -4) : name + const unnestName = name => name.split('$').slice(-1)[0] + const getNesting = name => name.split('$').slice(0, -1).join('$') + const makeRenameFunc = () => ( + name => { + name = unspecName(name) + return name// .replaceAll('$', '__') + // return renames[name] ?? unnestName(name) + } + ) + // The constructor IDs that can be used for enum types + // const enumConstructorIDs = [76672, 54302] + + const modules = await findAppModules() + + // Sort modules so that whatsapp module id changes don't change the order in the output protobuf schema + // const modules = [] + // for (const mod of wantedModules) { + // modules.push(unsortedModules.find(node => node.key.value === mod)) + // } + + // find aliases of cross references between the wanted modules + const modulesInfo = {} + const moduleIndentationMap = {} + modules.forEach(({ key, value }) => { + const requiringParam = value.params[2].name + modulesInfo[key.value] = { crossRefs: [] } + walk.simple(value, { + VariableDeclarator(node) { + if(node.init && node.init.type === 'CallExpression' && node.init.callee.name === requiringParam && node.init.arguments.length === 1) { + modulesInfo[key.value].crossRefs.push({ alias: node.id.name, module: node.init.arguments[0].value }) + } + } + }) + }) + + // find all identifiers and, for enums, their array of values + for(const mod of modules) { + const modInfo = modulesInfo[mod.key.value] + const rename = makeRenameFunc(mod.key.value) + + // all identifiers will be initialized to "void 0" (i.e. "undefined") at the start, so capture them here + walk.ancestor(mod, { + UnaryExpression(node, anc) { + if(!modInfo.identifiers && node.operator === 'void') { + const assignments = [] + let i = 1 + anc.reverse() + while(anc[i].type === 'AssignmentExpression') { + assignments.push(anc[i++].left) + } + + const makeBlankIdent = a => { + const key = rename(a.property.name) + const indentation = getNesting(key) + const value = { name: key } + + moduleIndentationMap[key] = moduleIndentationMap[key] || { } + moduleIndentationMap[key].indentation = indentation + + if(indentation.length) { + moduleIndentationMap[indentation] = moduleIndentationMap[indentation] || { } + moduleIndentationMap[indentation].members = moduleIndentationMap[indentation].members || new Set() + moduleIndentationMap[indentation].members.add(key) + } + + return [key, value] + } + + modInfo.identifiers = Object.fromEntries(assignments.map(makeBlankIdent).reverse()) + + } + } + }) + const enumAliases = {} + // enums are defined directly, and both enums and messages get a one-letter alias + walk.simple(mod, { + VariableDeclarator(node) { + if( + node.init?.type === 'CallExpression' + // && enumConstructorIDs.includes(node.init.callee?.arguments?.[0]?.value) + && !!node.init.arguments.length + && node.init.arguments[0].type === 'ObjectExpression' + && node.init.arguments[0].properties.length + ) { + const values = node.init.arguments[0].properties.map(p => ({ + name: p.key.name, + id: p.value.value + })) + enumAliases[node.id.name] = values + } + }, + AssignmentExpression(node) { + if(node.left.type === 'MemberExpression' && modInfo.identifiers[rename(node.left.property.name)]) { + const ident = modInfo.identifiers[rename(node.left.property.name)] + ident.alias = node.right.name + // enumAliases[ident.alias] = enumAliases[ident.alias] || [] + ident.enumValues = enumAliases[ident.alias] + } + }, + }) + } + + // find the contents for all protobuf messages + for(const mod of modules) { + const modInfo = modulesInfo[mod.key.value] + const rename = makeRenameFunc(mod.key.value) + + // message specifications are stored in a "internalSpec" attribute of the respective identifier alias + walk.simple(mod, { + AssignmentExpression(node) { + if(node.left.type === 'MemberExpression' && node.left.property.name === 'internalSpec' && node.right.type === 'ObjectExpression') { + const targetIdent = Object.values(modInfo.identifiers).find(v => v.alias === node.left.object.name) + if(!targetIdent) { + console.warn(`found message specification for unknown identifier alias: ${node.left.object.name}`) + return + } + + // partition spec properties by normal members and constraints (like "__oneofs__") which will be processed afterwards + const constraints = [] + let members = [] + for(const p of node.right.properties) { + p.key.name = p.key.type === 'Identifier' ? p.key.name : p.key.value + const arr = p.key.name.substr(0, 2) === '__' ? constraints : members + arr.push(p) + } + + members = members.map(({ key: { name }, value: { elements } }) => { + let type + const flags = [] + const unwrapBinaryOr = n => (n.type === 'BinaryExpression' && n.operator === '|') ? [].concat(unwrapBinaryOr(n.left), unwrapBinaryOr(n.right)) : [n] + + // find type and flags + unwrapBinaryOr(elements[1]).forEach(m => { + if(m.type === 'MemberExpression' && m.object.type === 'MemberExpression') { + if(m.object.property.name === 'TYPES') { + type = m.property.name.toLowerCase() + } else if(m.object.property.name === 'FLAGS') { + flags.push(m.property.name.toLowerCase()) + } + } + }) + + // determine cross reference name from alias if this member has type "message" or "enum" + if(type === 'message' || type === 'enum') { + const currLoc = ` from member '${name}' of message '${targetIdent.name}'` + if(elements[2].type === 'Identifier') { + type = Object.values(modInfo.identifiers).find(v => v.alias === elements[2].name)?.name + if(!type) { + console.warn(`unable to find reference of alias '${elements[2].name}'` + currLoc) + } + } else if(elements[2].type === 'MemberExpression') { + const crossRef = modInfo.crossRefs.find(r => r.alias === elements[2].object.name) + if(crossRef && modulesInfo[crossRef.module].identifiers[rename(elements[2].property.name)]) { + type = rename(elements[2].property.name) + } else { + console.warn(`unable to find reference of alias to other module '${elements[2].object.name}' or to message ${elements[2].property.name} of this module` + currLoc) + } + } + } + + return { name, id: elements[0].value, type, flags } + }) + + // resolve constraints for members + constraints.forEach(c => { + if(c.key.name === '__oneofs__' && c.value.type === 'ObjectExpression') { + const newOneOfs = c.value.properties.map(p => ({ + name: p.key.name, + type: '__oneof__', + members: p.value.elements.map(e => { + const idx = members.findIndex(m => m.name === e.value) + const member = members[idx] + members.splice(idx, 1) + return member + }) + })) + members.push(...newOneOfs) + } + }) + + targetIdent.members = members + } + } + }) + } + + const decodedProtoMap = { } + const spaceIndent = ' '.repeat(4) + for(const mod of modules) { + const modInfo = modulesInfo[mod.key.value] + const identifiers = Object.values(modInfo.identifiers) + + // enum stringifying function + const stringifyEnum = (ident, overrideName = null) => [].concat( + [`enum ${overrideName || ident.displayName || ident.name} {`], + addPrefix(ident.enumValues.map(v => `${v.name} = ${v.id};`), spaceIndent), + ['}'] + ) + + // message specification member stringifying function + const stringifyMessageSpecMember = (info, completeFlags, parentName = undefined) => { + if(info.type === '__oneof__') { + return [].concat( + [`oneof ${info.name} {`], + addPrefix([].concat(...info.members.map(m => stringifyMessageSpecMember(m, false))), spaceIndent), + ['}'] + ) + } else { + if(info.flags.includes('packed')) { + info.flags.splice(info.flags.indexOf('packed')) + info.packed = ' [packed=true]' + } + + if(completeFlags && info.flags.length === 0) { + info.flags.push('optional') + } + + const ret = [] + const indentation = moduleIndentationMap[info.type]?.indentation + let typeName = unnestName(info.type) + if(indentation !== parentName && indentation) { + typeName = `${indentation.replaceAll('$', '.')}.${typeName}` + } + + // if(info.enumValues) { + // // typeName = unnestName(info.type) + // ret = stringifyEnum(info, typeName) + // } + + ret.push(`${info.flags.join(' ') + (info.flags.length === 0 ? '' : ' ')}${typeName} ${info.name} = ${info.id}${info.packed || ''};`) + return ret + } + } + + // message specification stringifying function + const stringifyMessageSpec = (ident) => { + const members = moduleIndentationMap[ident.name]?.members + const result = [] + result.push( + `message ${ident.displayName || ident.name} {`, + ...addPrefix([].concat(...ident.members.map(m => stringifyMessageSpecMember(m, true, ident.name))), spaceIndent), + ) + + if(members?.size) { + const sortedMembers = Array.from(members).sort() + for(const memberName of sortedMembers) { + let entity = modInfo.identifiers[memberName] + if(entity) { + const displayName = entity.name.slice(ident.name.length + 1) + entity = { ...entity, displayName } + result.push(...addPrefix(getEntity(entity), spaceIndent)) + } else { + console.log('missing nested entity ', memberName) + } + } + } + + result.push('}') + result.push('') + + return result + } + + const getEntity = (v) => { + let result + if(v.members) { + result = stringifyMessageSpec(v) + } else if(v.enumValues?.length) { + result = stringifyEnum(v) + } else { + result = ['// Unknown entity ' + v.name] + } + + return result + } + + const stringifyEntity = v => { + return { + content: getEntity(v).join('\n'), + name: v.name + } + } + + for(const value of identifiers) { + const { name, content } = stringifyEntity(value) + if(!moduleIndentationMap[name]?.indentation?.length) { + decodedProtoMap[name] = content + } + // decodedProtoMap[name] = content + } + } + + // console.log(moduleIndentationMap) + const decodedProto = Object.keys(decodedProtoMap).sort() + const sortedStr = decodedProto.map(d => decodedProtoMap[d]).join('\n') + + const decodedProtoStr = `syntax = "proto2";\n\npackage it.auties.whatsapp.model.unsupported;\n\n\n${sortedStr}` + const destinationPath = '../whatsapp.proto' + await fs.writeFile(destinationPath, decodedProtoStr) + + console.log(`Extracted protobuf schema to "${destinationPath}"`) +})() diff --git a/proto/extractor/package-lock.json b/proto/extractor/package-lock.json new file mode 100644 index 000000000..2df632b85 --- /dev/null +++ b/proto/extractor/package-lock.json @@ -0,0 +1,509 @@ +{ + "name": "whatsapp-web-protobuf-extractor", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "whatsapp-web-protobuf-extractor", + "version": "1.0.0", + "dependencies": { + "acorn": "^6.4.1", + "acorn-walk": "^6.1.1", + "request": "^2.88.0", + "request-promise-core": "^1.1.2", + "request-promise-native": "^1.0.7" + } + }, + "node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM= sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= sha512-a3xHnILGMtk+hDOqNwHzF6e2fNbiMrXZvxKQiEv2MlgQP+pjIOzqAmKYD2mDpXYE/44M7g+n9p2bKkYWDUcXCQ==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= sha512-4Dj8Rf+fQ+/Pn7C5qeEX02op1WfOss3PKTE9Nsop3Dx+6UPxlm1dr/og7o2cRa5hNN07CACr4NFzRLtj/rjWog==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/mime-db": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "dependencies": { + "mime-db": "1.50.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dependencies": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/proto/extractor/package.json b/proto/extractor/package.json new file mode 100644 index 000000000..97ed950a4 --- /dev/null +++ b/proto/extractor/package.json @@ -0,0 +1,15 @@ +{ + "name": "whatsapp-web-protobuf-extractor", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "acorn": "^6.4.1", + "acorn-walk": "^6.1.1", + "request": "^2.88.0", + "request-promise-core": "^1.1.2", + "request-promise-native": "^1.0.7" + } +} diff --git a/proto/extractor/yarn.lock b/proto/extractor/yarn.lock new file mode 100644 index 000000000..61742b2ba --- /dev/null +++ b/proto/extractor/yarn.lock @@ -0,0 +1,352 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +acorn-walk@^6.1.1: + version "6.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +ajv@^6.12.3: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@^1.0.0, assert-plus@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@^1.2.0, extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= sha512-a3xHnILGMtk+hDOqNwHzF6e2fNbiMrXZvxKQiEv2MlgQP+pjIOzqAmKYD2mDpXYE/44M7g+n9p2bKkYWDUcXCQ== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= sha512-4Dj8Rf+fQ+/Pn7C5qeEX02op1WfOss3PKTE9Nsop3Dx+6UPxlm1dr/og7o2cRa5hNN07CACr4NFzRLtj/rjWog== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +lodash@^4.17.19: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +mime-db@1.50.0: + version "1.50.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz" + integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.33" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz" + integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== + dependencies: + mime-db "1.50.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +request-promise-core@^1.1.2, request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== + dependencies: + lodash "^4.17.19" + +request-promise-native@^1.0.7: + version "1.0.9" + resolved "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz" + integrity sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g== + dependencies: + request-promise-core "1.1.4" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.34, request@^2.88.0: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g== + +tough-cookie@^2.3.3, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" diff --git a/proto/signal.proto b/proto/signal.proto new file mode 100644 index 000000000..2b36a720b --- /dev/null +++ b/proto/signal.proto @@ -0,0 +1,151 @@ +syntax = "proto2"; + +message SessionStructure { + message Chain { + optional bytes senderRatchetKey = 1; + optional bytes senderRatchetKeyPrivate = 2; + + message ChainKey { + optional uint32 index = 1; + optional bytes key = 2; + } + + optional ChainKey chainKey = 3; + + message MessageKey { + optional uint32 index = 1; + optional bytes cipherKey = 2; + optional bytes macKey = 3; + optional bytes iv = 4; + } + + repeated MessageKey messageKeys = 4; + } + + message PendingKeyExchange { + optional uint32 sequence = 1; + optional bytes localBaseKey = 2; + optional bytes localBaseKeyPrivate = 3; + optional bytes localRatchetKey = 4; + optional bytes localRatchetKeyPrivate = 5; + optional bytes localIdentityKey = 7; + optional bytes localIdentityKeyPrivate = 8; + } + + message PendingPreKey { + optional uint32 preKeyId = 1; + optional int32 signedPreKeyId = 3; + optional bytes baseKey = 2; + } + + optional uint32 sessionVersion = 1; + optional bytes localIdentityPublic = 2; + optional bytes remoteIdentityPublic = 3; + + optional bytes rootKey = 4; + optional uint32 previousCounter = 5; + + optional Chain senderChain = 6; + repeated Chain receiverChains = 7; + + optional PendingKeyExchange pendingKeyExchange = 8; + optional PendingPreKey pendingPreKey = 9; + + optional uint32 remoteRegistrationId = 10; + optional uint32 localRegistrationId = 11; + + optional bool needsRefresh = 12; + optional bytes aliceBaseKey = 13; +} + +message RecordStructure { + optional SessionStructure currentSession = 1; + repeated SessionStructure previousSessions = 2; +} + +message PreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; +} + +message SignedPreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; + optional bytes signature = 4; + optional fixed64 timestamp = 5; +} + +message IdentityKeyPairStructure { + optional bytes publicKey = 1; + optional bytes privateKey = 2; +} + +message SenderKeyStateStructure { + message SenderChainKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderMessageKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderSigningKey { + optional bytes public = 1; + optional bytes private = 2; + } + + optional uint32 senderKeyId = 1; + optional SenderChainKey senderChainKey = 2; + optional SenderSigningKey senderSigningKey = 3; + repeated SenderMessageKey senderMessageKeys = 4; +} + +message SenderKeyRecordStructure { + repeated SenderKeyStateStructure senderKeyStates = 1; +} + +message SignalMessage { + optional bytes ratchetKey = 1; + optional uint32 counter = 2; + optional uint32 previousCounter = 3; + optional bytes ciphertext = 4; +} + +message PreKeySignalMessage { + optional uint32 registrationId = 5; + optional uint32 preKeyId = 1; + optional uint32 signedPreKeyId = 6; + optional bytes baseKey = 2; + optional bytes identityKey = 3; + optional bytes message = 4; // SignalMessage +} + +message KeyExchangeMessage { + optional uint32 id = 1; + optional bytes baseKey = 2; + optional bytes ratchetKey = 3; + optional bytes identityKey = 4; + optional bytes baseKeySignature = 5; +} + +message SenderKeyMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes ciphertext = 3; +} + +message SenderKeyDistributionMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes chainKey = 3; + optional bytes signingKey = 4; +} + +message DeviceConsistencyCodeMessage { + optional uint32 generation = 1; + optional bytes signature = 2; +} \ No newline at end of file diff --git a/proto/whatsapp.proto b/proto/whatsapp.proto new file mode 100644 index 000000000..9ff3abfc7 --- /dev/null +++ b/proto/whatsapp.proto @@ -0,0 +1,3351 @@ +syntax = "proto2"; + +package it.auties.whatsapp.model.unsupported; + + +message ADVDeviceIdentity { + optional uint32 rawId = 1; + optional uint64 timestamp = 2; + optional uint32 keyIndex = 3; + optional ADVEncryptionType accountType = 4; + optional ADVEncryptionType deviceType = 5; +} + +enum ADVEncryptionType { + E2EE = 0; + HOSTED = 1; +} +message ADVKeyIndexList { + optional uint32 rawId = 1; + optional uint64 timestamp = 2; + optional uint32 currentIndex = 3; + repeated uint32 validIndexes = 4 [packed=true]; + optional ADVEncryptionType accountType = 5; +} + +message ADVSignedDeviceIdentity { + optional bytes details = 1; + optional bytes accountSignatureKey = 2; + optional bytes accountSignature = 3; + optional bytes deviceSignature = 4; +} + +message ADVSignedDeviceIdentityHMAC { + optional bytes details = 1; + optional bytes hmac = 2; + optional ADVEncryptionType accountType = 3; +} + +message ADVSignedKeyIndexList { + optional bytes details = 1; + optional bytes accountSignature = 2; + optional bytes accountSignatureKey = 3; +} + +message ActionLink { + optional string url = 1; + optional string buttonTitle = 2; +} + +message AutoDownloadSettings { + optional bool downloadImages = 1; + optional bool downloadAudio = 2; + optional bool downloadVideo = 3; + optional bool downloadDocuments = 4; +} + +message AvatarUserSettings { + optional string fbid = 1; + optional string password = 2; +} + +message BizAccountLinkInfo { + optional uint64 whatsappBizAcctFbid = 1; + optional string whatsappAcctNumber = 2; + optional uint64 issueTime = 3; + optional HostStorageType hostStorage = 4; + optional AccountType accountType = 5; + enum AccountType { + ENTERPRISE = 0; + } + enum HostStorageType { + ON_PREMISE = 0; + FACEBOOK = 1; + } +} + +message BizAccountPayload { + optional VerifiedNameCertificate vnameCert = 1; + optional bytes bizAcctLinkInfo = 2; +} + +message BizIdentityInfo { + optional VerifiedLevelValue vlevel = 1; + optional VerifiedNameCertificate vnameCert = 2; + optional bool signed = 3; + optional bool revoked = 4; + optional HostStorageType hostStorage = 5; + optional ActualActorsType actualActors = 6; + optional uint64 privacyModeTs = 7; + optional uint64 featureControls = 8; + enum ActualActorsType { + SELF = 0; + BSP = 1; + } + enum HostStorageType { + ON_PREMISE = 0; + FACEBOOK = 1; + } + enum VerifiedLevelValue { + UNKNOWN = 0; + LOW = 1; + HIGH = 2; + } +} + +message BotAvatarMetadata { + optional uint32 sentiment = 1; + optional string behaviorGraph = 2; + optional uint32 action = 3; + optional uint32 intensity = 4; + optional uint32 wordCount = 5; +} + +message BotMetadata { + optional BotAvatarMetadata avatarMetadata = 1; + optional string personaId = 2; + optional BotPluginMetadata pluginMetadata = 3; + optional BotSuggestedPromptMetadata suggestedPromptMetadata = 4; + optional string invokerJid = 5; +} + +message BotPluginMetadata { + optional SearchProvider provider = 1; + optional PluginType pluginType = 2; + optional string thumbnailCdnUrl = 3; + optional string profilePhotoCdnUrl = 4; + optional string searchProviderUrl = 5; + optional uint32 referenceIndex = 6; + optional uint32 expectedLinksCount = 7; + optional uint32 pluginVersion = 8; + optional string searchQuery = 9; + enum PluginType { + REELS = 1; + SEARCH = 2; + } + enum SearchProvider { + BING = 1; + GOOGLE = 2; + } +} + +message BotSuggestedPromptMetadata { + repeated string suggestedPrompts = 1; + optional uint32 selectedPromptIndex = 2; +} + +message CallLogRecord { + optional CallResult callResult = 1; + optional bool isDndMode = 2; + optional SilenceReason silenceReason = 3; + optional int64 duration = 4; + optional int64 startTime = 5; + optional bool isIncoming = 6; + optional bool isVideo = 7; + optional bool isCallLink = 8; + optional string callLinkToken = 9; + optional string scheduledCallId = 10; + optional string callId = 11; + optional string callCreatorJid = 12; + optional string groupJid = 13; + repeated ParticipantInfo participants = 14; + optional CallType callType = 15; + enum CallResult { + CONNECTED = 0; + REJECTED = 1; + CANCELLED = 2; + ACCEPTEDELSEWHERE = 3; + MISSED = 4; + INVALID = 5; + UNAVAILABLE = 6; + UPCOMING = 7; + FAILED = 8; + ABANDONED = 9; + ONGOING = 10; + } + enum CallType { + REGULAR = 0; + SCHEDULED_CALL = 1; + VOICE_CHAT = 2; + } + message ParticipantInfo { + optional string userJid = 1; + optional CallLogRecord.CallResult callResult = 2; + } + + enum SilenceReason { + NONE = 0; + SCHEDULED = 1; + PRIVACY = 2; + LIGHTWEIGHT = 3; + } +} + +message CertChain { + optional NoiseCertificate leaf = 1; + optional NoiseCertificate intermediate = 2; + message NoiseCertificate { + optional bytes details = 1; + optional bytes signature = 2; + message Details { + optional uint32 serial = 1; + optional uint32 issuerSerial = 2; + optional bytes key = 3; + optional uint64 notBefore = 4; + optional uint64 notAfter = 5; + } + + } + +} + +message ChatRowOpaqueData { + optional DraftMessage draftMessage = 1; + message DraftMessage { + optional string text = 1; + optional string omittedUrl = 2; + optional CtwaContextLinkData ctwaContextLinkData = 3; + optional CtwaContextData ctwaContext = 4; + optional int64 timestamp = 5; + message CtwaContextData { + optional string conversionSource = 1; + optional bytes conversionData = 2; + optional string sourceUrl = 3; + optional string sourceId = 4; + optional string sourceType = 5; + optional string title = 6; + optional string description = 7; + optional string thumbnail = 8; + optional string thumbnailUrl = 9; + optional ContextInfoExternalAdReplyInfoMediaType mediaType = 10; + optional string mediaUrl = 11; + optional bool isSuspiciousLink = 12; + enum ContextInfoExternalAdReplyInfoMediaType { + NONE = 0; + IMAGE = 1; + VIDEO = 2; + } + } + + message CtwaContextLinkData { + optional string context = 1; + optional string sourceUrl = 2; + optional string icebreaker = 3; + optional string phone = 4; + } + + } + +} + +message ClientPayload { + optional uint64 username = 1; + optional bool passive = 3; + optional UserAgent userAgent = 5; + optional WebInfo webInfo = 6; + optional string pushName = 7; + optional sfixed32 sessionId = 9; + optional bool shortConnect = 10; + optional ConnectType connectType = 12; + optional ConnectReason connectReason = 13; + repeated int32 shards = 14; + optional DNSSource dnsSource = 15; + optional uint32 connectAttemptCount = 16; + optional uint32 device = 18; + optional DevicePairingRegistrationData devicePairingData = 19; + optional Product product = 20; + optional bytes fbCat = 21; + optional bytes fbUserAgent = 22; + optional bool oc = 23; + optional int32 lc = 24; + optional IOSAppExtension iosAppExtension = 30; + optional uint64 fbAppId = 31; + optional bytes fbDeviceId = 32; + optional bool pull = 33; + optional bytes paddingBytes = 34; + optional int32 yearClass = 36; + optional int32 memClass = 37; + optional InteropData interopData = 38; + enum ConnectReason { + PUSH = 0; + USER_ACTIVATED = 1; + SCHEDULED = 2; + ERROR_RECONNECT = 3; + NETWORK_SWITCH = 4; + PING_RECONNECT = 5; + UNKNOWN = 6; + } + enum ConnectType { + CELLULAR_UNKNOWN = 0; + WIFI_UNKNOWN = 1; + CELLULAR_EDGE = 100; + CELLULAR_IDEN = 101; + CELLULAR_UMTS = 102; + CELLULAR_EVDO = 103; + CELLULAR_GPRS = 104; + CELLULAR_HSDPA = 105; + CELLULAR_HSUPA = 106; + CELLULAR_HSPA = 107; + CELLULAR_CDMA = 108; + CELLULAR_1XRTT = 109; + CELLULAR_EHRPD = 110; + CELLULAR_LTE = 111; + CELLULAR_HSPAP = 112; + } + message DNSSource { + optional DNSResolutionMethod dnsMethod = 15; + optional bool appCached = 16; + enum DNSResolutionMethod { + SYSTEM = 0; + GOOGLE = 1; + HARDCODED = 2; + OVERRIDE = 3; + FALLBACK = 4; + } + } + + message DevicePairingRegistrationData { + optional bytes eRegid = 1; + optional bytes eKeytype = 2; + optional bytes eIdent = 3; + optional bytes eSkeyId = 4; + optional bytes eSkeyVal = 5; + optional bytes eSkeySig = 6; + optional bytes buildHash = 7; + optional bytes deviceProps = 8; + } + + enum IOSAppExtension { + SHARE_EXTENSION = 0; + SERVICE_EXTENSION = 1; + INTENTS_EXTENSION = 2; + } + message InteropData { + optional uint64 accountId = 1; + optional bytes token = 2; + } + + enum Product { + WHATSAPP = 0; + MESSENGER = 1; + INTEROP = 2; + INTEROP_MSGR = 3; + } + message UserAgent { + optional Platform platform = 1; + optional AppVersion appVersion = 2; + optional string mcc = 3; + optional string mnc = 4; + optional string osVersion = 5; + optional string manufacturer = 6; + optional string device = 7; + optional string osBuildNumber = 8; + optional string phoneId = 9; + optional ReleaseChannel releaseChannel = 10; + optional string localeLanguageIso6391 = 11; + optional string localeCountryIso31661Alpha2 = 12; + optional string deviceBoard = 13; + optional string deviceExpId = 14; + optional DeviceType deviceType = 15; + message AppVersion { + optional uint32 primary = 1; + optional uint32 secondary = 2; + optional uint32 tertiary = 3; + optional uint32 quaternary = 4; + optional uint32 quinary = 5; + } + + enum DeviceType { + PHONE = 0; + TABLET = 1; + DESKTOP = 2; + WEARABLE = 3; + VR = 4; + } + enum Platform { + ANDROID = 0; + IOS = 1; + WINDOWS_PHONE = 2; + BLACKBERRY = 3; + BLACKBERRYX = 4; + S40 = 5; + S60 = 6; + PYTHON_CLIENT = 7; + TIZEN = 8; + ENTERPRISE = 9; + SMB_ANDROID = 10; + KAIOS = 11; + SMB_IOS = 12; + WINDOWS = 13; + WEB = 14; + PORTAL = 15; + GREEN_ANDROID = 16; + GREEN_IPHONE = 17; + BLUE_ANDROID = 18; + BLUE_IPHONE = 19; + FBLITE_ANDROID = 20; + MLITE_ANDROID = 21; + IGLITE_ANDROID = 22; + PAGE = 23; + MACOS = 24; + OCULUS_MSG = 25; + OCULUS_CALL = 26; + MILAN = 27; + CAPI = 28; + WEAROS = 29; + ARDEVICE = 30; + VRDEVICE = 31; + BLUE_WEB = 32; + IPAD = 33; + TEST = 34; + SMART_GLASSES = 35; + } + enum ReleaseChannel { + RELEASE = 0; + BETA = 1; + ALPHA = 2; + DEBUG = 3; + } + } + + message WebInfo { + optional string refToken = 1; + optional string version = 2; + optional WebdPayload webdPayload = 3; + optional WebSubPlatform webSubPlatform = 4; + enum WebSubPlatform { + WEB_BROWSER = 0; + APP_STORE = 1; + WIN_STORE = 2; + DARWIN = 3; + WIN32 = 4; + } + message WebdPayload { + optional bool usesParticipantInKey = 1; + optional bool supportsStarredMessages = 2; + optional bool supportsDocumentMessages = 3; + optional bool supportsUrlMessages = 4; + optional bool supportsMediaRetry = 5; + optional bool supportsE2EImage = 6; + optional bool supportsE2EVideo = 7; + optional bool supportsE2EAudio = 8; + optional bool supportsE2EDocument = 9; + optional string documentTypes = 10; + optional bytes features = 11; + } + + } + +} + +message CommentMetadata { + optional MessageKey commentParentKey = 1; + optional uint32 replyCount = 2; +} + +message ContextInfo { + optional string stanzaId = 1; + optional string participant = 2; + optional Message quotedMessage = 3; + optional string remoteJid = 4; + repeated string mentionedJid = 15; + optional string conversionSource = 18; + optional bytes conversionData = 19; + optional uint32 conversionDelaySeconds = 20; + optional uint32 forwardingScore = 21; + optional bool isForwarded = 22; + optional AdReplyInfo quotedAd = 23; + optional MessageKey placeholderKey = 24; + optional uint32 expiration = 25; + optional int64 ephemeralSettingTimestamp = 26; + optional bytes ephemeralSharedSecret = 27; + optional ExternalAdReplyInfo externalAdReply = 28; + optional string entryPointConversionSource = 29; + optional string entryPointConversionApp = 30; + optional uint32 entryPointConversionDelaySeconds = 31; + optional DisappearingMode disappearingMode = 32; + optional ActionLink actionLink = 33; + optional string groupSubject = 34; + optional string parentGroupJid = 35; + optional string trustBannerType = 37; + optional uint32 trustBannerAction = 38; + optional bool isSampled = 39; + repeated GroupMention groupMentions = 40; + optional UTMInfo utm = 41; + optional ForwardedNewsletterMessageInfo forwardedNewsletterMessageInfo = 43; + optional BusinessMessageForwardInfo businessMessageForwardInfo = 44; + optional string smbClientCampaignId = 45; + optional string smbServerCampaignId = 46; + optional DataSharingContext dataSharingContext = 47; + message AdReplyInfo { + optional string advertiserName = 1; + optional MediaType mediaType = 2; + optional bytes jpegThumbnail = 16; + optional string caption = 17; + enum MediaType { + NONE = 0; + IMAGE = 1; + VIDEO = 2; + } + } + + message BusinessMessageForwardInfo { + optional string businessOwnerJid = 1; + } + + message DataSharingContext { + optional bool showMmDisclosure = 1; + } + + message ExternalAdReplyInfo { + optional string title = 1; + optional string body = 2; + optional MediaType mediaType = 3; + optional string thumbnailUrl = 4; + optional string mediaUrl = 5; + optional bytes thumbnail = 6; + optional string sourceType = 7; + optional string sourceId = 8; + optional string sourceUrl = 9; + optional bool containsAutoReply = 10; + optional bool renderLargerThumbnail = 11; + optional bool showAdAttribution = 12; + optional string ctwaClid = 13; + optional string ref = 14; + enum MediaType { + NONE = 0; + IMAGE = 1; + VIDEO = 2; + } + } + + message ForwardedNewsletterMessageInfo { + optional string newsletterJid = 1; + optional int32 serverMessageId = 2; + optional string newsletterName = 3; + optional ContentType contentType = 4; + optional string accessibilityText = 5; + enum ContentType { + UPDATE = 1; + UPDATE_CARD = 2; + LINK_CARD = 3; + } + } + + message UTMInfo { + optional string utmSource = 1; + optional string utmCampaign = 2; + } + +} + +message Conversation { + required string id = 1; + repeated HistorySyncMsg messages = 2; + optional string newJid = 3; + optional string oldJid = 4; + optional uint64 lastMsgTimestamp = 5; + optional uint32 unreadCount = 6; + optional bool readOnly = 7; + optional bool endOfHistoryTransfer = 8; + optional uint32 ephemeralExpiration = 9; + optional int64 ephemeralSettingTimestamp = 10; + optional EndOfHistoryTransferType endOfHistoryTransferType = 11; + optional uint64 conversationTimestamp = 12; + optional string name = 13; + optional string pHash = 14; + optional bool notSpam = 15; + optional bool archived = 16; + optional DisappearingMode disappearingMode = 17; + optional uint32 unreadMentionCount = 18; + optional bool markedAsUnread = 19; + repeated GroupParticipant participant = 20; + optional bytes tcToken = 21; + optional uint64 tcTokenTimestamp = 22; + optional bytes contactPrimaryIdentityKey = 23; + optional uint32 pinned = 24; + optional uint64 muteEndTime = 25; + optional WallpaperSettings wallpaper = 26; + optional MediaVisibility mediaVisibility = 27; + optional uint64 tcTokenSenderTimestamp = 28; + optional bool suspended = 29; + optional bool terminated = 30; + optional uint64 createdAt = 31; + optional string createdBy = 32; + optional string description = 33; + optional bool support = 34; + optional bool isParentGroup = 35; + optional string parentGroupId = 37; + optional bool isDefaultSubgroup = 36; + optional string displayName = 38; + optional string pnJid = 39; + optional bool shareOwnPn = 40; + optional bool pnhDuplicateLidThread = 41; + optional string lidJid = 42; + optional string username = 43; + optional string lidOriginType = 44; + optional uint32 commentsCount = 45; + enum EndOfHistoryTransferType { + COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY = 0; + COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY = 1; + COMPLETE_ON_DEMAND_SYNC_BUT_MORE_MSG_REMAIN_ON_PRIMARY = 2; + } +} + +message DeviceConsistencyCodeMessage { + optional uint32 generation = 1; + optional bytes signature = 2; +} + +message DeviceListMetadata { + optional bytes senderKeyHash = 1; + optional uint64 senderTimestamp = 2; + repeated uint32 senderKeyIndexes = 3 [packed=true]; + optional ADVEncryptionType senderAccountType = 4; + optional ADVEncryptionType receiverAccountType = 5; + optional bytes recipientKeyHash = 8; + optional uint64 recipientTimestamp = 9; + repeated uint32 recipientKeyIndexes = 10 [packed=true]; +} + +message DeviceProps { + optional string os = 1; + optional AppVersion version = 2; + optional PlatformType platformType = 3; + optional bool requireFullSync = 4; + optional HistorySyncConfig historySyncConfig = 5; + message AppVersion { + optional uint32 primary = 1; + optional uint32 secondary = 2; + optional uint32 tertiary = 3; + optional uint32 quaternary = 4; + optional uint32 quinary = 5; + } + + message HistorySyncConfig { + optional uint32 fullSyncDaysLimit = 1; + optional uint32 fullSyncSizeMbLimit = 2; + optional uint32 storageQuotaMb = 3; + optional bool inlineInitialPayloadInE2EeMsg = 4; + optional uint32 recentSyncDaysLimit = 5; + optional bool supportCallLogHistory = 6; + optional bool supportBotUserAgentChatHistory = 7; + optional bool supportCagReactionsAndPolls = 8; + } + + enum PlatformType { + UNKNOWN = 0; + CHROME = 1; + FIREFOX = 2; + IE = 3; + OPERA = 4; + SAFARI = 5; + EDGE = 6; + DESKTOP = 7; + IPAD = 8; + ANDROID_TABLET = 9; + OHANA = 10; + ALOHA = 11; + CATALINA = 12; + TCL_TV = 13; + IOS_PHONE = 14; + IOS_CATALYST = 15; + ANDROID_PHONE = 16; + ANDROID_AMBIGUOUS = 17; + WEAR_OS = 18; + AR_WRIST = 19; + AR_DEVICE = 20; + UWP = 21; + VR = 22; + } +} + +message DisappearingMode { + optional Initiator initiator = 1; + optional Trigger trigger = 2; + optional string initiatorDeviceJid = 3; + optional bool initiatedByMe = 4; + enum Initiator { + CHANGED_IN_CHAT = 0; + INITIATED_BY_ME = 1; + INITIATED_BY_OTHER = 2; + BIZ_UPGRADE_FB_HOSTING = 3; + } + enum Trigger { + UNKNOWN = 0; + CHAT_SETTING = 1; + ACCOUNT_SETTING = 2; + BULK_CHANGE = 3; + BIZ_SUPPORTS_FB_HOSTING = 4; + } +} + +message EphemeralSetting { + optional sfixed32 duration = 1; + optional sfixed64 timestamp = 2; +} + +message EventResponse { + optional MessageKey eventResponseMessageKey = 1; + optional int64 timestampMs = 2; + optional Message.EventResponseMessage eventResponseMessage = 3; + optional bool unread = 4; +} + +message ExitCode { + optional uint64 code = 1; + optional string text = 2; +} + +message ExternalBlobReference { + optional bytes mediaKey = 1; + optional string directPath = 2; + optional string handle = 3; + optional uint64 fileSizeBytes = 4; + optional bytes fileSha256 = 5; + optional bytes fileEncSha256 = 6; +} + +message GlobalSettings { + optional WallpaperSettings lightThemeWallpaper = 1; + optional MediaVisibility mediaVisibility = 2; + optional WallpaperSettings darkThemeWallpaper = 3; + optional AutoDownloadSettings autoDownloadWiFi = 4; + optional AutoDownloadSettings autoDownloadCellular = 5; + optional AutoDownloadSettings autoDownloadRoaming = 6; + optional bool showIndividualNotificationsPreview = 7; + optional bool showGroupNotificationsPreview = 8; + optional int32 disappearingModeDuration = 9; + optional int64 disappearingModeTimestamp = 10; + optional AvatarUserSettings avatarUserSettings = 11; + optional int32 fontSize = 12; + optional bool securityNotifications = 13; + optional bool autoUnarchiveChats = 14; + optional int32 videoQualityMode = 15; + optional int32 photoQualityMode = 16; + optional NotificationSettings individualNotificationSettings = 17; + optional NotificationSettings groupNotificationSettings = 18; +} + +message GroupMention { + optional string groupJid = 1; + optional string groupSubject = 2; +} + +message GroupParticipant { + required string userJid = 1; + optional Rank rank = 2; + enum Rank { + REGULAR = 0; + ADMIN = 1; + SUPERADMIN = 2; + } +} + +message HandshakeMessage { + optional ClientHello clientHello = 2; + optional ServerHello serverHello = 3; + optional ClientFinish clientFinish = 4; + message ClientFinish { + optional bytes static = 1; + optional bytes payload = 2; + } + + message ClientHello { + optional bytes ephemeral = 1; + optional bytes static = 2; + optional bytes payload = 3; + } + + message ServerHello { + optional bytes ephemeral = 1; + optional bytes static = 2; + optional bytes payload = 3; + } + +} + +message HistorySync { + required HistorySyncType syncType = 1; + repeated Conversation conversations = 2; + repeated WebMessageInfo statusV3Messages = 3; + optional uint32 chunkOrder = 5; + optional uint32 progress = 6; + repeated Pushname pushnames = 7; + optional GlobalSettings globalSettings = 8; + optional bytes threadIdUserSecret = 9; + optional uint32 threadDsTimeframeOffset = 10; + repeated StickerMetadata recentStickers = 11; + repeated PastParticipants pastParticipants = 12; + repeated CallLogRecord callLogRecords = 13; + optional BotAIWaitListState aiWaitListState = 14; + repeated PhoneNumberToLIDMapping phoneNumberToLidMappings = 15; + enum BotAIWaitListState { + IN_WAITLIST = 0; + AI_AVAILABLE = 1; + } + enum HistorySyncType { + INITIAL_BOOTSTRAP = 0; + INITIAL_STATUS_V3 = 1; + FULL = 2; + RECENT = 3; + PUSH_NAME = 4; + NON_BLOCKING_DATA = 5; + ON_DEMAND = 6; + } +} + +message HistorySyncMsg { + optional WebMessageInfo message = 1; + optional uint64 msgOrderId = 2; +} + +message HydratedTemplateButton { + optional uint32 index = 4; + oneof hydratedButton { + HydratedTemplateButton.HydratedQuickReplyButton quickReplyButton = 1; + HydratedTemplateButton.HydratedURLButton urlButton = 2; + HydratedTemplateButton.HydratedCallButton callButton = 3; + } + message HydratedCallButton { + optional string displayText = 1; + optional string phoneNumber = 2; + } + + message HydratedQuickReplyButton { + optional string displayText = 1; + optional string id = 2; + } + + message HydratedURLButton { + optional string displayText = 1; + optional string url = 2; + optional string consentedUsersUrl = 3; + optional WebviewPresentationType webviewPresentation = 4; + enum WebviewPresentationType { + FULL = 1; + TALL = 2; + COMPACT = 3; + } + } + +} + +message IdentityKeyPairStructure { + optional bytes publicKey = 1; + optional bytes privateKey = 2; +} + +message InteractiveAnnotation { + repeated Point polygonVertices = 1; + optional bool shouldSkipConfirmation = 4; + oneof action { + Location location = 2; + ContextInfo.ForwardedNewsletterMessageInfo newsletter = 3; + } +} + +message KeepInChat { + optional KeepType keepType = 1; + optional int64 serverTimestamp = 2; + optional MessageKey key = 3; + optional string deviceJid = 4; + optional int64 clientTimestampMs = 5; + optional int64 serverTimestampMs = 6; +} + +enum KeepType { + UNKNOWN = 0; + KEEP_FOR_ALL = 1; + UNDO_KEEP_FOR_ALL = 2; +} +message KeyExchangeMessage { + optional uint32 id = 1; + optional bytes baseKey = 2; + optional bytes ratchetKey = 3; + optional bytes identityKey = 4; + optional bytes baseKeySignature = 5; +} + +message KeyId { + optional bytes id = 1; +} + +message LocalizedName { + optional string lg = 1; + optional string lc = 2; + optional string verifiedName = 3; +} + +message Location { + optional double degreesLatitude = 1; + optional double degreesLongitude = 2; + optional string name = 3; +} + +message MediaData { + optional string localPath = 1; +} + +message MediaEntry { + optional bytes fileSha256 = 1; + optional bytes mediaKey = 2; + optional bytes fileEncSha256 = 3; + optional string directPath = 4; + optional int64 mediaKeyTimestamp = 5; + optional string serverMediaType = 6; + optional bytes uploadToken = 7; + optional bytes validatedTimestamp = 8; + optional bytes sidecar = 9; + optional string objectId = 10; + optional string fbid = 11; + optional DownloadableThumbnail downloadableThumbnail = 12; + optional string handle = 13; + optional string filename = 14; + optional ProgressiveJpegDetails progressiveJpegDetails = 15; + message DownloadableThumbnail { + optional bytes fileSha256 = 1; + optional bytes fileEncSha256 = 2; + optional string directPath = 3; + optional bytes mediaKey = 4; + optional int64 mediaKeyTimestamp = 5; + optional string objectId = 6; + } + + message ProgressiveJpegDetails { + repeated int64 scanLengths = 1; + optional bytes sidecar = 2; + } + +} + +message MediaNotifyMessage { + optional string expressPathUrl = 1; + optional bytes fileEncSha256 = 2; + optional uint64 fileLength = 3; +} + +message MediaRetryNotification { + optional string stanzaId = 1; + optional string directPath = 2; + optional ResultType result = 3; + enum ResultType { + GENERAL_ERROR = 0; + SUCCESS = 1; + NOT_FOUND = 2; + DECRYPTION_ERROR = 3; + } +} + +enum MediaVisibility { + DEFAULT = 0; + OFF = 1; + ON = 2; +} +message Message { + optional string conversation = 1; + optional SenderKeyDistributionMessage senderKeyDistributionMessage = 2; + optional ImageMessage imageMessage = 3; + optional ContactMessage contactMessage = 4; + optional LocationMessage locationMessage = 5; + optional ExtendedTextMessage extendedTextMessage = 6; + optional DocumentMessage documentMessage = 7; + optional AudioMessage audioMessage = 8; + optional VideoMessage videoMessage = 9; + optional Call call = 10; + optional Chat chat = 11; + optional ProtocolMessage protocolMessage = 12; + optional ContactsArrayMessage contactsArrayMessage = 13; + optional HighlyStructuredMessage highlyStructuredMessage = 14; + optional SenderKeyDistributionMessage fastRatchetKeySenderKeyDistributionMessage = 15; + optional SendPaymentMessage sendPaymentMessage = 16; + optional LiveLocationMessage liveLocationMessage = 18; + optional RequestPaymentMessage requestPaymentMessage = 22; + optional DeclinePaymentRequestMessage declinePaymentRequestMessage = 23; + optional CancelPaymentRequestMessage cancelPaymentRequestMessage = 24; + optional TemplateMessage templateMessage = 25; + optional StickerMessage stickerMessage = 26; + optional GroupInviteMessage groupInviteMessage = 28; + optional TemplateButtonReplyMessage templateButtonReplyMessage = 29; + optional ProductMessage productMessage = 30; + optional DeviceSentMessage deviceSentMessage = 31; + optional MessageContextInfo messageContextInfo = 35; + optional ListMessage listMessage = 36; + optional FutureProofMessage viewOnceMessage = 37; + optional OrderMessage orderMessage = 38; + optional ListResponseMessage listResponseMessage = 39; + optional FutureProofMessage ephemeralMessage = 40; + optional InvoiceMessage invoiceMessage = 41; + optional ButtonsMessage buttonsMessage = 42; + optional ButtonsResponseMessage buttonsResponseMessage = 43; + optional PaymentInviteMessage paymentInviteMessage = 44; + optional InteractiveMessage interactiveMessage = 45; + optional ReactionMessage reactionMessage = 46; + optional StickerSyncRMRMessage stickerSyncRmrMessage = 47; + optional InteractiveResponseMessage interactiveResponseMessage = 48; + optional PollCreationMessage pollCreationMessage = 49; + optional PollUpdateMessage pollUpdateMessage = 50; + optional KeepInChatMessage keepInChatMessage = 51; + optional FutureProofMessage documentWithCaptionMessage = 53; + optional RequestPhoneNumberMessage requestPhoneNumberMessage = 54; + optional FutureProofMessage viewOnceMessageV2 = 55; + optional EncReactionMessage encReactionMessage = 56; + optional FutureProofMessage editedMessage = 58; + optional FutureProofMessage viewOnceMessageV2Extension = 59; + optional PollCreationMessage pollCreationMessageV2 = 60; + optional ScheduledCallCreationMessage scheduledCallCreationMessage = 61; + optional FutureProofMessage groupMentionedMessage = 62; + optional PinInChatMessage pinInChatMessage = 63; + optional PollCreationMessage pollCreationMessageV3 = 64; + optional ScheduledCallEditMessage scheduledCallEditMessage = 65; + optional VideoMessage ptvMessage = 66; + optional FutureProofMessage botInvokeMessage = 67; + optional CallLogMessage callLogMesssage = 69; + optional MessageHistoryBundle messageHistoryBundle = 70; + optional EncCommentMessage encCommentMessage = 71; + optional BCallMessage bcallMessage = 72; + optional FutureProofMessage lottieStickerMessage = 74; + optional EventMessage eventMessage = 75; + optional EncEventResponseMessage encEventResponseMessage = 76; + optional CommentMessage commentMessage = 77; + optional NewsletterAdminInviteMessage newsletterAdminInviteMessage = 78; + optional ExtendedTextMessageWithParentKey extendedTextMessageWithParentKey = 79; + optional PlaceholderMessage placeholderMessage = 80; + optional SecretEncryptedMessage secretEncryptedMessage = 82; + message AppStateFatalExceptionNotification { + repeated string collectionNames = 1; + optional int64 timestamp = 2; + } + + message AppStateSyncKey { + optional Message.AppStateSyncKeyId keyId = 1; + optional Message.AppStateSyncKeyData keyData = 2; + } + + message AppStateSyncKeyData { + optional bytes keyData = 1; + optional Message.AppStateSyncKeyFingerprint fingerprint = 2; + optional int64 timestamp = 3; + } + + message AppStateSyncKeyFingerprint { + optional uint32 rawId = 1; + optional uint32 currentIndex = 2; + repeated uint32 deviceIndexes = 3 [packed=true]; + } + + message AppStateSyncKeyId { + optional bytes keyId = 1; + } + + message AppStateSyncKeyRequest { + repeated Message.AppStateSyncKeyId keyIds = 1; + } + + message AppStateSyncKeyShare { + repeated Message.AppStateSyncKey keys = 1; + } + + message AudioMessage { + optional string url = 1; + optional string mimetype = 2; + optional bytes fileSha256 = 3; + optional uint64 fileLength = 4; + optional uint32 seconds = 5; + optional bool ptt = 6; + optional bytes mediaKey = 7; + optional bytes fileEncSha256 = 8; + optional string directPath = 9; + optional int64 mediaKeyTimestamp = 10; + optional ContextInfo contextInfo = 17; + optional bytes streamingSidecar = 18; + optional bytes waveform = 19; + optional fixed32 backgroundArgb = 20; + optional bool viewOnce = 21; + } + + message BCallMessage { + optional string sessionId = 1; + optional MediaType mediaType = 2; + optional bytes masterKey = 3; + optional string caption = 4; + enum MediaType { + UNKNOWN = 0; + AUDIO = 1; + VIDEO = 2; + } + } + + message BotFeedbackMessage { + optional MessageKey messageKey = 1; + optional BotFeedbackKind kind = 2; + optional string text = 3; + optional uint64 kindNegative = 4; + optional uint64 kindPositive = 5; + enum BotFeedbackKind { + BOT_FEEDBACK_POSITIVE = 0; + BOT_FEEDBACK_NEGATIVE_GENERIC = 1; + BOT_FEEDBACK_NEGATIVE_HELPFUL = 2; + BOT_FEEDBACK_NEGATIVE_INTERESTING = 3; + BOT_FEEDBACK_NEGATIVE_ACCURATE = 4; + BOT_FEEDBACK_NEGATIVE_SAFE = 5; + BOT_FEEDBACK_NEGATIVE_OTHER = 6; + BOT_FEEDBACK_NEGATIVE_REFUSED = 7; + BOT_FEEDBACK_NEGATIVE_NOT_VISUALLY_APPEALING = 8; + BOT_FEEDBACK_NEGATIVE_NOT_RELEVANT_TO_TEXT = 9; + } + enum BotFeedbackKindMultipleNegative { + BOT_FEEDBACK_MULTIPLE_NEGATIVE_GENERIC = 1; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_HELPFUL = 2; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_INTERESTING = 4; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_ACCURATE = 8; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_SAFE = 16; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_OTHER = 32; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_REFUSED = 64; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_NOT_VISUALLY_APPEALING = 128; + BOT_FEEDBACK_MULTIPLE_NEGATIVE_NOT_RELEVANT_TO_TEXT = 256; + } + enum BotFeedbackKindMultiplePositive { + BOT_FEEDBACK_MULTIPLE_POSITIVE_GENERIC = 1; + } + } + + message ButtonsMessage { + optional string contentText = 6; + optional string footerText = 7; + optional ContextInfo contextInfo = 8; + repeated Button buttons = 9; + optional HeaderType headerType = 10; + oneof header { + string text = 1; + Message.DocumentMessage documentMessage = 2; + Message.ImageMessage imageMessage = 3; + Message.VideoMessage videoMessage = 4; + Message.LocationMessage locationMessage = 5; + } + message Button { + optional string buttonId = 1; + optional ButtonText buttonText = 2; + optional Type type = 3; + optional NativeFlowInfo nativeFlowInfo = 4; + message ButtonText { + optional string displayText = 1; + } + + message NativeFlowInfo { + optional string name = 1; + optional string paramsJson = 2; + } + + enum Type { + UNKNOWN = 0; + RESPONSE = 1; + NATIVE_FLOW = 2; + } + } + + enum HeaderType { + UNKNOWN = 0; + EMPTY = 1; + TEXT = 2; + DOCUMENT = 3; + IMAGE = 4; + VIDEO = 5; + LOCATION = 6; + } + } + + message ButtonsResponseMessage { + optional string selectedButtonId = 1; + optional ContextInfo contextInfo = 3; + optional Type type = 4; + oneof response { + string selectedDisplayText = 2; + } + enum Type { + UNKNOWN = 0; + DISPLAY_TEXT = 1; + } + } + + message Call { + optional bytes callKey = 1; + optional string conversionSource = 2; + optional bytes conversionData = 3; + optional uint32 conversionDelaySeconds = 4; + } + + message CallLogMessage { + optional bool isVideo = 1; + optional CallOutcome callOutcome = 2; + optional int64 durationSecs = 3; + optional CallType callType = 4; + repeated CallParticipant participants = 5; + enum CallOutcome { + CONNECTED = 0; + MISSED = 1; + FAILED = 2; + REJECTED = 3; + ACCEPTED_ELSEWHERE = 4; + ONGOING = 5; + SILENCED_BY_DND = 6; + SILENCED_UNKNOWN_CALLER = 7; + } + message CallParticipant { + optional string jid = 1; + optional Message.CallLogMessage.CallOutcome callOutcome = 2; + } + + enum CallType { + REGULAR = 0; + SCHEDULED_CALL = 1; + VOICE_CHAT = 2; + } + } + + message CancelPaymentRequestMessage { + optional MessageKey key = 1; + } + + message Chat { + optional string displayName = 1; + optional string id = 2; + } + + message CommentMessage { + optional Message message = 1; + optional MessageKey targetMessageKey = 2; + } + + message ContactMessage { + optional string displayName = 1; + optional string vcard = 16; + optional ContextInfo contextInfo = 17; + } + + message ContactsArrayMessage { + optional string displayName = 1; + repeated Message.ContactMessage contacts = 2; + optional ContextInfo contextInfo = 17; + } + + message DeclinePaymentRequestMessage { + optional MessageKey key = 1; + } + + message DeviceSentMessage { + optional string destinationJid = 1; + optional Message message = 2; + optional string phash = 3; + } + + message DocumentMessage { + optional string url = 1; + optional string mimetype = 2; + optional string title = 3; + optional bytes fileSha256 = 4; + optional uint64 fileLength = 5; + optional uint32 pageCount = 6; + optional bytes mediaKey = 7; + optional string fileName = 8; + optional bytes fileEncSha256 = 9; + optional string directPath = 10; + optional int64 mediaKeyTimestamp = 11; + optional bool contactVcard = 12; + optional string thumbnailDirectPath = 13; + optional bytes thumbnailSha256 = 14; + optional bytes thumbnailEncSha256 = 15; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + optional uint32 thumbnailHeight = 18; + optional uint32 thumbnailWidth = 19; + optional string caption = 20; + } + + message EncCommentMessage { + optional MessageKey targetMessageKey = 1; + optional bytes encPayload = 2; + optional bytes encIv = 3; + } + + message EncEventResponseMessage { + optional MessageKey eventCreationMessageKey = 1; + optional bytes encPayload = 2; + optional bytes encIv = 3; + } + + message EncReactionMessage { + optional MessageKey targetMessageKey = 1; + optional bytes encPayload = 2; + optional bytes encIv = 3; + } + + message EventMessage { + optional ContextInfo contextInfo = 1; + optional bool isCanceled = 2; + optional string name = 3; + optional string description = 4; + optional Message.LocationMessage location = 5; + optional string joinLink = 6; + optional int64 startTime = 7; + } + + message EventResponseMessage { + optional EventResponseType response = 1; + optional int64 timestampMs = 2; + enum EventResponseType { + UNKNOWN = 0; + GOING = 1; + NOT_GOING = 2; + } + } + + message ExtendedTextMessage { + optional string text = 1; + optional string matchedText = 2; + optional string canonicalUrl = 4; + optional string description = 5; + optional string title = 6; + optional fixed32 textArgb = 7; + optional fixed32 backgroundArgb = 8; + optional FontType font = 9; + optional PreviewType previewType = 10; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + optional bool doNotPlayInline = 18; + optional string thumbnailDirectPath = 19; + optional bytes thumbnailSha256 = 20; + optional bytes thumbnailEncSha256 = 21; + optional bytes mediaKey = 22; + optional int64 mediaKeyTimestamp = 23; + optional uint32 thumbnailHeight = 24; + optional uint32 thumbnailWidth = 25; + optional InviteLinkGroupType inviteLinkGroupType = 26; + optional string inviteLinkParentGroupSubjectV2 = 27; + optional bytes inviteLinkParentGroupThumbnailV2 = 28; + optional InviteLinkGroupType inviteLinkGroupTypeV2 = 29; + optional bool viewOnce = 30; + enum FontType { + SYSTEM = 0; + SYSTEM_TEXT = 1; + FB_SCRIPT = 2; + SYSTEM_BOLD = 6; + MORNINGBREEZE_REGULAR = 7; + CALISTOGA_REGULAR = 8; + EXO2_EXTRABOLD = 9; + COURIERPRIME_BOLD = 10; + } + enum InviteLinkGroupType { + DEFAULT = 0; + PARENT = 1; + SUB = 2; + DEFAULT_SUB = 3; + } + enum PreviewType { + NONE = 0; + VIDEO = 1; + PLACEHOLDER = 4; + IMAGE = 5; + } + } + + message ExtendedTextMessageWithParentKey { + optional MessageKey key = 1; + optional Message.ExtendedTextMessage extendedTextMessage = 2; + } + + message FutureProofMessage { + optional Message message = 1; + } + + message GroupInviteMessage { + optional string groupJid = 1; + optional string inviteCode = 2; + optional int64 inviteExpiration = 3; + optional string groupName = 4; + optional bytes jpegThumbnail = 5; + optional string caption = 6; + optional ContextInfo contextInfo = 7; + optional GroupType groupType = 8; + enum GroupType { + DEFAULT = 0; + PARENT = 1; + } + } + + message HighlyStructuredMessage { + optional string namespace = 1; + optional string elementName = 2; + repeated string params = 3; + optional string fallbackLg = 4; + optional string fallbackLc = 5; + repeated HSMLocalizableParameter localizableParams = 6; + optional string deterministicLg = 7; + optional string deterministicLc = 8; + optional Message.TemplateMessage hydratedHsm = 9; + message HSMLocalizableParameter { + optional string default = 1; + oneof paramOneof { + Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMCurrency currency = 2; + Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime dateTime = 3; + } + message HSMCurrency { + optional string currencyCode = 1; + optional int64 amount1000 = 2; + } + + message HSMDateTime { + oneof datetimeOneof { + Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent component = 1; + Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeUnixEpoch unixEpoch = 2; + } + message HSMDateTimeComponent { + optional DayOfWeekType dayOfWeek = 1; + optional uint32 year = 2; + optional uint32 month = 3; + optional uint32 dayOfMonth = 4; + optional uint32 hour = 5; + optional uint32 minute = 6; + optional CalendarType calendar = 7; + enum CalendarType { + GREGORIAN = 1; + SOLAR_HIJRI = 2; + } + enum DayOfWeekType { + MONDAY = 1; + TUESDAY = 2; + WEDNESDAY = 3; + THURSDAY = 4; + FRIDAY = 5; + SATURDAY = 6; + SUNDAY = 7; + } + } + + message HSMDateTimeUnixEpoch { + optional int64 timestamp = 1; + } + + } + + } + + } + + message HistorySyncNotification { + optional bytes fileSha256 = 1; + optional uint64 fileLength = 2; + optional bytes mediaKey = 3; + optional bytes fileEncSha256 = 4; + optional string directPath = 5; + optional HistorySyncType syncType = 6; + optional uint32 chunkOrder = 7; + optional string originalMessageId = 8; + optional uint32 progress = 9; + optional int64 oldestMsgInChunkTimestampSec = 10; + optional bytes initialHistBootstrapInlinePayload = 11; + optional string peerDataRequestSessionId = 12; + enum HistorySyncType { + INITIAL_BOOTSTRAP = 0; + INITIAL_STATUS_V3 = 1; + FULL = 2; + RECENT = 3; + PUSH_NAME = 4; + NON_BLOCKING_DATA = 5; + ON_DEMAND = 6; + } + } + + message ImageMessage { + optional string url = 1; + optional string mimetype = 2; + optional string caption = 3; + optional bytes fileSha256 = 4; + optional uint64 fileLength = 5; + optional uint32 height = 6; + optional uint32 width = 7; + optional bytes mediaKey = 8; + optional bytes fileEncSha256 = 9; + repeated InteractiveAnnotation interactiveAnnotations = 10; + optional string directPath = 11; + optional int64 mediaKeyTimestamp = 12; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + optional bytes firstScanSidecar = 18; + optional uint32 firstScanLength = 19; + optional uint32 experimentGroupId = 20; + optional bytes scansSidecar = 21; + repeated uint32 scanLengths = 22; + optional bytes midQualityFileSha256 = 23; + optional bytes midQualityFileEncSha256 = 24; + optional bool viewOnce = 25; + optional string thumbnailDirectPath = 26; + optional bytes thumbnailSha256 = 27; + optional bytes thumbnailEncSha256 = 28; + optional string staticUrl = 29; + repeated InteractiveAnnotation annotations = 30; + } + + message InitialSecurityNotificationSettingSync { + optional bool securityNotificationEnabled = 1; + } + + message InteractiveMessage { + optional Header header = 1; + optional Body body = 2; + optional Footer footer = 3; + optional ContextInfo contextInfo = 15; + oneof interactiveMessage { + Message.InteractiveMessage.ShopMessage shopStorefrontMessage = 4; + Message.InteractiveMessage.CollectionMessage collectionMessage = 5; + Message.InteractiveMessage.NativeFlowMessage nativeFlowMessage = 6; + Message.InteractiveMessage.CarouselMessage carouselMessage = 7; + } + message Body { + optional string text = 1; + } + + message CarouselMessage { + repeated Message.InteractiveMessage cards = 1; + optional int32 messageVersion = 2; + } + + message CollectionMessage { + optional string bizJid = 1; + optional string id = 2; + optional int32 messageVersion = 3; + } + + message Footer { + optional string text = 1; + } + + message Header { + optional string title = 1; + optional string subtitle = 2; + optional bool hasMediaAttachment = 5; + oneof media { + Message.DocumentMessage documentMessage = 3; + Message.ImageMessage imageMessage = 4; + bytes jpegThumbnail = 6; + Message.VideoMessage videoMessage = 7; + Message.LocationMessage locationMessage = 8; + Message.ProductMessage productMessage = 9; + } + } + + message NativeFlowMessage { + repeated NativeFlowButton buttons = 1; + optional string messageParamsJson = 2; + optional int32 messageVersion = 3; + message NativeFlowButton { + optional string name = 1; + optional string buttonParamsJson = 2; + } + + } + + message ShopMessage { + optional string id = 1; + optional Surface surface = 2; + optional int32 messageVersion = 3; + enum Surface { + UNKNOWN_SURFACE = 0; + FB = 1; + IG = 2; + WA = 3; + } + } + + } + + message InteractiveResponseMessage { + optional Body body = 1; + optional ContextInfo contextInfo = 15; + oneof interactiveResponseMessage { + Message.InteractiveResponseMessage.NativeFlowResponseMessage nativeFlowResponseMessage = 2; + } + message Body { + optional string text = 1; + optional Format format = 2; + enum Format { + DEFAULT = 0; + EXTENSIONS_1 = 1; + } + } + + message NativeFlowResponseMessage { + optional string name = 1; + optional string paramsJson = 2; + optional int32 version = 3; + } + + } + + message InvoiceMessage { + optional string note = 1; + optional string token = 2; + optional AttachmentType attachmentType = 3; + optional string attachmentMimetype = 4; + optional bytes attachmentMediaKey = 5; + optional int64 attachmentMediaKeyTimestamp = 6; + optional bytes attachmentFileSha256 = 7; + optional bytes attachmentFileEncSha256 = 8; + optional string attachmentDirectPath = 9; + optional bytes attachmentJpegThumbnail = 10; + enum AttachmentType { + IMAGE = 0; + PDF = 1; + } + } + + message KeepInChatMessage { + optional MessageKey key = 1; + optional KeepType keepType = 2; + optional int64 timestampMs = 3; + } + + message ListMessage { + optional string title = 1; + optional string description = 2; + optional string buttonText = 3; + optional ListType listType = 4; + repeated Section sections = 5; + optional ProductListInfo productListInfo = 6; + optional string footerText = 7; + optional ContextInfo contextInfo = 8; + enum ListType { + UNKNOWN = 0; + SINGLE_SELECT = 1; + PRODUCT_LIST = 2; + } + message Product { + optional string productId = 1; + } + + message ProductListHeaderImage { + optional string productId = 1; + optional bytes jpegThumbnail = 2; + } + + message ProductListInfo { + repeated Message.ListMessage.ProductSection productSections = 1; + optional Message.ListMessage.ProductListHeaderImage headerImage = 2; + optional string businessOwnerJid = 3; + } + + message ProductSection { + optional string title = 1; + repeated Message.ListMessage.Product products = 2; + } + + message Row { + optional string title = 1; + optional string description = 2; + optional string rowId = 3; + } + + message Section { + optional string title = 1; + repeated Message.ListMessage.Row rows = 2; + } + + } + + message ListResponseMessage { + optional string title = 1; + optional ListType listType = 2; + optional SingleSelectReply singleSelectReply = 3; + optional ContextInfo contextInfo = 4; + optional string description = 5; + enum ListType { + UNKNOWN = 0; + SINGLE_SELECT = 1; + } + message SingleSelectReply { + optional string selectedRowId = 1; + } + + } + + message LiveLocationMessage { + optional double degreesLatitude = 1; + optional double degreesLongitude = 2; + optional uint32 accuracyInMeters = 3; + optional float speedInMps = 4; + optional uint32 degreesClockwiseFromMagneticNorth = 5; + optional string caption = 6; + optional int64 sequenceNumber = 7; + optional uint32 timeOffset = 8; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + } + + message LocationMessage { + optional double degreesLatitude = 1; + optional double degreesLongitude = 2; + optional string name = 3; + optional string address = 4; + optional string url = 5; + optional bool isLive = 6; + optional uint32 accuracyInMeters = 7; + optional float speedInMps = 8; + optional uint32 degreesClockwiseFromMagneticNorth = 9; + optional string comment = 11; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + } + + message MessageHistoryBundle { + optional string mimetype = 2; + optional bytes fileSha256 = 3; + optional bytes mediaKey = 5; + optional bytes fileEncSha256 = 6; + optional string directPath = 7; + optional int64 mediaKeyTimestamp = 8; + optional ContextInfo contextInfo = 9; + repeated string participants = 10; + } + + message NewsletterAdminInviteMessage { + optional string newsletterJid = 1; + optional string newsletterName = 2; + optional bytes jpegThumbnail = 3; + optional string caption = 4; + optional int64 inviteExpiration = 5; + } + + message OrderMessage { + optional string orderId = 1; + optional bytes thumbnail = 2; + optional int32 itemCount = 3; + optional OrderStatus status = 4; + optional OrderSurface surface = 5; + optional string message = 6; + optional string orderTitle = 7; + optional string sellerJid = 8; + optional string token = 9; + optional int64 totalAmount1000 = 10; + optional string totalCurrencyCode = 11; + optional ContextInfo contextInfo = 17; + optional int32 messageVersion = 12; + optional MessageKey orderRequestMessageId = 13; + enum OrderStatus { + INQUIRY = 1; + ACCEPTED = 2; + DECLINED = 3; + } + enum OrderSurface { + CATALOG = 1; + } + } + + message PaymentInviteMessage { + optional ServiceType serviceType = 1; + optional int64 expiryTimestamp = 2; + enum ServiceType { + UNKNOWN = 0; + FBPAY = 1; + NOVI = 2; + UPI = 3; + } + } + + message PeerDataOperationRequestMessage { + optional Message.PeerDataOperationRequestType peerDataOperationRequestType = 1; + repeated RequestStickerReupload requestStickerReupload = 2; + repeated RequestUrlPreview requestUrlPreview = 3; + optional HistorySyncOnDemandRequest historySyncOnDemandRequest = 4; + repeated PlaceholderMessageResendRequest placeholderMessageResendRequest = 5; + message HistorySyncOnDemandRequest { + optional string chatJid = 1; + optional string oldestMsgId = 2; + optional bool oldestMsgFromMe = 3; + optional int32 onDemandMsgCount = 4; + optional int64 oldestMsgTimestampMs = 5; + } + + message PlaceholderMessageResendRequest { + optional MessageKey messageKey = 1; + } + + message RequestStickerReupload { + optional string fileSha256 = 1; + } + + message RequestUrlPreview { + optional string url = 1; + optional bool includeHqThumbnail = 2; + } + + } + + message PeerDataOperationRequestResponseMessage { + optional Message.PeerDataOperationRequestType peerDataOperationRequestType = 1; + optional string stanzaId = 2; + repeated PeerDataOperationResult peerDataOperationResult = 3; + message PeerDataOperationResult { + optional MediaRetryNotification.ResultType mediaUploadResult = 1; + optional Message.StickerMessage stickerMessage = 2; + optional LinkPreviewResponse linkPreviewResponse = 3; + optional PlaceholderMessageResendResponse placeholderMessageResendResponse = 4; + message LinkPreviewResponse { + optional string url = 1; + optional string title = 2; + optional string description = 3; + optional bytes thumbData = 4; + optional string canonicalUrl = 5; + optional string matchText = 6; + optional string previewType = 7; + optional LinkPreviewHighQualityThumbnail hqThumbnail = 8; + message LinkPreviewHighQualityThumbnail { + optional string directPath = 1; + optional string thumbHash = 2; + optional string encThumbHash = 3; + optional bytes mediaKey = 4; + optional int64 mediaKeyTimestampMs = 5; + optional int32 thumbWidth = 6; + optional int32 thumbHeight = 7; + } + + } + + message PlaceholderMessageResendResponse { + optional bytes webMessageInfoBytes = 1; + } + + } + + } + + enum PeerDataOperationRequestType { + UPLOAD_STICKER = 0; + SEND_RECENT_STICKER_BOOTSTRAP = 1; + GENERATE_LINK_PREVIEW = 2; + HISTORY_SYNC_ON_DEMAND = 3; + PLACEHOLDER_MESSAGE_RESEND = 4; + } + message PinInChatMessage { + optional MessageKey key = 1; + optional Type type = 2; + optional int64 senderTimestampMs = 3; + enum Type { + UNKNOWN_TYPE = 0; + PIN_FOR_ALL = 1; + UNPIN_FOR_ALL = 2; + } + } + + message PlaceholderMessage { + optional PlaceholderType type = 1; + enum PlaceholderType { + MASK_LINKED_DEVICES = 0; + } + } + + message PollCreationMessage { + optional bytes encKey = 1; + optional string name = 2; + repeated Option options = 3; + optional uint32 selectableOptionsCount = 4; + optional ContextInfo contextInfo = 5; + message Option { + optional string optionName = 1; + } + + } + + message PollEncValue { + optional bytes encPayload = 1; + optional bytes encIv = 2; + } + + message PollUpdateMessage { + optional MessageKey pollCreationMessageKey = 1; + optional Message.PollEncValue vote = 2; + optional Message.PollUpdateMessageMetadata metadata = 3; + optional int64 senderTimestampMs = 4; + } + + message PollUpdateMessageMetadata { + } + + message PollVoteMessage { + repeated bytes selectedOptions = 1; + } + + message ProductMessage { + optional ProductSnapshot product = 1; + optional string businessOwnerJid = 2; + optional CatalogSnapshot catalog = 4; + optional string body = 5; + optional string footer = 6; + optional ContextInfo contextInfo = 17; + message CatalogSnapshot { + optional Message.ImageMessage catalogImage = 1; + optional string title = 2; + optional string description = 3; + } + + message ProductSnapshot { + optional Message.ImageMessage productImage = 1; + optional string productId = 2; + optional string title = 3; + optional string description = 4; + optional string currencyCode = 5; + optional int64 priceAmount1000 = 6; + optional string retailerId = 7; + optional string url = 8; + optional uint32 productImageCount = 9; + optional string firstImageId = 11; + optional int64 salePriceAmount1000 = 12; + } + + } + + message ProtocolMessage { + optional MessageKey key = 1; + optional Type type = 2; + optional uint32 ephemeralExpiration = 4; + optional int64 ephemeralSettingTimestamp = 5; + optional Message.HistorySyncNotification historySyncNotification = 6; + optional Message.AppStateSyncKeyShare appStateSyncKeyShare = 7; + optional Message.AppStateSyncKeyRequest appStateSyncKeyRequest = 8; + optional Message.InitialSecurityNotificationSettingSync initialSecurityNotificationSettingSync = 9; + optional Message.AppStateFatalExceptionNotification appStateFatalExceptionNotification = 10; + optional DisappearingMode disappearingMode = 11; + optional Message editedMessage = 14; + optional int64 timestampMs = 15; + optional Message.PeerDataOperationRequestMessage peerDataOperationRequestMessage = 16; + optional Message.PeerDataOperationRequestResponseMessage peerDataOperationRequestResponseMessage = 17; + optional Message.BotFeedbackMessage botFeedbackMessage = 18; + optional string invokerJid = 19; + optional Message.RequestWelcomeMessageMetadata requestWelcomeMessageMetadata = 20; + optional MediaNotifyMessage mediaNotifyMessage = 21; + enum Type { + REVOKE = 0; + EPHEMERAL_SETTING = 3; + EPHEMERAL_SYNC_RESPONSE = 4; + HISTORY_SYNC_NOTIFICATION = 5; + APP_STATE_SYNC_KEY_SHARE = 6; + APP_STATE_SYNC_KEY_REQUEST = 7; + MSG_FANOUT_BACKFILL_REQUEST = 8; + INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC = 9; + APP_STATE_FATAL_EXCEPTION_NOTIFICATION = 10; + SHARE_PHONE_NUMBER = 11; + MESSAGE_EDIT = 14; + PEER_DATA_OPERATION_REQUEST_MESSAGE = 16; + PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE = 17; + REQUEST_WELCOME_MESSAGE = 18; + BOT_FEEDBACK_MESSAGE = 19; + MEDIA_NOTIFY_MESSAGE = 20; + } + } + + message ReactionMessage { + optional MessageKey key = 1; + optional string text = 2; + optional string groupingKey = 3; + optional int64 senderTimestampMs = 4; + } + + message RequestPaymentMessage { + optional Message noteMessage = 4; + optional string currencyCodeIso4217 = 1; + optional uint64 amount1000 = 2; + optional string requestFrom = 3; + optional int64 expiryTimestamp = 5; + optional Money amount = 6; + optional PaymentBackground background = 7; + } + + message RequestPhoneNumberMessage { + optional ContextInfo contextInfo = 1; + } + + message RequestWelcomeMessageMetadata { + optional LocalChatState localChatState = 1; + enum LocalChatState { + EMPTY = 0; + NON_EMPTY = 1; + } + } + + message ScheduledCallCreationMessage { + optional int64 scheduledTimestampMs = 1; + optional CallType callType = 2; + optional string title = 3; + enum CallType { + UNKNOWN = 0; + VOICE = 1; + VIDEO = 2; + } + } + + message ScheduledCallEditMessage { + optional MessageKey key = 1; + optional EditType editType = 2; + enum EditType { + UNKNOWN = 0; + CANCEL = 1; + } + } + + message SecretEncryptedMessage { + optional MessageKey targetMessageKey = 1; + optional bytes encPayload = 2; + optional bytes encIv = 3; + optional SecretEncType secretEncType = 4; + enum SecretEncType { + UNKNOWN = 0; + EVENT_RESPONSE = 1; + EVENT_EDIT = 2; + } + } + + message SendPaymentMessage { + optional Message noteMessage = 2; + optional MessageKey requestMessageKey = 3; + optional PaymentBackground background = 4; + } + + message SenderKeyDistributionMessage { + optional string groupId = 1; + optional bytes axolotlSenderKeyDistributionMessage = 2; + } + + message StickerMessage { + optional string url = 1; + optional bytes fileSha256 = 2; + optional bytes fileEncSha256 = 3; + optional bytes mediaKey = 4; + optional string mimetype = 5; + optional uint32 height = 6; + optional uint32 width = 7; + optional string directPath = 8; + optional uint64 fileLength = 9; + optional int64 mediaKeyTimestamp = 10; + optional uint32 firstFrameLength = 11; + optional bytes firstFrameSidecar = 12; + optional bool isAnimated = 13; + optional bytes pngThumbnail = 16; + optional ContextInfo contextInfo = 17; + optional int64 stickerSentTs = 18; + optional bool isAvatar = 19; + optional bool isAiSticker = 20; + optional bool isLottie = 21; + } + + message StickerSyncRMRMessage { + repeated string filehash = 1; + optional string rmrSource = 2; + optional int64 requestTimestamp = 3; + } + + message TemplateButtonReplyMessage { + optional string selectedId = 1; + optional string selectedDisplayText = 2; + optional ContextInfo contextInfo = 3; + optional uint32 selectedIndex = 4; + optional uint32 selectedCarouselCardIndex = 5; + } + + message TemplateMessage { + optional ContextInfo contextInfo = 3; + optional HydratedFourRowTemplate hydratedTemplate = 4; + optional string templateId = 9; + oneof format { + Message.TemplateMessage.FourRowTemplate fourRowTemplate = 1; + Message.TemplateMessage.HydratedFourRowTemplate hydratedFourRowTemplate = 2; + Message.InteractiveMessage interactiveMessageTemplate = 5; + } + message FourRowTemplate { + optional Message.HighlyStructuredMessage content = 6; + optional Message.HighlyStructuredMessage footer = 7; + repeated TemplateButton buttons = 8; + oneof title { + Message.DocumentMessage documentMessage = 1; + Message.HighlyStructuredMessage highlyStructuredMessage = 2; + Message.ImageMessage imageMessage = 3; + Message.VideoMessage videoMessage = 4; + Message.LocationMessage locationMessage = 5; + } + } + + message HydratedFourRowTemplate { + optional string hydratedContentText = 6; + optional string hydratedFooterText = 7; + repeated HydratedTemplateButton hydratedButtons = 8; + optional string templateId = 9; + optional bool maskLinkedDevices = 10; + oneof title { + Message.DocumentMessage documentMessage = 1; + string hydratedTitleText = 2; + Message.ImageMessage imageMessage = 3; + Message.VideoMessage videoMessage = 4; + Message.LocationMessage locationMessage = 5; + } + } + + } + + message VideoMessage { + optional string url = 1; + optional string mimetype = 2; + optional bytes fileSha256 = 3; + optional uint64 fileLength = 4; + optional uint32 seconds = 5; + optional bytes mediaKey = 6; + optional string caption = 7; + optional bool gifPlayback = 8; + optional uint32 height = 9; + optional uint32 width = 10; + optional bytes fileEncSha256 = 11; + repeated InteractiveAnnotation interactiveAnnotations = 12; + optional string directPath = 13; + optional int64 mediaKeyTimestamp = 14; + optional bytes jpegThumbnail = 16; + optional ContextInfo contextInfo = 17; + optional bytes streamingSidecar = 18; + optional Attribution gifAttribution = 19; + optional bool viewOnce = 20; + optional string thumbnailDirectPath = 21; + optional bytes thumbnailSha256 = 22; + optional bytes thumbnailEncSha256 = 23; + optional string staticUrl = 24; + repeated InteractiveAnnotation annotations = 25; + enum Attribution { + NONE = 0; + GIPHY = 1; + TENOR = 2; + } + } + +} + +message MessageAddOnContextInfo { + optional uint32 messageAddOnDurationInSecs = 1; +} + +message MessageContextInfo { + optional DeviceListMetadata deviceListMetadata = 1; + optional int32 deviceListMetadataVersion = 2; + optional bytes messageSecret = 3; + optional bytes paddingBytes = 4; + optional uint32 messageAddOnDurationInSecs = 5; + optional bytes botMessageSecret = 6; + optional BotMetadata botMetadata = 7; + optional int32 reportingTokenVersion = 8; +} + +message MessageKey { + optional string remoteJid = 1; + optional bool fromMe = 2; + optional string id = 3; + optional string participant = 4; +} + +message MessageSecretMessage { + optional sfixed32 version = 1; + optional bytes encIv = 2; + optional bytes encPayload = 3; +} + +message Money { + optional int64 value = 1; + optional uint32 offset = 2; + optional string currencyCode = 3; +} + +message MsgOpaqueData { + optional string body = 1; + optional string caption = 3; + optional double lng = 5; + optional bool isLive = 6; + optional double lat = 7; + optional int32 paymentAmount1000 = 8; + optional string paymentNoteMsgBody = 9; + optional string canonicalUrl = 10; + optional string matchedText = 11; + optional string title = 12; + optional string description = 13; + optional bytes futureproofBuffer = 14; + optional string clientUrl = 15; + optional string loc = 16; + optional string pollName = 17; + repeated PollOption pollOptions = 18; + optional uint32 pollSelectableOptionsCount = 20; + optional bytes messageSecret = 21; + optional string originalSelfAuthor = 51; + optional int64 senderTimestampMs = 22; + optional string pollUpdateParentKey = 23; + optional PollEncValue encPollVote = 24; + optional bool isSentCagPollCreation = 28; + optional string encReactionTargetMessageKey = 25; + optional bytes encReactionEncPayload = 26; + optional bytes encReactionEncIv = 27; + optional bytes botMessageSecret = 29; + optional string targetMessageKey = 30; + optional bytes encPayload = 31; + optional bytes encIv = 32; + message PollOption { + optional string name = 1; + } + +} + +message MsgRowOpaqueData { + optional MsgOpaqueData currentMsg = 1; + optional MsgOpaqueData quotedMsg = 2; +} + +message NoiseCertificate { + optional bytes details = 1; + optional bytes signature = 2; + message Details { + optional uint32 serial = 1; + optional string issuer = 2; + optional uint64 expires = 3; + optional string subject = 4; + optional bytes key = 5; + } + +} + +message NotificationMessageInfo { + optional MessageKey key = 1; + optional Message message = 2; + optional uint64 messageTimestamp = 3; + optional string participant = 4; +} + +message NotificationSettings { + optional string messageVibrate = 1; + optional string messagePopup = 2; + optional string messageLight = 3; + optional bool lowPriorityNotifications = 4; + optional bool reactionsMuted = 5; + optional string callVibrate = 6; +} + +message PastParticipant { + optional string userJid = 1; + optional LeaveReason leaveReason = 2; + optional uint64 leaveTs = 3; + enum LeaveReason { + LEFT = 0; + REMOVED = 1; + } +} + +message PastParticipants { + optional string groupJid = 1; + repeated PastParticipant pastParticipants = 2; +} + +message PatchDebugData { + optional bytes currentLthash = 1; + optional bytes newLthash = 2; + optional bytes patchVersion = 3; + optional bytes collectionName = 4; + optional bytes firstFourBytesFromAHashOfSnapshotMacKey = 5; + optional bytes newLthashSubtract = 6; + optional int32 numberAdd = 7; + optional int32 numberRemove = 8; + optional int32 numberOverride = 9; + optional Platform senderPlatform = 10; + optional bool isSenderPrimary = 11; + enum Platform { + ANDROID = 0; + SMBA = 1; + IPHONE = 2; + SMBI = 3; + WEB = 4; + UWP = 5; + DARWIN = 6; + } +} + +message PaymentBackground { + optional string id = 1; + optional uint64 fileLength = 2; + optional uint32 width = 3; + optional uint32 height = 4; + optional string mimetype = 5; + optional fixed32 placeholderArgb = 6; + optional fixed32 textArgb = 7; + optional fixed32 subtextArgb = 8; + optional MediaData mediaData = 9; + optional Type type = 10; + message MediaData { + optional bytes mediaKey = 1; + optional int64 mediaKeyTimestamp = 2; + optional bytes fileSha256 = 3; + optional bytes fileEncSha256 = 4; + optional string directPath = 5; + } + + enum Type { + UNKNOWN = 0; + DEFAULT = 1; + } +} + +message PaymentInfo { + optional Currency currencyDeprecated = 1; + optional uint64 amount1000 = 2; + optional string receiverJid = 3; + optional Status status = 4; + optional uint64 transactionTimestamp = 5; + optional MessageKey requestMessageKey = 6; + optional uint64 expiryTimestamp = 7; + optional bool futureproofed = 8; + optional string currency = 9; + optional TxnStatus txnStatus = 10; + optional bool useNoviFiatFormat = 11; + optional Money primaryAmount = 12; + optional Money exchangeAmount = 13; + enum Currency { + UNKNOWN_CURRENCY = 0; + INR = 1; + } + enum Status { + UNKNOWN_STATUS = 0; + PROCESSING = 1; + SENT = 2; + NEED_TO_ACCEPT = 3; + COMPLETE = 4; + COULD_NOT_COMPLETE = 5; + REFUNDED = 6; + EXPIRED = 7; + REJECTED = 8; + CANCELLED = 9; + WAITING_FOR_PAYER = 10; + WAITING = 11; + } + enum TxnStatus { + UNKNOWN = 0; + PENDING_SETUP = 1; + PENDING_RECEIVER_SETUP = 2; + INIT = 3; + SUCCESS = 4; + COMPLETED = 5; + FAILED = 6; + FAILED_RISK = 7; + FAILED_PROCESSING = 8; + FAILED_RECEIVER_PROCESSING = 9; + FAILED_DA = 10; + FAILED_DA_FINAL = 11; + REFUNDED_TXN = 12; + REFUND_FAILED = 13; + REFUND_FAILED_PROCESSING = 14; + REFUND_FAILED_DA = 15; + EXPIRED_TXN = 16; + AUTH_CANCELED = 17; + AUTH_CANCEL_FAILED_PROCESSING = 18; + AUTH_CANCEL_FAILED = 19; + COLLECT_INIT = 20; + COLLECT_SUCCESS = 21; + COLLECT_FAILED = 22; + COLLECT_FAILED_RISK = 23; + COLLECT_REJECTED = 24; + COLLECT_EXPIRED = 25; + COLLECT_CANCELED = 26; + COLLECT_CANCELLING = 27; + IN_REVIEW = 28; + REVERSAL_SUCCESS = 29; + REVERSAL_PENDING = 30; + REFUND_PENDING = 31; + } +} + +message PhoneNumberToLIDMapping { + optional string pnJid = 1; + optional string lidJid = 2; +} + +message PhotoChange { + optional bytes oldPhoto = 1; + optional bytes newPhoto = 2; + optional uint32 newPhotoId = 3; +} + +message PinInChat { + optional Type type = 1; + optional MessageKey key = 2; + optional int64 senderTimestampMs = 3; + optional int64 serverTimestampMs = 4; + optional MessageAddOnContextInfo messageAddOnContextInfo = 5; + enum Type { + UNKNOWN_TYPE = 0; + PIN_FOR_ALL = 1; + UNPIN_FOR_ALL = 2; + } +} + +message Point { + optional int32 xDeprecated = 1; + optional int32 yDeprecated = 2; + optional double x = 3; + optional double y = 4; +} + +message PollAdditionalMetadata { + optional bool pollInvalidated = 1; +} + +message PollEncValue { + optional bytes encPayload = 1; + optional bytes encIv = 2; +} + +message PollUpdate { + optional MessageKey pollUpdateMessageKey = 1; + optional Message.PollVoteMessage vote = 2; + optional int64 senderTimestampMs = 3; + optional int64 serverTimestampMs = 4; + optional bool unread = 5; +} + +message PreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; +} + +message PreKeySignalMessage { + optional uint32 registrationId = 5; + optional uint32 preKeyId = 1; + optional uint32 signedPreKeyId = 6; + optional bytes baseKey = 2; + optional bytes identityKey = 3; + optional bytes message = 4; +} + +message PremiumMessageInfo { + optional string serverCampaignId = 1; +} + +message Pushname { + optional string id = 1; + optional string pushname = 2; +} + +message QP { + enum ClauseType { + AND = 1; + OR = 2; + NOR = 3; + } + message Filter { + required string filterName = 1; + repeated QP.FilterParameters parameters = 2; + optional QP.FilterResult filterResult = 3; + required QP.FilterClientNotSupportedConfig clientNotSupportedConfig = 4; + } + + message FilterClause { + required QP.ClauseType clauseType = 1; + repeated QP.FilterClause clauses = 2; + repeated QP.Filter filters = 3; + } + + enum FilterClientNotSupportedConfig { + PASS_BY_DEFAULT = 1; + FAIL_BY_DEFAULT = 2; + } + message FilterParameters { + optional string key = 1; + optional string value = 2; + } + + enum FilterResult { + TRUE = 1; + FALSE = 2; + UNKNOWN = 3; + } +} + +message Reaction { + optional MessageKey key = 1; + optional string text = 2; + optional string groupingKey = 3; + optional int64 senderTimestampMs = 4; + optional bool unread = 5; +} + +message RecentEmojiWeight { + optional string emoji = 1; + optional float weight = 2; +} + +message RecordStructure { + optional SessionStructure currentSession = 1; + repeated SessionStructure previousSessions = 2; +} + +message ReportingTokenInfo { + optional bytes reportingTag = 1; +} + +message SenderKeyDistributionMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes chainKey = 3; + optional bytes signingKey = 4; +} + +message SenderKeyMessage { + optional uint32 id = 1; + optional uint32 iteration = 2; + optional bytes ciphertext = 3; +} + +message SenderKeyRecordStructure { + repeated SenderKeyStateStructure senderKeyStates = 1; +} + +message SenderKeyStateStructure { + optional uint32 senderKeyId = 1; + optional SenderChainKey senderChainKey = 2; + optional SenderSigningKey senderSigningKey = 3; + repeated SenderMessageKey senderMessageKeys = 4; + message SenderChainKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderMessageKey { + optional uint32 iteration = 1; + optional bytes seed = 2; + } + + message SenderSigningKey { + optional bytes public = 1; + optional bytes private = 2; + } + +} + +message ServerErrorReceipt { + optional string stanzaId = 1; +} + +message SessionStructure { + optional uint32 sessionVersion = 1; + optional bytes localIdentityPublic = 2; + optional bytes remoteIdentityPublic = 3; + optional bytes rootKey = 4; + optional uint32 previousCounter = 5; + optional Chain senderChain = 6; + repeated Chain receiverChains = 7; + optional PendingKeyExchange pendingKeyExchange = 8; + optional PendingPreKey pendingPreKey = 9; + optional uint32 remoteRegistrationId = 10; + optional uint32 localRegistrationId = 11; + optional bool needsRefresh = 12; + optional bytes aliceBaseKey = 13; + message Chain { + optional bytes senderRatchetKey = 1; + optional bytes senderRatchetKeyPrivate = 2; + optional ChainKey chainKey = 3; + repeated MessageKey messageKeys = 4; + message ChainKey { + optional uint32 index = 1; + optional bytes key = 2; + } + + message MessageKey { + optional uint32 index = 1; + optional bytes cipherKey = 2; + optional bytes macKey = 3; + optional bytes iv = 4; + } + + } + + message PendingKeyExchange { + optional uint32 sequence = 1; + optional bytes localBaseKey = 2; + optional bytes localBaseKeyPrivate = 3; + optional bytes localRatchetKey = 4; + optional bytes localRatchetKeyPrivate = 5; + optional bytes localIdentityKey = 7; + optional bytes localIdentityKeyPrivate = 8; + } + + message PendingPreKey { + optional uint32 preKeyId = 1; + optional int32 signedPreKeyId = 3; + optional bytes baseKey = 2; + } + +} + +message SignalMessage { + optional bytes ratchetKey = 1; + optional uint32 counter = 2; + optional uint32 previousCounter = 3; + optional bytes ciphertext = 4; +} + +message SignedPreKeyRecordStructure { + optional uint32 id = 1; + optional bytes publicKey = 2; + optional bytes privateKey = 3; + optional bytes signature = 4; + optional fixed64 timestamp = 5; +} + +message StatusPSA { + required uint64 campaignId = 44; + optional uint64 campaignExpirationTimestamp = 45; +} + +message StickerMetadata { + optional string url = 1; + optional bytes fileSha256 = 2; + optional bytes fileEncSha256 = 3; + optional bytes mediaKey = 4; + optional string mimetype = 5; + optional uint32 height = 6; + optional uint32 width = 7; + optional string directPath = 8; + optional uint64 fileLength = 9; + optional float weight = 10; + optional int64 lastStickerSentTs = 11; +} + +message SyncActionData { + optional bytes index = 1; + optional SyncActionValue value = 2; + optional bytes padding = 3; + optional int32 version = 4; +} + +message SyncActionValue { + optional int64 timestamp = 1; + optional StarAction starAction = 2; + optional ContactAction contactAction = 3; + optional MuteAction muteAction = 4; + optional PinAction pinAction = 5; + optional SecurityNotificationSetting securityNotificationSetting = 6; + optional PushNameSetting pushNameSetting = 7; + optional QuickReplyAction quickReplyAction = 8; + optional RecentEmojiWeightsAction recentEmojiWeightsAction = 11; + optional LabelEditAction labelEditAction = 14; + optional LabelAssociationAction labelAssociationAction = 15; + optional LocaleSetting localeSetting = 16; + optional ArchiveChatAction archiveChatAction = 17; + optional DeleteMessageForMeAction deleteMessageForMeAction = 18; + optional KeyExpiration keyExpiration = 19; + optional MarkChatAsReadAction markChatAsReadAction = 20; + optional ClearChatAction clearChatAction = 21; + optional DeleteChatAction deleteChatAction = 22; + optional UnarchiveChatsSetting unarchiveChatsSetting = 23; + optional PrimaryFeature primaryFeature = 24; + optional AndroidUnsupportedActions androidUnsupportedActions = 26; + optional AgentAction agentAction = 27; + optional SubscriptionAction subscriptionAction = 28; + optional UserStatusMuteAction userStatusMuteAction = 29; + optional TimeFormatAction timeFormatAction = 30; + optional NuxAction nuxAction = 31; + optional PrimaryVersionAction primaryVersionAction = 32; + optional StickerAction stickerAction = 33; + optional RemoveRecentStickerAction removeRecentStickerAction = 34; + optional ChatAssignmentAction chatAssignment = 35; + optional ChatAssignmentOpenedStatusAction chatAssignmentOpenedStatus = 36; + optional PnForLidChatAction pnForLidChatAction = 37; + optional MarketingMessageAction marketingMessageAction = 38; + optional MarketingMessageBroadcastAction marketingMessageBroadcastAction = 39; + optional ExternalWebBetaAction externalWebBetaAction = 40; + optional PrivacySettingRelayAllCalls privacySettingRelayAllCalls = 41; + optional CallLogAction callLogAction = 42; + optional StatusPrivacyAction statusPrivacy = 44; + optional BotWelcomeRequestAction botWelcomeRequestAction = 45; + optional DeleteIndividualCallLogAction deleteIndividualCallLog = 46; + optional LabelReorderingAction labelReorderingAction = 47; + optional PaymentInfoAction paymentInfoAction = 48; + optional CustomPaymentMethodsAction customPaymentMethodsAction = 49; + message AgentAction { + optional string name = 1; + optional int32 deviceID = 2; + optional bool isDeleted = 3; + } + + message AndroidUnsupportedActions { + optional bool allowed = 1; + } + + message ArchiveChatAction { + optional bool archived = 1; + optional SyncActionValue.SyncActionMessageRange messageRange = 2; + } + + message BotWelcomeRequestAction { + optional bool isSent = 1; + } + + message CallLogAction { + optional CallLogRecord callLogRecord = 1; + } + + message ChatAssignmentAction { + optional string deviceAgentID = 1; + } + + message ChatAssignmentOpenedStatusAction { + optional bool chatOpened = 1; + } + + message ClearChatAction { + optional SyncActionValue.SyncActionMessageRange messageRange = 1; + } + + message ContactAction { + optional string fullName = 1; + optional string firstName = 2; + optional string lidJid = 3; + optional bool saveOnPrimaryAddressbook = 4; + } + + message CustomPaymentMethod { + required string credentialId = 1; + required string country = 2; + required string type = 3; + repeated SyncActionValue.CustomPaymentMethodMetadata metadata = 4; + } + + message CustomPaymentMethodMetadata { + required string key = 1; + required string value = 2; + } + + message CustomPaymentMethodsAction { + repeated SyncActionValue.CustomPaymentMethod customPaymentMethods = 1; + } + + message DeleteChatAction { + optional SyncActionValue.SyncActionMessageRange messageRange = 1; + } + + message DeleteIndividualCallLogAction { + optional string peerJid = 1; + optional bool isIncoming = 2; + } + + message DeleteMessageForMeAction { + optional bool deleteMedia = 1; + optional int64 messageTimestamp = 2; + } + + message ExternalWebBetaAction { + optional bool isOptIn = 1; + } + + message KeyExpiration { + optional int32 expiredKeyEpoch = 1; + } + + message LabelAssociationAction { + optional bool labeled = 1; + } + + message LabelEditAction { + optional string name = 1; + optional int32 color = 2; + optional int32 predefinedId = 3; + optional bool deleted = 4; + optional int32 orderIndex = 5; + } + + message LabelReorderingAction { + repeated int32 sortedLabelIds = 1; + } + + message LocaleSetting { + optional string locale = 1; + } + + message MarkChatAsReadAction { + optional bool read = 1; + optional SyncActionValue.SyncActionMessageRange messageRange = 2; + } + + message MarketingMessageAction { + optional string name = 1; + optional string message = 2; + optional MarketingMessagePrototypeType type = 3; + optional int64 createdAt = 4; + optional int64 lastSentAt = 5; + optional bool isDeleted = 6; + optional string mediaId = 7; + enum MarketingMessagePrototypeType { + PERSONALIZED = 0; + } + } + + message MarketingMessageBroadcastAction { + optional int32 repliedCount = 1; + } + + message MuteAction { + optional bool muted = 1; + optional int64 muteEndTimestamp = 2; + optional bool autoMuted = 3; + } + + message NuxAction { + optional bool acknowledged = 1; + } + + message PaymentInfoAction { + optional string cpi = 1; + } + + message PinAction { + optional bool pinned = 1; + } + + message PnForLidChatAction { + optional string pnJid = 1; + } + + message PrimaryFeature { + repeated string flags = 1; + } + + message PrimaryVersionAction { + optional string version = 1; + } + + message PrivacySettingRelayAllCalls { + optional bool isEnabled = 1; + } + + message PushNameSetting { + optional string name = 1; + } + + message QuickReplyAction { + optional string shortcut = 1; + optional string message = 2; + repeated string keywords = 3; + optional int32 count = 4; + optional bool deleted = 5; + } + + message RecentEmojiWeightsAction { + repeated RecentEmojiWeight weights = 1; + } + + message RemoveRecentStickerAction { + optional int64 lastStickerSentTs = 1; + } + + message SecurityNotificationSetting { + optional bool showNotification = 1; + } + + message StarAction { + optional bool starred = 1; + } + + message StatusPrivacyAction { + optional StatusDistributionMode mode = 1; + repeated string userJid = 2; + enum StatusDistributionMode { + ALLOW_LIST = 0; + DENY_LIST = 1; + CONTACTS = 2; + } + } + + message StickerAction { + optional string url = 1; + optional bytes fileEncSha256 = 2; + optional bytes mediaKey = 3; + optional string mimetype = 4; + optional uint32 height = 5; + optional uint32 width = 6; + optional string directPath = 7; + optional uint64 fileLength = 8; + optional bool isFavorite = 9; + optional uint32 deviceIdHint = 10; + } + + message SubscriptionAction { + optional bool isDeactivated = 1; + optional bool isAutoRenewing = 2; + optional int64 expirationDate = 3; + } + + message SyncActionMessage { + optional MessageKey key = 1; + optional int64 timestamp = 2; + } + + message SyncActionMessageRange { + optional int64 lastMessageTimestamp = 1; + optional int64 lastSystemMessageTimestamp = 2; + repeated SyncActionValue.SyncActionMessage messages = 3; + } + + message TimeFormatAction { + optional bool isTwentyFourHourFormatEnabled = 1; + } + + message UnarchiveChatsSetting { + optional bool unarchiveChats = 1; + } + + message UserStatusMuteAction { + optional bool muted = 1; + } + +} + +message SyncdIndex { + optional bytes blob = 1; +} + +message SyncdMutation { + optional SyncdOperation operation = 1; + optional SyncdRecord record = 2; + enum SyncdOperation { + SET = 0; + REMOVE = 1; + } +} + +message SyncdMutations { + repeated SyncdMutation mutations = 1; +} + +message SyncdPatch { + optional SyncdVersion version = 1; + repeated SyncdMutation mutations = 2; + optional ExternalBlobReference externalMutations = 3; + optional bytes snapshotMac = 4; + optional bytes patchMac = 5; + optional KeyId keyId = 6; + optional ExitCode exitCode = 7; + optional uint32 deviceIndex = 8; + optional bytes clientDebugData = 9; +} + +message SyncdRecord { + optional SyncdIndex index = 1; + optional SyncdValue value = 2; + optional KeyId keyId = 3; +} + +message SyncdSnapshot { + optional SyncdVersion version = 1; + repeated SyncdRecord records = 2; + optional bytes mac = 3; + optional KeyId keyId = 4; +} + +message SyncdValue { + optional bytes blob = 1; +} + +message SyncdVersion { + optional uint64 version = 1; +} + +message TemplateButton { + optional uint32 index = 4; + oneof button { + TemplateButton.QuickReplyButton quickReplyButton = 1; + TemplateButton.URLButton urlButton = 2; + TemplateButton.CallButton callButton = 3; + } + message CallButton { + optional Message.HighlyStructuredMessage displayText = 1; + optional Message.HighlyStructuredMessage phoneNumber = 2; + } + + message QuickReplyButton { + optional Message.HighlyStructuredMessage displayText = 1; + optional string id = 2; + } + + message URLButton { + optional Message.HighlyStructuredMessage displayText = 1; + optional Message.HighlyStructuredMessage url = 2; + } + +} + +message UserReceipt { + required string userJid = 1; + optional int64 receiptTimestamp = 2; + optional int64 readTimestamp = 3; + optional int64 playedTimestamp = 4; + repeated string pendingDeviceJid = 5; + repeated string deliveredDeviceJid = 6; +} + +message VerifiedNameCertificate { + optional bytes details = 1; + optional bytes signature = 2; + optional bytes serverSignature = 3; + message Details { + optional uint64 serial = 1; + optional string issuer = 2; + optional string verifiedName = 4; + repeated LocalizedName localizedNames = 8; + optional uint64 issueTime = 10; + } + +} + +message WallpaperSettings { + optional string filename = 1; + optional uint32 opacity = 2; +} + +message WebFeatures { + optional Flag labelsDisplay = 1; + optional Flag voipIndividualOutgoing = 2; + optional Flag groupsV3 = 3; + optional Flag groupsV3Create = 4; + optional Flag changeNumberV2 = 5; + optional Flag queryStatusV3Thumbnail = 6; + optional Flag liveLocations = 7; + optional Flag queryVname = 8; + optional Flag voipIndividualIncoming = 9; + optional Flag quickRepliesQuery = 10; + optional Flag payments = 11; + optional Flag stickerPackQuery = 12; + optional Flag liveLocationsFinal = 13; + optional Flag labelsEdit = 14; + optional Flag mediaUpload = 15; + optional Flag mediaUploadRichQuickReplies = 18; + optional Flag vnameV2 = 19; + optional Flag videoPlaybackUrl = 20; + optional Flag statusRanking = 21; + optional Flag voipIndividualVideo = 22; + optional Flag thirdPartyStickers = 23; + optional Flag frequentlyForwardedSetting = 24; + optional Flag groupsV4JoinPermission = 25; + optional Flag recentStickers = 26; + optional Flag catalog = 27; + optional Flag starredStickers = 28; + optional Flag voipGroupCall = 29; + optional Flag templateMessage = 30; + optional Flag templateMessageInteractivity = 31; + optional Flag ephemeralMessages = 32; + optional Flag e2ENotificationSync = 33; + optional Flag recentStickersV2 = 34; + optional Flag recentStickersV3 = 36; + optional Flag userNotice = 37; + optional Flag support = 39; + optional Flag groupUiiCleanup = 40; + optional Flag groupDogfoodingInternalOnly = 41; + optional Flag settingsSync = 42; + optional Flag archiveV2 = 43; + optional Flag ephemeralAllowGroupMembers = 44; + optional Flag ephemeral24HDuration = 45; + optional Flag mdForceUpgrade = 46; + optional Flag disappearingMode = 47; + optional Flag externalMdOptInAvailable = 48; + optional Flag noDeleteMessageTimeLimit = 49; + enum Flag { + NOT_STARTED = 0; + FORCE_UPGRADE = 1; + DEVELOPMENT = 2; + PRODUCTION = 3; + } +} + +message WebMessageInfo { + required MessageKey key = 1; + optional Message message = 2; + optional uint64 messageTimestamp = 3; + optional Status status = 4; + optional string participant = 5; + optional uint64 messageC2STimestamp = 6; + optional bool ignore = 16; + optional bool starred = 17; + optional bool broadcast = 18; + optional string pushName = 19; + optional bytes mediaCiphertextSha256 = 20; + optional bool multicast = 21; + optional bool urlText = 22; + optional bool urlNumber = 23; + optional StubType messageStubType = 24; + optional bool clearMedia = 25; + repeated string messageStubParameters = 26; + optional uint32 duration = 27; + repeated string labels = 28; + optional PaymentInfo paymentInfo = 29; + optional Message.LiveLocationMessage finalLiveLocation = 30; + optional PaymentInfo quotedPaymentInfo = 31; + optional uint64 ephemeralStartTimestamp = 32; + optional uint32 ephemeralDuration = 33; + optional bool ephemeralOffToOn = 34; + optional bool ephemeralOutOfSync = 35; + optional BizPrivacyStatus bizPrivacyStatus = 36; + optional string verifiedBizName = 37; + optional MediaData mediaData = 38; + optional PhotoChange photoChange = 39; + repeated UserReceipt userReceipt = 40; + repeated Reaction reactions = 41; + optional MediaData quotedStickerData = 42; + optional bytes futureproofData = 43; + optional StatusPSA statusPsa = 44; + repeated PollUpdate pollUpdates = 45; + optional PollAdditionalMetadata pollAdditionalMetadata = 46; + optional string agentId = 47; + optional bool statusAlreadyViewed = 48; + optional bytes messageSecret = 49; + optional KeepInChat keepInChat = 50; + optional string originalSelfAuthorUserJidString = 51; + optional uint64 revokeMessageTimestamp = 52; + optional PinInChat pinInChat = 54; + optional PremiumMessageInfo premiumMessageInfo = 55; + optional bool is1PBizBotMessage = 56; + optional bool isGroupHistoryMessage = 57; + optional string botMessageInvokerJid = 58; + optional CommentMetadata commentMetadata = 59; + repeated EventResponse eventResponses = 61; + optional ReportingTokenInfo reportingTokenInfo = 62; + optional uint64 newsletterServerId = 63; + enum BizPrivacyStatus { + E2EE = 0; + FB = 2; + BSP = 1; + BSP_AND_FB = 3; + } + enum Status { + ERROR = 0; + PENDING = 1; + SERVER_ACK = 2; + DELIVERY_ACK = 3; + READ = 4; + PLAYED = 5; + } + enum StubType { + UNKNOWN = 0; + REVOKE = 1; + CIPHERTEXT = 2; + FUTUREPROOF = 3; + NON_VERIFIED_TRANSITION = 4; + UNVERIFIED_TRANSITION = 5; + VERIFIED_TRANSITION = 6; + VERIFIED_LOW_UNKNOWN = 7; + VERIFIED_HIGH = 8; + VERIFIED_INITIAL_UNKNOWN = 9; + VERIFIED_INITIAL_LOW = 10; + VERIFIED_INITIAL_HIGH = 11; + VERIFIED_TRANSITION_ANY_TO_NONE = 12; + VERIFIED_TRANSITION_ANY_TO_HIGH = 13; + VERIFIED_TRANSITION_HIGH_TO_LOW = 14; + VERIFIED_TRANSITION_HIGH_TO_UNKNOWN = 15; + VERIFIED_TRANSITION_UNKNOWN_TO_LOW = 16; + VERIFIED_TRANSITION_LOW_TO_UNKNOWN = 17; + VERIFIED_TRANSITION_NONE_TO_LOW = 18; + VERIFIED_TRANSITION_NONE_TO_UNKNOWN = 19; + GROUP_CREATE = 20; + GROUP_CHANGE_SUBJECT = 21; + GROUP_CHANGE_ICON = 22; + GROUP_CHANGE_INVITE_LINK = 23; + GROUP_CHANGE_DESCRIPTION = 24; + GROUP_CHANGE_RESTRICT = 25; + GROUP_CHANGE_ANNOUNCE = 26; + GROUP_PARTICIPANT_ADD = 27; + GROUP_PARTICIPANT_REMOVE = 28; + GROUP_PARTICIPANT_PROMOTE = 29; + GROUP_PARTICIPANT_DEMOTE = 30; + GROUP_PARTICIPANT_INVITE = 31; + GROUP_PARTICIPANT_LEAVE = 32; + GROUP_PARTICIPANT_CHANGE_NUMBER = 33; + BROADCAST_CREATE = 34; + BROADCAST_ADD = 35; + BROADCAST_REMOVE = 36; + GENERIC_NOTIFICATION = 37; + E2E_IDENTITY_CHANGED = 38; + E2E_ENCRYPTED = 39; + CALL_MISSED_VOICE = 40; + CALL_MISSED_VIDEO = 41; + INDIVIDUAL_CHANGE_NUMBER = 42; + GROUP_DELETE = 43; + GROUP_ANNOUNCE_MODE_MESSAGE_BOUNCE = 44; + CALL_MISSED_GROUP_VOICE = 45; + CALL_MISSED_GROUP_VIDEO = 46; + PAYMENT_CIPHERTEXT = 47; + PAYMENT_FUTUREPROOF = 48; + PAYMENT_TRANSACTION_STATUS_UPDATE_FAILED = 49; + PAYMENT_TRANSACTION_STATUS_UPDATE_REFUNDED = 50; + PAYMENT_TRANSACTION_STATUS_UPDATE_REFUND_FAILED = 51; + PAYMENT_TRANSACTION_STATUS_RECEIVER_PENDING_SETUP = 52; + PAYMENT_TRANSACTION_STATUS_RECEIVER_SUCCESS_AFTER_HICCUP = 53; + PAYMENT_ACTION_ACCOUNT_SETUP_REMINDER = 54; + PAYMENT_ACTION_SEND_PAYMENT_REMINDER = 55; + PAYMENT_ACTION_SEND_PAYMENT_INVITATION = 56; + PAYMENT_ACTION_REQUEST_DECLINED = 57; + PAYMENT_ACTION_REQUEST_EXPIRED = 58; + PAYMENT_ACTION_REQUEST_CANCELLED = 59; + BIZ_VERIFIED_TRANSITION_TOP_TO_BOTTOM = 60; + BIZ_VERIFIED_TRANSITION_BOTTOM_TO_TOP = 61; + BIZ_INTRO_TOP = 62; + BIZ_INTRO_BOTTOM = 63; + BIZ_NAME_CHANGE = 64; + BIZ_MOVE_TO_CONSUMER_APP = 65; + BIZ_TWO_TIER_MIGRATION_TOP = 66; + BIZ_TWO_TIER_MIGRATION_BOTTOM = 67; + OVERSIZED = 68; + GROUP_CHANGE_NO_FREQUENTLY_FORWARDED = 69; + GROUP_V4_ADD_INVITE_SENT = 70; + GROUP_PARTICIPANT_ADD_REQUEST_JOIN = 71; + CHANGE_EPHEMERAL_SETTING = 72; + E2E_DEVICE_CHANGED = 73; + VIEWED_ONCE = 74; + E2E_ENCRYPTED_NOW = 75; + BLUE_MSG_BSP_FB_TO_BSP_PREMISE = 76; + BLUE_MSG_BSP_FB_TO_SELF_FB = 77; + BLUE_MSG_BSP_FB_TO_SELF_PREMISE = 78; + BLUE_MSG_BSP_FB_UNVERIFIED = 79; + BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 80; + BLUE_MSG_BSP_FB_VERIFIED = 81; + BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 82; + BLUE_MSG_BSP_PREMISE_TO_SELF_PREMISE = 83; + BLUE_MSG_BSP_PREMISE_UNVERIFIED = 84; + BLUE_MSG_BSP_PREMISE_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 85; + BLUE_MSG_BSP_PREMISE_VERIFIED = 86; + BLUE_MSG_BSP_PREMISE_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 87; + BLUE_MSG_CONSUMER_TO_BSP_FB_UNVERIFIED = 88; + BLUE_MSG_CONSUMER_TO_BSP_PREMISE_UNVERIFIED = 89; + BLUE_MSG_CONSUMER_TO_SELF_FB_UNVERIFIED = 90; + BLUE_MSG_CONSUMER_TO_SELF_PREMISE_UNVERIFIED = 91; + BLUE_MSG_SELF_FB_TO_BSP_PREMISE = 92; + BLUE_MSG_SELF_FB_TO_SELF_PREMISE = 93; + BLUE_MSG_SELF_FB_UNVERIFIED = 94; + BLUE_MSG_SELF_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED = 95; + BLUE_MSG_SELF_FB_VERIFIED = 96; + BLUE_MSG_SELF_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED = 97; + BLUE_MSG_SELF_PREMISE_TO_BSP_PREMISE = 98; + BLUE_MSG_SELF_PREMISE_UNVERIFIED = 99; + BLUE_MSG_SELF_PREMISE_VERIFIED = 100; + BLUE_MSG_TO_BSP_FB = 101; + BLUE_MSG_TO_CONSUMER = 102; + BLUE_MSG_TO_SELF_FB = 103; + BLUE_MSG_UNVERIFIED_TO_BSP_FB_VERIFIED = 104; + BLUE_MSG_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 105; + BLUE_MSG_UNVERIFIED_TO_SELF_FB_VERIFIED = 106; + BLUE_MSG_UNVERIFIED_TO_VERIFIED = 107; + BLUE_MSG_VERIFIED_TO_BSP_FB_UNVERIFIED = 108; + BLUE_MSG_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 109; + BLUE_MSG_VERIFIED_TO_SELF_FB_UNVERIFIED = 110; + BLUE_MSG_VERIFIED_TO_UNVERIFIED = 111; + BLUE_MSG_BSP_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 112; + BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_FB_VERIFIED = 113; + BLUE_MSG_BSP_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 114; + BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_FB_UNVERIFIED = 115; + BLUE_MSG_SELF_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED = 116; + BLUE_MSG_SELF_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED = 117; + E2E_IDENTITY_UNAVAILABLE = 118; + GROUP_CREATING = 119; + GROUP_CREATE_FAILED = 120; + GROUP_BOUNCED = 121; + BLOCK_CONTACT = 122; + EPHEMERAL_SETTING_NOT_APPLIED = 123; + SYNC_FAILED = 124; + SYNCING = 125; + BIZ_PRIVACY_MODE_INIT_FB = 126; + BIZ_PRIVACY_MODE_INIT_BSP = 127; + BIZ_PRIVACY_MODE_TO_FB = 128; + BIZ_PRIVACY_MODE_TO_BSP = 129; + DISAPPEARING_MODE = 130; + E2E_DEVICE_FETCH_FAILED = 131; + ADMIN_REVOKE = 132; + GROUP_INVITE_LINK_GROWTH_LOCKED = 133; + COMMUNITY_LINK_PARENT_GROUP = 134; + COMMUNITY_LINK_SIBLING_GROUP = 135; + COMMUNITY_LINK_SUB_GROUP = 136; + COMMUNITY_UNLINK_PARENT_GROUP = 137; + COMMUNITY_UNLINK_SIBLING_GROUP = 138; + COMMUNITY_UNLINK_SUB_GROUP = 139; + GROUP_PARTICIPANT_ACCEPT = 140; + GROUP_PARTICIPANT_LINKED_GROUP_JOIN = 141; + COMMUNITY_CREATE = 142; + EPHEMERAL_KEEP_IN_CHAT = 143; + GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST = 144; + GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE = 145; + INTEGRITY_UNLINK_PARENT_GROUP = 146; + COMMUNITY_PARTICIPANT_PROMOTE = 147; + COMMUNITY_PARTICIPANT_DEMOTE = 148; + COMMUNITY_PARENT_GROUP_DELETED = 149; + COMMUNITY_LINK_PARENT_GROUP_MEMBERSHIP_APPROVAL = 150; + GROUP_PARTICIPANT_JOINED_GROUP_AND_PARENT_GROUP = 151; + MASKED_THREAD_CREATED = 152; + MASKED_THREAD_UNMASKED = 153; + BIZ_CHAT_ASSIGNMENT = 154; + CHAT_PSA = 155; + CHAT_POLL_CREATION_MESSAGE = 156; + CAG_MASKED_THREAD_CREATED = 157; + COMMUNITY_PARENT_GROUP_SUBJECT_CHANGED = 158; + CAG_INVITE_AUTO_ADD = 159; + BIZ_CHAT_ASSIGNMENT_UNASSIGN = 160; + CAG_INVITE_AUTO_JOINED = 161; + SCHEDULED_CALL_START_MESSAGE = 162; + COMMUNITY_INVITE_RICH = 163; + COMMUNITY_INVITE_AUTO_ADD_RICH = 164; + SUB_GROUP_INVITE_RICH = 165; + SUB_GROUP_PARTICIPANT_ADD_RICH = 166; + COMMUNITY_LINK_PARENT_GROUP_RICH = 167; + COMMUNITY_PARTICIPANT_ADD_RICH = 168; + SILENCED_UNKNOWN_CALLER_AUDIO = 169; + SILENCED_UNKNOWN_CALLER_VIDEO = 170; + GROUP_MEMBER_ADD_MODE = 171; + GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD = 172; + COMMUNITY_CHANGE_DESCRIPTION = 173; + SENDER_INVITE = 174; + RECEIVER_INVITE = 175; + COMMUNITY_ALLOW_MEMBER_ADDED_GROUPS = 176; + PINNED_MESSAGE_IN_CHAT = 177; + PAYMENT_INVITE_SETUP_INVITER = 178; + PAYMENT_INVITE_SETUP_INVITEE_RECEIVE_ONLY = 179; + PAYMENT_INVITE_SETUP_INVITEE_SEND_AND_RECEIVE = 180; + LINKED_GROUP_CALL_START = 181; + REPORT_TO_ADMIN_ENABLED_STATUS = 182; + EMPTY_SUBGROUP_CREATE = 183; + SCHEDULED_CALL_CANCEL = 184; + SUBGROUP_ADMIN_TRIGGERED_AUTO_ADD_RICH = 185; + GROUP_CHANGE_RECENT_HISTORY_SHARING = 186; + PAID_MESSAGE_SERVER_CAMPAIGN_ID = 187; + GENERAL_CHAT_CREATE = 188; + GENERAL_CHAT_ADD = 189; + GENERAL_CHAT_AUTO_ADD_DISABLED = 190; + SUGGESTED_SUBGROUP_ANNOUNCE = 191; + BIZ_BOT_1P_MESSAGING_ENABLED = 192; + CHANGE_USERNAME = 193; + BIZ_COEX_PRIVACY_INIT_SELF = 194; + BIZ_COEX_PRIVACY_TRANSITION_SELF = 195; + SUPPORT_AI_EDUCATION = 196; + BIZ_BOT_3P_MESSAGING_ENABLED = 197; + REMINDER_SETUP_MESSAGE = 198; + REMINDER_SENT_MESSAGE = 199; + REMINDER_CANCEL_MESSAGE = 200; + } +} + +message WebNotificationsInfo { + optional uint64 timestamp = 2; + optional uint32 unreadChats = 3; + optional uint32 notifyMessageCount = 4; + repeated WebMessageInfo notifyMessages = 5; +} diff --git a/src/main/java/it/auties/whatsapp/api/AsyncVerificationCodeSupplier.java b/src/main/java/it/auties/whatsapp/api/AsyncVerificationCodeSupplier.java new file mode 100644 index 000000000..9f3ff07bb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/AsyncVerificationCodeSupplier.java @@ -0,0 +1,20 @@ +package it.auties.whatsapp.api; + + +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +/** + * An interface to represent a supplier that returns a code wrapped in a CompletableFuture + */ +public interface AsyncVerificationCodeSupplier extends Supplier> { + /** + * Creates an asynchronous supplier from a synchronous one + * + * @param supplier a non-null supplier + * @return a non-null async supplier + */ + static AsyncVerificationCodeSupplier of(Supplier supplier) { + return () -> CompletableFuture.completedFuture(supplier.get()); + } +} diff --git a/src/main/java/it/auties/whatsapp/api/ClientType.java b/src/main/java/it/auties/whatsapp/api/ClientType.java new file mode 100644 index 000000000..91a09cfb7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/ClientType.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.api; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +/** + * The constants of this enumerated type describe the various types of API that can be used to make + * {@link Whatsapp} work + */ +public enum ClientType implements ProtobufEnum { + /** + * A standalone client that requires the QR code to be scanned by its companion on log-in Reversed + * from Whatsapp Web Client + */ + WEB(0), + /** + * A standalone client that requires an SMS code sent to the companion's phone number on log-in + * Reversed from KaiOS Mobile App + */ + MOBILE(1); + + final int index; + ClientType(@ProtobufEnumIndex int index) { + this.index = index; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java b/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java new file mode 100644 index 000000000..8f99a9dd5 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/ConnectionBuilder.java @@ -0,0 +1,203 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.controller.ControllerSerializer; +import it.auties.whatsapp.controller.KeysBuilder; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.controller.StoreKeysPair; +import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.model.mobile.SixPartsKeys; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * A builder to specify the type of connection to use + * + * @param the type of the newsletters + */ +@SuppressWarnings("unused") +public final class ConnectionBuilder> { + private final ClientType clientType; + private ControllerSerializer serializer; + + ConnectionBuilder(ClientType clientType) { + this.clientType = clientType; + this.serializer = ControllerSerializer.toProtobuf(); + } + + /** + * Uses a custom serializer + * + * @param serializer the non-null serializer to use + * @return the same instance for chaining + */ + public ConnectionBuilder serializer(ControllerSerializer serializer) { + this.serializer = serializer; + return this; + } + + /** + * Creates a new connection using a random uuid + * + * @return a non-null options selector + */ + public T newConnection() { + return newConnection(UUID.randomUUID()); + } + + /** + * Creates a new connection using a unique identifier + * If a session with the given id already whatsappOldEligible, it will be retrieved. + * Otherwise, a new one will be created. + * + * @param uuid the nullable uuid to use to create the connection + * @return a non-null options selector + */ + public T newConnection(UUID uuid) { + var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID); + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(sessionUuid, null, null, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(sessionUuid, null, null, clientType)); + return createConnection(sessionStoreAndKeys); + } + + /** + * Creates a new connection using a phone number + * If a session with the given phone number already whatsappOldEligible, it will be retrieved. + * Otherwise, a new one will be created. + * + * @param phoneNumber the nullable uuid to use to create the connection + * @return a non-null options selector + */ + public T newConnection(long phoneNumber) { + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(null, phoneNumber, null, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(UUID.randomUUID(), phoneNumber, null, clientType)); + return createConnection(sessionStoreAndKeys); + } + + /** + * Creates a new connection using an alias + * If a session with the given alias already whatsappOldEligible, it will be retrieved. + * Otherwise, a new one will be created. + * + * @param alias the nullable alias to use to create the connection + * @return a non-null options selector + */ + public T newConnection(String alias) { + var sessionStoreAndKeys = serializer.deserializeStoreKeysPair(null, null, alias, clientType) + .orElseGet(() -> serializer.newStoreKeysPair(UUID.randomUUID(), null, alias != null ? List.of(alias) : null, clientType)); + return createConnection(sessionStoreAndKeys); + } + + /** + * Creates a new connection using a six parts key representation + * + * @param sixParts the non-null six parts to use to create the connection + * @return a non-null options selector + */ + public T newConnection(SixPartsKeys sixParts) { + var serialized = serializer.deserializeStoreKeysPair(null, sixParts.phoneNumber().number(), null, ClientType.MOBILE); + if(serialized.isPresent()) { + return createConnection(serialized.get()); + } + + var uuid = UUID.randomUUID(); + var keys = new KeysBuilder() + .uuid(uuid) + .phoneNumber(sixParts.phoneNumber()) + .noiseKeyPair(sixParts.noiseKeyPair()) + .identityKeyPair(sixParts.identityKeyPair()) + .identityId(sixParts.identityId()) + .registered(true) + .build(); + keys.setSerializer(serializer); + var phoneNumber = keys.phoneNumber() + .map(PhoneNumber::number) + .orElse(null); + var store = Store.newStore(uuid, phoneNumber, null, ClientType.MOBILE); + store.setSerializer(serializer); + return createConnection(new StoreKeysPair(store, keys)); + } + + /** + * Creates a new connection from the first connection that was serialized + * If no connection is available, a new one will be created + * + * @return a non-null options selector + */ + public T firstConnection() { + return newConnection(serializer.listIds(clientType).peekFirst()); + } + + /** + * Creates a new connection from the last connection that was serialized + * If no connection is available, a new one will be created + * + * @return a non-null options selector + */ + public T lastConnection() { + return newConnection(serializer.listIds(clientType).peekLast()); + } + + /** + * Creates a new connection from the last connection that was serialized + * If no connection is available, an empty optional will be returned + * + * @return a non-null options selector + */ + public Optional newOptionalConnection(UUID uuid) { + var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID); + return serializer.deserializeStoreKeysPair(sessionUuid, null, null, clientType) + .map(this::createConnection); + } + + /** + * Creates a new connection from the last connection that was serialized + * If no connection is available, an empty optional will be returned + * + * @return a non-null options selector + */ + public Optional newOptionalConnection(Long phoneNumber) { + return serializer.deserializeStoreKeysPair(null, phoneNumber, null, clientType) + .map(this::createConnection); + } + + /** + * Creates a new connection using an alias + * If no connection is available, an empty optional will be returned + * + * @param alias the nullable alias to use to create the connection + * @return a non-null options selector + */ + public Optional newOptionalConnection(String alias) { + return serializer.deserializeStoreKeysPair(null, null, alias, clientType) + .map(this::createConnection); + } + + /** + * Creates a new connection from the first connection that was serialized + * + * @return an optional + */ + public Optional firstOptionalConnection() { + return newOptionalConnection(serializer.listIds(clientType).peekFirst()); + } + + /** + * Creates a new connection from the last connection that was serialized + * + * @return an optional + */ + public Optional lastOptionalConnection() { + return newOptionalConnection(serializer.listIds(clientType).peekLast()); + } + + @SuppressWarnings("unchecked") + private T createConnection(StoreKeysPair sessionStoreAndKeys) { + return (T) switch (clientType) { + case WEB -> new WebOptionsBuilder(sessionStoreAndKeys.store(), sessionStoreAndKeys.keys()); + case MOBILE -> new MobileOptionsBuilder(sessionStoreAndKeys.store(), sessionStoreAndKeys.keys()); + }; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/ConnectionType.java b/src/main/java/it/auties/whatsapp/api/ConnectionType.java new file mode 100644 index 000000000..bf7170c25 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/ConnectionType.java @@ -0,0 +1,25 @@ +package it.auties.whatsapp.api; + +/** + * The constants of this enumerated type describe the various types of connections that can be initialized + */ +public enum ConnectionType { + /** + * Creates a new connection using a unique identifier + * If no uuid is provided, a new connection will be created + * If the connection doesn't exist, a new one will be created + */ + NEW, + + /** + * Creates a new connection from the first session that was serialized + * If no connection is available, a new one will be created + */ + FIRST, + + /** + * Creates a new connection from the last session that was serialized + * If no connection is available, a new one will be created + */ + LAST +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/api/DisconnectReason.java b/src/main/java/it/auties/whatsapp/api/DisconnectReason.java new file mode 100644 index 000000000..8e68c33ea --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/DisconnectReason.java @@ -0,0 +1,27 @@ +package it.auties.whatsapp.api; + +/** + * The constants of this enumerated type describe the various reasons for which a session can be + * terminated + */ +public enum DisconnectReason { + /** + * Default errorReason + */ + DISCONNECTED, + + /** + * Reconnect + */ + RECONNECTING, + + /** + * Logged out + */ + LOGGED_OUT, + + /** + * Session restore + */ + RESTORE +} diff --git a/src/main/java/it/auties/whatsapp/api/Emoji.java b/src/main/java/it/auties/whatsapp/api/Emoji.java new file mode 100644 index 000000000..b9975eed7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/Emoji.java @@ -0,0 +1,1894 @@ +package it.auties.whatsapp.api; + +/** + * A list of all emojis supported by Whatsapp + * Source + */ +@SuppressWarnings("SpellCheckingInspection") +public enum Emoji { + GRINNING_FACE("😀"), + GRINNING_FACE_WITH_BIG_EYES("😃"), + GRINNING_FACE_WITH_SMILING_EYES("😄"), + BEAMING_FACE_WITH_SMILING_EYES("😁"), + GRINNING_SQUINTING_FACE("😆"), + GRINNING_FACE_WITH_SWEAT("😅"), + ROLLING_ON_THE_FLOOR_LAUGHING("🤣"), + FACE_WITH_TEARS_OF_JOY("😂"), + SLIGHTLY_SMILING_FACE("🙂"), + UPSIDE_DOWN_FACE("🙃"), + MELTING_FACE("🫠"), + WINKING_FACE("😉"), + SMILING_FACE_WITH_SMILING_EYES("😊"), + SMILING_FACE_WITH_HALO("😇"), + SMILING_FACE_WITH_HEARTS("🥰"), + SMILING_FACE_WITH_HEART_EYES("😍"), + STAR_STRUCK("🤩"), + FACE_BLOWING_A_KISS("😘"), + KISSING_FACE("😗"), + SMILING_FACE("☺"), + KISSING_FACE_WITH_CLOSED_EYES("😚"), + KISSING_FACE_WITH_SMILING_EYES("😙"), + SMILING_FACE_WITH_TEAR("🥲"), + FACE_SAVORING_FOOD("😋"), + FACE_WITH_TONGUE("😛"), + WINKING_FACE_WITH_TONGUE("😜"), + ZANY_FACE("🤪"), + SQUINTING_FACE_WITH_TONGUE("😝"), + MONEY_MOUTH_FACE("🤑"), + SMILING_FACE_WITH_OPEN_HANDS("🤗"), + FACE_WITH_HAND_OVER_MOUTH("🤭"), + FACE_WITH_OPEN_EYES_AND_HAND_OVER_MOUTH("🫢"), + FACE_WITH_PEEKING_EYE("🫣"), + SHUSHING_FACE("🤫"), + THINKING_FACE("🤔"), + SALUTING_FACE("🫡"), + ZIPPER_MOUTH_FACE("🤐"), + FACE_WITH_RAISED_EYEBROW("🤨"), + NEUTRAL_FACE("😐"), + EXPRESSIONLESS_FACE("😑"), + FACE_WITHOUT_MOUTH("😶"), + DOTTED_LINE_FACE("🫥"), + FACE_IN_CLOUDS("😶‍🌫️"), + SMIRKING_FACE("😏"), + UNAMUSED_FACE("😒"), + FACE_WITH_ROLLING_EYES("🙄"), + GRIMACING_FACE("😬"), + FACE_EXHALING("😮‍💨"), + LYING_FACE("🤥"), + SHAKING_FACE("🫨"), + RELIEVED_FACE("😌"), + PENSIVE_FACE("😔"), + SLEEPY_FACE("😪"), + DROOLING_FACE("🤤"), + SLEEPING_FACE("😴"), + FACE_WITH_MEDICAL_MASK("😷"), + FACE_WITH_THERMOMETER("🤒"), + FACE_WITH_HEAD_BANDAGE("🤕"), + NAUSEATED_FACE("🤢"), + FACE_VOMITING("🤮"), + SNEEZING_FACE("🤧"), + HOT_FACE("🥵"), + COLD_FACE("🥶"), + WOOZY_FACE("🥴"), + FACE_WITH_CROSSED_OUT_EYES("😵"), + FACE_WITH_SPIRAL_EYES("😵‍💫"), + EXPLODING_HEAD("🤯"), + COWBOY_HAT_FACE("🤠"), + PARTYING_FACE("🥳"), + DISGUISED_FACE("🥸"), + SMILING_FACE_WITH_SUNGLASSES("😎"), + NERD_FACE("🤓"), + FACE_WITH_MONOCLE("🧐"), + CONFUSED_FACE("😕"), + FACE_WITH_DIAGONAL_MOUTH("🫤"), + WORRIED_FACE("😟"), + SLIGHTLY_FROWNING_FACE("🙁"), + FROWNING_FACE("☹"), + FACE_WITH_OPEN_MOUTH("😮"), + HUSHED_FACE("😯"), + ASTONISHED_FACE("😲"), + FLUSHED_FACE("😳"), + PLEADING_FACE("🥺"), + FACE_HOLDING_BACK_TEARS("🥹"), + FROWNING_FACE_WITH_OPEN_MOUTH("😦"), + ANGUISHED_FACE("😧"), + FEARFUL_FACE("😨"), + ANXIOUS_FACE_WITH_SWEAT("😰"), + SAD_BUT_RELIEVED_FACE("😥"), + CRYING_FACE("😢"), + LOUDLY_CRYING_FACE("😭"), + FACE_SCREAMING_IN_FEAR("😱"), + CONFOUNDED_FACE("😖"), + PERSEVERING_FACE("😣"), + DISAPPOINTED_FACE("😞"), + DOWNCAST_FACE_WITH_SWEAT("😓"), + WEARY_FACE("😩"), + TIRED_FACE("😫"), + YAWNING_FACE("🥱"), + FACE_WITH_STEAM_FROM_NOSE("😤"), + ENRAGED_FACE("😡"), + ANGRY_FACE("😠"), + FACE_WITH_SYMBOLS_ON_MOUTH("🤬"), + SMILING_FACE_WITH_HORNS("😈"), + ANGRY_FACE_WITH_HORNS("👿"), + SKULL("💀"), + SKULL_AND_CROSSBONES("☠"), + PILE_OF_POO("💩"), + CLOWN_FACE("🤡"), + OGRE("👹"), + GOBLIN("👺"), + GHOST("👻"), + ALIEN("👽"), + ALIEN_MONSTER("👾"), + ROBOT("🤖"), + GRINNING_CAT("😺"), + GRINNING_CAT_WITH_SMILING_EYES("😸"), + CAT_WITH_TEARS_OF_JOY("😹"), + SMILING_CAT_WITH_HEART_EYES("😻"), + CAT_WITH_WRY_SMILE("😼"), + KISSING_CAT("😽"), + WEARY_CAT("🙀"), + CRYING_CAT("😿"), + POUTING_CAT("😾"), + SEE_NO_EVIL_MONKEY("🙈"), + HEAR_NO_EVIL_MONKEY("🙉"), + SPEAK_NO_EVIL_MONKEY("🙊"), + LOVE_LETTER("💌"), + HEART_WITH_ARROW("💘"), + HEART_WITH_RIBBON("💝"), + SPARKLING_HEART("💖"), + GROWING_HEART("💗"), + BEATING_HEART("💓"), + REVOLVING_HEARTS("💞"), + TWO_HEARTS("💕"), + HEART_DECORATION("💟"), + HEART_EXCLAMATION("❣"), + BROKEN_HEART("💔"), + HEART_ON_FIRE("❤️‍🔥"), + MENDING_HEART("❤️‍🩹"), + RED_HEART("❤"), + PINK_HEART("🩷"), + ORANGE_HEART("🧡"), + YELLOW_HEART("💛"), + GREEN_HEART("💚"), + BLUE_HEART("💙"), + LIGHT_BLUE_HEART("🩵"), + PURPLE_HEART("💜"), + BROWN_HEART("🤎"), + BLACK_HEART("🖤"), + GREY_HEART("🩶"), + WHITE_HEART("🤍"), + KISS_MARK("💋"), + HUNDRED_POINTS("💯"), + ANGER_SYMBOL("💢"), + COLLISION("💥"), + DIZZY("💫"), + SWEAT_DROPLETS("💦"), + DASHING_AWAY("💨"), + HOLE("🕳"), + SPEECH_BALLOON("💬"), + EYE_IN_SPEECH_BUBBLE("👁️‍🗨️"), + LEFT_SPEECH_BUBBLE("🗨"), + RIGHT_ANGER_BUBBLE("🗯"), + THOUGHT_BALLOON("💭"), + ZZZ("💤"), + WAVING_HAND("👋"), + RAISED_BACK_OF_HAND("🤚"), + HAND_WITH_FINGERS_SPLAYED("🖐"), + RAISED_HAND("✋"), + VULCAN_SALUTE("🖖"), + RIGHTWARDS_HAND("🫱"), + LEFTWARDS_HAND("🫲"), + PALM_DOWN_HAND("🫳"), + PALM_UP_HAND("🫴"), + LEFTWARDS_PUSHING_HAND("🫷"), + RIGHTWARDS_PUSHING_HAND("🫸"), + OK_HAND("👌"), + PINCHED_FINGERS("🤌"), + PINCHING_HAND("🤏"), + VICTORY_HAND("✌"), + CROSSED_FINGERS("🤞"), + HAND_WITH_INDEX_FINGER_AND_THUMB_CROSSED("🫰"), + LOVE_YOU_GESTURE("🤟"), + SIGN_OF_THE_HORNS("🤘"), + CALL_ME_HAND("🤙"), + BACKHAND_INDEX_POINTING_LEFT("👈"), + BACKHAND_INDEX_POINTING_RIGHT("👉"), + BACKHAND_INDEX_POINTING_UP("👆"), + MIDDLE_FINGER("🖕"), + BACKHAND_INDEX_POINTING_DOWN("👇"), + INDEX_POINTING_UP("☝"), + INDEX_POINTING_AT_THE_VIEWER("🫵"), + THUMBS_UP("👍"), + THUMBS_DOWN("👎"), + RAISED_FIST("✊"), + ONCOMING_FIST("👊"), + LEFT_FACING_FIST("🤛"), + RIGHT_FACING_FIST("🤜"), + CLAPPING_HANDS("👏"), + RAISING_HANDS("🙌"), + HEART_HANDS("🫶"), + OPEN_HANDS("👐"), + PALMS_UP_TOGETHER("🤲"), + HANDSHAKE("🤝"), + FOLDED_HANDS("🙏"), + WRITING_HAND("✍"), + NAIL_POLISH("💅"), + SELFIE("🤳"), + FLEXED_BICEPS("💪"), + MECHANICAL_ARM("🦾"), + MECHANICAL_LEG("🦿"), + LEG("🦵"), + FOOT("🦶"), + EAR("👂"), + EAR_WITH_HEARING_AID("🦻"), + NOSE("👃"), + BRAIN("🧠"), + ANATOMICAL_HEART("🫀"), + LUNGS("🫁"), + TOOTH("🦷"), + BONE("🦴"), + EYES("👀"), + EYE("👁"), + TONGUE("👅"), + MOUTH("👄"), + BITING_LIP("🫦"), + BABY("👶"), + CHILD("🧒"), + BOY("👦"), + GIRL("👧"), + PERSON("🧑"), + PERSON_BLOND_HAIR("👱"), + MAN("👨"), + PERSON_BEARD("🧔"), + MAN_BEARD("🧔‍♂️"), + WOMAN_BEARD("🧔‍♀️"), + MAN_RED_HAIR("👨‍🦰"), + MAN_CURLY_HAIR("👨‍🦱"), + MAN_WHITE_HAIR("👨‍🦳"), + MAN_BALD("👨‍🦲"), + WOMAN("👩"), + WOMAN_RED_HAIR("👩‍🦰"), + PERSON_RED_HAIR("🧑‍🦰"), + WOMAN_CURLY_HAIR("👩‍🦱"), + PERSON_CURLY_HAIR("🧑‍🦱"), + WOMAN_WHITE_HAIR("👩‍🦳"), + PERSON_WHITE_HAIR("🧑‍🦳"), + WOMAN_BALD("👩‍🦲"), + PERSON_BALD("🧑‍🦲"), + WOMAN_BLOND_HAIR("👱‍♀️"), + MAN_BLOND_HAIR("👱‍♂️"), + OLDER_PERSON("🧓"), + OLD_MAN("👴"), + OLD_WOMAN("👵"), + PERSON_FROWNING("🙍"), + MAN_FROWNING("🙍‍♂️"), + WOMAN_FROWNING("🙍‍♀️"), + PERSON_POUTING("🙎"), + MAN_POUTING("🙎‍♂️"), + WOMAN_POUTING("🙎‍♀️"), + PERSON_GESTURING_NO("🙅"), + MAN_GESTURING_NO("🙅‍♂️"), + WOMAN_GESTURING_NO("🙅‍♀️"), + PERSON_GESTURING_OK("🙆"), + MAN_GESTURING_OK("🙆‍♂️"), + WOMAN_GESTURING_OK("🙆‍♀️"), + PERSON_TIPPING_HAND("💁"), + MAN_TIPPING_HAND("💁‍♂️"), + WOMAN_TIPPING_HAND("💁‍♀️"), + PERSON_RAISING_HAND("🙋"), + MAN_RAISING_HAND("🙋‍♂️"), + WOMAN_RAISING_HAND("🙋‍♀️"), + DEAF_PERSON("🧏"), + DEAF_MAN("🧏‍♂️"), + DEAF_WOMAN("🧏‍♀️"), + PERSON_BOWING("🙇"), + MAN_BOWING("🙇‍♂️"), + WOMAN_BOWING("🙇‍♀️"), + PERSON_FACEPALMING("🤦"), + MAN_FACEPALMING("🤦‍♂️"), + WOMAN_FACEPALMING("🤦‍♀️"), + PERSON_SHRUGGING("🤷"), + MAN_SHRUGGING("🤷‍♂️"), + WOMAN_SHRUGGING("🤷‍♀️"), + HEALTH_WORKER("🧑‍⚕️"), + MAN_HEALTH_WORKER("👨‍⚕️"), + WOMAN_HEALTH_WORKER("👩‍⚕️"), + STUDENT("🧑‍🎓"), + MAN_STUDENT("👨‍🎓"), + WOMAN_STUDENT("👩‍🎓"), + TEACHER("🧑‍🏫"), + MAN_TEACHER("👨‍🏫"), + WOMAN_TEACHER("👩‍🏫"), + JUDGE("🧑‍⚖️"), + MAN_JUDGE("👨‍⚖️"), + WOMAN_JUDGE("👩‍⚖️"), + FARMER("🧑‍🌾"), + MAN_FARMER("👨‍🌾"), + WOMAN_FARMER("👩‍🌾"), + COOK("🧑‍🍳"), + MAN_COOK("👨‍🍳"), + WOMAN_COOK("👩‍🍳"), + MECHANIC("🧑‍🔧"), + MAN_MECHANIC("👨‍🔧"), + WOMAN_MECHANIC("👩‍🔧"), + FACTORY_WORKER("🧑‍🏭"), + MAN_FACTORY_WORKER("👨‍🏭"), + WOMAN_FACTORY_WORKER("👩‍🏭"), + OFFICE_WORKER("🧑‍💼"), + MAN_OFFICE_WORKER("👨‍💼"), + WOMAN_OFFICE_WORKER("👩‍💼"), + SCIENTIST("🧑‍🔬"), + MAN_SCIENTIST("👨‍🔬"), + WOMAN_SCIENTIST("👩‍🔬"), + TECHNOLOGIST("🧑‍💻"), + MAN_TECHNOLOGIST("👨‍💻"), + WOMAN_TECHNOLOGIST("👩‍💻"), + SINGER("🧑‍🎤"), + MAN_SINGER("👨‍🎤"), + WOMAN_SINGER("👩‍🎤"), + ARTIST("🧑‍🎨"), + MAN_ARTIST("👨‍🎨"), + WOMAN_ARTIST("👩‍🎨"), + PILOT("🧑‍✈️"), + MAN_PILOT("👨‍✈️"), + WOMAN_PILOT("👩‍✈️"), + ASTRONAUT("🧑‍🚀"), + MAN_ASTRONAUT("👨‍🚀"), + WOMAN_ASTRONAUT("👩‍🚀"), + FIREFIGHTER("🧑‍🚒"), + MAN_FIREFIGHTER("👨‍🚒"), + WOMAN_FIREFIGHTER("👩‍🚒"), + POLICE_OFFICER("👮"), + MAN_POLICE_OFFICER("👮‍♂️"), + WOMAN_POLICE_OFFICER("👮‍♀️"), + DETECTIVE("🕵"), + MAN_DETECTIVE("🕵️‍♂️"), + WOMAN_DETECTIVE("🕵️‍♀️"), + GUARD("💂"), + MAN_GUARD("💂‍♂️"), + WOMAN_GUARD("💂‍♀️"), + NINJA("🥷"), + CONSTRUCTION_WORKER("👷"), + MAN_CONSTRUCTION_WORKER("👷‍♂️"), + WOMAN_CONSTRUCTION_WORKER("👷‍♀️"), + PERSON_WITH_CROWN("🫅"), + PRINCE("🤴"), + PRINCESS("👸"), + PERSON_WEARING_TURBAN("👳"), + MAN_WEARING_TURBAN("👳‍♂️"), + WOMAN_WEARING_TURBAN("👳‍♀️"), + PERSON_WITH_SKULLCAP("👲"), + WOMAN_WITH_HEADSCARF("🧕"), + PERSON_IN_TUXEDO("🤵"), + MAN_IN_TUXEDO("🤵‍♂️"), + WOMAN_IN_TUXEDO("🤵‍♀️"), + PERSON_WITH_VEIL("👰"), + MAN_WITH_VEIL("👰‍♂️"), + WOMAN_WITH_VEIL("👰‍♀️"), + PREGNANT_WOMAN("🤰"), + PREGNANT_MAN("🫃"), + PREGNANT_PERSON("🫄"), + BREAST_FEEDING("🤱"), + WOMAN_FEEDING_BABY("👩‍🍼"), + MAN_FEEDING_BABY("👨‍🍼"), + PERSON_FEEDING_BABY("🧑‍🍼"), + BABY_ANGEL("👼"), + SANTA_CLAUS("🎅"), + MRS__CLAUS("🤶"), + MX_CLAUS("🧑‍🎄"), + SUPERHERO("🦸"), + MAN_SUPERHERO("🦸‍♂️"), + WOMAN_SUPERHERO("🦸‍♀️"), + SUPERVILLAIN("🦹"), + MAN_SUPERVILLAIN("🦹‍♂️"), + WOMAN_SUPERVILLAIN("🦹‍♀️"), + MAGE("🧙"), + MAN_MAGE("🧙‍♂️"), + WOMAN_MAGE("🧙‍♀️"), + FAIRY("🧚"), + MAN_FAIRY("🧚‍♂️"), + WOMAN_FAIRY("🧚‍♀️"), + VAMPIRE("🧛"), + MAN_VAMPIRE("🧛‍♂️"), + WOMAN_VAMPIRE("🧛‍♀️"), + MERPERSON("🧜"), + MERMAN("🧜‍♂️"), + MERMAID("🧜‍♀️"), + ELF("🧝"), + MAN_ELF("🧝‍♂️"), + WOMAN_ELF("🧝‍♀️"), + GENIE("🧞"), + MAN_GENIE("🧞‍♂️"), + WOMAN_GENIE("🧞‍♀️"), + ZOMBIE("🧟"), + MAN_ZOMBIE("🧟‍♂️"), + WOMAN_ZOMBIE("🧟‍♀️"), + TROLL("🧌"), + PERSON_GETTING_MASSAGE("💆"), + MAN_GETTING_MASSAGE("💆‍♂️"), + WOMAN_GETTING_MASSAGE("💆‍♀️"), + PERSON_GETTING_HAIRCUT("💇"), + MAN_GETTING_HAIRCUT("💇‍♂️"), + WOMAN_GETTING_HAIRCUT("💇‍♀️"), + PERSON_WALKING("🚶"), + MAN_WALKING("🚶‍♂️"), + WOMAN_WALKING("🚶‍♀️"), + PERSON_STANDING("🧍"), + MAN_STANDING("🧍‍♂️"), + WOMAN_STANDING("🧍‍♀️"), + PERSON_KNEELING("🧎"), + MAN_KNEELING("🧎‍♂️"), + WOMAN_KNEELING("🧎‍♀️"), + PERSON_WITH_WHITE_CANE("🧑‍🦯"), + MAN_WITH_WHITE_CANE("👨‍🦯"), + WOMAN_WITH_WHITE_CANE("👩‍🦯"), + PERSON_IN_MOTORIZED_WHEELCHAIR("🧑‍🦼"), + MAN_IN_MOTORIZED_WHEELCHAIR("👨‍🦼"), + WOMAN_IN_MOTORIZED_WHEELCHAIR("👩‍🦼"), + PERSON_IN_MANUAL_WHEELCHAIR("🧑‍🦽"), + MAN_IN_MANUAL_WHEELCHAIR("👨‍🦽"), + WOMAN_IN_MANUAL_WHEELCHAIR("👩‍🦽"), + PERSON_RUNNING("🏃"), + MAN_RUNNING("🏃‍♂️"), + WOMAN_RUNNING("🏃‍♀️"), + WOMAN_DANCING("💃"), + MAN_DANCING("🕺"), + PERSON_IN_SUIT_LEVITATING("🕴"), + PEOPLE_WITH_BUNNY_EARS("👯"), + MEN_WITH_BUNNY_EARS("👯‍♂️"), + WOMEN_WITH_BUNNY_EARS("👯‍♀️"), + PERSON_IN_STEAMY_ROOM("🧖"), + MAN_IN_STEAMY_ROOM("🧖‍♂️"), + WOMAN_IN_STEAMY_ROOM("🧖‍♀️"), + PERSON_CLIMBING("🧗"), + MAN_CLIMBING("🧗‍♂️"), + WOMAN_CLIMBING("🧗‍♀️"), + PERSON_FENCING("🤺"), + HORSE_RACING("🏇"), + SKIER("⛷"), + SNOWBOARDER("🏂"), + PERSON_GOLFING("🏌"), + MAN_GOLFING("🏌️‍♂️"), + WOMAN_GOLFING("🏌️‍♀️"), + PERSON_SURFING("🏄"), + MAN_SURFING("🏄‍♂️"), + WOMAN_SURFING("🏄‍♀️"), + PERSON_ROWING_BOAT("🚣"), + MAN_ROWING_BOAT("🚣‍♂️"), + WOMAN_ROWING_BOAT("🚣‍♀️"), + PERSON_SWIMMING("🏊"), + MAN_SWIMMING("🏊‍♂️"), + WOMAN_SWIMMING("🏊‍♀️"), + PERSON_BOUNCING_BALL("⛹"), + MAN_BOUNCING_BALL("⛹️‍♂️"), + WOMAN_BOUNCING_BALL("⛹️‍♀️"), + PERSON_LIFTING_WEIGHTS("🏋"), + MAN_LIFTING_WEIGHTS("🏋️‍♂️"), + WOMAN_LIFTING_WEIGHTS("🏋️‍♀️"), + PERSON_BIKING("🚴"), + MAN_BIKING("🚴‍♂️"), + WOMAN_BIKING("🚴‍♀️"), + PERSON_MOUNTAIN_BIKING("🚵"), + MAN_MOUNTAIN_BIKING("🚵‍♂️"), + WOMAN_MOUNTAIN_BIKING("🚵‍♀️"), + PERSON_CARTWHEELING("🤸"), + MAN_CARTWHEELING("🤸‍♂️"), + WOMAN_CARTWHEELING("🤸‍♀️"), + PEOPLE_WRESTLING("🤼"), + MEN_WRESTLING("🤼‍♂️"), + WOMEN_WRESTLING("🤼‍♀️"), + PERSON_PLAYING_WATER_POLO("🤽"), + MAN_PLAYING_WATER_POLO("🤽‍♂️"), + WOMAN_PLAYING_WATER_POLO("🤽‍♀️"), + PERSON_PLAYING_HANDBALL("🤾"), + MAN_PLAYING_HANDBALL("🤾‍♂️"), + WOMAN_PLAYING_HANDBALL("🤾‍♀️"), + PERSON_JUGGLING("🤹"), + MAN_JUGGLING("🤹‍♂️"), + WOMAN_JUGGLING("🤹‍♀️"), + PERSON_IN_LOTUS_POSITION("🧘"), + MAN_IN_LOTUS_POSITION("🧘‍♂️"), + WOMAN_IN_LOTUS_POSITION("🧘‍♀️"), + PERSON_TAKING_BATH("🛀"), + PERSON_IN_BED("🛌"), + PEOPLE_HOLDING_HANDS("🧑‍🤝‍🧑"), + WOMEN_HOLDING_HANDS("👭"), + WOMAN_AND_MAN_HOLDING_HANDS("👫"), + MEN_HOLDING_HANDS("👬"), + KISS("💏"), + KISS_WOMAN_MAN("👩‍❤️‍💋‍👨"), + KISS_MAN_MAN("👨‍❤️‍💋‍👨"), + KISS_WOMAN_WOMAN("👩‍❤️‍💋‍👩"), + COUPLE_WITH_HEART("💑"), + COUPLE_WITH_HEART_WOMAN_MAN("👩‍❤️‍👨"), + COUPLE_WITH_HEART_MAN_MAN("👨‍❤️‍👨"), + COUPLE_WITH_HEART_WOMAN_WOMAN("👩‍❤️‍👩"), + FAMILY("👪"), + FAMILY_MAN_WOMAN_BOY("👨‍👩‍👦"), + FAMILY_MAN_WOMAN_GIRL("👨‍👩‍👧"), + FAMILY_MAN_WOMAN_GIRL_BOY("👨‍👩‍👧‍👦"), + FAMILY_MAN_WOMAN_BOY_BOY("👨‍👩‍👦‍👦"), + FAMILY_MAN_WOMAN_GIRL_GIRL("👨‍👩‍👧‍👧"), + FAMILY_MAN_MAN_BOY("👨‍👨‍👦"), + FAMILY_MAN_MAN_GIRL("👨‍👨‍👧"), + FAMILY_MAN_MAN_GIRL_BOY("👨‍👨‍👧‍👦"), + FAMILY_MAN_MAN_BOY_BOY("👨‍👨‍👦‍👦"), + FAMILY_MAN_MAN_GIRL_GIRL("👨‍👨‍👧‍👧"), + FAMILY_WOMAN_WOMAN_BOY("👩‍👩‍👦"), + FAMILY_WOMAN_WOMAN_GIRL("👩‍👩‍👧"), + FAMILY_WOMAN_WOMAN_GIRL_BOY("👩‍👩‍👧‍👦"), + FAMILY_WOMAN_WOMAN_BOY_BOY("👩‍👩‍👦‍👦"), + FAMILY_WOMAN_WOMAN_GIRL_GIRL("👩‍👩‍👧‍👧"), + FAMILY_MAN_BOY("👨‍👦"), + FAMILY_MAN_BOY_BOY("👨‍👦‍👦"), + FAMILY_MAN_GIRL("👨‍👧"), + FAMILY_MAN_GIRL_BOY("👨‍👧‍👦"), + FAMILY_MAN_GIRL_GIRL("👨‍👧‍👧"), + FAMILY_WOMAN_BOY("👩‍👦"), + FAMILY_WOMAN_BOY_BOY("👩‍👦‍👦"), + FAMILY_WOMAN_GIRL("👩‍👧"), + FAMILY_WOMAN_GIRL_BOY("👩‍👧‍👦"), + FAMILY_WOMAN_GIRL_GIRL("👩‍👧‍👧"), + SPEAKING_HEAD("🗣"), + BUST_IN_SILHOUETTE("👤"), + BUSTS_IN_SILHOUETTE("👥"), + PEOPLE_HUGGING("🫂"), + FOOTPRINTS("👣"), + RED_HAIR("🦰"), + CURLY_HAIR("🦱"), + WHITE_HAIR("🦳"), + BALD("🦲"), + MONKEY_FACE("🐵"), + MONKEY("🐒"), + GORILLA("🦍"), + ORANGUTAN("🦧"), + DOG_FACE("🐶"), + DOG("🐕"), + GUIDE_DOG("🦮"), + SERVICE_DOG("🐕‍🦺"), + POODLE("🐩"), + WOLF("🐺"), + FOX("🦊"), + RACCOON("🦝"), + CAT_FACE("🐱"), + CAT("🐈"), + BLACK_CAT("🐈‍⬛"), + LION("🦁"), + TIGER_FACE("🐯"), + TIGER("🐅"), + LEOPARD("🐆"), + HORSE_FACE("🐴"), + MOOSE("🫎"), + DONKEY("🫏"), + HORSE("🐎"), + UNICORN("🦄"), + ZEBRA("🦓"), + DEER("🦌"), + BISON("🦬"), + COW_FACE("🐮"), + OX("🐂"), + WATER_BUFFALO("🐃"), + COW("🐄"), + PIG_FACE("🐷"), + PIG("🐖"), + BOAR("🐗"), + PIG_NOSE("🐽"), + RAM("🐏"), + EWE("🐑"), + GOAT("🐐"), + CAMEL("🐪"), + TWO_HUMP_CAMEL("🐫"), + LLAMA("🦙"), + GIRAFFE("🦒"), + ELEPHANT("🐘"), + MAMMOTH("🦣"), + RHINOCEROS("🦏"), + HIPPOPOTAMUS("🦛"), + MOUSE_FACE("🐭"), + MOUSE("🐁"), + RAT("🐀"), + HAMSTER("🐹"), + RABBIT_FACE("🐰"), + RABBIT("🐇"), + CHIPMUNK("🐿"), + BEAVER("🦫"), + HEDGEHOG("🦔"), + BAT("🦇"), + BEAR("🐻"), + POLAR_BEAR("🐻‍❄️"), + KOALA("🐨"), + PANDA("🐼"), + SLOTH("🦥"), + OTTER("🦦"), + SKUNK("🦨"), + KANGAROO("🦘"), + BADGER("🦡"), + PAW_PRINTS("🐾"), + TURKEY("🦃"), + CHICKEN("🐔"), + ROOSTER("🐓"), + HATCHING_CHICK("🐣"), + BABY_CHICK("🐤"), + FRONT_FACING_BABY_CHICK("🐥"), + BIRD("🐦"), + PENGUIN("🐧"), + DOVE("🕊"), + EAGLE("🦅"), + DUCK("🦆"), + SWAN("🦢"), + OWL("🦉"), + DODO("🦤"), + FEATHER("🪶"), + FLAMINGO("🦩"), + PEACOCK("🦚"), + PARROT("🦜"), + WING("🪽"), + BLACK_BIRD("🐦‍⬛"), + GOOSE("🪿"), + FROG("🐸"), + CROCODILE("🐊"), + TURTLE("🐢"), + LIZARD("🦎"), + SNAKE("🐍"), + DRAGON_FACE("🐲"), + DRAGON("🐉"), + SAUROPOD("🦕"), + T_REX("🦖"), + SPOUTING_WHALE("🐳"), + WHALE("🐋"), + DOLPHIN("🐬"), + SEAL("🦭"), + FISH("🐟"), + TROPICAL_FISH("🐠"), + BLOWFISH("🐡"), + SHARK("🦈"), + OCTOPUS("🐙"), + SPIRAL_SHELL("🐚"), + CORAL("🪸"), + JELLYFISH("🪼"), + SNAIL("🐌"), + BUTTERFLY("🦋"), + BUG("🐛"), + ANT("🐜"), + HONEYBEE("🐝"), + BEETLE("🪲"), + LADY_BEETLE("🐞"), + CRICKET("🦗"), + COCKROACH("🪳"), + SPIDER("🕷"), + SPIDER_WEB("🕸"), + SCORPION("🦂"), + MOSQUITO("🦟"), + FLY("🪰"), + WORM("🪱"), + MICROBE("🦠"), + BOUQUET("💐"), + CHERRY_BLOSSOM("🌸"), + WHITE_FLOWER("💮"), + LOTUS("🪷"), + ROSETTE("🏵"), + ROSE("🌹"), + WILTED_FLOWER("🥀"), + HIBISCUS("🌺"), + SUNFLOWER("🌻"), + BLOSSOM("🌼"), + TULIP("🌷"), + HYACINTH("🪻"), + SEEDLING("🌱"), + POTTED_PLANT("🪴"), + EVERGREEN_TREE("🌲"), + DECIDUOUS_TREE("🌳"), + PALM_TREE("🌴"), + CACTUS("🌵"), + SHEAF_OF_RICE("🌾"), + HERB("🌿"), + SHAMROCK("☘"), + FOUR_LEAF_CLOVER("🍀"), + MAPLE_LEAF("🍁"), + FALLEN_LEAF("🍂"), + LEAF_FLUTTERING_IN_WIND("🍃"), + EMPTY_NEST("🪹"), + NEST_WITH_EGGS("🪺"), + MUSHROOM("🍄"), + GRAPES("🍇"), + MELON("🍈"), + WATERMELON("🍉"), + TANGERINE("🍊"), + LEMON("🍋"), + BANANA("🍌"), + PINEAPPLE("🍍"), + MANGO("🥭"), + RED_APPLE("🍎"), + GREEN_APPLE("🍏"), + PEAR("🍐"), + PEACH("🍑"), + CHERRIES("🍒"), + STRAWBERRY("🍓"), + BLUEBERRIES("🫐"), + KIWI_FRUIT("🥝"), + TOMATO("🍅"), + OLIVE("🫒"), + COCONUT("🥥"), + AVOCADO("🥑"), + EGGPLANT("🍆"), + POTATO("🥔"), + CARROT("🥕"), + EAR_OF_CORN("🌽"), + HOT_PEPPER("🌶"), + BELL_PEPPER("🫑"), + CUCUMBER("🥒"), + LEAFY_GREEN("🥬"), + BROCCOLI("🥦"), + GARLIC("🧄"), + ONION("🧅"), + PEANUTS("🥜"), + BEANS("🫘"), + CHESTNUT("🌰"), + GINGER_ROOT("🫚"), + PEA_POD("🫛"), + BREAD("🍞"), + CROISSANT("🥐"), + BAGUETTE_BREAD("🥖"), + FLATBREAD("🫓"), + PRETZEL("🥨"), + BAGEL("🥯"), + PANCAKES("🥞"), + WAFFLE("🧇"), + CHEESE_WEDGE("🧀"), + MEAT_ON_BONE("🍖"), + POULTRY_LEG("🍗"), + CUT_OF_MEAT("🥩"), + BACON("🥓"), + HAMBURGER("🍔"), + FRENCH_FRIES("🍟"), + PIZZA("🍕"), + HOT_DOG("🌭"), + SANDWICH("🥪"), + TACO("🌮"), + BURRITO("🌯"), + TAMALE("🫔"), + STUFFED_FLATBREAD("🥙"), + FALAFEL("🧆"), + EGG("🥚"), + COOKING("🍳"), + SHALLOW_PAN_OF_FOOD("🥘"), + POT_OF_FOOD("🍲"), + FONDUE("🫕"), + BOWL_WITH_SPOON("🥣"), + GREEN_SALAD("🥗"), + POPCORN("🍿"), + BUTTER("🧈"), + SALT("🧂"), + CANNED_FOOD("🥫"), + BENTO_BOX("🍱"), + RICE_CRACKER("🍘"), + RICE_BALL("🍙"), + COOKED_RICE("🍚"), + CURRY_RICE("🍛"), + STEAMING_BOWL("🍜"), + SPAGHETTI("🍝"), + ROASTED_SWEET_POTATO("🍠"), + ODEN("🍢"), + SUSHI("🍣"), + FRIED_SHRIMP("🍤"), + FISH_CAKE_WITH_SWIRL("🍥"), + MOON_CAKE("🥮"), + DANGO("🍡"), + DUMPLING("🥟"), + FORTUNE_COOKIE("🥠"), + TAKEOUT_BOX("🥡"), + CRAB("🦀"), + LOBSTER("🦞"), + SHRIMP("🦐"), + SQUID("🦑"), + OYSTER("🦪"), + SOFT_ICE_CREAM("🍦"), + SHAVED_ICE("🍧"), + ICE_CREAM("🍨"), + DOUGHNUT("🍩"), + COOKIE("🍪"), + BIRTHDAY_CAKE("🎂"), + SHORTCAKE("🍰"), + CUPCAKE("🧁"), + PIE("🥧"), + CHOCOLATE_BAR("🍫"), + CANDY("🍬"), + LOLLIPOP("🍭"), + CUSTARD("🍮"), + HONEY_POT("🍯"), + BABY_BOTTLE("🍼"), + GLASS_OF_MILK("🥛"), + HOT_BEVERAGE("☕"), + TEAPOT("🫖"), + TEACUP_WITHOUT_HANDLE("🍵"), + SAKE("🍶"), + BOTTLE_WITH_POPPING_CORK("🍾"), + WINE_GLASS("🍷"), + COCKTAIL_GLASS("🍸"), + TROPICAL_DRINK("🍹"), + BEER_MUG("🍺"), + CLINKING_BEER_MUGS("🍻"), + CLINKING_GLASSES("🥂"), + TUMBLER_GLASS("🥃"), + POURING_LIQUID("🫗"), + CUP_WITH_STRAW("🥤"), + BUBBLE_TEA("🧋"), + BEVERAGE_BOX("🧃"), + MATE("🧉"), + ICE("🧊"), + CHOPSTICKS("🥢"), + FORK_AND_KNIFE_WITH_PLATE("🍽"), + FORK_AND_KNIFE("🍴"), + SPOON("🥄"), + KITCHEN_KNIFE("🔪"), + JAR("🫙"), + AMPHORA("🏺"), + GLOBE_SHOWING_EUROPE_AFRICA("🌍"), + GLOBE_SHOWING_AMERICAS("🌎"), + GLOBE_SHOWING_ASIA_AUSTRALIA("🌏"), + GLOBE_WITH_MERIDIANS("🌐"), + WORLD_MAP("🗺"), + MAP_OF_JAPAN("🗾"), + COMPASS("🧭"), + SNOW_CAPPED_MOUNTAIN("🏔"), + MOUNTAIN("⛰"), + VOLCANO("🌋"), + MOUNT_FUJI("🗻"), + CAMPING("🏕"), + BEACH_WITH_UMBRELLA("🏖"), + DESERT("🏜"), + DESERT_ISLAND("🏝"), + NATIONAL_PARK("🏞"), + STADIUM("🏟"), + CLASSICAL_BUILDING("🏛"), + BUILDING_CONSTRUCTION("🏗"), + BRICK("🧱"), + ROCK("🪨"), + WOOD("🪵"), + HUT("🛖"), + HOUSES("🏘"), + DERELICT_HOUSE("🏚"), + HOUSE("🏠"), + HOUSE_WITH_GARDEN("🏡"), + OFFICE_BUILDING("🏢"), + JAPANESE_POST_OFFICE("🏣"), + POST_OFFICE("🏤"), + HOSPITAL("🏥"), + BANK("🏦"), + HOTEL("🏨"), + LOVE_HOTEL("🏩"), + CONVENIENCE_STORE("🏪"), + SCHOOL("🏫"), + DEPARTMENT_STORE("🏬"), + FACTORY("🏭"), + JAPANESE_CASTLE("🏯"), + CASTLE("🏰"), + WEDDING("💒"), + TOKYO_TOWER("🗼"), + STATUE_OF_LIBERTY("🗽"), + CHURCH("⛪"), + MOSQUE("🕌"), + HINDU_TEMPLE("🛕"), + SYNAGOGUE("🕍"), + SHINTO_SHRINE("⛩"), + KAABA("🕋"), + FOUNTAIN("⛲"), + TENT("⛺"), + FOGGY("🌁"), + NIGHT_WITH_STARS("🌃"), + CITYSCAPE("🏙"), + SUNRISE_OVER_MOUNTAINS("🌄"), + SUNRISE("🌅"), + CITYSCAPE_AT_DUSK("🌆"), + SUNSET("🌇"), + BRIDGE_AT_NIGHT("🌉"), + HOT_SPRINGS("♨"), + CAROUSEL_HORSE("🎠"), + PLAYGROUND_SLIDE("🛝"), + FERRIS_WHEEL("🎡"), + ROLLER_COASTER("🎢"), + BARBER_POLE("💈"), + CIRCUS_TENT("🎪"), + LOCOMOTIVE("🚂"), + RAILWAY_CAR("🚃"), + HIGH_SPEED_TRAIN("🚄"), + BULLET_TRAIN("🚅"), + TRAIN("🚆"), + METRO("🚇"), + LIGHT_RAIL("🚈"), + STATION("🚉"), + TRAM("🚊"), + MONORAIL("🚝"), + MOUNTAIN_RAILWAY("🚞"), + TRAM_CAR("🚋"), + BUS("🚌"), + ONCOMING_BUS("🚍"), + TROLLEYBUS("🚎"), + MINIBUS("🚐"), + AMBULANCE("🚑"), + FIRE_ENGINE("🚒"), + POLICE_CAR("🚓"), + ONCOMING_POLICE_CAR("🚔"), + TAXI("🚕"), + ONCOMING_TAXI("🚖"), + AUTOMOBILE("🚗"), + ONCOMING_AUTOMOBILE("🚘"), + SPORT_UTILITY_VEHICLE("🚙"), + PICKUP_TRUCK("🛻"), + DELIVERY_TRUCK("🚚"), + ARTICULATED_LORRY("🚛"), + TRACTOR("🚜"), + RACING_CAR("🏎"), + MOTORCYCLE("🏍"), + MOTOR_SCOOTER("🛵"), + MANUAL_WHEELCHAIR("🦽"), + MOTORIZED_WHEELCHAIR("🦼"), + AUTO_RICKSHAW("🛺"), + BICYCLE("🚲"), + KICK_SCOOTER("🛴"), + SKATEBOARD("🛹"), + ROLLER_SKATE("🛼"), + BUS_STOP("🚏"), + MOTORWAY("🛣"), + RAILWAY_TRACK("🛤"), + OIL_DRUM("🛢"), + FUEL_PUMP("⛽"), + WHEEL("🛞"), + POLICE_CAR_LIGHT("🚨"), + HORIZONTAL_TRAFFIC_LIGHT("🚥"), + VERTICAL_TRAFFIC_LIGHT("🚦"), + STOP_SIGN("🛑"), + CONSTRUCTION("🚧"), + ANCHOR("⚓"), + RING_BUOY("🛟"), + SAILBOAT("⛵"), + CANOE("🛶"), + SPEEDBOAT("🚤"), + PASSENGER_SHIP("🛳"), + FERRY("⛴"), + MOTOR_BOAT("🛥"), + SHIP("🚢"), + AIRPLANE("✈"), + SMALL_AIRPLANE("🛩"), + AIRPLANE_DEPARTURE("🛫"), + AIRPLANE_ARRIVAL("🛬"), + PARACHUTE("🪂"), + SEAT("💺"), + HELICOPTER("🚁"), + SUSPENSION_RAILWAY("🚟"), + MOUNTAIN_CABLEWAY("🚠"), + AERIAL_TRAMWAY("🚡"), + SATELLITE("🛰"), + ROCKET("🚀"), + FLYING_SAUCER("🛸"), + BELLHOP_BELL("🛎"), + LUGGAGE("🧳"), + HOURGLASS_DONE("⌛"), + HOURGLASS_NOT_DONE("⏳"), + WATCH("⌚"), + ALARM_CLOCK("⏰"), + STOPWATCH("⏱"), + TIMER_CLOCK("⏲"), + MANTELPIECE_CLOCK("🕰"), + TWELVE_O_CLOCK("🕛"), + TWELVE_THIRTY("🕧"), + ONE_O_CLOCK("🕐"), + ONE_THIRTY("🕜"), + TWO_O_CLOCK("🕑"), + TWO_THIRTY("🕝"), + THREE_O_CLOCK("🕒"), + THREE_THIRTY("🕞"), + FOUR_O_CLOCK("🕓"), + FOUR_THIRTY("🕟"), + FIVE_O_CLOCK("🕔"), + FIVE_THIRTY("🕠"), + SIX_O_CLOCK("🕕"), + SIX_THIRTY("🕡"), + SEVEN_O_CLOCK("🕖"), + SEVEN_THIRTY("🕢"), + EIGHT_O_CLOCK("🕗"), + EIGHT_THIRTY("🕣"), + NINE_O_CLOCK("🕘"), + NINE_THIRTY("🕤"), + TEN_O_CLOCK("🕙"), + TEN_THIRTY("🕥"), + ELEVEN_O_CLOCK("🕚"), + ELEVEN_THIRTY("🕦"), + NEW_MOON("🌑"), + WAXING_CRESCENT_MOON("🌒"), + FIRST_QUARTER_MOON("🌓"), + WAXING_GIBBOUS_MOON("🌔"), + FULL_MOON("🌕"), + WANING_GIBBOUS_MOON("🌖"), + LAST_QUARTER_MOON("🌗"), + WANING_CRESCENT_MOON("🌘"), + CRESCENT_MOON("🌙"), + NEW_MOON_FACE("🌚"), + FIRST_QUARTER_MOON_FACE("🌛"), + LAST_QUARTER_MOON_FACE("🌜"), + THERMOMETER("🌡"), + SUN("☀"), + FULL_MOON_FACE("🌝"), + SUN_WITH_FACE("🌞"), + RINGED_PLANET("🪐"), + STAR("⭐"), + GLOWING_STAR("🌟"), + SHOOTING_STAR("🌠"), + MILKY_WAY("🌌"), + CLOUD("☁"), + SUN_BEHIND_CLOUD("⛅"), + CLOUD_WITH_LIGHTNING_AND_RAIN("⛈"), + SUN_BEHIND_SMALL_CLOUD("🌤"), + SUN_BEHIND_LARGE_CLOUD("🌥"), + SUN_BEHIND_RAIN_CLOUD("🌦"), + CLOUD_WITH_RAIN("🌧"), + CLOUD_WITH_SNOW("🌨"), + CLOUD_WITH_LIGHTNING("🌩"), + TORNADO("🌪"), + FOG("🌫"), + WIND_FACE("🌬"), + CYCLONE("🌀"), + RAINBOW("🌈"), + CLOSED_UMBRELLA("🌂"), + UMBRELLA("☂"), + UMBRELLA_WITH_RAIN_DROPS("☔"), + UMBRELLA_ON_GROUND("⛱"), + HIGH_VOLTAGE("⚡"), + SNOWFLAKE("❄"), + SNOWMAN("☃"), + SNOWMAN_WITHOUT_SNOW("⛄"), + COMET("☄"), + FIRE("🔥"), + DROPLET("💧"), + WATER_WAVE("🌊"), + JACK_O_LANTERN("🎃"), + CHRISTMAS_TREE("🎄"), + FIREWORKS("🎆"), + SPARKLER("🎇"), + FIRECRACKER("🧨"), + SPARKLES("✨"), + BALLOON("🎈"), + PARTY_POPPER("🎉"), + CONFETTI_BALL("🎊"), + TANABATA_TREE("🎋"), + PINE_DECORATION("🎍"), + JAPANESE_DOLLS("🎎"), + CARP_STREAMER("🎏"), + WIND_CHIME("🎐"), + MOON_VIEWING_CEREMONY("🎑"), + RED_ENVELOPE("🧧"), + RIBBON("🎀"), + WRAPPED_GIFT("🎁"), + REMINDER_RIBBON("🎗"), + ADMISSION_TICKETS("🎟"), + TICKET("🎫"), + MILITARY_MEDAL("🎖"), + TROPHY("🏆"), + SPORTS_MEDAL("🏅"), + FIRST_PLACE_MEDAL("🥇"), + SECOND_PLACE_MEDAL("🥈"), + THIRD_PLACE_MEDAL("🥉"), + SOCCER_BALL("⚽"), + BASEBALL("⚾"), + SOFTBALL("🥎"), + BASKETBALL("🏀"), + VOLLEYBALL("🏐"), + AMERICAN_FOOTBALL("🏈"), + RUGBY_FOOTBALL("🏉"), + TENNIS("🎾"), + FLYING_DISC("🥏"), + BOWLING("🎳"), + CRICKET_GAME("🏏"), + FIELD_HOCKEY("🏑"), + ICE_HOCKEY("🏒"), + LACROSSE("🥍"), + PING_PONG("🏓"), + BADMINTON("🏸"), + BOXING_GLOVE("🥊"), + MARTIAL_ARTS_UNIFORM("🥋"), + GOAL_NET("🥅"), + FLAG_IN_HOLE("⛳"), + ICE_SKATE("⛸"), + FISHING_POLE("🎣"), + DIVING_MASK("🤿"), + RUNNING_SHIRT("🎽"), + SKIS("🎿"), + SLED("🛷"), + CURLING_STONE("🥌"), + BULLSEYE("🎯"), + YO_YO("🪀"), + KITE("🪁"), + WATER_PISTOL("🔫"), + POOL_8_BALL("🎱"), + CRYSTAL_BALL("🔮"), + MAGIC_WAND("🪄"), + VIDEO_GAME("🎮"), + JOYSTICK("🕹"), + SLOT_MACHINE("🎰"), + GAME_DIE("🎲"), + PUZZLE_PIECE("🧩"), + TEDDY_BEAR("🧸"), + PINATA("🪅"), + MIRROR_BALL("🪩"), + NESTING_DOLLS("🪆"), + SPADE_SUIT("♠"), + HEART_SUIT("♥"), + DIAMOND_SUIT("♦"), + CLUB_SUIT("♣"), + CHESS_PAWN("♟"), + JOKER("🃏"), + MAHJONG_RED_DRAGON("🀄"), + FLOWER_PLAYING_CARDS("🎴"), + PERFORMING_ARTS("🎭"), + FRAMED_PICTURE("🖼"), + ARTIST_PALETTE("🎨"), + THREAD("🧵"), + SEWING_NEEDLE("🪡"), + YARN("🧶"), + KNOT("🪢"), + GLASSES("👓"), + SUNGLASSES("🕶"), + GOGGLES("🥽"), + LAB_COAT("🥼"), + SAFETY_VEST("🦺"), + NECKTIE("👔"), + T_SHIRT("👕"), + JEANS("👖"), + SCARF("🧣"), + GLOVES("🧤"), + COAT("🧥"), + SOCKS("🧦"), + DRESS("👗"), + KIMONO("👘"), + SARI("🥻"), + ONE_PIECE_SWIMSUIT("🩱"), + BRIEFS("🩲"), + SHORTS("🩳"), + BIKINI("👙"), + WOMAN_S_CLOTHES("👚"), + FOLDING_HAND_FAN("🪭"), + PURSE("👛"), + HANDBAG("👜"), + CLUTCH_BAG("👝"), + SHOPPING_BAGS("🛍"), + BACKPACK("🎒"), + THONG_SANDAL("🩴"), + MAN_S_SHOE("👞"), + RUNNING_SHOE("👟"), + HIKING_BOOT("🥾"), + FLAT_SHOE("🥿"), + HIGH_HEELED_SHOE("👠"), + WOMAN_S_SANDAL("👡"), + BALLET_SHOES("🩰"), + WOMAN_S_BOOT("👢"), + HAIR_PICK("🪮"), + CROWN("👑"), + WOMAN_S_HAT("👒"), + TOP_HAT("🎩"), + GRADUATION_CAP("🎓"), + BILLED_CAP("🧢"), + MILITARY_HELMET("🪖"), + RESCUE_WORKER_S_HELMET("⛑"), + PRAYER_BEADS("📿"), + LIPSTICK("💄"), + RING("💍"), + GEM_STONE("💎"), + MUTED_SPEAKER("🔇"), + SPEAKER_LOW_VOLUME("🔈"), + SPEAKER_MEDIUM_VOLUME("🔉"), + SPEAKER_HIGH_VOLUME("🔊"), + LOUDSPEAKER("📢"), + MEGAPHONE("📣"), + POSTAL_HORN("📯"), + BELL("🔔"), + BELL_WITH_SLASH("🔕"), + MUSICAL_SCORE("🎼"), + MUSICAL_NOTE("🎵"), + MUSICAL_NOTES("🎶"), + STUDIO_MICROPHONE("🎙"), + LEVEL_SLIDER("🎚"), + CONTROL_KNOBS("🎛"), + MICROPHONE("🎤"), + HEADPHONE("🎧"), + RADIO("📻"), + SAXOPHONE("🎷"), + ACCORDION("🪗"), + GUITAR("🎸"), + MUSICAL_KEYBOARD("🎹"), + TRUMPET("🎺"), + VIOLIN("🎻"), + BANJO("🪕"), + DRUM("🥁"), + LONG_DRUM("🪘"), + MARACAS("🪇"), + FLUTE("🪈"), + MOBILE_PHONE("📱"), + MOBILE_PHONE_WITH_ARROW("📲"), + TELEPHONE("☎"), + TELEPHONE_RECEIVER("📞"), + PAGER("📟"), + FAX_MACHINE("📠"), + BATTERY("🔋"), + LOW_BATTERY("🪫"), + ELECTRIC_PLUG("🔌"), + LAPTOP("💻"), + DESKTOP_COMPUTER("🖥"), + PRINTER("🖨"), + KEYBOARD("⌨"), + COMPUTER_MOUSE("🖱"), + TRACKBALL("🖲"), + COMPUTER_DISK("💽"), + FLOPPY_DISK("💾"), + OPTICAL_DISK("💿"), + DVD("📀"), + ABACUS("🧮"), + MOVIE_CAMERA("🎥"), + FILM_FRAMES("🎞"), + FILM_PROJECTOR("📽"), + CLAPPER_BOARD("🎬"), + TELEVISION("📺"), + CAMERA("📷"), + CAMERA_WITH_FLASH("📸"), + VIDEO_CAMERA("📹"), + VIDEOCASSETTE("📼"), + MAGNIFYING_GLASS_TILTED_LEFT("🔍"), + MAGNIFYING_GLASS_TILTED_RIGHT("🔎"), + CANDLE("🕯"), + LIGHT_BULB("💡"), + FLASHLIGHT("🔦"), + RED_PAPER_LANTERN("🏮"), + DIYA_LAMP("🪔"), + NOTEBOOK_WITH_DECORATIVE_COVER("📔"), + CLOSED_BOOK("📕"), + OPEN_BOOK("📖"), + GREEN_BOOK("📗"), + BLUE_BOOK("📘"), + ORANGE_BOOK("📙"), + BOOKS("📚"), + NOTEBOOK("📓"), + LEDGER("📒"), + PAGE_WITH_CURL("📃"), + SCROLL("📜"), + PAGE_FACING_UP("📄"), + NEWSPAPER("📰"), + ROLLED_UP_NEWSPAPER("🗞"), + BOOKMARK_TABS("📑"), + BOOKMARK("🔖"), + LABEL("🏷"), + MONEY_BAG("💰"), + COIN("🪙"), + YEN_BANKNOTE("💴"), + DOLLAR_BANKNOTE("💵"), + EURO_BANKNOTE("💶"), + POUND_BANKNOTE("💷"), + MONEY_WITH_WINGS("💸"), + CREDIT_CARD("💳"), + RECEIPT("🧾"), + CHART_INCREASING_WITH_YEN("💹"), + ENVELOPE("✉"), + E_MAIL("📧"), + INCOMING_ENVELOPE("📨"), + ENVELOPE_WITH_ARROW("📩"), + OUTBOX_TRAY("📤"), + INBOX_TRAY("📥"), + PACKAGE("📦"), + CLOSED_MAILBOX_WITH_RAISED_FLAG("📫"), + CLOSED_MAILBOX_WITH_LOWERED_FLAG("📪"), + OPEN_MAILBOX_WITH_RAISED_FLAG("📬"), + OPEN_MAILBOX_WITH_LOWERED_FLAG("📭"), + POSTBOX("📮"), + BALLOT_BOX_WITH_BALLOT("🗳"), + PENCIL("✏"), + BLACK_NIB("✒"), + FOUNTAIN_PEN("🖋"), + PEN("🖊"), + PAINTBRUSH("🖌"), + CRAYON("🖍"), + MEMO("📝"), + BRIEFCASE("💼"), + FILE_FOLDER("📁"), + OPEN_FILE_FOLDER("📂"), + CARD_INDEX_DIVIDERS("🗂"), + CALENDAR("📅"), + TEAR_OFF_CALENDAR("📆"), + SPIRAL_NOTEPAD("🗒"), + SPIRAL_CALENDAR("🗓"), + CARD_INDEX("📇"), + CHART_INCREASING("📈"), + CHART_DECREASING("📉"), + BAR_CHART("📊"), + CLIPBOARD("📋"), + PUSHPIN("📌"), + ROUND_PUSHPIN("📍"), + PAPERCLIP("📎"), + LINKED_PAPERCLIPS("🖇"), + STRAIGHT_RULER("📏"), + TRIANGULAR_RULER("📐"), + SCISSORS("✂"), + CARD_FILE_BOX("🗃"), + FILE_CABINET("🗄"), + WASTEBASKET("🗑"), + LOCKED("🔒"), + UNLOCKED("🔓"), + LOCKED_WITH_PEN("🔏"), + LOCKED_WITH_KEY("🔐"), + KEY("🔑"), + OLD_KEY("🗝"), + HAMMER("🔨"), + AXE("🪓"), + PICK("⛏"), + HAMMER_AND_PICK("⚒"), + HAMMER_AND_WRENCH("🛠"), + DAGGER("🗡"), + CROSSED_SWORDS("⚔"), + BOMB("💣"), + BOOMERANG("🪃"), + BOW_AND_ARROW("🏹"), + SHIELD("🛡"), + CARPENTRY_SAW("🪚"), + WRENCH("🔧"), + SCREWDRIVER("🪛"), + NUT_AND_BOLT("🔩"), + GEAR("⚙"), + CLAMP("🗜"), + BALANCE_SCALE("⚖"), + WHITE_CANE("🦯"), + LINK("🔗"), + CHAINS("⛓"), + HOOK("🪝"), + TOOLBOX("🧰"), + MAGNET("🧲"), + LADDER("🪜"), + ALEMBIC("⚗"), + TEST_TUBE("🧪"), + PETRI_DISH("🧫"), + DNA("🧬"), + MICROSCOPE("🔬"), + TELESCOPE("🔭"), + SATELLITE_ANTENNA("📡"), + SYRINGE("💉"), + DROP_OF_BLOOD("🩸"), + PILL("💊"), + ADHESIVE_BANDAGE("🩹"), + CRUTCH("🩼"), + STETHOSCOPE("🩺"), + X_RAY("🩻"), + DOOR("🚪"), + ELEVATOR("🛗"), + MIRROR("🪞"), + WINDOW("🪟"), + BED("🛏"), + COUCH_AND_LAMP("🛋"), + CHAIR("🪑"), + TOILET("🚽"), + PLUNGER("🪠"), + SHOWER("🚿"), + BATHTUB("🛁"), + MOUSE_TRAP("🪤"), + RAZOR("🪒"), + LOTION_BOTTLE("🧴"), + SAFETY_PIN("🧷"), + BROOM("🧹"), + BASKET("🧺"), + ROLL_OF_PAPER("🧻"), + BUCKET("🪣"), + SOAP("🧼"), + BUBBLES("🫧"), + TOOTHBRUSH("🪥"), + SPONGE("🧽"), + FIRE_EXTINGUISHER("🧯"), + SHOPPING_CART("🛒"), + CIGARETTE("🚬"), + COFFIN("⚰"), + HEADSTONE("🪦"), + FUNERAL_URN("⚱"), + NAZAR_AMULET("🧿"), + HAMSA("🪬"), + MOAI("🗿"), + PLACARD("🪧"), + IDENTIFICATION_CARD("🪪"), + ATM_SIGN("🏧"), + LITTER_IN_BIN_SIGN("🚮"), + POTABLE_WATER("🚰"), + WHEELCHAIR_SYMBOL("♿"), + MEN_S_ROOM("🚹"), + WOMEN_S_ROOM("🚺"), + RESTROOM("🚻"), + BABY_SYMBOL("🚼"), + WATER_CLOSET("🚾"), + PASSPORT_CONTROL("🛂"), + CUSTOMS("🛃"), + BAGGAGE_CLAIM("🛄"), + LEFT_LUGGAGE("🛅"), + WARNING("⚠"), + CHILDREN_CROSSING("🚸"), + NO_ENTRY("⛔"), + PROHIBITED("🚫"), + NO_BICYCLES("🚳"), + NO_SMOKING("🚭"), + NO_LITTERING("🚯"), + NON_POTABLE_WATER("🚱"), + NO_PEDESTRIANS("🚷"), + NO_MOBILE_PHONES("📵"), + NO_ONE_UNDER_EIGHTEEN("🔞"), + RADIOACTIVE("☢"), + BIOHAZARD("☣"), + UP_ARROW("⬆"), + UP_RIGHT_ARROW("↗"), + RIGHT_ARROW("➡"), + DOWN_RIGHT_ARROW("↘"), + DOWN_ARROW("⬇"), + DOWN_LEFT_ARROW("↙"), + LEFT_ARROW("⬅"), + UP_LEFT_ARROW("↖"), + UP_DOWN_ARROW("↕"), + LEFT_RIGHT_ARROW("↔"), + RIGHT_ARROW_CURVING_LEFT("↩"), + LEFT_ARROW_CURVING_RIGHT("↪"), + RIGHT_ARROW_CURVING_UP("⤴"), + RIGHT_ARROW_CURVING_DOWN("⤵"), + CLOCKWISE_VERTICAL_ARROWS("🔃"), + COUNTERCLOCKWISE_ARROWS_BUTTON("🔄"), + BACK_ARROW("🔙"), + END_ARROW("🔚"), + ON_ARROW("🔛"), + SOON_ARROW("🔜"), + TOP_ARROW("🔝"), + PLACE_OF_WORSHIP("🛐"), + ATOM_SYMBOL("⚛"), + OM("🕉"), + STAR_OF_DAVID("✡"), + WHEEL_OF_DHARMA("☸"), + YIN_YANG("☯"), + LATIN_CROSS("✝"), + ORTHODOX_CROSS("☦"), + STAR_AND_CRESCENT("☪"), + PEACE_SYMBOL("☮"), + MENORAH("🕎"), + DOTTED_SIX_POINTED_STAR("🔯"), + KHANDA("🪯"), + ARIES("♈"), + TAURUS("♉"), + GEMINI("♊"), + CANCER("♋"), + LEO("♌"), + VIRGO("♍"), + LIBRA("♎"), + SCORPIO("♏"), + SAGITTARIUS("♐"), + CAPRICORN("♑"), + AQUARIUS("♒"), + PISCES("♓"), + OPHIUCHUS("⛎"), + SHUFFLE_TRACKS_BUTTON("🔀"), + REPEAT_BUTTON("🔁"), + REPEAT_SINGLE_BUTTON("🔂"), + PLAY_BUTTON("▶"), + FAST_FORWARD_BUTTON("⏩"), + NEXT_TRACK_BUTTON("⏭"), + PLAY_OR_PAUSE_BUTTON("⏯"), + REVERSE_BUTTON("◀"), + FAST_REVERSE_BUTTON("⏪"), + LAST_TRACK_BUTTON("⏮"), + UPWARDS_BUTTON("🔼"), + FAST_UP_BUTTON("⏫"), + DOWNWARDS_BUTTON("🔽"), + FAST_DOWN_BUTTON("⏬"), + PAUSE_BUTTON("⏸"), + STOP_BUTTON("⏹"), + RECORD_BUTTON("⏺"), + EJECT_BUTTON("⏏"), + CINEMA("🎦"), + DIM_BUTTON("🔅"), + BRIGHT_BUTTON("🔆"), + ANTENNA_BARS("📶"), + WIRELESS("🛜"), + VIBRATION_MODE("📳"), + MOBILE_PHONE_OFF("📴"), + FEMALE_SIGN("♀"), + MALE_SIGN("♂"), + TRANSGENDER_SYMBOL("⚧"), + MULTIPLY("✖"), + PLUS("➕"), + MINUS("➖"), + DIVIDE("➗"), + HEAVY_EQUALS_SIGN("🟰"), + INFINITY("♾"), + DOUBLE_EXCLAMATION_MARK("‼"), + EXCLAMATION_QUESTION_MARK("⁉"), + RED_QUESTION_MARK("❓"), + WHITE_QUESTION_MARK("❔"), + WHITE_EXCLAMATION_MARK("❕"), + RED_EXCLAMATION_MARK("❗"), + WAVY_DASH("〰"), + CURRENCY_EXCHANGE("💱"), + HEAVY_DOLLAR_SIGN("💲"), + MEDICAL_SYMBOL("⚕"), + RECYCLING_SYMBOL("♻"), + FLEUR_DE_LIS("⚜"), + TRIDENT_EMBLEM("🔱"), + NAME_BADGE("📛"), + JAPANESE_SYMBOL_FOR_BEGINNER("🔰"), + HOLLOW_RED_CIRCLE("⭕"), + CHECK_MARK_BUTTON("✅"), + CHECK_BOX_WITH_CHECK("☑"), + CHECK_MARK("✔"), + CROSS_MARK("❌"), + CROSS_MARK_BUTTON("❎"), + CURLY_LOOP("➰"), + DOUBLE_CURLY_LOOP("➿"), + PART_ALTERNATION_MARK("〽"), + EIGHT_SPOKED_ASTERISK("✳"), + EIGHT_POINTED_STAR("✴"), + SPARKLE("❇"), + COPYRIGHT("©"), + REGISTERED("®"), + TRADE_MARK("™"), + KEYCAP_SHARP("#️⃣"), + KEYCAP_ASTERISK("*️⃣"), + KEYCAP_0("0️⃣"), + KEYCAP_1("1️⃣"), + KEYCAP_2("2️⃣"), + KEYCAP_3("3️⃣"), + KEYCAP_4("4️⃣"), + KEYCAP_5("5️⃣"), + KEYCAP_6("6️⃣"), + KEYCAP_7("7️⃣"), + KEYCAP_8("8️⃣"), + KEYCAP_9("9️⃣"), + KEYCAP_10("🔟"), + INPUT_LATIN_UPPERCASE("🔠"), + INPUT_LATIN_LOWERCASE("🔡"), + INPUT_NUMBERS("🔢"), + INPUT_SYMBOLS("🔣"), + INPUT_LATIN_LETTERS("🔤"), + A_BUTTON_BLOOD_TYPE("🅰"), + AB_BUTTON_BLOOD_TYPE("🆎"), + B_BUTTON_BLOOD_TYPE("🅱"), + CL_BUTTON("🆑"), + COOL_BUTTON("🆒"), + FREE_BUTTON("🆓"), + INFORMATION("ℹ"), + ID_BUTTON("🆔"), + CIRCLED_M("Ⓜ"), + NEW_BUTTON("🆕"), + NG_BUTTON("🆖"), + O_BUTTON_BLOOD_TYPE("🅾"), + OK_BUTTON("🆗"), + P_BUTTON("🅿"), + SOS_BUTTON("🆘"), + UP_BUTTON("🆙"), + VS_BUTTON("🆚"), + JAPANESE_HERE_BUTTON("🈁"), + JAPANESE_SERVICE_CHARGE_BUTTON("🈂"), + JAPANESE_MONTHLY_AMOUNT_BUTTON("🈷"), + JAPANESE_NOT_FREE_OF_CHARGE_BUTTON("🈶"), + JAPANESE_RESERVED_BUTTON("🈯"), + JAPANESE_BARGAIN_BUTTON("🉐"), + JAPANESE_DISCOUNT_BUTTON("🈹"), + JAPANESE_FREE_OF_CHARGE_BUTTON("🈚"), + JAPANESE_PROHIBITED_BUTTON("🈲"), + JAPANESE_ACCEPTABLE_BUTTON("🉑"), + JAPANESE_APPLICATION_BUTTON("🈸"), + JAPANESE_PASSING_GRADE_BUTTON("🈴"), + JAPANESE_VACANCY_BUTTON("🈳"), + JAPANESE_CONGRATULATIONS_BUTTON("㊗"), + JAPANESE_SECRET_BUTTON("㊙"), + JAPANESE_OPEN_FOR_BUSINESS_BUTTON("🈺"), + JAPANESE_NO_VACANCY_BUTTON("🈵"), + RED_CIRCLE("🔴"), + ORANGE_CIRCLE("🟠"), + YELLOW_CIRCLE("🟡"), + GREEN_CIRCLE("🟢"), + BLUE_CIRCLE("🔵"), + PURPLE_CIRCLE("🟣"), + BROWN_CIRCLE("🟤"), + BLACK_CIRCLE("⚫"), + WHITE_CIRCLE("⚪"), + RED_SQUARE("🟥"), + ORANGE_SQUARE("🟧"), + YELLOW_SQUARE("🟨"), + GREEN_SQUARE("🟩"), + BLUE_SQUARE("🟦"), + PURPLE_SQUARE("🟪"), + BROWN_SQUARE("🟫"), + BLACK_LARGE_SQUARE("⬛"), + WHITE_LARGE_SQUARE("⬜"), + BLACK_MEDIUM_SQUARE("◼"), + WHITE_MEDIUM_SQUARE("◻"), + BLACK_MEDIUM_SMALL_SQUARE("◾"), + WHITE_MEDIUM_SMALL_SQUARE("◽"), + BLACK_SMALL_SQUARE("▪"), + WHITE_SMALL_SQUARE("▫"), + LARGE_ORANGE_DIAMOND("🔶"), + LARGE_BLUE_DIAMOND("🔷"), + SMALL_ORANGE_DIAMOND("🔸"), + SMALL_BLUE_DIAMOND("🔹"), + RED_TRIANGLE_POINTED_UP("🔺"), + RED_TRIANGLE_POINTED_DOWN("🔻"), + DIAMOND_WITH_A_DOT("💠"), + RADIO_BUTTON("🔘"), + WHITE_SQUARE_BUTTON("🔳"), + BLACK_SQUARE_BUTTON("🔲"), + CHEQUERED_FLAG("🏁"), + TRIANGULAR_FLAG("🚩"), + CROSSED_FLAGS("🎌"), + BLACK_FLAG("🏴"), + WHITE_FLAG("🏳"), + RAINBOW_FLAG("🏳️‍🌈"), + TRANSGENDER_FLAG("🏳️‍⚧️"), + PIRATE_FLAG("🏴‍☠️"), + FLAG_ASCENSION_ISLAND("🇦🇨"), + FLAG_ANDORRA("🇦🇩"), + FLAG_UNITED_ARAB_EMIRATES("🇦🇪"), + FLAG_AFGHANISTAN("🇦🇫"), + FLAG_ANTIGUA_BARBUDA("🇦🇬"), + FLAG_ANGUILLA("🇦🇮"), + FLAG_ALBANIA("🇦🇱"), + FLAG_ARMENIA("🇦🇲"), + FLAG_ANGOLA("🇦🇴"), + FLAG_ANTARCTICA("🇦🇶"), + FLAG_ARGENTINA("🇦🇷"), + FLAG_AMERICAN_SAMOA("🇦🇸"), + FLAG_AUSTRIA("🇦🇹"), + FLAG_AUSTRALIA("🇦🇺"), + FLAG_ARUBA("🇦🇼"), + FLAG_ALAND_ISLANDS("🇦🇽"), + FLAG_AZERBAIJAN("🇦🇿"), + FLAG_BOSNIA_HERZEGOVINA("🇧🇦"), + FLAG_BARBADOS("🇧🇧"), + FLAG_BANGLADESH("🇧🇩"), + FLAG_BELGIUM("🇧🇪"), + FLAG_BURKINA_FASO("🇧🇫"), + FLAG_BULGARIA("🇧🇬"), + FLAG_BAHRAIN("🇧🇭"), + FLAG_BURUNDI("🇧🇮"), + FLAG_BENIN("🇧🇯"), + FLAG_ST_BARTHELEMY("🇧🇱"), + FLAG_BERMUDA("🇧🇲"), + FLAG_BRUNEI("🇧🇳"), + FLAG_BOLIVIA("🇧🇴"), + FLAG_CARIBBEAN_NETHERLANDS("🇧🇶"), + FLAG_BRAZIL("🇧🇷"), + FLAG_BAHAMAS("🇧🇸"), + FLAG_BHUTAN("🇧🇹"), + FLAG_BOUVET_ISLAND("🇧🇻"), + FLAG_BOTSWANA("🇧🇼"), + FLAG_BELARUS("🇧🇾"), + FLAG_BELIZE("🇧🇿"), + FLAG_CANADA("🇨🇦"), + FLAG_COCOS_KEELING_ISLANDS("🇨🇨"), + FLAG_CONGO___KINSHASA("🇨🇩"), + FLAG_CENTRAL_AFRICAN_REPUBLIC("🇨🇫"), + FLAG_CONGO___BRAZZAVILLE("🇨🇬"), + FLAG_SWITZERLAND("🇨🇭"), + FLAG_COTE_IVOIRE("🇨🇮"), + FLAG_COOK_ISLANDS("🇨🇰"), + FLAG_CHILE("🇨🇱"), + FLAG_CAMEROON("🇨🇲"), + FLAG_CHINA("🇨🇳"), + FLAG_COLOMBIA("🇨🇴"), + FLAG_CLIPPERTON_ISLAND("🇨🇵"), + FLAG_COSTA_RICA("🇨🇷"), + FLAG_CUBA("🇨🇺"), + FLAG_CAPE_VERDE("🇨🇻"), + FLAG_CURACAO("🇨🇼"), + FLAG_CHRISTMAS_ISLAND("🇨🇽"), + FLAG_CYPRUS("🇨🇾"), + FLAG_CZECHIA("🇨🇿"), + FLAG_GERMANY("🇩🇪"), + FLAG_DIEGO_GARCIA("🇩🇬"), + FLAG_DJIBOUTI("🇩🇯"), + FLAG_DENMARK("🇩🇰"), + FLAG_DOMINICA("🇩🇲"), + FLAG_DOMINICAN_REPUBLIC("🇩🇴"), + FLAG_ALGERIA("🇩🇿"), + FLAG_CEUTA_MELILLA("🇪🇦"), + FLAG_ECUADOR("🇪🇨"), + FLAG_ESTONIA("🇪🇪"), + FLAG_EGYPT("🇪🇬"), + FLAG_WESTERN_SAHARA("🇪🇭"), + FLAG_ERITREA("🇪🇷"), + FLAG_SPAIN("🇪🇸"), + FLAG_ETHIOPIA("🇪🇹"), + FLAG_EUROPEAN_UNION("🇪🇺"), + FLAG_FINLAND("🇫🇮"), + FLAG_FIJI("🇫🇯"), + FLAG_FALKLAND_ISLANDS("🇫🇰"), + FLAG_MICRONESIA("🇫🇲"), + FLAG_FAROE_ISLANDS("🇫🇴"), + FLAG_FRANCE("🇫🇷"), + FLAG_GABON("🇬🇦"), + FLAG_UNITED_KINGDOM("🇬🇧"), + FLAG_GRENADA("🇬🇩"), + FLAG_GEORGIA("🇬🇪"), + FLAG_FRENCH_GUIANA("🇬🇫"), + FLAG_GUERNSEY("🇬🇬"), + FLAG_GHANA("🇬🇭"), + FLAG_GIBRALTAR("🇬🇮"), + FLAG_GREENLAND("🇬🇱"), + FLAG_GAMBIA("🇬🇲"), + FLAG_GUINEA("🇬🇳"), + FLAG_GUADELOUPE("🇬🇵"), + FLAG_EQUATORIAL_GUINEA("🇬🇶"), + FLAG_GREECE("🇬🇷"), + FLAG_SOUTH_GEORGIA_SOUTH_SANDWICH_ISLANDS("🇬🇸"), + FLAG_GUATEMALA("🇬🇹"), + FLAG_GUAM("🇬🇺"), + FLAG_GUINEA_BISSAU("🇬🇼"), + FLAG_GUYANA("🇬🇾"), + FLAG_HONG_KONG_SAR_CHINA("🇭🇰"), + FLAG_HEARD_MCDONALD_ISLANDS("🇭🇲"), + FLAG_HONDURAS("🇭🇳"), + FLAG_CROATIA("🇭🇷"), + FLAG_HAITI("🇭🇹"), + FLAG_HUNGARY("🇭🇺"), + FLAG_CANARY_ISLANDS("🇮🇨"), + FLAG_INDONESIA("🇮🇩"), + FLAG_IRELAND("🇮🇪"), + FLAG_ISRAEL("🇮🇱"), + FLAG_ISLE_OF_MAN("🇮🇲"), + FLAG_INDIA("🇮🇳"), + FLAG_BRITISH_INDIAN_OCEAN_TERRITORY("🇮🇴"), + FLAG_IRAQ("🇮🇶"), + FLAG_IRAN("🇮🇷"), + FLAG_ICELAND("🇮🇸"), + FLAG_ITALY("🇮🇹"), + FLAG_JERSEY("🇯🇪"), + FLAG_JAMAICA("🇯🇲"), + FLAG_JORDAN("🇯🇴"), + FLAG_JAPAN("🇯🇵"), + FLAG_KENYA("🇰🇪"), + FLAG_KYRGYZSTAN("🇰🇬"), + FLAG_CAMBODIA("🇰🇭"), + FLAG_KIRIBATI("🇰🇮"), + FLAG_COMOROS("🇰🇲"), + FLAG_ST__KITTS_NEVIS("🇰🇳"), + FLAG_NORTH_KOREA("🇰🇵"), + FLAG_SOUTH_KOREA("🇰🇷"), + FLAG_KUWAIT("🇰🇼"), + FLAG_CAYMAN_ISLANDS("🇰🇾"), + FLAG_KAZAKHSTAN("🇰🇿"), + FLAG_LAOS("🇱🇦"), + FLAG_LEBANON("🇱🇧"), + FLAG_ST__LUCIA("🇱🇨"), + FLAG_LIECHTENSTEIN("🇱🇮"), + FLAG_SRI_LANKA("🇱🇰"), + FLAG_LIBERIA("🇱🇷"), + FLAG_LESOTHO("🇱🇸"), + FLAG_LITHUANIA("🇱🇹"), + FLAG_LUXEMBOURG("🇱🇺"), + FLAG_LATVIA("🇱🇻"), + FLAG_LIBYA("🇱🇾"), + FLAG_MOROCCO("🇲🇦"), + FLAG_MONACO("🇲🇨"), + FLAG_MOLDOVA("🇲🇩"), + FLAG_MONTENEGRO("🇲🇪"), + FLAG_ST__MARTIN("🇲🇫"), + FLAG_MADAGASCAR("🇲🇬"), + FLAG_MARSHALL_ISLANDS("🇲🇭"), + FLAG_NORTH_MACEDONIA("🇲🇰"), + FLAG_MALI("🇲🇱"), + FLAG_MYANMAR_BURMA("🇲🇲"), + FLAG_MONGOLIA("🇲🇳"), + FLAG_MACAO_SAR_CHINA("🇲🇴"), + FLAG_NORTHERN_MARIANA_ISLANDS("🇲🇵"), + FLAG_MARTINIQUE("🇲🇶"), + FLAG_MAURITANIA("🇲🇷"), + FLAG_MONTSERRAT("🇲🇸"), + FLAG_MALTA("🇲🇹"), + FLAG_MAURITIUS("🇲🇺"), + FLAG_MALDIVES("🇲🇻"), + FLAG_MALAWI("🇲🇼"), + FLAG_MEXICO("🇲🇽"), + FLAG_MALAYSIA("🇲🇾"), + FLAG_MOZAMBIQUE("🇲🇿"), + FLAG_NAMIBIA("🇳🇦"), + FLAG_NEW_CALEDONIA("🇳🇨"), + FLAG_NIGER("🇳🇪"), + FLAG_NORFOLK_ISLAND("🇳🇫"), + FLAG_NIGERIA("🇳🇬"), + FLAG_NICARAGUA("🇳🇮"), + FLAG_NETHERLANDS("🇳🇱"), + FLAG_NORWAY("🇳🇴"), + FLAG_NEPAL("🇳🇵"), + FLAG_NAURU("🇳🇷"), + FLAG_NIUE("🇳🇺"), + FLAG_NEW_ZEALAND("🇳🇿"), + FLAG_OMAN("🇴🇲"), + FLAG_PANAMA("🇵🇦"), + FLAG_PERU("🇵🇪"), + FLAG_FRENCH_POLYNESIA("🇵🇫"), + FLAG_PAPUA_NEW_GUINEA("🇵🇬"), + FLAG_PHILIPPINES("🇵🇭"), + FLAG_PAKISTAN("🇵🇰"), + FLAG_POLAND("🇵🇱"), + FLAG_ST__PIERRE_MIQUELON("🇵🇲"), + FLAG_PITCAIRN_ISLANDS("🇵🇳"), + FLAG_PUERTO_RICO("🇵🇷"), + FLAG_PALESTINIAN_TERRITORIES("🇵🇸"), + FLAG_PORTUGAL("🇵🇹"), + FLAG_PALAU("🇵🇼"), + FLAG_PARAGUAY("🇵🇾"), + FLAG_QATAR("🇶🇦"), + FLAG_REUNION("🇷🇪"), + FLAG_ROMANIA("🇷🇴"), + FLAG_SERBIA("🇷🇸"), + FLAG_RUSSIA("🇷🇺"), + FLAG_RWANDA("🇷🇼"), + FLAG_SAUDI_ARABIA("🇸🇦"), + FLAG_SOLOMON_ISLANDS("🇸🇧"), + FLAG_SEYCHELLES("🇸🇨"), + FLAG_SUDAN("🇸🇩"), + FLAG_SWEDEN("🇸🇪"), + FLAG_SINGAPORE("🇸🇬"), + FLAG_ST__HELENA("🇸🇭"), + FLAG_SLOVENIA("🇸🇮"), + FLAG_SVALBARD_JAN_MAYEN("🇸🇯"), + FLAG_SLOVAKIA("🇸🇰"), + FLAG_SIERRA_LEONE("🇸🇱"), + FLAG_SAN_MARINO("🇸🇲"), + FLAG_SENEGAL("🇸🇳"), + FLAG_SOMALIA("🇸🇴"), + FLAG_SURINAME("🇸🇷"), + FLAG_SOUTH_SUDAN("🇸🇸"), + FLAG_SAO_TOME_PRINCIPE("🇸🇹"), + FLAG_EL_SALVADOR("🇸🇻"), + FLAG_SINT_MAARTEN("🇸🇽"), + FLAG_SYRIA("🇸🇾"), + FLAG_ESWATINI("🇸🇿"), + FLAG_TRISTAN_DA_CUNHA("🇹🇦"), + FLAG_TURKS_CAICOS_ISLANDS("🇹🇨"), + FLAG_CHAD("🇹🇩"), + FLAG_FRENCH_SOUTHERN_TERRITORIES("🇹🇫"), + FLAG_TOGO("🇹🇬"), + FLAG_THAILAND("🇹🇭"), + FLAG_TAJIKISTAN("🇹🇯"), + FLAG_TOKELAU("🇹🇰"), + FLAG_TIMOR_LESTE("🇹🇱"), + FLAG_TURKMENISTAN("🇹🇲"), + FLAG_TUNISIA("🇹🇳"), + FLAG_TONGA("🇹🇴"), + FLAG_TURKEY("🇹🇷"), + FLAG_TRINIDAD_TOBAGO("🇹🇹"), + FLAG_TUVALU("🇹🇻"), + FLAG_TAIWAN("🇹🇼"), + FLAG_TANZANIA("🇹🇿"), + FLAG_UKRAINE("🇺🇦"), + FLAG_UGANDA("🇺🇬"), + FLAG_U_S__OUTLYING_ISLANDS("🇺🇲"), + FLAG_UNITED_NATIONS("🇺🇳"), + FLAG_UNITED_STATES("🇺🇸"), + FLAG_URUGUAY("🇺🇾"), + FLAG_UZBEKISTAN("🇺🇿"), + FLAG_VATICAN_CITY("🇻🇦"), + FLAG_ST__VINCENT_GRENADINES("🇻🇨"), + FLAG_VENEZUELA("🇻🇪"), + FLAG_BRITISH_VIRGIN_ISLANDS("🇻🇬"), + FLAG_U_S__VIRGIN_ISLANDS("🇻🇮"), + FLAG_VIETNAM("🇻🇳"), + FLAG_VANUATU("🇻🇺"), + FLAG_WALLIS_FUTUNA("🇼🇫"), + FLAG_SAMOA("🇼🇸"), + FLAG_KOSOVO("🇽🇰"), + FLAG_YEMEN("🇾🇪"), + FLAG_MAYOTTE("🇾🇹"), + FLAG_SOUTH_AFRICA("🇿🇦"), + FLAG_ZAMBIA("🇿🇲"), + FLAG_ZIMBABWE("🇿🇼"), + FLAG_ENGLAND("🏴󠁧󠁢󠁥󠁮󠁧󠁿"), + FLAG_SCOTLAND("🏴󠁧󠁢󠁳󠁣󠁴󠁿"), + FLAG_WALES("🏴󠁧󠁢󠁷󠁬󠁳󠁿"); + + private final String value; + + Emoji(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/ErrorHandler.java b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java new file mode 100644 index 000000000..9e858624d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java @@ -0,0 +1,162 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.exception.HmacValidationException; +import it.auties.whatsapp.util.Exceptions; + +import java.nio.file.Path; +import java.util.function.Consumer; + +import static it.auties.whatsapp.api.ErrorHandler.Location.*; +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.WARNING; + +/** + * This interface allows to handle a socket error and provides a default way to do so + */ +@SuppressWarnings("unused") +public interface ErrorHandler { + /** + * Handles an error that occurred inside the api + * + * @param type the type of client experiencing the error + * @param location the location where the error occurred + * @param throwable a stacktrace of the error, if available + * @return a newsletters determining what should be done + */ + Result handleError(ClientType type, Location location, Throwable throwable); + + /** + * Default error handler. Prints the exception on the terminal. + * + * @return a non-null error handler + */ + static ErrorHandler toTerminal() { + return defaultErrorHandler(Throwable::printStackTrace); + } + + /** + * Default error handler. Saves the exception locally. + * The file will be saved in $HOME/.cobalt/errors + * + * @return a non-null error handler + */ + static ErrorHandler toFile() { + return defaultErrorHandler(Exceptions::save); + } + + /** + * Default error handler. Saves the exception locally. + * The file will be saved in {@code directory}. + * + * @param directory the directory where the error should be saved + * @return a non-null error handler + */ + static ErrorHandler toFile(Path directory) { + return defaultErrorHandler(throwable -> Exceptions.save(directory, throwable)); + } + + /** + * Default error handler + * + * @param printer a consumer that handles the printing of the throwable, can be null + * @return a non-null error handler + */ + static ErrorHandler defaultErrorHandler(Consumer printer) { + return (type, location, throwable) -> { + var logger = System.getLogger("ErrorHandler"); + logger.log(ERROR, "Socket failure at %s".formatted(location)); + if (printer != null) { + printer.accept(throwable); + } + + if (location == CRYPTOGRAPHY && type == ClientType.MOBILE) { + logger.log(WARNING, "Reconnecting"); + return Result.RECONNECT; + } + + if (location == INITIAL_APP_STATE_SYNC + || location == CRYPTOGRAPHY + || (location == MESSAGE && throwable instanceof HmacValidationException)) { + logger.log(WARNING, "Socket failure at %s".formatted(location)); + return Result.RESTORE; + } + + logger.log(WARNING, "Ignored failure"); + return Result.DISCARD; + }; + } + + /** + * The constants of this enumerated type describe the various locations where an error can occur + * in the socket + */ + enum Location { + /** + * Unknown + */ + UNKNOWN, + /** + * Called when an error is thrown while logging in + */ + LOGIN, + /** + * Cryptographic error + */ + CRYPTOGRAPHY, + /** + * Called when the media connection cannot be renewed + */ + MEDIA_CONNECTION, + /** + * Called when an error arrives from the stream + */ + STREAM, + /** + * Called when an error is thrown while pulling app data + */ + PULL_APP_STATE, + /** + * Called when an error is thrown while pushing app data + */ + PUSH_APP_STATE, + /** + * Called when an error is thrown while pulling initial app data + */ + INITIAL_APP_STATE_SYNC, + /** + * Called when an error occurs when serializing or deserializing a Whatsapp message + */ + MESSAGE, + /** + * Called when syncing messages after first QR scan + */ + HISTORY_SYNC + } + + /** + * The constants of this enumerated type describe the various types of actions that can be + * performed by an error handler in newsletters to a throwable + */ + enum Result { + /** + * Ignores an error that was thrown by the socket + */ + DISCARD, + /** + * Deletes the current session and creates a new one instantly + */ + RESTORE, + /** + * Disconnects from the current session without deleting it + */ + DISCONNECT, + /** + * Disconnects from the current session without deleting it and reconnects to it + */ + RECONNECT, + /** + * Deletes the current session + */ + LOG_OUT + } +} diff --git a/src/main/java/it/auties/whatsapp/api/MobileOptionsBuilder.java b/src/main/java/it/auties/whatsapp/api/MobileOptionsBuilder.java new file mode 100644 index 000000000..b3fc0e760 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/MobileOptionsBuilder.java @@ -0,0 +1,150 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.api.MobileRegistrationBuilder.Unregistered; +import it.auties.whatsapp.api.MobileRegistrationBuilder.Unverified; +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.model.business.BusinessCategory; +import it.auties.whatsapp.model.companion.CompanionDevice; +import it.auties.whatsapp.registration.WhatsappRegistration; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +@SuppressWarnings("unused") +public final class MobileOptionsBuilder extends OptionsBuilder { + MobileOptionsBuilder(Store store, Keys keys) { + super(store, keys); + } + + /** + * Set the device to emulate + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder device(CompanionDevice device) { + store.setDevice(device); + return this; + } + + /** + * Sets the business' address + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessAddress(String businessAddress) { + store.setBusinessAddress(businessAddress); + return this; + } + + /** + * Sets the business' address longitude + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessLongitude(Double businessLongitude) { + store.setBusinessLongitude(businessLongitude); + return this; + } + + /** + * Sets the business' address latitude + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessLatitude(Double businessLatitude) { + store.setBusinessLatitude(businessLatitude); + return this; + } + + /** + * Sets the business' description + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessDescription(String businessDescription) { + store.setBusinessDescription(businessDescription); + return this; + } + + /** + * Sets the business' website + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessWebsite(String businessWebsite) { + store.setBusinessWebsite(businessWebsite); + return this; + } + + /** + * Sets the business' email + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessEmail(String businessEmail) { + store.setBusinessEmail(businessEmail); + return this; + } + + /** + * Sets the business' category + * + * @return the same instance for chaining + */ + public MobileOptionsBuilder businessCategory(BusinessCategory businessCategory) { + store.setBusinessCategory(businessCategory); + return this; + } + + /** + * Expects the session to be already registered + * This means that the verification code has already been sent to Whatsapp + * If this is not the case, an exception will be thrown + * + * @return a non-null optional of whatsapp + */ + public Optional registered() { + if (!keys.registered()) { + return Optional.empty(); + } + + return Optional.of(Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .socketExecutor(socketExecutor) + .build()); + } + + /** + * Expects the session to still need verification + * This means that you already have a code, but it hasn't already been sent to Whatsapp + * + * @return a non-null selector + */ + public Unverified unverified() { + return new Unverified(store, keys, errorHandler, socketExecutor, null); + } + + /** + * Expects the session to still need registration + * This means that you may or may not have a verification code, but that it hasn't already been sent to Whatsapp + * + * @return a non-null selector + */ + public Unregistered unregistered() { + return new Unregistered(store, keys, errorHandler, socketExecutor); + } + + /** + * Checks if a number is already registered on Whatsapp + * + * @param phoneNumber a phone number(include the prefix) + * @return a future + */ + public CompletableFuture exists(long phoneNumber) { + var service = new WhatsappRegistration(store, keys, null, null); + return service.exists(); + } +} diff --git a/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java b/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java new file mode 100644 index 000000000..5fc343355 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java @@ -0,0 +1,196 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.model.companion.CompanionDevice; +import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.model.mobile.VerificationCodeMethod; +import it.auties.whatsapp.model.response.RegistrationResponse; +import it.auties.whatsapp.registration.WhatsappRegistration; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * A builder to specify the options for the mobile api + */ +@SuppressWarnings("unused") +public sealed class MobileRegistrationBuilder { + final Store store; + final Keys keys; + final ErrorHandler errorHandler; + final ExecutorService socketExecutor; + RegisteredResult result; + AsyncVerificationCodeSupplier verificationCodeSupplier; + + MobileRegistrationBuilder(Store store, Keys keys, ErrorHandler errorHandler, ExecutorService socketExecutor) { + this.store = store; + this.keys = keys; + this.errorHandler = errorHandler; + this.socketExecutor = socketExecutor; + } + + public final static class Unregistered extends MobileRegistrationBuilder { + private UnverifiedResult unregisteredResult; + private VerificationCodeMethod verificationCodeMethod; + + Unregistered(Store store, Keys keys, ErrorHandler errorHandler, ExecutorService socketExecutor) { + super(store, keys, errorHandler, socketExecutor); + this.verificationCodeMethod = VerificationCodeMethod.SMS; + } + + public Unregistered verificationCodeSupplier(Supplier verificationCodeSupplier) { + this.verificationCodeSupplier = AsyncVerificationCodeSupplier.of(verificationCodeSupplier); + return this; + } + + public Unregistered verificationCodeSupplier(AsyncVerificationCodeSupplier verificationCodeSupplier) { + this.verificationCodeSupplier = verificationCodeSupplier; + return this; + } + + public Unregistered device(CompanionDevice device) { + store.setDevice(device); + return this; + } + + public Unregistered verificationCodeMethod(VerificationCodeMethod verificationCodeMethod) { + this.verificationCodeMethod = verificationCodeMethod; + return this; + } + + /** + * Registers a phone number by asking for a verification code and then sending it to Whatsapp + * + * @param phoneNumber a phone number(include the prefix) + * @return a future + */ + public CompletableFuture register(long phoneNumber) { + if (result != null) { + return CompletableFuture.completedFuture(result); + } + + Objects.requireNonNull(verificationCodeSupplier, "Expected a valid verification code supplier"); + Objects.requireNonNull(verificationCodeMethod, "Expected a valid verification method"); + if (!keys.registered()) { + var number = PhoneNumber.of(phoneNumber); + keys.setPhoneNumber(number); + store.setPhoneNumber(number); + var registration = new WhatsappRegistration(store, keys, verificationCodeSupplier, verificationCodeMethod); + return registration.registerPhoneNumber().thenApply(response -> { + var api = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .socketExecutor(socketExecutor) + .build(); + return this.result = new RegisteredResult(api, Optional.ofNullable(response)); + }); + } + + var api = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .socketExecutor(socketExecutor) + .build(); + return CompletableFuture.completedFuture(result); + } + + + /** + * Asks Whatsapp for a one-time-password to start the registration process + * + * @param phoneNumber a phone number(include the prefix) + * @return a future + */ + public CompletableFuture requestVerificationCode(long phoneNumber) { + if(unregisteredResult != null) { + return CompletableFuture.completedFuture(unregisteredResult); + } + + var number = PhoneNumber.of(phoneNumber); + keys.setPhoneNumber(number); + store.setPhoneNumber(number); + if (!keys.registered()) { + var registration = new WhatsappRegistration(store, keys, verificationCodeSupplier, verificationCodeMethod); + return registration.requestVerificationCode().thenApply(response -> { + var unverified = new Unverified(store, keys, errorHandler, socketExecutor, verificationCodeSupplier); + return this.unregisteredResult = new UnverifiedResult(unverified, Optional.ofNullable(response)); + }); + } + + var unverified = new Unverified(store, keys, errorHandler, socketExecutor, verificationCodeSupplier); + return CompletableFuture.completedFuture(this.unregisteredResult = new UnverifiedResult(unverified, Optional.empty())); + } + } + + public final static class Unverified extends MobileRegistrationBuilder { + Unverified(Store store, Keys keys, ErrorHandler errorHandler, ExecutorService socketExecutor, AsyncVerificationCodeSupplier verificationCodeSupplier) { + super(store, keys, errorHandler, socketExecutor); + this.verificationCodeSupplier = verificationCodeSupplier; + } + + public Unverified verificationCodeSupplier(Supplier verificationCodeSupplier) { + this.verificationCodeSupplier = AsyncVerificationCodeSupplier.of(verificationCodeSupplier); + return this; + } + + public Unverified verificationCodeSupplier(AsyncVerificationCodeSupplier verificationCodeSupplier) { + this.verificationCodeSupplier = verificationCodeSupplier; + return this; + } + + public Unverified device(CompanionDevice device) { + store.setDevice(device); + return this; + } + + /** + * Sends the verification code you already requested to Whatsapp + * + * @return the same instance for chaining + */ + public CompletableFuture verify(long phoneNumber) { + var number = PhoneNumber.of(phoneNumber); + keys.setPhoneNumber(number); + store.setPhoneNumber(number); + return verify(); + } + + /** + * Sends the verification code you already requested to Whatsapp + * + * @return the same instance for chaining + */ + public CompletableFuture verify() { + if(result != null) { + return CompletableFuture.completedFuture(result); + } + + Objects.requireNonNull(store.phoneNumber(), "Missing phone number: please specify it"); + Objects.requireNonNull(verificationCodeSupplier, "Expected a valid verification code supplier"); + var registration = new WhatsappRegistration(store, keys, verificationCodeSupplier, VerificationCodeMethod.NONE); + return registration.sendVerificationCode().thenApply(response -> { + var api = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .socketExecutor(socketExecutor) + .build(); + return this.result = new RegisteredResult(api, Optional.ofNullable(response)); + }); + } + } + + public record RegisteredResult(Whatsapp whatsapp, Optional response) { + + } + + public record UnverifiedResult(Unverified unverified, Optional response) { + + } +} diff --git a/src/main/java/it/auties/whatsapp/api/OptionsBuilder.java b/src/main/java/it/auties/whatsapp/api/OptionsBuilder.java new file mode 100644 index 000000000..ccfba261d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/OptionsBuilder.java @@ -0,0 +1,142 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.listener.RegisterListener; +import it.auties.whatsapp.model.signal.auth.UserAgent.ReleaseChannel; +import it.auties.whatsapp.model.signal.auth.Version; + +import java.net.URI; +import java.util.concurrent.ExecutorService; + +@SuppressWarnings("unused") +public sealed class OptionsBuilder> permits MobileOptionsBuilder, WebOptionsBuilder { + Store store; + Keys keys; + ErrorHandler errorHandler; + ExecutorService socketExecutor; + + OptionsBuilder(Store store, Keys keys) { + this.store = store; + this.keys = keys; + } + + /** + * Sets the name to provide to Whatsapp during the authentication process + * The web api will display this name in the devices section, while the mobile api will show it to the people you send messages to + * By default, this value will be set to this library's name + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T name(String name) { + store.setName(name); + return (T) this; + } + + /** + * Sets the version of Whatsapp to use + * If the version is too outdated, the server will refuse to connect + * If you are using the mobile api and the version doesn't match the hash, the server will refuse to connect + * By default the latest stable version will be used + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T version(Version version) { + store.setVersion(version); + return (T) this; + } + + /** + * Sets whether listeners marked with the {@link RegisterListener} annotation should be automatically detected and registered + * By default, this option is enabled + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T autodetectListeners(boolean autodetectListeners) { + store.setAutodetectListeners(autodetectListeners); + return (T) this; + } + + /** + * Sets whether a preview should be automatically generated and attached to text messages that contain links + * By default, it's enabled with inference + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T textPreviewSetting(TextPreviewSetting textPreviewSetting) { + store.setTextPreviewSetting(textPreviewSetting); + return (T) this; + } + + /** + * Sets the error handler for this session + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T errorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + return (T) this; + } + + + /** + * Sets the executor to use for the socket + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T socketExecutor(ExecutorService socketExecutor) { + this.socketExecutor = socketExecutor; + return (T) this; + } + + /** + * Sets the release channel + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T releaseChannel(ReleaseChannel releaseChannel) { + store.setReleaseChannel(releaseChannel); + return (T) this; + } + + /** + * Sets the proxy to use for the socket + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T proxy(URI proxy) { + store.setProxy(proxy); + return (T) this; + } + + /** + * Whether presence updates should be handled automatically + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T automaticPresenceUpdates(boolean automaticPresenceUpdates) { + store.setAutomaticPresenceUpdates(automaticPresenceUpdates); + return (T) this; + } + + /** + * Sets whether the mac of every app state patch should be validated or not + * By default, it's set to false + * + * @return the same instance for chaining + */ + @SuppressWarnings("unchecked") + public T checkPatchMacks(boolean checkPatchMacs) { + store.setCheckPatchMacs(checkPatchMacs); + return (T) this; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/api/PairingCodeHandler.java b/src/main/java/it/auties/whatsapp/api/PairingCodeHandler.java new file mode 100644 index 000000000..1057e545b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/PairingCodeHandler.java @@ -0,0 +1,24 @@ +package it.auties.whatsapp.api; + +import java.util.function.Consumer; + +/** + * This interface allows to consume a pairing code sent by WhatsappWeb + */ +@FunctionalInterface +@SuppressWarnings("unused") +public non-sealed interface PairingCodeHandler extends Consumer, WebVerificationHandler { + /** + * Prints the pairing code to the terminal + */ + static PairingCodeHandler toTerminal() { + return System.out::println; + } + + /** + * Discards the pairing code + */ + static PairingCodeHandler discarding() { + return ignored -> {}; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/api/QrHandler.java b/src/main/java/it/auties/whatsapp/api/QrHandler.java new file mode 100644 index 000000000..d9ea50562 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/QrHandler.java @@ -0,0 +1,143 @@ +package it.auties.whatsapp.api; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import it.auties.qr.QrTerminal; + +import java.awt.*; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +import static com.google.zxing.client.j2se.MatrixToImageWriter.writeToPath; +import static java.lang.System.Logger.Level.INFO; +import static java.nio.file.Files.createTempFile; + +/** + * This interface allows to consume a qr code and provides default common implementations to do so + */ +@FunctionalInterface +@SuppressWarnings("unused") +public non-sealed interface QrHandler extends Consumer, WebVerificationHandler { + /** + * Prints the QR code to the terminal. If your terminal doesn't support utf, you may see random + * characters. + */ + static QrHandler toTerminal() { + return toString(System.out::println); + } + + /** + * Transforms the qr code in a UTF-8 string and accepts a consumer for the latter + * + * @param smallQrConsumer the non-null consumer + */ + static QrHandler toString(Consumer smallQrConsumer) { + return qr -> { + var matrix = createMatrix(qr, 10, 0); + smallQrConsumer.accept(QrTerminal.toString(matrix, true)); + }; + } + + /** + * Transforms the qr code in a UTF-8 plain string and accepts a consumer for the latter + * + * @param qrConsumer the non-null consumer + */ + static QrHandler toPlainString(Consumer qrConsumer) { + return qrConsumer::accept; + } + + /** + * Utility method to create a matrix from a qr countryCode + * + * @param qr the non-null source + * @param size the size of the qr countryCode + * @param margin the margin for the qr countryCode + * @return a non-null matrix + */ + static BitMatrix createMatrix(String qr, int size, int margin) { + try { + var writer = new MultiFormatWriter(); + return writer.encode(qr, BarcodeFormat.QR_CODE, size, size, Map.of(EncodeHintType.MARGIN, margin, EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L)); + } catch (WriterException exception) { + throw new UnsupportedOperationException("Cannot create qr countryCode", exception); + } + } + + /** + * Saves the QR code to a temp file + * + * @param fileConsumer the consumer to digest the created file + */ + static QrHandler toFile(ToFileConsumer fileConsumer) { + try { + var file = createTempFile("qr", ".jpg"); + return toFile(file, fileConsumer); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot create temp file for qr handler", exception); + } + } + + /** + * Saves the QR code to a specified file + * + * @param path the location where the qr will be written + * @param fileConsumer the consumer to digest the created file + */ + static QrHandler toFile(Path path, ToFileConsumer fileConsumer) { + return qr -> { + try { + var matrix = createMatrix(qr, 500, 5); + writeToPath(matrix, "jpg", path); + fileConsumer.accept(path); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot save qr to file", exception); + } + }; + } + + /** + * This interface allows to consume a file created by + * {@link QrHandler#toFile(Path, ToFileConsumer)} easily + */ + interface ToFileConsumer extends Consumer { + /** + * Discard the newly created file + */ + static ToFileConsumer discarding() { + return ignored -> { + }; + } + + /** + * Prints the location of the file on the terminal using the system logger + */ + static ToFileConsumer toTerminal() { + return path -> System.getLogger(QrHandler.class.getName()) + .log(INFO, "Saved qr code at %s".formatted(path)); + } + + /** + * Opens the file if a Desktop environment is available + */ + static ToFileConsumer toDesktop() { + return path -> { + try { + if (!Desktop.isDesktopSupported()) { + return; + } + Desktop.getDesktop().open(path.toFile()); + } catch (IOException exception) { + throw new UncheckedIOException("Cannot open file with desktop", exception); + } + }; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/api/SocketEvent.java b/src/main/java/it/auties/whatsapp/api/SocketEvent.java new file mode 100644 index 000000000..ae43d7115 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/SocketEvent.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.api; + +/** + * The constants of this enumerated type describe the various types of events regarding a socket + */ +public enum SocketEvent { + /** + * Called when the socket is opened + */ + OPEN, + + /** + * Called when the socket is closed + */ + CLOSE, + + /** + * Called when an unexpected error is thrown, can be used as a safety mechanism + */ + ERROR, + + /** + * Called when a ping is sent + */ + PING +} diff --git a/src/main/java/it/auties/whatsapp/api/TextPreviewSetting.java b/src/main/java/it/auties/whatsapp/api/TextPreviewSetting.java new file mode 100644 index 000000000..b815aff23 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/TextPreviewSetting.java @@ -0,0 +1,37 @@ +package it.auties.whatsapp.api; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +/** + * The constants of this enumerated type describe the various types of text preview that can be + * used + */ +public enum TextPreviewSetting implements ProtobufEnum { + /** + * Link previews will be generated. If a message contains an url without a schema(for example + * wikipedia.com), the message will be autocorrected to include it and a preview will be + * generated + */ + ENABLED_WITH_INFERENCE(0), + + /** + * Link previews will be generated. No inference will be used. + */ + ENABLED(1), + + /** + * Link previews will not be generated + */ + DISABLED(2); + + final int index; + + TextPreviewSetting(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java new file mode 100644 index 000000000..b18c90e99 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/WebHistoryLength.java @@ -0,0 +1,70 @@ +package it.auties.whatsapp.api; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.Specification; + +/** + * The constants of this enumerated type describe the various chat history's codeLength that Whatsapp + * can send on the first login attempt + */ +public record WebHistoryLength( + @ProtobufProperty(index = 1, type = ProtobufType.INT32) + int size +) implements ProtobufMessage { + private static final WebHistoryLength ZERO = new WebHistoryLength(0); + private static final WebHistoryLength STANDARD = new WebHistoryLength(Specification.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 + */ + public static WebHistoryLength zero() { + return ZERO; + } + + + /** + * This is the default setting for the web client + * This is also the recommended setting + */ + 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 + */ + public static WebHistoryLength extended() { + return EXTENDED; + } + + /** + * Custom size + */ + public static WebHistoryLength custom(int size) { + return new WebHistoryLength(size); + } + + /** + * Returns whether this history size counts as zero + * + * @return a boolean + */ + public boolean isZero() { + return size == 0; + } + + /** + * Returns whether this history size counts as extended + * + * @return a boolean + */ + public boolean isExtended() { + return size > Specification.Whatsapp.DEFAULT_HISTORY_SIZE; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/WebOptionsBuilder.java b/src/main/java/it/auties/whatsapp/api/WebOptionsBuilder.java new file mode 100644 index 000000000..bc230a6ac --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/WebOptionsBuilder.java @@ -0,0 +1,93 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.model.mobile.PhoneNumber; + +import java.util.Optional; + +@SuppressWarnings("unused") +public final class WebOptionsBuilder extends OptionsBuilder { + private Whatsapp whatsapp; + + WebOptionsBuilder(Store store, Keys keys) { + super(store, keys); + } + + /** + * Sets how much chat history Whatsapp should send when the QR is first scanned. + * By default, one year + * + * @return the same instance for chaining + */ + public WebOptionsBuilder historyLength(WebHistoryLength historyLength) { + store.setHistoryLength(historyLength); + return this; + } + + /** + * Creates a Whatsapp instance with a qr handler + * + * @param qrHandler the non-null handler to use + * @return a Whatsapp instance + */ + public Whatsapp unregistered(QrHandler qrHandler) { + if (whatsapp == null) { + this.whatsapp = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .webVerificationSupport(qrHandler) + .socketExecutor(socketExecutor) + .build(); + } + + return whatsapp; + } + + /** + * Creates a Whatsapp instance with an OTP handler + * + * @param phoneNumber the phone number of the user + * @param pairingCodeHandler the non-null handler for the pairing code + * @return a Whatsapp instance + */ + public Whatsapp unregistered(long phoneNumber, PairingCodeHandler pairingCodeHandler) { + if (whatsapp == null) { + store.setPhoneNumber(PhoneNumber.of(phoneNumber)); + this.whatsapp = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .webVerificationSupport(pairingCodeHandler) + .socketExecutor(socketExecutor) + .build(); + } + + return whatsapp; + } + + /** + * Creates a Whatsapp instance with no handlers + * This method assumes that you have already logged in using a QR code or OTP + * Otherwise, it returns an empty optional. + * + * @return an optional + */ + public Optional registered() { + if (!keys.registered()) { + return Optional.empty(); + } + + if (whatsapp == null) { + this.whatsapp = Whatsapp.customBuilder() + .store(store) + .keys(keys) + .errorHandler(errorHandler) + .socketExecutor(socketExecutor) + .build(); + } + + return Optional.of(whatsapp); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/api/WebVerificationHandler.java b/src/main/java/it/auties/whatsapp/api/WebVerificationHandler.java new file mode 100644 index 000000000..91d0a90e9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/WebVerificationHandler.java @@ -0,0 +1,7 @@ +package it.auties.whatsapp.api; + +/** + * A utility sealed interface to represent methods that can be used to verify a WhatsappWeb Client + */ +public sealed interface WebVerificationHandler permits QrHandler, PairingCodeHandler { +} diff --git a/src/main/java/it/auties/whatsapp/api/Whatsapp.java b/src/main/java/it/auties/whatsapp/api/Whatsapp.java new file mode 100644 index 000000000..dba0805ce --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java @@ -0,0 +1,3585 @@ +package it.auties.whatsapp.api; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.client.j2se.BufferedImageLuminanceSource; +import com.google.zxing.common.HybridBinarizer; +import com.google.zxing.qrcode.QRCodeReader; +import it.auties.curve25519.Curve25519; +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.crypto.AesGcm; +import it.auties.whatsapp.crypto.Hkdf; +import it.auties.whatsapp.crypto.Hmac; +import it.auties.whatsapp.crypto.SessionCipher; +import it.auties.whatsapp.listener.*; +import it.auties.whatsapp.model.action.*; +import it.auties.whatsapp.model.business.*; +import it.auties.whatsapp.model.call.Call; +import it.auties.whatsapp.model.call.CallStatus; +import it.auties.whatsapp.model.chat.*; +import it.auties.whatsapp.model.companion.CompanionLinkResult; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.info.*; +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.message.model.*; +import it.auties.whatsapp.model.message.model.reserved.ExtendedMediaMessage; +import it.auties.whatsapp.model.message.server.ProtocolMessage; +import it.auties.whatsapp.model.message.server.ProtocolMessageBuilder; +import it.auties.whatsapp.model.message.standard.CallMessageBuilder; +import it.auties.whatsapp.model.message.standard.ReactionMessageBuilder; +import it.auties.whatsapp.model.message.standard.TextMessage; +import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.newsletter.NewsletterViewerMetadata; +import it.auties.whatsapp.model.newsletter.NewsletterViewerRole; +import it.auties.whatsapp.model.node.Attributes; +import it.auties.whatsapp.model.node.Node; +import it.auties.whatsapp.model.privacy.GdprAccountReport; +import it.auties.whatsapp.model.privacy.PrivacySettingEntry; +import it.auties.whatsapp.model.privacy.PrivacySettingType; +import it.auties.whatsapp.model.privacy.PrivacySettingValue; +import it.auties.whatsapp.model.product.LeaveNewsletterRequest; +import it.auties.whatsapp.model.request.*; +import it.auties.whatsapp.model.request.UpdateNewsletterRequest.UpdatePayload; +import it.auties.whatsapp.model.response.*; +import it.auties.whatsapp.model.setting.LocaleSettings; +import it.auties.whatsapp.model.setting.PushNameSettings; +import it.auties.whatsapp.model.signal.auth.*; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.sync.*; +import it.auties.whatsapp.model.sync.PatchRequest.PatchEntry; +import it.auties.whatsapp.model.sync.RecordSync.Operation; +import it.auties.whatsapp.socket.SocketHandler; +import it.auties.whatsapp.socket.SocketState; +import it.auties.whatsapp.util.*; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.chrono.ChronoZonedDateTime; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static it.auties.whatsapp.model.contact.ContactStatus.COMPOSING; +import static it.auties.whatsapp.model.contact.ContactStatus.RECORDING; + +/** + * A class used to interface a user to WhatsappWeb's WebSocket + */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public class Whatsapp { + // The instances are added and removed when the client connects/disconnects + // This is to make sure that the instances remain in memory only as long as it's needed + private static final Map instances = new ConcurrentHashMap<>(); + + static Optional getInstanceByUuid(UUID uuid) { + return Optional.ofNullable(instances.get(uuid)); + } + + static void removeInstanceByUuid(UUID uuid) { + instances.remove(uuid); + } + + private final SocketHandler socketHandler; + private RegistrationResponse response; + + /** + * Checks if a connection exists + * + * @param uuid the non-null uuid + * @return a boolean + */ + public static boolean isConnected(UUID uuid) { + return SocketHandler.isConnected(uuid); + } + + /** + * Checks if a connection exists + * + * @param phoneNumber the non-null phone number + * @return a boolean + */ + public static boolean isConnected(long phoneNumber) { + return SocketHandler.isConnected(phoneNumber); + } + + /** + * Checks if a connection exists + * + * @param alias the non-null alias + * @return a boolean + */ + public static boolean isConnected(String alias) { + return SocketHandler.isConnected(alias); + } + + protected Whatsapp(Store store, Keys keys, ErrorHandler errorHandler, WebVerificationHandler webVerificationHandler, ExecutorService socketExecutor) { + this.socketHandler = new SocketHandler(this, store, keys, errorHandler, webVerificationHandler, socketExecutor); + store.addListener((OnDisconnected) (reason) -> { + if (reason != DisconnectReason.RECONNECTING) { + removeInstanceByUuid(store.uuid()); + } + }); + if (store.autodetectListeners()) { + return; + } + + store.addListeners(ListenerScanner.scan(this, store.cacheDetectedListeners())); + } + + /** + * Creates a new web api + * The web api is based around the WhatsappWeb client + * + * @return a web api builder + */ + public static ConnectionBuilder webBuilder() { + return new ConnectionBuilder<>(ClientType.WEB); + } + + /** + * Creates a new mobile api + * The mobile api is based around the Whatsapp App available on IOS and Android + * + * @return a web mobile builder + */ + public static ConnectionBuilder mobileBuilder() { + return new ConnectionBuilder<>(ClientType.MOBILE); + } + + /** + * Creates an advanced builder if you need more customization + * + * @return a custom builder + */ + public static WhatsappCustomBuilder customBuilder() { + return new WhatsappCustomBuilder(); + } + + /** + * Connects to Whatsapp + * + * @return a future + */ + public synchronized CompletableFuture connect() { + return socketHandler.connect() + .thenRunAsync(() -> instances.put(store().uuid(), this)) + .thenApply(ignored -> this); + } + + /** + * Returns whether the connection is active or not + * + * @return a boolean + */ + public boolean isConnected() { + return socketHandler.state() == SocketState.CONNECTED; + } + + /** + * Returns the keys associated with this session + * + * @return a non-null WhatsappKeys + */ + public Keys keys() { + return socketHandler.keys(); + } + + /** + * Returns the store associated with this session + * + * @return a non-null WhatsappStore + */ + public Store store() { + return socketHandler.store(); + } + + /** + * Disconnects from Whatsapp Web's WebSocket if a previous connection exists + * + * @return a future + */ + public CompletableFuture disconnect() { + return socketHandler.disconnect(DisconnectReason.DISCONNECTED); + } + + /** + * Disconnects and reconnects to Whatsapp Web's WebSocket if a previous connection exists + * + * @return a future + */ + public CompletableFuture reconnect() { + return socketHandler.disconnect(DisconnectReason.RECONNECTING); + } + + /** + * Disconnects from Whatsapp Web's WebSocket and logs out of WhatsappWeb invalidating the previous + * saved credentials. The next time the API is used, the QR code will need to be scanned again. + * + * @return a future + */ + public CompletableFuture logout() { + if (jidOrThrowError() == null) { + return socketHandler.disconnect(DisconnectReason.LOGGED_OUT); + } + + var metadata = Map.of("jid", jidOrThrowError(), "reason", "user_initiated"); + var device = Node.of("remove-companion-device", metadata); + return socketHandler.sendQuery("set", "md", device) + .thenRun(() -> {}); + } + + /** + * Changes a privacy setting in Whatsapp's settings. If the value is + * {@link PrivacySettingValue#CONTACTS_EXCEPT}, the excluded parameter should also be filled or an + * exception will be thrown, otherwise it will be ignored. + * + * @param type the non-null setting to change + * @param value the non-null value to attribute to the setting + * @param excluded the non-null excluded contacts if value is {@link PrivacySettingValue#CONTACTS_EXCEPT} + * @return the same instance wrapped in a completable future + */ + public final CompletableFuture changePrivacySetting(PrivacySettingType type, PrivacySettingValue value, JidProvider... 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() + .put("name", type.data()) + .put("value", value.data()) + .put("dhash", "none", () -> value == PrivacySettingValue.CONTACTS_EXCEPT) + .toMap(); + 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(); + return socketHandler.sendQuery("set", "privacy", Node.of("privacy", Node.of("category", attributes, children))) + .thenRun(() -> onPrivacyFeatureChanged(type, value, 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); + socketHandler.onPrivacySettingChanged(oldEntry, newEntry); + } + + /** + * Changes the default ephemeral timer of new chats. + * + * @param timer the new ephemeral timer + * @return the same instance wrapped in a completable future + */ + public CompletableFuture changeNewChatsEphemeralTimer(ChatEphemeralTimer timer) { + return socketHandler.sendQuery("set", "disappearing_mode", Node.of("disappearing_mode", Map.of("duration", timer.period().toSeconds()))) + .thenRun(() -> store().setNewChatsEphemeralTimer(timer)); + } + + /** + * Creates a new request to get a document containing all the data that was collected by Whatsapp + * about this user. It takes three business days to receive it. To query the newsletters status, use + * {@link Whatsapp#getGdprAccountInfoStatus()} + * + * @return the same instance wrapped in a completable future + */ + public CompletableFuture createGdprAccountInfo() { + return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("gdpr", Map.of("gdpr", "request"))) + .thenRun(() -> {}); + } + + /** + * Queries the document containing all the data that was collected by Whatsapp about this user. To + * create a request for this document, use {@link Whatsapp#createGdprAccountInfo()} + * + * @return the same instance wrapped in a completable future + */ + // TODO: Implement ready and error states + public CompletableFuture getGdprAccountInfoStatus() { + return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("gdpr", Map.of("gdpr", "status"))) + .thenApplyAsync(result -> GdprAccountReport.ofPending(result.attributes().getLong("timestamp"))); + } + + /** + * Changes the name of this user + * + * @param newName the non-null new name + * @return the same instance wrapped in a completable future + */ + public CompletableFuture changeName(String newName) { + var oldName = store().name(); + return socketHandler.send(Node.of("presence", Map.of("name", newName))) + .thenRun(() -> socketHandler.updateUserName(newName, oldName)); + } + + /** + * Changes the about of this user + * + * @param newAbout the non-null new status + * @return the same instance wrapped in a completable future + */ + public CompletableFuture changeAbout(String newAbout) { + return socketHandler.changeAbout(newAbout); + } + + /** + * Sends a request to Whatsapp in order to receive updates when the status of a contact changes. + * These changes include the last known presence and the seconds the contact was last seen. + * + * @param jid the contact whose status the api should receive updates on + * @return a CompletableFuture + */ + public CompletableFuture subscribeToPresence(JidProvider jid) { + return socketHandler.subscribeToPresence(jid); + } + + /** + * Remove a reaction from a message + * + * @param message the non-null message + * @return a CompletableFuture + */ + public CompletableFuture removeReaction(MessageInfo message) { + return sendReaction(message, (String) null); + } + + /** + * Send a reaction to a message + * + * @param message the non-null message + * @param reaction the reaction to send, null if you want to remove the reaction + * @return a CompletableFuture + */ + public CompletableFuture sendReaction(MessageInfo message, Emoji reaction) { + return sendReaction(message, Objects.toString(reaction)); + } + + /** + * Send a reaction to a message + * + * @param message the non-null message + * @param reaction the reaction to send, null if you want to remove the reaction. If a string that + * isn't an emoji supported by Whatsapp is used, it will not get displayed + * correctly. Use {@link Whatsapp#sendReaction(MessageInfo, Emoji)} if + * you need a typed emoji enum. + * @return a CompletableFuture + */ + public CompletableFuture sendReaction(MessageInfo message, String reaction) { + var key = new ChatMessageKeyBuilder() + .id(ChatMessageKey.randomId()) + .chatJid(message.parentJid()) + .senderJid(message.senderJid()) + .fromMe(Objects.equals(message.senderJid().withoutDevice(), jidOrThrowError().withoutDevice())) + .id(message.id()) + .build(); + var reactionMessage = new ReactionMessageBuilder() + .key(key) + .content(reaction) + .timestampSeconds(Instant.now().toEpochMilli()) + .build(); + return sendMessage(message.parentJid(), reactionMessage); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(JidProvider chat, String message) { + return sendMessage(chat, MessageContainer.of(message)); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendChatMessage(JidProvider chat, String message) { + return sendChatMessage(chat, MessageContainer.of(message)); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendsNewsletterMessage(JidProvider chat, String message) { + return sendNewsletterMessage(chat, MessageContainer.of(message)); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(JidProvider chat, String message, MessageInfo quotedMessage) { + return sendMessage(chat, TextMessage.of(message), quotedMessage); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendChatMessage(JidProvider chat, String message, MessageInfo quotedMessage) { + return sendChatMessage(chat, TextMessage.of(message), quotedMessage); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendNewsletterMessage(JidProvider chat, String message, MessageInfo quotedMessage) { + return sendNewsletterMessage(chat, TextMessage.of(message), quotedMessage); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(JidProvider chat, ContextualMessage message, MessageInfo quotedMessage) { + var contextInfo = ContextInfo.of(quotedMessage); + message.setContextInfo(contextInfo); + return sendMessage(chat, MessageContainer.of(message)); + } + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendChatMessage(JidProvider chat, ContextualMessage message, MessageInfo quotedMessage) { + var contextInfo = ContextInfo.of(quotedMessage); + message.setContextInfo(contextInfo); + return sendChatMessage(chat, MessageContainer.of(message)); + } + + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @param quotedMessage the message to quote + * @return a CompletableFuture + */ + public CompletableFuture sendNewsletterMessage(JidProvider chat, ContextualMessage message, MessageInfo quotedMessage) { + var contextInfo = ContextInfo.of(quotedMessage); + message.setContextInfo(contextInfo); + return sendNewsletterMessage(chat, MessageContainer.of(message)); + } + + + /** + * Builds and sends a message from a chat and a message + * + * @param chat the chat where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(JidProvider chat, Message message) { + return sendMessage(chat, MessageContainer.of(message)); + } + + /** + * Builds and sends a message from a recipient and a message + * + * @param recipient the recipient where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(JidProvider recipient, MessageContainer message) { + return recipient.toJid().server() == JidServer.NEWSLETTER ? sendNewsletterMessage(recipient, message) : sendChatMessage(recipient, message); + } + + /** + * Builds and sends a message from a recipient and a message + * + * @param recipient the recipient where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendChatMessage(JidProvider recipient, MessageContainer message) { + Validate.isTrue(!recipient.toJid().hasServer(JidServer.NEWSLETTER), "Use sendNewsletterMessage to send a message in a newsletter"); + var timestamp = Clock.nowSeconds(); + var deviceInfo = new DeviceContextInfoBuilder() + .deviceListMetadataVersion(2) + .build(); + var key = new ChatMessageKeyBuilder() + .id(ChatMessageKey.randomId()) + .chatJid(recipient.toJid()) + .fromMe(true) + .senderJid(jidOrThrowError()) + .build(); + var info = new ChatMessageInfoBuilder() + .status(MessageStatus.PENDING) + .senderJid(jidOrThrowError()) + .key(key) + .message(message.withDeviceInfo(deviceInfo)) + .timestampSeconds(timestamp) + .broadcast(recipient.toJid().hasServer(JidServer.BROADCAST)) + .build(); + return sendMessage(info); + } + + /** + * Builds and sends a message from a recipient and a message + * + * @param recipient the recipient where the message should be sent + * @param message the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendNewsletterMessage(JidProvider recipient, MessageContainer message) { + var newsletter = store().findNewsletterByJid(recipient); + Validate.isTrue(newsletter.isPresent(), "Cannot send a message in a newsletter that you didn't join"); + var oldServerId = newsletter.get() + .newestMessage() + .map(NewsletterMessageInfo::serverId) + .orElse(0); + var info = new NewsletterMessageInfo( + ChatMessageKey.randomId(), + oldServerId + 1, + Clock.nowSeconds(), + null, + new ConcurrentHashMap<>(), + message, + MessageStatus.PENDING + ); + info.setNewsletter(newsletter.get()); + return sendMessage(info); + } + + /** + * Builds and sends an edited message + * + * @param oldMessage the message to edit + * @param newMessage the new message's content + * @return a CompletableFuture + */ + public CompletableFuture editMessage(T oldMessage, Message newMessage) { + var oldMessageType = oldMessage.message().content().type(); + var newMessageType = newMessage.type(); + Validate.isTrue(oldMessageType == newMessageType, + "Message type mismatch: %s != %s", + oldMessageType, newMessageType); + return switch (oldMessage) { + case NewsletterMessageInfo oldNewsletterInfo -> { + var info = new NewsletterMessageInfo( + oldNewsletterInfo.id(), + oldNewsletterInfo.serverId(), + Clock.nowSeconds(), + null, + new ConcurrentHashMap<>(), + MessageContainer.ofEditedMessage(newMessage), + MessageStatus.PENDING + ); + info.setNewsletter(oldNewsletterInfo.newsletter()); + var request = new MessageSendRequest.Newsletter(info, Map.of("edit", getEditBit(info))); + yield socketHandler.sendMessage(request) + .thenApply(ignored -> oldMessage); + } + case ChatMessageInfo oldChatInfo -> { + var key = new ChatMessageKeyBuilder() + .id(oldChatInfo.id()) + .chatJid(oldChatInfo.chatJid()) + .fromMe(true) + .senderJid(jidOrThrowError()) + .build(); + var info = new ChatMessageInfoBuilder() + .status(MessageStatus.PENDING) + .senderJid(jidOrThrowError()) + .key(key) + .message(MessageContainer.ofEditedMessage(newMessage)) + .timestampSeconds(Clock.nowSeconds()) + .broadcast(oldChatInfo.chatJid().hasServer(JidServer.BROADCAST)) + .build(); + var request = new MessageSendRequest.Chat(info, null, false, false, Map.of("edit", getEditBit(info))); + yield socketHandler.sendMessage(request) + .thenApply(ignored -> oldMessage); + } + default -> throw new IllegalStateException("Unsupported edit: " + oldMessage); + }; + } + + public CompletableFuture sendStatus(String message) { + return sendStatus(MessageContainer.of(message)); + } + + public CompletableFuture sendStatus(Message message) { + return sendStatus(MessageContainer.of(message)); + } + + public CompletableFuture sendStatus(MessageContainer message) { + var timestamp = Clock.nowSeconds(); + var deviceInfo = new DeviceContextInfoBuilder() + .deviceListMetadataVersion(2) + .build(); + var key = new ChatMessageKeyBuilder() + .id(ChatMessageKey.randomId()) + .chatJid(Jid.of("status@broadcast")) + .fromMe(true) + .senderJid(jidOrThrowError()) + .build(); + var info = new ChatMessageInfoBuilder() + .status(MessageStatus.PENDING) + .senderJid(jidOrThrowError()) + .key(key) + .message(message.withDeviceInfo(deviceInfo)) + .timestampSeconds(timestamp) + .broadcast(false) + .build(); + return sendMessage(info); + } + + /** + * Sends a message to a chat + * + * @param info the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(ChatMessageInfo info) { + return socketHandler.sendMessage(new MessageSendRequest.Chat(info)) + .thenApply(ignored -> info); + } + + /** + * Sends a message to a newsletter + * + * @param info the message to send + * @return a CompletableFuture + */ + public CompletableFuture sendMessage(NewsletterMessageInfo info) { + return socketHandler.sendMessage(new MessageSendRequest.Newsletter(info)) + .thenApply(ignored -> info); + } + + /** + * Marks a chat as read. + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture markChatRead(JidProvider chat) { + return mark(chat, true) + .thenComposeAsync(ignored -> markAllAsRead(chat)); + } + + private CompletableFuture markAllAsRead(JidProvider chat) { + var all = store() + .findChatByJid(chat.toJid()) + .stream() + .map(Chat::unreadMessages) + .flatMap(Collection::stream) + .map(this::markMessageRead) + .toArray(CompletableFuture[]::new); + return CompletableFuture.allOf(all); + } + + /** + * Marks a chat as unread + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture markChatUnread(JidProvider chat) { + return mark(chat, false); + } + + private CompletableFuture mark(JidProvider chat, boolean read) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat.toJid()) + .ifPresent(entry -> entry.setMarkedAsUnread(read)); + return CompletableFuture.completedFuture(null); + } + + var range = createRange(chat, false); + var markAction = new MarkChatAsReadAction(read, Optional.of(range)); + var syncAction = ActionValueSync.of(markAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString()); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request); + } + + private ActionMessageRangeSync createRange(JidProvider chat, boolean allMessages) { + var known = store().findChatByJid(chat.toJid()).orElseGet(() -> store().addNewChat(chat.toJid())); + return new ActionMessageRangeSync(known, allMessages); + } + + /** + * Marks a message as read + * + * @param info the target message + * @return a CompletableFuture + */ + public CompletableFuture markMessageRead(ChatMessageInfo info) { + var type = store().findPrivacySetting(PrivacySettingType.READ_RECEIPTS) + .value() == PrivacySettingValue.EVERYONE ? "read" : "read-self"; + socketHandler.sendReceipt(info.chatJid(), info.senderJid(), List.of(info.id()), type); + info.chat().ifPresent(chat -> { + var count = chat.unreadMessagesCount(); + if (count > 0) { + chat.setUnreadMessagesCount(count - 1); + } + }); + info.setStatus(MessageStatus.READ); + return CompletableFuture.completedFuture(info); + } + + /** + * Awaits for a single newsletters to a message + * + * @param info the non-null message whose newsletters is pending + * @return a non-null newsletters + */ + public CompletableFuture awaitMessageReply(ChatMessageInfo info) { + return awaitMessageReply(info.id()); + } + + /** + * Awaits for a single newsletters to a message + * + * @param id the non-null id of message whose newsletters is pending + * @return a non-null newsletters + */ + public CompletableFuture awaitMessageReply(String id) { + return store().addPendingReply(id); + } + + /** + * Executes a query to determine whether a user has an account on Whatsapp + * + * @param contact the contact to check + * @return a CompletableFuture that wraps a non-null newsletters + */ + public CompletableFuture hasWhatsapp(JidProvider contact) { + return hasWhatsapp(new JidProvider[]{contact}).thenApply(result -> result.get(contact.toJid())); + } + + /** + * Executes a query to determine whether any number of users have an account on Whatsapp + * + * @param contacts the contacts to check + * @return a CompletableFuture that wraps a non-null map + */ + public CompletableFuture> hasWhatsapp(JidProvider... contacts) { + var jids = Arrays.stream(contacts) + .map(JidProvider::toJid) + .toList(); + var contactNodes = jids.stream() + .map(jid -> Node.of("user", Node.of("contact", jid.toPhoneNumber()))) + .toList(); + return socketHandler.sendInteractiveQuery(List.of(Node.of("contact")), contactNodes, List.of()) + .thenApplyAsync(result -> parseHasWhatsappResponse(jids, result)); + } + + private Map parseHasWhatsappResponse(List contacts, List nodes) { + var result = nodes.stream() + .map(this::parseHasWhatsappResponse) + .collect(Collectors.toMap(HasWhatsappResponse::contact, Function.identity(), (first, second) -> first, HashMap::new)); + contacts.stream() + .filter(contact -> !result.containsKey(contact)) + .forEach(contact -> result.put(contact, new HasWhatsappResponse(contact, false))); + return Collections.unmodifiableMap(result); + } + + private HasWhatsappResponse parseHasWhatsappResponse(Node node) { + var jid = node.attributes() + .getRequiredJid("jid"); + var in = node.findNode("contact") + .orElseThrow(() -> new NoSuchElementException("Missing contact in HasWhatsappResponse")) + .attributes() + .getRequiredString("type") + .equals("in"); + return new HasWhatsappResponse(jid, in); + } + + /** + * Queries the block list + * + * @return a CompletableFuture + */ + public CompletableFuture> queryBlockList() { + return socketHandler.queryBlockList(); + } + + /** + * Queries the display name of a contact + * + * @param contactJid the non-null contact + * @return a CompletableFuture + */ + public CompletableFuture> queryName(JidProvider contactJid) { + var contact = store().findContactByJid(contactJid); + return contact.map(value -> CompletableFuture.completedFuture(value.chosenName())) + .orElseGet(() -> queryNameFromServer(contactJid)); + } + + private CompletableFuture> queryNameFromServer(JidProvider contactJid) { + 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(UserChosenNameResponse::ofJson) + .flatMap(UserChosenNameResponse::name); + } + + /** + * Queries the written whatsapp status of a Contact + * + * @param chat the target contact + * @return a CompletableFuture that wraps an optional contact status newsletters + */ + public CompletableFuture> queryAbout(JidProvider chat) { + return socketHandler.queryAbout(chat); + } + + /** + * Queries the profile picture + * + * @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(JidProvider chat) { + return socketHandler.queryPicture(chat); + } + + /** + * Queries the metadata of a group + * + * @param chat the target group + * @return a CompletableFuture + */ + public CompletableFuture queryGroupMetadata(JidProvider chat) { + return socketHandler.queryGroupMetadata(chat.toJid()); + } + + /** + * Queries a business profile, if available + * + * @param contact the target contact + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessProfile(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); + } + + private Optional getBusinessProfile(Node result) { + return result.findNode("business_profile") + .flatMap(entry -> entry.findNode("profile")) + .map(BusinessProfile::of); + } + + /** + * Queries all the known business categories + * + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCategories() { + return socketHandler.queryBusinessCategories(); + } + + /** + * Queries the invite code of a group + * + * @param chat the target group + * @return a CompletableFuture + */ + public CompletableFuture queryGroupInviteCode(JidProvider chat) { + return socketHandler.sendQuery(chat.toJid(), "get", "w:g2", Node.of("invite")) + .thenApplyAsync(this::parseInviteCode); + } + + private String parseInviteCode(Node result) { + return result.findNode("invite") + .orElseThrow(() -> new NoSuchElementException("Missing invite code in invite newsletters")) + .attributes() + .getRequiredString("code"); + } + + /** + * Revokes the invite code of a group + * + * @param chat the target group + * @return a CompletableFuture + */ + public CompletableFuture revokeGroupInvite(JidProvider chat) { + return socketHandler.sendQuery(chat.toJid(), "set", "w:g2", Node.of("invite")) + .thenRun(() -> {}); + } + + /** + * Accepts the invite for a group + * + * @param inviteCode the invite countryCode + * @return a CompletableFuture + */ + public CompletableFuture> acceptGroupInvite(String inviteCode) { + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", Node.of("invite", Map.of("code", inviteCode))) + .thenApplyAsync(this::parseAcceptInvite); + } + + private Optional parseAcceptInvite(Node result) { + return result.findNode("group") + .flatMap(group -> group.attributes().getOptionalJid("jid")) + .map(jid -> store().findChatByJid(jid).orElseGet(() -> store().addNewChat(jid))); + } + + /** + * Changes your presence for everyone on Whatsapp + * + * @param available whether you are online or not + * @return a CompletableFuture + */ + public CompletableFuture changePresence(boolean available) { + var status = socketHandler.store().online(); + if (status == available) { + return CompletableFuture.completedFuture(status); + } + + var presence = available ? ContactStatus.AVAILABLE : ContactStatus.UNAVAILABLE; + var node = Node.of("presence", Map.of("name", store().name(), "type", presence.toString())); + return socketHandler.sendWithNoResponse(node) + .thenAcceptAsync(socketHandler -> updateSelfPresence(null, presence)) + .thenApplyAsync(ignored -> available); + } + + private void updateSelfPresence(JidProvider chatJid, ContactStatus presence) { + if (chatJid == null) { + store().setOnline(presence == ContactStatus.AVAILABLE); + } + + var self = store().findContactByJid(jidOrThrowError().withoutDevice()); + if (self.isEmpty()) { + return; + } + + if (presence == ContactStatus.AVAILABLE || presence == ContactStatus.UNAVAILABLE) { + self.get().setLastKnownPresence(presence); + } + if (chatJid != null) { + store().findChatByJid(chatJid).ifPresent(chat -> chat.presences().put(self.get().jid(), presence)); + } + self.get().setLastSeen(ZonedDateTime.now()); + } + + /** + * Changes your presence for a specific chat + * + * @param chatJid the target chat + * @param presence the new status + * @return a CompletableFuture + */ + public CompletableFuture changePresence(JidProvider chatJid, ContactStatus presence) { + var knownPresence = store().findChatByJid(chatJid) + .map(Chat::presences) + .map(entry -> entry.get(jidOrThrowError().withoutDevice())) + .orElse(null); + if (knownPresence == COMPOSING || knownPresence == RECORDING) { + var node = Node.of("chatstate", Map.of("to", chatJid.toJid()), Node.of("paused")); + return socketHandler.sendWithNoResponse(node); + } + + if (presence == COMPOSING || presence == RECORDING) { + var tag = presence == RECORDING ? COMPOSING : presence; + var node = Node.of("chatstate", + Map.of("to", chatJid.toJid()), + Node.of(COMPOSING.toString(), presence == RECORDING ? Map.of("media", "audio") : Map.of())); + return socketHandler.sendWithNoResponse(node) + .thenAcceptAsync(socketHandler -> updateSelfPresence(chatJid, presence)); + } + + var node = Node.of("presence", Map.of("type", presence.toString(), "name", store().name())); + return socketHandler.sendWithNoResponse(node) + .thenAcceptAsync(socketHandler -> updateSelfPresence(chatJid, presence)); + } + + /** + * Promotes any number of contacts to admin in a group + * + * @param group the target group + * @param contacts the target contacts + * @return a CompletableFuture + */ + public CompletableFuture> promoteGroupParticipant(JidProvider group, JidProvider... contacts) { + return executeActionOnGroupParticipant(group, GroupAction.PROMOTE, contacts); + } + + /** + * Demotes any number of contacts to admin in a group + * + * @param group the target group + * @param contacts the target contacts + * @return a CompletableFuture + */ + public CompletableFuture> demoteGroupParticipant(JidProvider group, JidProvider... contacts) { + return executeActionOnGroupParticipant(group, GroupAction.DEMOTE, contacts); + } + + /** + * Adds any number of contacts to a group + * + * @param group the target group + * @param contacts the target contact/s + * @return a CompletableFuture + */ + public CompletableFuture> addGroupParticipant(JidProvider group, JidProvider... contacts) { + return executeActionOnGroupParticipant(group, GroupAction.ADD, contacts); + } + + /** + * Removes any number of contacts from group + * + * @param group the target group + * @param contacts the target contact/s + * @return a CompletableFuture + */ + public CompletableFuture> removeGroupParticipant(JidProvider group, JidProvider... contacts) { + return executeActionOnGroupParticipant(group, GroupAction.REMOVE, contacts); + } + + private CompletableFuture> executeActionOnGroupParticipant(JidProvider group, GroupAction action, JidProvider... jids) { + var body = Arrays.stream(jids) + .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); + return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body) + .thenApplyAsync(result -> parseGroupActionResponse(result, group, action)); + } + + private Jid checkGroupParticipantJid(Jid jid, String errorMessage) { + Validate.isTrue(!Objects.equals(jid.withoutDevice(), jidOrThrowError().withoutDevice()), errorMessage); + return jid; + } + + 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") + .stream() + .filter(participant -> !participant.attributes().hasKey("error")) + .map(participant -> participant.attributes().getOptionalJid("jid")) + .flatMap(Optional::stream) + .toList(); + var chat = groupJid instanceof Chat entry ? entry : store() + .findChatByJid(groupJid) + .orElse(null); + if (chat != null) { + results.forEach(entry -> handleGroupAction(action, chat, entry)); + } + + return results; + } + + private void handleGroupAction(GroupAction action, Chat chat, Jid entry) { + switch (action) { + case ADD -> chat.addParticipant(entry, GroupRole.USER); + case REMOVE -> { + chat.removeParticipant(entry); + chat.addPastParticipant(new GroupPastParticipant(entry, GroupPastParticipant.Reason.REMOVED, Clock.nowSeconds())); + } + case PROMOTE -> chat.findParticipant(entry) + .ifPresent(participant -> participant.setRole(GroupRole.ADMIN)); + case DEMOTE -> chat.findParticipant(entry) + .ifPresent(participant -> participant.setRole(GroupRole.USER)); + } + } + + /** + * Changes the name of a group + * + * @param group the target group + * @param newName the new name for the group + * @return a CompletableFuture + * @throws IllegalArgumentException if the provided new name is empty or blank + */ + public CompletableFuture changeGroupSubject(JidProvider group, String newName) { + var body = Node.of("subject", newName.getBytes(StandardCharsets.UTF_8)); + return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body) + .thenRun(() -> {}); + } + + /** + * Changes the description of a group + * + * @param group the target group + * @param description the new name for the group, can be null if you want to remove it + * @return a CompletableFuture + */ + public CompletableFuture changeGroupDescription(JidProvider group, String description) { + return socketHandler.queryGroupMetadata(group.toJid()) + .thenApplyAsync(GroupMetadata::descriptionId) + .thenComposeAsync(descriptionId -> changeGroupDescription(group, description, descriptionId.orElse(null))) + .thenRun(() -> {}); + } + + 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); + var attributes = Attributes.of() + .put("id", ChatMessageKey.randomId(), () -> description != null) + .put("delete", true, () -> description == null) + .put("prev", descriptionId, () -> descriptionId != null) + .toMap(); + var body = Node.of("description", attributes, descriptionNode); + return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body) + .thenRun(() -> onDescriptionSet(group, description)); + } + + private void onDescriptionSet(JidProvider groupJid, String description) { + if (groupJid instanceof Chat chat) { + chat.setDescription(description); + return; + } + + var group = store().findChatByJid(groupJid); + group.ifPresent(chat -> chat.setDescription(description)); + } + + /** + * Changes a group setting + * + * @param group the non-null group affected by this change + * @param setting the non-null setting + * @param policy the non-null policy + * @return a future + */ + public CompletableFuture changeGroupSetting(JidProvider group, GroupSetting setting, ChatSettingPolicy policy) { + Validate.isTrue(group.toJid().hasServer(JidServer.GROUP), "This method only accepts groups"); + var body = switch (setting) { + case EDIT_GROUP_INFO -> Node.of(policy == ChatSettingPolicy.ADMINS ? "locked" : "unlocked"); + case SEND_MESSAGES -> Node.of(policy == ChatSettingPolicy.ADMINS ? "announcement" : "not_announcement"); + case ADD_PARTICIPANTS -> + Node.of("member_add_mode", policy == ChatSettingPolicy.ADMINS ? "admin_add".getBytes(StandardCharsets.UTF_8) : "all_member_add".getBytes(StandardCharsets.UTF_8)); + case APPROVE_PARTICIPANTS -> + Node.of("membership_approval_mode", Node.of("group_join", Map.of("state", policy == ChatSettingPolicy.ADMINS ? "on" : "off"))); + }; + return socketHandler.sendQuery(group.toJid(), "set", "w:g2", body) + .thenRun(() -> {}); + } + + /** + * Changes the profile picture of yourself + * + * @param image the new image, can be null if you want to remove it + * @return a CompletableFuture + */ + public CompletableFuture changeProfilePicture(byte[] image) { + return changeGroupPicture(jidOrThrowError(), image); + } + + /** + * Changes the picture of a group + * + * @param group the target group + * @param image the new image, can be null if you want to remove it + * @return a CompletableFuture + */ + public CompletableFuture changeGroupPicture(JidProvider group, URI image) { + var imageFuture = image == null ? CompletableFuture.completedFuture((byte[]) null) : Medias.downloadAsync(image); + return imageFuture.thenComposeAsync(imageResult -> changeGroupPicture(group, imageResult)); + } + + /** + * Changes the picture of a group + * + * @param group the target group + * @param image the new image, can be null if you want to remove it + * @return a CompletableFuture + */ + public CompletableFuture changeGroupPicture(JidProvider 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) + .thenRun(() -> {}); + } + + /** + * Creates a new group + * + * @param subject the new group's name + * @param contacts at least one contact to add to the group + * @return a CompletableFuture + */ + public CompletableFuture> createGroup(String subject, JidProvider... contacts) { + return createGroup(subject, ChatEphemeralTimer.OFF, contacts); + } + + /** + * Creates a new group + * + * @param subject the new group's name + * @param timer the default ephemeral timer for messages sent in this group + * @param contacts at least one contact to add to the group + * @return a CompletableFuture + */ + public CompletableFuture> createGroup(String subject, ChatEphemeralTimer timer, JidProvider... contacts) { + return createGroup(subject, timer, null, contacts); + } + + /** + * Creates a new group + * + * @param subject the new group's name + * @param timer the default ephemeral timer for messages sent in this group + * @param parentGroup the community to whom the new group will be linked + * @return a CompletableFuture + */ + public CompletableFuture> createGroup(String subject, ChatEphemeralTimer timer, JidProvider parentGroup) { + return createGroup(subject, timer, parentGroup, new JidProvider[0]); + } + + /** + * Creates a new group + * + * @param subject the new group's name + * @param timer the default ephemeral timer for messages sent in this group + * @param parentCommunity the community to whom the new group will be linked + * @param contacts at least one contact to add to the group, not enforced if part of a community + * @return a CompletableFuture + */ + public CompletableFuture> createGroup(String subject, ChatEphemeralTimer timer, JidProvider parentCommunity, JidProvider... contacts) { + Validate.isTrue(!subject.isBlank(), "The subject of a group cannot be blank"); + Validate.isTrue( parentCommunity != null || contacts.length >= 1, "Expected at least 1 member for this group"); + var children = new ArrayList(); + if (parentCommunity != null) { + children.add(Node.of("linked_parent", Map.of("jid", parentCommunity.toJid()))); + } + if (timer != ChatEphemeralTimer.OFF) { + children.add(Node.of("ephemeral", Map.of("expiration", timer.periodSeconds()))); + } + Arrays.stream(contacts) + .map(contact -> Node.of("participant", Map.of("jid", checkGroupParticipantJid(contact.toJid(), "Cannot create group with yourself as a participant")))) + .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(JidServer.GROUP.toJid(), "set", "w:g2", body) + .thenApplyAsync(this::parseGroupResponse); + } + + private Optional parseGroupResponse(Node response) { + return Optional.ofNullable(response) + .flatMap(node -> node.findNode("group")) + .map(socketHandler::parseGroupMetadata) + .map(this::addNewGroup); + } + + private GroupMetadata addNewGroup(GroupMetadata result) { + var chatBuilder = new ChatBuilder() + .jid(result.jid()) + .description(result.description().orElse(null)) + .participants(new ArrayList<>(result.participants())) + .founder(result.founder().orElse(null)); + result.foundationTimestamp() + .map(ChronoZonedDateTime::toEpochSecond) + .ifPresent(chatBuilder::foundationTimestampSeconds); + store().addChat(chatBuilder.build()); + return result; + } + + private String findErrorNode(Node result) { + return Optional.ofNullable(result) + .flatMap(node -> node.findNode("error")) + .map(Node::toString) + .orElseGet(() -> Objects.toString(result)); + } + + /** + * Leaves a group + * + * @param group the target group + * @throws IllegalArgumentException if the provided chat is not a group + */ + public CompletableFuture leaveGroup(JidProvider group) { + var body = Node.of("leave", Node.of("group", Map.of("id", group.toJid()))); + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", body) + .thenAcceptAsync(ignored -> handleLeaveGroup(group)); + } + + private void handleLeaveGroup(JidProvider group) { + var chat = group instanceof Chat entry ? entry : store() + .findChatByJid(group) + .orElse(null); + if (chat != null) { + var pastParticipant = new GroupPastParticipantBuilder() + .jid(jidOrThrowError().withoutDevice()) + .reason(GroupPastParticipant.Reason.REMOVED) + .timestampSeconds(Clock.nowSeconds()) + .build(); + chat.addPastParticipant(pastParticipant); + } + } + + /** + * Links any number of groups to a community + * + * @param community the non-null community where the groups will be added + * @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(JidProvider community, JidProvider... groups) { + var body = Arrays.stream(groups) + .map(entry -> Node.of("group", Map.of("jid", entry.toJid()))) + .toArray(Node[]::new); + return socketHandler.sendQuery(community.toJid(), "set", "w:g2", Node.of("links", Node.of("link", Map.of("link_type", "sub_group"), body))) + .thenApplyAsync(result -> parseLinksResponse(result, groups)); + } + + private Map parseLinksResponse(Node result, JidProvider[] groups) { + var success = result.findNode("links") + .stream() + .map(entry -> entry.findNodes("link")) + .flatMap(Collection::stream) + .filter(entry -> entry.attributes().hasValue("link_type", "sub_group")) + .map(entry -> entry.findNode("group")) + .flatMap(Optional::stream) + .map(entry -> entry.attributes().getOptionalJid("jid")) + .flatMap(Optional::stream) + .collect(Collectors.toUnmodifiableSet()); + return Arrays.stream(groups) + .map(JidProvider::toJid) + .collect(Collectors.toUnmodifiableMap(Function.identity(), success::contains)); + } + + /** + * Unlinks a group from a community + * + * @param community the non-null parent community + * @param group the non-null group to unlink + * @return a CompletableFuture that indicates whether the request was successful + */ + public CompletableFuture unlinkGroupFromCommunity(JidProvider community, 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, JidProvider group) { + return result.findNode("unlink") + .filter(entry -> entry.attributes().hasValue("unlink_type", "sub_group")) + .flatMap(entry -> entry.findNode("group")) + .map(entry -> entry.attributes().hasValue("jid", group.toJid().toString())) + .isPresent(); + } + + /** + * Mutes a chat indefinitely + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture muteChat(JidProvider chat) { + return muteChat(chat, ChatMute.muted()); + } + + /** + * Mutes a chat + * + * @param chat the target chat + * @param mute the type of mute + * @return a CompletableFuture + */ + public CompletableFuture muteChat(JidProvider chat, ChatMute mute) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat) + .ifPresent(entry -> entry.setMute(mute)); + return CompletableFuture.completedFuture(null); + } + + var endTimeStamp = mute.type() == ChatMute.Type.MUTED_FOR_TIMEFRAME ? mute.endTimeStamp() * 1000L : mute.endTimeStamp(); + var muteAction = new MuteAction(true, OptionalLong.of(endTimeStamp), false); + var syncAction = ActionValueSync.of(muteAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString()); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Unmutes a chat + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture unmuteChat(JidProvider chat) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat) + .ifPresent(entry -> entry.setMute(ChatMute.notMuted())); + return CompletableFuture.completedFuture(null); + } + + var muteAction = new MuteAction(false, null, false); + var syncAction = ActionValueSync.of(muteAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString()); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Blocks a contact + * + * @param contact the target chat + * @return a CompletableFuture + */ + public CompletableFuture blockContact(JidProvider contact) { + var body = Node.of("item", Map.of("action", "block", "jid", contact.toJid())); + return socketHandler.sendQuery("set", "blocklist", body) + .thenRun(() -> {}); + } + + /** + * Unblocks a contact + * + * @param contact the target chat + * @return a CompletableFuture + */ + public CompletableFuture unblockContact(JidProvider contact) { + var body = Node.of("item", Map.of("action", "unblock", "jid", contact.toJid())); + return socketHandler.sendQuery("set", "blocklist", body) + .thenRun(() -> {}); + } + + /** + * Enables ephemeral messages in a chat, this means that messages will be automatically cancelled + * in said chat after a week + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture changeEphemeralTimer(JidProvider chat, ChatEphemeralTimer timer) { + return switch (chat.toJid().server()) { + case USER, WHATSAPP -> { + var message = new ProtocolMessageBuilder() + .protocolType(ProtocolMessage.Type.EPHEMERAL_SETTING) + .ephemeralExpiration(timer.period().toSeconds()) + .build(); + yield sendMessage(chat, message) + .thenRun(() -> { + }); + } + case GROUP -> { + var body = timer == ChatEphemeralTimer.OFF ? Node.of("not_ephemeral") : Node.of("ephemeral", Map.of("expiration", timer.period() + .toSeconds())); + yield socketHandler.sendQuery(chat.toJid(), "set", "w:g2", body) + .thenRun(() -> { + }); + } + default -> + throw new IllegalArgumentException("Unexpected chat %s: ephemeral messages are only supported for conversations and groups".formatted(chat.toJid())); + }; + } + + /** + * Marks a message as played + * + * @param info the target message + * @return a CompletableFuture + */ + public CompletableFuture markMessagePlayed(ChatMessageInfo info) { + if (store().findPrivacySetting(PrivacySettingType.READ_RECEIPTS).value() != PrivacySettingValue.EVERYONE) { + return CompletableFuture.completedFuture(info); + } + socketHandler.sendReceipt(info.chatJid(), info.senderJid(), List.of(info.id()), "played"); + info.setStatus(MessageStatus.PLAYED); + return CompletableFuture.completedFuture(info); + } + + /** + * Pins a chat to the top. A maximum of three chats can be pinned to the top. This condition can + * be checked using;. + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture pinChat(JidProvider chat) { + return pinChat(chat, true); + } + + /** + * Unpins a chat from the top + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture unpinChat(JidProvider chat) { + return pinChat(chat, false); + } + + private CompletableFuture pinChat(JidProvider chat, boolean pin) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat) + .ifPresent(entry -> entry.setPinnedTimestampSeconds(pin ? (int) Clock.nowSeconds() : 0)); + return CompletableFuture.completedFuture(null); + } + + var pinAction = new PinAction(pin); + var syncAction = ActionValueSync.of(pinAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString()); + var request = new PatchRequest(PatchType.REGULAR_LOW, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Stars a message + * + * @param info the target message + * @return a CompletableFuture + */ + public CompletableFuture starMessage(ChatMessageInfo info) { + return starMessage(info, true); + } + + private CompletableFuture starMessage(ChatMessageInfo info, boolean star) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + info.setStarred(star); + return CompletableFuture.completedFuture(info); + } + + var starAction = new StarAction(star); + var syncAction = ActionValueSync.of(starAction); + var entry = PatchEntry.of(syncAction, Operation.SET, info.chatJid() + .toString(), info.id(), fromMeToFlag(info), participantToFlag(info)); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request).thenApplyAsync(ignored -> info); + } + + private String fromMeToFlag(MessageInfo info) { + var fromMe = Objects.equals(info.senderJid().withoutDevice(), jidOrThrowError().withoutDevice()); + return booleanToInt(fromMe); + } + + private String participantToFlag(MessageInfo info) { + var fromMe = Objects.equals(info.senderJid().withoutDevice(), jidOrThrowError().withoutDevice()); + return info.parentJid().hasServer(JidServer.GROUP) + && !fromMe ? info.senderJid().toString() : "0"; + } + + private String booleanToInt(boolean keepStarredMessages) { + return keepStarredMessages ? "1" : "0"; + } + + /** + * Removes star from a message + * + * @param info the target message + * @return a CompletableFuture + */ + public CompletableFuture unstarMessage(ChatMessageInfo info) { + return starMessage(info, false); + } + + /** + * Archives a chat. If said chat is pinned, it will be unpinned. + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture archiveChat(JidProvider chat) { + return archiveChat(chat, true); + } + + private CompletableFuture archiveChat(JidProvider chat, boolean archive) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat) + .ifPresent(entry -> entry.setArchived(archive)); + return CompletableFuture.completedFuture(null); + } + + var range = createRange(chat, false); + var archiveAction = new ArchiveChatAction(archive, Optional.of(range)); + var syncAction = ActionValueSync.of(archiveAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString()); + var request = new PatchRequest(PatchType.REGULAR_LOW, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Unarchives a chat + * + * @param chat the target chat + * @return a CompletableFuture + */ + public CompletableFuture unarchive(JidProvider chat) { + return archiveChat(chat, false); + } + + /** + * Deletes a message + * + * @param messageInfo the non-null message to delete + * @return a CompletableFuture + */ + public CompletableFuture deleteMessage(NewsletterMessageInfo messageInfo) { + var revokeInfo = new NewsletterMessageInfo( + messageInfo.id(), + messageInfo.serverId(), + Clock.nowSeconds(), + null, + new ConcurrentHashMap<>(), + MessageContainer.empty(), + MessageStatus.PENDING + ); + revokeInfo.setNewsletter(messageInfo.newsletter()); + var attrs = Map.of("edit", getDeleteBit(messageInfo)); + var request = new MessageSendRequest.Newsletter(revokeInfo, attrs); + return socketHandler.sendMessage(request); + } + + /** + * Deletes a message + * + * @param messageInfo non-null message to delete + * @param everyone whether the message should be deleted for everyone or only for this client and + * its companions + * @return a CompletableFuture + */ + public CompletableFuture deleteMessage(ChatMessageInfo messageInfo, boolean everyone) { + if (everyone) { + var message = new ProtocolMessageBuilder() + .protocolType(ProtocolMessage.Type.REVOKE) + .key(messageInfo.key()) + .build(); + var sender = messageInfo.chatJid().hasServer(JidServer.GROUP) ? jidOrThrowError() : null; + var key = new ChatMessageKeyBuilder() + .id(ChatMessageKey.randomId()) + .chatJid(messageInfo.chatJid()) + .fromMe(true) + .senderJid(sender) + .build(); + var revokeInfo = new ChatMessageInfoBuilder() + .status(MessageStatus.PENDING) + .senderJid(sender) + .key(key) + .message(MessageContainer.of(message)) + .timestampSeconds(Clock.nowSeconds()) + .build(); + var attrs = Map.of("edit", getDeleteBit(messageInfo)); + var request = new MessageSendRequest.Chat(revokeInfo, null, false, false, attrs); + return socketHandler.sendMessage(request); + } + + return switch (store().clientType()) { + case WEB -> { + var range = createRange(messageInfo.chatJid(), false); + var deleteMessageAction = new DeleteMessageForMeAction(false, messageInfo.timestampSeconds().orElse(0L)); + var syncAction = ActionValueSync.of(deleteMessageAction); + var entry = PatchEntry.of(syncAction, Operation.SET, messageInfo.chatJid().toString(), messageInfo.id(), fromMeToFlag(messageInfo), participantToFlag(messageInfo)); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + yield socketHandler.pushPatch(request); + } + case MOBILE -> { + // TODO: Send notification to companions + messageInfo.chat().ifPresent(chat -> chat.removeMessage(messageInfo)); + yield CompletableFuture.completedFuture(null); + } + }; + } + + + private int getEditBit(MessageInfo info) { + if (info.parentJid().hasServer(JidServer.NEWSLETTER)) { + return 3; + } + + return 1; + } + + private int getDeleteBit(MessageInfo info) { + if (info.parentJid().hasServer(JidServer.NEWSLETTER)) { + return 8; + } + + var fromMe = Objects.equals(info.senderJid().withoutDevice(), jidOrThrowError().withoutDevice()); + if (info.parentJid().hasServer(JidServer.GROUP) && !fromMe) { + return 8; + } + + return 7; + } + + /** + * Deletes a chat for this client and its companions using a modern version of Whatsapp Important: + * this message doesn't seem to work always as of now + * + * @param chat the non-null chat to delete + * @return a CompletableFuture + */ + public CompletableFuture deleteChat(JidProvider chat) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().removeChat(chat.toJid()); + return CompletableFuture.completedFuture(null); + } + + var range = createRange(chat.toJid(), false); + var deleteChatAction = new DeleteChatAction(Optional.of(range)); + var syncAction = ActionValueSync.of(deleteChatAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString(), "1"); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Clears the content of a chat for this client and its companions using a modern version of + * Whatsapp Important: this message doesn't seem to work always as of now + * + * @param chat the non-null chat to clear + * @param keepStarredMessages whether starred messages in this chat should be kept + * @return a CompletableFuture + */ + public CompletableFuture clearChat(JidProvider chat, boolean keepStarredMessages) { + if (store().clientType() == ClientType.MOBILE) { + // TODO: Send notification to companions + store().findChatByJid(chat.toJid()) + .ifPresent(Chat::removeMessages); + return CompletableFuture.completedFuture(null); + } + + var known = store().findChatByJid(chat); + var range = createRange(chat.toJid(), true); + var clearChatAction = new ClearChatAction(Optional.of(range)); + var syncAction = ActionValueSync.of(clearChatAction); + var entry = PatchEntry.of(syncAction, Operation.SET, chat.toJid().toString(), booleanToInt(keepStarredMessages), "0"); + var request = new PatchRequest(PatchType.REGULAR_HIGH, List.of(entry)); + return socketHandler.pushPatch(request); + } + + /** + * Change the description of this business profile + * + * @param description the new description, can be null + * @return a CompletableFuture + */ + public CompletableFuture changeBusinessDescription(String description) { + return changeBusinessAttribute("description", description); + } + + private CompletableFuture changeBusinessAttribute(String key, String value) { + return socketHandler.sendQuery("set", "w:biz", Node.of("business_profile", Map.of("v", "3", "mutation_type", "delta"), Node.of(key, Objects.requireNonNullElse(value, "").getBytes(StandardCharsets.UTF_8)))) + .thenAcceptAsync(result -> checkBusinessAttributeConflict(key, value, result)) + .thenApplyAsync(ignored -> value); + } + + private void checkBusinessAttributeConflict(String key, String value, Node result) { + var keyNode = result.findNode("profile").flatMap(entry -> entry.findNode(key)); + if (keyNode.isEmpty()) { + return; + } + var actual = keyNode.get() + .contentAsString() + .orElseThrow(() -> new NoSuchElementException("Missing business %s newsletters, something went wrong: %s".formatted(key, findErrorNode(result)))); + Validate.isTrue(value == null || value.equals(actual), "Cannot change business %s: conflict(expected %s, got %s)", key, value, actual); + } + + /** + * Change the address of this business profile + * + * @param address the new address, can be null + * @return a CompletableFuture + */ + public CompletableFuture changeBusinessAddress(String address) { + return changeBusinessAttribute("address", address); + } + + /** + * Change the email of this business profile + * + * @param email the new email, can be null + * @return a CompletableFuture + */ + public CompletableFuture changeBusinessEmail(String email) { + Validate.isTrue(email == null || isValidEmail(email), "Invalid email: %s", email); + return changeBusinessAttribute("email", email); + } + + private boolean isValidEmail(String email) { + return Pattern.compile("^(.+)@(\\S+)$") + .matcher(email) + .matches(); + } + + /** + * Change the categories of this business profile + * + * @param categories the new categories, can be null + * @return a CompletableFuture + */ + public CompletableFuture> changeBusinessCategories(List categories) { + return socketHandler.sendQuery("set", "w:biz", Node.of("business_profile", Map.of("v", "3", "mutation_type", "delta"), Node.of("categories", createCategories(categories)))) + .thenApplyAsync(ignored -> categories); + } + + private Collection createCategories(List categories) { + if (categories == null) { + return List.of(); + } + return categories.stream().map(entry -> Node.of("category", Map.of("id", entry.id()))).toList(); + } + + /** + * Change the websites of this business profile + * + * @param websites the new websites, can be null + * @return a CompletableFuture + */ + public CompletableFuture> changeBusinessWebsites(List websites) { + return socketHandler.sendQuery("set", "w:biz", Node.of("business_profile", Map.of("v", "3", "mutation_type", "delta"), createWebsites(websites))) + .thenApplyAsync(ignored -> websites); + } + + private List createWebsites(List websites) { + if (websites == null) { + return List.of(); + } + return websites.stream() + .map(entry -> Node.of("website", entry.toString().getBytes(StandardCharsets.UTF_8))) + .toList(); + } + + /** + * Query the catalog of this business + * + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCatalog() { + return queryBusinessCatalog(10); + } + + /** + * Query the catalog of this business + * + * @param productsLimit the maximum number of products to query + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCatalog(int productsLimit) { + return queryBusinessCatalog(jidOrThrowError().withoutDevice(), productsLimit); + } + + /** + * Query the catalog of a business + * + * @param contact the business + * @param productsLimit the maximum number of products to query + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCatalog(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); + } + + private List parseCatalog(Node result) { + return Objects.requireNonNull(result, "Cannot query business catalog, missing newsletters node") + .findNode("product_catalog") + .map(entry -> entry.findNodes("product")) + .stream() + .flatMap(Collection::stream) + .map(BusinessCatalogEntry::of) + .toList(); + } + + /** + * Query the catalog of a business + * + * @param contact the business + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCatalog(JidProvider contact) { + return queryBusinessCatalog(contact, 10); + } + + /** + * Query the collections of this business + * + * @return a CompletableFuture + */ + public CompletableFuture queryBusinessCollections() { + return queryBusinessCollections(50); + } + + /** + * Query the collections of this business + * + * @param collectionsLimit the maximum number of collections to query + * @return a CompletableFuture + */ + public CompletableFuture queryBusinessCollections(int collectionsLimit) { + return queryBusinessCollections(jidOrThrowError().withoutDevice(), collectionsLimit); + } + + /** + * Query the collections of a business + * + * @param contact the business + * @param collectionsLimit the maximum number of collections to query + * @return a CompletableFuture + */ + public CompletableFuture> queryBusinessCollections(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)))) + .thenApplyAsync(this::parseCollections); + } + + private List parseCollections(Node result) { + return Objects.requireNonNull(result, "Cannot query business collections, missing newsletters node") + .findNode("collections") + .stream() + .map(entry -> entry.findNodes("collection")) + .flatMap(Collection::stream) + .map(BusinessCollectionEntry::of) + .toList(); + } + + /** + * Query the collections of a business + * + * @param contact the business + * @return a CompletableFuture + */ + public CompletableFuture queryBusinessCollections(JidProvider contact) { + return queryBusinessCollections(contact, 50); + } + + /** + * Downloads a media from Whatsapp's servers. + * If the media was already downloaded, the cached version will be returned. + * If the download fails because the media is too old/invalid, a reupload request will be sent to Whatsapp. + * If the latter fails as well, an empty optional will be returned. + * + * @param info the non-null message info wrapping the media + * @return a CompletableFuture + */ + public CompletableFuture> downloadMedia(ChatMessageInfo info) { + if (!(info.message().content() instanceof MediaMessage mediaMessage)) { + throw new IllegalArgumentException("Expected media message, got: " + info.message().category()); + } + + return downloadMedia(mediaMessage).thenCompose(result -> { + if (result.isPresent()) { + return CompletableFuture.completedFuture(result); + } + + return requireMediaReupload(info) + .thenCompose(ignored -> downloadMedia(mediaMessage)); + }); + } + + /** + * Downloads a media from Whatsapp's servers. + * If the media was already downloaded, the cached version will be returned. + * If the download fails because the media is too old/invalid, an empty optional will be returned. + * + * @param info the non-null message info wrapping the media + * @return a CompletableFuture + */ + public CompletableFuture> downloadMedia(NewsletterMessageInfo info) { + if (!(info.message().content() instanceof MediaMessage mediaMessage)) { + throw new IllegalArgumentException("Expected media message, got: " + info.message().category()); + } + + return downloadMedia(mediaMessage); + } + + /** + * Downloads a media from Whatsapp's servers. + * If the media was already downloaded, the cached version will be returned. + * If the download fails because the media is too old/invalid, an empty optional will be returned. + * + * @param mediaMessage the non-null media + * @return a CompletableFuture + */ + public CompletableFuture> downloadMedia(MediaMessage mediaMessage) { + if (!(mediaMessage instanceof ExtendedMediaMessage extendedMediaMessage)) { + return Medias.downloadAsync(mediaMessage); + } + + var decodedMedia = extendedMediaMessage.decodedMedia(); + if (decodedMedia.isPresent()) { + return CompletableFuture.completedFuture(decodedMedia); + } + + + return Medias.downloadAsync(mediaMessage).thenApply(result -> { + result.ifPresent(extendedMediaMessage::setDecodedMedia); + return result; + }); + } + + /** + * Asks Whatsapp for a media reupload for a specific media + * + * @param info the non-null message info wrapping the media + * @return a CompletableFuture + */ + public CompletableFuture requireMediaReupload(ChatMessageInfo info) { + if (!(info.message().content() instanceof MediaMessage mediaMessage)) { + throw new IllegalArgumentException("Expected media message, got: " + info.message().category()); + } + + var mediaKey = mediaMessage.mediaKey() + .orElseThrow(() -> new NoSuchElementException("Missing media key")); + var retryKey = Hkdf.extractAndExpand(mediaKey, "WhatsApp Media Retry Notification".getBytes(StandardCharsets.UTF_8), 32); + var retryIv = BytesHelper.random(12); + var retryIdData = info.key().id().getBytes(StandardCharsets.UTF_8); + var receipt = ServerErrorReceiptSpec.encode(new ServerErrorReceipt(info.id())); + var ciphertext = AesGcm.encrypt(retryIv, receipt, retryKey, retryIdData); + var rmrAttributes = Attributes.of() + .put("jid", info.chatJid()) + .put("from_me", String.valueOf(info.fromMe())) + .put("participant", info.senderJid(), () -> !Objects.equals(info.chatJid(), info.senderJid())) + .toMap(); + var node = Node.of("receipt", Map.of("id", info.key().id(), "to", jidOrThrowError() + .withoutDevice(), "type", "server-error"), Node.of("encrypt", Node.of("enc_p", ciphertext), Node.of("enc_iv", retryIv)), Node.of("rmr", rmrAttributes)); + return socketHandler.send(node, result -> result.hasDescription("notification")) + .thenAcceptAsync(result -> parseMediaReupload(info, mediaMessage, retryKey, retryIdData, result)); + } + + private void parseMediaReupload(ChatMessageInfo info, MediaMessage mediaMessage, byte[] retryKey, byte[] retryIdData, Node node) { + Validate.isTrue(!node.hasNode("error"), "Erroneous response from media reupload: %s", node.attributes() + .getInt("code")); + var encryptNode = node.findNode("encrypt") + .orElseThrow(() -> new NoSuchElementException("Missing encrypt node in media reupload")); + var mediaPayload = encryptNode.findNode("enc_p") + .flatMap(Node::contentAsBytes) + .orElseThrow(() -> new NoSuchElementException("Missing encrypted payload node in media reupload")); + var mediaIv = encryptNode.findNode("enc_iv") + .flatMap(Node::contentAsBytes) + .orElseThrow(() -> new NoSuchElementException("Missing encrypted iv node in media reupload")); + var mediaRetryNotificationData = AesGcm.decrypt(mediaIv, mediaPayload, retryKey, retryIdData); + var mediaRetryNotification = MediaRetryNotificationSpec.decode(mediaRetryNotificationData); + var directPath = mediaRetryNotification.directPath() + .orElseThrow(() -> new RuntimeException("Media reupload failed")); + mediaMessage.setMediaUrl(Medias.createMediaUrl(directPath)); + mediaMessage.setMediaDirectPath(directPath); + } + + /** + * Sends a custom node to Whatsapp + * + * @param node the non-null node to send + * @return the newsletters from Whatsapp + */ + public CompletableFuture sendNode(Node node) { + return socketHandler.send(node); + } + + /** + * Creates a new community + * + * @param subject the non-null name of the new community + * @param body the nullable description of the new community + * @return a CompletableFuture + */ + public CompletableFuture> createCommunity(String subject, String body) { + var descriptionId = HexFormat.of().formatHex(BytesHelper.random(12)); + var entry = Node.of("create", Map.of("subject", subject), + Node.of("description", Map.of("id", descriptionId), + Node.of("body", Objects.requireNonNullElse(body, "").getBytes(StandardCharsets.UTF_8))), + Node.of("parent", Map.of("default_membership_approval_mode", "request_required")), + Node.of("allow_non_admin_sub_group_creation")); + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", entry) + .thenApplyAsync(node -> node.findNode("group").map(socketHandler::parseGroupMetadata)); + } + + /** + * Changes a community setting + * + * @param community the non-null community affected by this change + * @param setting the non-null setting + * @param policy the non-null policy + * @return a future + */ + public CompletableFuture changeCommunitySetting(JidProvider community, CommunitySetting setting, ChatSettingPolicy policy) { + Validate.isTrue(community.toJid().hasServer(JidServer.GROUP), "This method only accepts communities"); + var body = switch (setting) { + case MODIFY_GROUPS -> + Node.of(policy == ChatSettingPolicy.ANYONE ? "allow_non_admin_sub_group_creation" : "not_allow_non_admin_sub_group_creation"); + }; + return socketHandler.sendQuery(JidServer.GROUP.toJid(), "set", "w:g2", body) + .thenRun(() -> {}); + } + + + /** + * Unlinks all the companions of this device + * + * @return a future + */ + public CompletableFuture unlinkDevices() { + return socketHandler.sendQuery("set", "md", Node.of("remove-companion-device", Map.of("all", true, "reason", "user_initiated"))) + .thenRun(() -> store().removeLinkedCompanions()) + .thenApply(ignored -> this); + } + + /** + * Unlinks a specific companion + * + * @param companion the non-null companion to unlink + * @return a future + */ + public CompletableFuture unlinkDevice(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)) + .thenApply(ignored -> this); + } + + /** + * Links a companion to this device + * + * @param qrCode the non-null qr code as an image + * @return a future + */ + public CompletableFuture linkDevice(byte[] qrCode) { + try { + var inputStream = new ByteArrayInputStream(qrCode); + var luminanceSource = new BufferedImageLuminanceSource(ImageIO.read(inputStream)); + var hybridBinarizer = new HybridBinarizer(luminanceSource); + var binaryBitmap = new BinaryBitmap(hybridBinarizer); + var reader = new QRCodeReader(); + var result = reader.decode(binaryBitmap); + return linkDevice(result.getText()); + } catch (IOException | NotFoundException | ChecksumException | FormatException exception) { + throw new IllegalArgumentException("Cannot read qr code", exception); + } + } + + /** + * Links a companion to this device + * Mobile api only + * + * @param qrCodeData the non-null qr code as a String + * @return a future + */ + public CompletableFuture linkDevice(String qrCodeData) { + Validate.isTrue(store().clientType() == ClientType.MOBILE, "Device linking is only available for the mobile api"); + var maxDevices = getMaxLinkedDevices(); + if (store().linkedDevices().size() > maxDevices) { + return CompletableFuture.completedFuture(CompanionLinkResult.MAX_DEVICES_ERROR); + } + + var qrCodeParts = qrCodeData.split(","); + Validate.isTrue(qrCodeParts.length >= 4, "Expected qr code to be made up of at least four parts"); + var ref = qrCodeParts[0]; + var publicKey = Base64.getDecoder().decode(qrCodeParts[1]); + var advIdentity = Base64.getDecoder().decode(qrCodeParts[2]); + var identityKey = Base64.getDecoder().decode(qrCodeParts[3]); + return socketHandler.sendQuery("set", "w:sync:app:state", Node.of("delete_all_data")) + .thenComposeAsync(ignored -> linkDevice(advIdentity, identityKey, ref, publicKey)); + } + + private CompletableFuture linkDevice(byte[] advIdentity, byte[] identityKey, String ref, byte[] publicKey) { + var deviceIdentity = new DeviceIdentityBuilder() + .rawId(KeyHelper.agent()) + .keyIndex(store().linkedDevices().size() + 1) + .timestamp(Clock.nowSeconds()) + .build(); + var deviceIdentityBytes = DeviceIdentitySpec.encode(deviceIdentity); + var accountSignatureMessage = BytesHelper.concat( + Specification.Whatsapp.ACCOUNT_SIGNATURE_HEADER, + deviceIdentityBytes, + advIdentity + ); + var accountSignature = Curve25519.sign(keys().identityKeyPair().privateKey(), accountSignatureMessage, true); + var signedDeviceIdentity = new SignedDeviceIdentityBuilder() + .accountSignature(accountSignature) + .accountSignatureKey(keys().identityKeyPair().publicKey()) + .details(deviceIdentityBytes) + .build(); + var signedDeviceIdentityBytes = SignedDeviceIdentitySpec.encode(signedDeviceIdentity); + var deviceIdentityHmac = new SignedDeviceIdentityHMACBuilder() + .hmac(Hmac.calculateSha256(signedDeviceIdentityBytes, identityKey)) + .details(signedDeviceIdentityBytes) + .build(); + var knownDevices = store().linkedDevices() + .stream() + .map(Jid::device) + .toList(); + var keyIndexList = new KeyIndexListBuilder() + .rawId(deviceIdentity.rawId()) + .timestamp(deviceIdentity.timestamp()) + .validIndexes(knownDevices) + .build(); + var keyIndexListBytes = KeyIndexListSpec.encode(keyIndexList); + var deviceSignatureMessage = BytesHelper.concat(Specification.Whatsapp.DEVICE_MOBILE_SIGNATURE_HEADER, keyIndexListBytes); + var keyAccountSignature = Curve25519.sign(keys().identityKeyPair().privateKey(), deviceSignatureMessage, true); + var signedKeyIndexList = new SignedKeyIndexListBuilder() + .accountSignature(keyAccountSignature) + .details(keyIndexListBytes) + .build(); + return socketHandler.sendQuery("set", "md", Node.of("pair-device", + Node.of("ref", ref), + Node.of("pub-key", publicKey), + Node.of("device-identity", SignedDeviceIdentityHMACSpec.encode(deviceIdentityHmac)), + Node.of("key-index-list", Map.of("ts", deviceIdentity.timestamp()), SignedKeyIndexListSpec.encode(signedKeyIndexList)))) + .thenComposeAsync(result -> handleCompanionPairing(result, deviceIdentity.keyIndex())); + } + + private int getMaxLinkedDevices() { + var maxDevices = socketHandler.store().properties().get("linked_device_max_count"); + if (maxDevices == null) { + return Specification.Whatsapp.MAX_COMPANIONS; + } + + try { + return Integer.parseInt(maxDevices); + } catch (NumberFormatException exception) { + return Specification.Whatsapp.MAX_COMPANIONS; + } + } + + private CompletableFuture handleCompanionPairing(Node result, int keyId) { + if (result.attributes().hasValue("type", "error")) { + var error = result.findNode("error") + .filter(entry -> entry.attributes().hasValue("text", "resource-limit")) + .map(entry -> CompanionLinkResult.MAX_DEVICES_ERROR) + .orElse(CompanionLinkResult.RETRY_ERROR); + return CompletableFuture.completedFuture(error); + } + + var device = result.findNode("device") + .flatMap(entry -> entry.attributes().getOptionalJid("jid")) + .orElse(null); + if (device == null) { + return CompletableFuture.completedFuture(CompanionLinkResult.RETRY_ERROR); + } + + return awaitCompanionRegistration(device) + .thenComposeAsync(ignored -> socketHandler.sendQuery("get", "encrypt", Node.of("key", Node.of("user", Map.of("jid", device))))) + .thenComposeAsync(encryptResult -> handleCompanionEncrypt(encryptResult, device, keyId)); + } + + private CompletableFuture awaitCompanionRegistration(Jid device) { + var future = new CompletableFuture(); + OnLinkedDevices listener = data -> { + if (data.contains(device)) { + future.complete(null); + } + }; + addLinkedDevicesListener(listener); + return future.orTimeout(Specification.Whatsapp.COMPANION_PAIRING_TIMEOUT, TimeUnit.SECONDS) + .exceptionally(ignored -> null) + .thenRun(() -> removeListener(listener)); + } + + private CompletableFuture handleCompanionEncrypt(Node result, Jid companion, int keyId) { + store().addLinkedDevice(companion, keyId); + socketHandler.parseSessions(result); + return sendInitialSecurityMessage(companion) + .thenComposeAsync(ignore -> sendAppStateKeysMessage(companion)) + .thenComposeAsync(ignore -> sendInitialNullMessage(companion)) + .thenComposeAsync(ignore -> sendInitialStatusMessage(companion)) + .thenComposeAsync(ignore -> sendPushNamesMessage(companion)) + .thenComposeAsync(ignore -> sendInitialBootstrapMessage(companion)) + .thenComposeAsync(ignore -> sendRecentMessage(companion)) + .thenComposeAsync(ignored -> syncCompanionState(companion)) + .thenApplyAsync(ignored -> CompanionLinkResult.SUCCESS); + } + + private CompletableFuture syncCompanionState(Jid companion) { + var criticalUnblockLowRequest = createCriticalUnblockLowRequest(); + var criticalBlockRequest = createCriticalBlockRequest(); + return socketHandler.pushPatches(companion, List.of(criticalBlockRequest, criticalUnblockLowRequest)).thenComposeAsync(ignored -> { + var regularLowRequests = createRegularLowRequests(); + var regularRequests = createRegularRequests(); + return socketHandler.pushPatches(companion, List.of(regularLowRequests, regularRequests)); + }); + } + + private PatchRequest createRegularRequests() { + return new PatchRequest(PatchType.REGULAR, List.of()); + } + + private PatchRequest createRegularLowRequests() { + var timeFormatEntry = createTimeFormatEntry(); + var primaryVersion = new PrimaryVersionAction(store().version().toString()); + var sessionVersionEntry = createPrimaryVersionEntry(primaryVersion, "session@s.whatsapp.net"); + var keepVersionEntry = createPrimaryVersionEntry(primaryVersion, "current@s.whatsapp.net"); + var nuxEntry = createNuxRequest(); + var androidEntry = createAndroidEntry(); + var entries = Stream.of(timeFormatEntry, sessionVersionEntry, keepVersionEntry, nuxEntry, androidEntry) + .filter(Objects::nonNull) + .toList(); + // TODO: Archive chat actions, StickerAction + return new PatchRequest(PatchType.REGULAR_LOW, entries); + } + + // FIXME: Settings can't be serialized + private PatchRequest createCriticalBlockRequest() { + var localeEntry = createLocaleEntry(); + var pushNameEntry = createPushNameEntry(); + return new PatchRequest(PatchType.CRITICAL_BLOCK, List.of(localeEntry, pushNameEntry)); + } + + private PatchRequest createCriticalUnblockLowRequest() { + var criticalUnblockLow = createContactEntries(); + return new PatchRequest(PatchType.CRITICAL_UNBLOCK_LOW, criticalUnblockLow); + } + + private List createContactEntries() { + return store().contacts() + .stream() + .filter(entry -> entry.shortName().isPresent() || entry.fullName().isPresent()) + .map(this::createContactRequestEntry) + .collect(Collectors.toList()); + } + + private PatchEntry createPushNameEntry() { + var pushNameSetting = new PushNameSettings(store().name()); + return PatchEntry.of(ActionValueSync.of(pushNameSetting), Operation.SET); + } + + private PatchEntry createLocaleEntry() { + var localeSetting = new LocaleSettings(store().locale().toString()); + return PatchEntry.of(ActionValueSync.of(localeSetting), Operation.SET); + } + + private PatchEntry createAndroidEntry() { + if (!store().device().platform().isAndroid()) { + return null; + } + + var action = new AndroidUnsupportedActions(true); + return PatchEntry.of(ActionValueSync.of(action), Operation.SET); + } + + private PatchEntry createNuxRequest() { + var nuxAction = new NuxAction(true); + var timeFormatSync = ActionValueSync.of(nuxAction); + return PatchEntry.of(timeFormatSync, Operation.SET, "keep@s.whatsapp.net"); + } + + private PatchEntry createPrimaryVersionEntry(PrimaryVersionAction primaryVersion, String to) { + var timeFormatSync = ActionValueSync.of(primaryVersion); + return PatchEntry.of(timeFormatSync, Operation.SET, to); + } + + private PatchEntry createTimeFormatEntry() { + var timeFormatAction = new TimeFormatAction(store().twentyFourHourFormat()); + var timeFormatSync = ActionValueSync.of(timeFormatAction); + return PatchEntry.of(timeFormatSync, Operation.SET); + } + + private PatchEntry createContactRequestEntry(Contact contact) { + var action = new ContactAction(null, contact.shortName(), contact.fullName()); + var sync = ActionValueSync.of(action); + return PatchEntry.of(sync, Operation.SET, contact.jid().toString()); + } + + private CompletableFuture sendRecentMessage(Jid jid) { + var pushNames = new HistorySyncBuilder() + .conversations(List.of()) + .syncType(HistorySync.Type.RECENT) + .build(); + return sendHistoryProtocolMessage(jid, pushNames, HistorySync.Type.PUSH_NAME); + } + + private CompletableFuture sendPushNamesMessage(Jid jid) { + var pushNamesData = store() + .contacts() + .stream() + .filter(entry -> entry.chosenName().isPresent()) + .map(entry -> new PushName(entry.jid().toString(), entry.chosenName())) + .toList(); + var pushNames = new HistorySyncBuilder() + .pushNames(pushNamesData) + .syncType(HistorySync.Type.PUSH_NAME) + .build(); + return sendHistoryProtocolMessage(jid, pushNames, HistorySync.Type.PUSH_NAME); + } + + private CompletableFuture sendInitialStatusMessage(Jid jid) { + var initialStatus = new HistorySyncBuilder() + .statusV3Messages(new ArrayList<>(store().status())) + .syncType(HistorySync.Type.INITIAL_STATUS_V3) + .build(); + return sendHistoryProtocolMessage(jid, initialStatus, HistorySync.Type.INITIAL_STATUS_V3); + } + + private CompletableFuture sendInitialBootstrapMessage(Jid jid) { + var chats = store().chats() + .stream() + .toList(); + var initialBootstrap = new HistorySyncBuilder() + .conversations(chats) + .syncType(HistorySync.Type.INITIAL_BOOTSTRAP) + .build(); + return sendHistoryProtocolMessage(jid, initialBootstrap, HistorySync.Type.INITIAL_BOOTSTRAP); + } + + private CompletableFuture sendInitialNullMessage(Jid jid) { + var pastParticipants = store().chats() + .stream() + .map(this::getPastParticipants) + .filter(Objects::nonNull) + .toList(); + var initialBootstrap = new HistorySyncBuilder() + .syncType(HistorySync.Type.NON_BLOCKING_DATA) + .pastParticipants(pastParticipants) + .build(); + return sendHistoryProtocolMessage(jid, initialBootstrap, null); + } + + private GroupPastParticipants getPastParticipants(Chat chat) { + if (chat.pastParticipants().isEmpty()) { + return null; + } + + return new GroupPastParticipantsBuilder() + .groupJid(chat.jid()) + .pastParticipants(new ArrayList<>(chat.pastParticipants())) + .build(); + } + + private CompletableFuture sendAppStateKeysMessage(Jid companion) { + var preKeys = IntStream.range(0, 10) + .mapToObj(index -> createAppKey(companion, index)) + .toList(); + keys().addAppKeys(companion, preKeys); + var appStateSyncKeyShare = new AppStateSyncKeyShareBuilder() + .keys(preKeys) + .build(); + var result = new ProtocolMessageBuilder() + .protocolType(ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE) + .appStateSyncKeyShare(appStateSyncKeyShare) + .build(); + return socketHandler.sendPeerMessage(companion, result); + } + + private AppStateSyncKey createAppKey(Jid jid, int index) { + return new AppStateSyncKeyBuilder() + .keyId(new AppStateSyncKeyId(KeyHelper.appKeyId())) + .keyData(createAppKeyData(jid, index)) + .build(); + } + + private AppStateSyncKeyData createAppKeyData(Jid jid, int index) { + return new AppStateSyncKeyDataBuilder() + .keyData(SignalKeyPair.random().publicKey()) + .fingerprint(createAppKeyFingerprint(jid, index)) + .timestamp(Clock.nowMilliseconds()) + .build(); + } + + private AppStateSyncKeyFingerprint createAppKeyFingerprint(Jid jid, int index) { + return new AppStateSyncKeyFingerprintBuilder() + .rawId(KeyHelper.senderKeyId()) + .currentIndex(index) + .deviceIndexes(new ArrayList<>(store().linkedDevicesKeys().values())) + .build(); + } + + private CompletableFuture sendInitialSecurityMessage(Jid jid) { + var protocolMessage = new ProtocolMessageBuilder() + .protocolType(ProtocolMessage.Type.INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC) + .initialSecurityNotificationSettingSync(new InitialSecurityNotificationSettingSync(true)) + .build(); + return socketHandler.sendPeerMessage(jid, protocolMessage); + } + + 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)) + .thenComposeAsync(result -> socketHandler.sendPeerMessage(jid, result)); + } + + private ProtocolMessage createHistoryProtocolMessage(MediaFile upload, HistorySync.Type type) { + var notification = new HistorySyncNotificationBuilder() + .mediaSha256(upload.fileSha256()) + .mediaEncryptedSha256(upload.fileEncSha256()) + .mediaKey(upload.mediaKey()) + .mediaDirectPath(upload.directPath()) + .mediaSize(upload.fileLength()) + .syncType(type) + .build(); + return new ProtocolMessageBuilder() + .protocolType(ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION) + .historySyncNotification(notification) + .build(); + } + + /** + * Gets the verified name certificate + * + * @return a future + */ + public CompletableFuture> queryBusinessCertificate(JidProvider provider) { + return socketHandler.sendQuery("get", "w:biz", Node.of("verified_name", Map.of("jid", provider.toJid()))) + .thenApplyAsync(this::parseCertificate); + } + + private Optional parseCertificate(Node result) { + return result.findNode("verified_name") + .flatMap(Node::contentAsBytes) + .map(BusinessVerifiedNameCertificateSpec::decode); + } + + /** + * Enables two-factor authentication + * Mobile API only + * + * @param code the six digits non-null numeric code + * @return a future + */ + public CompletableFuture enable2fa(String code) { + return set2fa(code, null); + } + + /** + * Enables two-factor authentication + * Mobile API only + * + * @param code the six digits non-null numeric code + * @param email the nullable recovery email + * @return a future + */ + public CompletableFuture enable2fa(String code, String email) { + return set2fa(code, email); + } + + /** + * Disables two-factor authentication + * Mobile API only + * + * @return a future + */ + public CompletableFuture disable2fa() { + return set2fa(null, null); + } + + private CompletableFuture set2fa(String code, String email) { + Validate.isTrue(store().clientType() == ClientType.MOBILE, "2FA is only available for the mobile api"); + Validate.isTrue(code == null || (code.matches("^[0-9]*$") && code.length() == 6), + "Invalid 2fa code: expected a numeric six digits string"); + Validate.isTrue(email == null || isValidEmail(email), + "Invalid email: %s", email); + var body = new ArrayList(); + body.add(Node.of("code", Objects.requireNonNullElse(code, "").getBytes(StandardCharsets.UTF_8))); + if (code != null && email != null) { + body.add(Node.of("email", email.getBytes(StandardCharsets.UTF_8))); + } + return socketHandler.sendQuery("set", "urn:xmpp:whatsapp:account", Node.of("2fa", body)) + .thenApplyAsync(result -> !result.hasNode("error")); + } + + /** + * Starts a call with a contact + * Mobile API only + * + * @param contact the non-null contact + * @return a future + */ + public CompletableFuture startCall(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(JidProvider provider) { + var callId = ChatMessageKey.randomId(); + var audioStream = Node.of("audio", Map.of("rate", 8000, "enc", "opus")); + var audioStreamTwo = Node.of("audio", Map.of("rate", 16000, "enc", "opus")); + var net = Node.of("net", Map.of("medium", 3)); + var encopt = Node.of("encopt", Map.of("keygen", 2)); + var enc = createCallNode(provider); + var capability = Node.of("capability", Map.of("ver", 1), HexFormat.of().parseHex("0104ff09c4fa")); + var callCreator = "%s:0@s.whatsapp.net".formatted(jidOrThrowError().user()); + var offer = Node.of("offer", + Map.of("call-creator", callCreator, "call-id", callId), + audioStream, audioStreamTwo, net, capability, encopt, enc); + return socketHandler.send(Node.of("call", Map.of("to", provider.toJid()), offer)) + .thenApply(result -> onCallSent(provider, callId, result)); + } + + private Call onCallSent(JidProvider provider, String callId, Node result) { + var call = new Call(provider.toJid(), jidOrThrowError(), callId, Clock.nowSeconds(), false, CallStatus.RINGING, false); + store().addCall(call); + socketHandler.onCall(call); + return call; + } + + private Node createCallNode(JidProvider provider) { + var call = new CallMessageBuilder() + .key(SignalKeyPair.random().publicKey()) + .build(); + var message = MessageContainer.of(call); + var cipher = new SessionCipher(provider.toJid().toSignalAddress(), keys()); + var encodedMessage = BytesHelper.messageToBytes(message); + var cipheredMessage = cipher.encrypt(encodedMessage); + return Node.of("enc", Map.of("v", 2, "type", cipheredMessage.type()), cipheredMessage.message()); + } + + + /** + * Rejects an incoming call or stops an active call + * Mobile API only + * + * @param callId the non-null id of the call to reject + * @return a future + */ + public CompletableFuture stopCall(String callId) { + Validate.isTrue(store().clientType() == ClientType.MOBILE, "Calling is only available for the mobile api"); + return store().findCallById(callId) + .map(this::stopCall) + .orElseGet(() -> CompletableFuture.completedFuture(false)); + } + + /** + * Rejects an incoming call or stops an active call + * Mobile API only + * + * @param call the non-null call to reject + * @return a future + */ + public CompletableFuture stopCall(Call call) { + Validate.isTrue(store().clientType() == ClientType.MOBILE, "Calling is only available for the mobile api"); + var callCreator = "%s.%s:%s@s.whatsapp.net".formatted(call.caller().user(), call.caller().device(), call.caller().device()); + if (Objects.equals(call.caller().user(), jidOrThrowError().user())) { + var rejectNode = Node.of("terminate", Map.of("reason", "timeout", "call-id", call.id(), "call-creator", callCreator)); + var body = Node.of("call", Map.of("to", call.chat()), rejectNode); + return socketHandler.send(body) + .thenApplyAsync(result -> !result.hasNode("error")); + } + + var rejectNode = Node.of("reject", Map.of("call-id", call.id(), "call-creator", callCreator, "count", 0)); + var body = Node.of("call", Map.of("from", socketHandler.store().jid(), "to", call.caller()), rejectNode); + return socketHandler.send(body) + .thenApplyAsync(result -> !result.hasNode("error")); + } + + + /** + * Queries a list of fifty recommended newsletters by country + * + * @param countryCode the non-null country code + * @return a list of recommended newsletters, if the feature is available + */ + public CompletableFuture> queryRecommendedNewsletters(String countryCode) { + return queryRecommendedNewsletters(countryCode, 50); + } + + + /** + * Queries a list of recommended newsletters by country + * + * @param countryCode the non-null country code + * @param limit how many entries should be returned + * @return a list of recommended newsletters, if the feature is available + */ + public CompletableFuture> queryRecommendedNewsletters(String countryCode, int limit) { + var filters = new RecommendedNewslettersRequest.Filters(List.of(countryCode)); + var input = new RecommendedNewslettersRequest.Input("RECOMMENDED", filters, limit); + var variable = new RecommendedNewslettersRequest.Variable(input); + var query = new RecommendedNewslettersRequest(variable); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6190824427689257"), Json.writeValueAsBytes(query))) + .thenApplyAsync(this::parseRecommendedNewsletters); + } + + private Optional parseRecommendedNewsletters(Node response) { + return response.findNode("result") + .flatMap(Node::contentAsString) + .flatMap(RecommendedNewslettersResponse::ofJson); + } + + /** + * Queries any number of messages from a newsletter + * + * @param newsletterJid the non-null jid of the newsletter + * @param count how many messages should be queried + * @return a future + */ + public CompletableFuture queryNewsletterMessages(JidProvider newsletterJid, int count) { + return socketHandler.queryNewsletterMessages(newsletterJid, count); + } + + /** + * Subscribes to a public newsletter's event stream of reactions + * + * @param channel the non-null channel + * @return the time, in minutes, during which updates will be sent + */ + public CompletableFuture subscribeToNewsletterReactions(JidProvider channel) { + return socketHandler.subscribeToNewsletterReactions(channel); + } + + /** + * Creates a newsletter + * + * @param name the non-null name of the newsletter + * @return a future + */ + public CompletableFuture> createNewsletter(String name) { + return createNewsletter(name, null, null); + } + + /** + * Creates newsletter channel + * + * @param name the non-null name of the newsletter + * @param description the nullable description of the newsletter + * @return a future + */ + public CompletableFuture> createNewsletter(String name, String description) { + return createNewsletter(name, description, null); + } + + /** + * Creates a newsletter + * + * @param name the non-null name of the newsletter + * @param description the nullable description of the newsletter + * @param picture the nullable profile picture of the newsletter + * @return a future + */ + public CompletableFuture> createNewsletter(String name, String description, byte[] picture) { + var input = new CreateNewsletterRequest.NewsletterInput(name, description, picture != null ? Base64.getEncoder().encodeToString(picture) : null); + var variable = new CreateNewsletterRequest.Variable(input); + var request = new CreateNewsletterRequest(variable); + return socketHandler.sendQuery("set", "tos", Node.of("notice", Map.of("stage", 5, "id", ChatMessageKey.randomId()))) + .thenComposeAsync(ignored -> socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6234210096708695"), Json.writeValueAsBytes(request)))) + .thenApplyAsync(this::parseNewsletterCreation) + .thenComposeAsync(this::onNewsletterCreation); + } + + private Optional parseNewsletterCreation(Node response) { + return response.findNode("result") + .flatMap(Node::contentAsString) + .flatMap(NewsletterResponse::ofJson) + .map(NewsletterResponse::newsletter); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private CompletableFuture> onNewsletterCreation(Optional result) { + if (result.isEmpty()) { + return CompletableFuture.completedFuture(result); + } + + return subscribeToNewsletterReactions(result.get().jid()) + .thenApply(ignored -> result); + } + + /** + * Changes the description of a newsletter + * + * @param newsletter the non-null target newsletter + * @param description the nullable new description + * @return a future + */ + public CompletableFuture changeNewsletterDescription(JidProvider newsletter, String description) { + var safeDescription = Objects.requireNonNullElse(description, ""); + var payload = new UpdatePayload(safeDescription); + var body = new UpdateNewsletterRequest.Variable(newsletter.toJid(), payload); + var request = new UpdateNewsletterRequest(body); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7150902998257522"), Json.writeValueAsBytes(request))) + .thenRun(() -> {}); + } + + /** + * Joins a newsletter + * + * @param newsletter a non-null newsletter + * @return a future + */ + public CompletableFuture joinNewsletter(JidProvider newsletter) { + var body = new JoinNewsletterRequest.Variable(newsletter.toJid()); + var request = new JoinNewsletterRequest(body); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "9926858900719341"), Json.writeValueAsBytes(request))) + .thenRun(() -> {}); + } + + /** + * Leaves a newsletter + * + * @param newsletter a non-null newsletter + * @return a future + */ + public CompletableFuture leaveNewsletter(JidProvider newsletter) { + var body = new LeaveNewsletterRequest.Variable(newsletter.toJid()); + var request = new LeaveNewsletterRequest(body); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6392786840836363"), Json.writeValueAsBytes(request))) + .thenRun(() -> {}); + } + + /** + * Queries the number of people subscribed to a newsletter + * + * @param newsletterJid the id of the newsletter + * @return a CompletableFuture + */ + public CompletableFuture queryNewsletterSubscribers(JidProvider newsletterJid) { + var newsletterRole = store().findNewsletterByJid(newsletterJid) + .flatMap(Newsletter::viewerMetadata) + .map(NewsletterViewerMetadata::role) + .orElse(NewsletterViewerRole.GUEST); + var input = new NewsletterSubscribersRequest.Input(newsletterJid.toJid(), "JID", newsletterRole.name()); + var body = new NewsletterSubscribersRequest.Variable(input); + var request = new NewsletterSubscribersRequest(body); + return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7272540469429201"), Json.writeValueAsBytes(request))) + .thenApply(this::parseNewsletterSubscribers); + } + + private long parseNewsletterSubscribers(Node response) { + return response.findNode("result") + .flatMap(Node::contentAsString) + .flatMap(NewsletterSubscribersResponse::ofJson) + .map(NewsletterSubscribersResponse::subscribersCount) + .orElse(0L); + } + + /** + * Registers a listener + * + * @param listener the listener to register + * @return the same instance + */ + public Whatsapp addListener(Listener listener) { + store().addListener(listener); + return this; + } + + /** + * Unregisters a listener + * + * @param listener the listener to unregister + * @return the same instance + */ + public Whatsapp removeListener(Listener listener) { + store().removeListener(listener); + return this; + } + + /** + * Registers an action listener + * + * @param onAction the listener to register + * @return the same instance + */ + public Whatsapp addActionListener(OnAction onAction) { + return addListener(onAction); + } + + /** + * Registers a chat recent messages listener + * + * @param onChatRecentMessages the listener to register + * @return the same instance + */ + public Whatsapp addChatMessagesSyncListener(OnChatMessagesSync onChatRecentMessages) { + return addListener(onChatRecentMessages); + } + + /** + * Registers a chats listener + * + * @param onChats the listener to register + * @return the same instance + */ + public Whatsapp addChatsListener(OnChats onChats) { + return addListener(onChats); + } + + /** + * Registers a chats listener + * + * @param onChats the listener to register + * @return the same instance + */ + public Whatsapp addChatsListener(OnWhatsappChats onChats) { + return addListener(onChats); + } + + /** + * Registers a newsletters listener + * + * @param onNewsletters the listener to register + * @return the same instance + */ + public Whatsapp addNewslettersListener(OnNewsletters onNewsletters) { + return addListener(onNewsletters); + } + + /** + * Registers a newsletters listener + * + * @param onNewsletters the listener to register + * @return the same instance + */ + public Whatsapp addNewslettersListener(OnWhatsappNewsletters onNewsletters) { + return addListener(onNewsletters); + } + + + /** + * Registers a contact presence listener + * + * @param onContactPresence the listener to register + * @return the same instance + */ + public Whatsapp addContactPresenceListener(OnContactPresence onContactPresence) { + return addListener(onContactPresence); + } + + /** + * Registers a contacts listener + * + * @param onContacts the listener to register + * @return the same instance + */ + public Whatsapp addContactsListener(OnContacts onContacts) { + return addListener(onContacts); + } + + /** + * Registers a message status listener + * + * @param onAnyMessageStatus the listener to register + * @return the same instance + */ + public Whatsapp addMessageStatusListener(OnMessageStatus onAnyMessageStatus) { + return addListener(onAnyMessageStatus); + } + + /** + * Registers a disconnected listener + * + * @param onDisconnected the listener to register + * @return the same instance + */ + public Whatsapp addDisconnectedListener(OnDisconnected onDisconnected) { + return addListener(onDisconnected); + } + + /** + * Registers a features listener + * + * @param onFeatures the listener to register + * @return the same instance + */ + public Whatsapp addFeaturesListener(OnFeatures onFeatures) { + return addListener(onFeatures); + } + + /** + * Registers a logged in listener + * + * @param onLoggedIn the listener to register + * @return the same instance + */ + public Whatsapp addLoggedInListener(OnLoggedIn onLoggedIn) { + return addListener(onLoggedIn); + } + + /** + * Registers a message deleted listener + * + * @param onMessageDeleted the listener to register + * @return the same instance + */ + public Whatsapp addMessageDeletedListener(OnMessageDeleted onMessageDeleted) { + return addListener(onMessageDeleted); + } + + /** + * Registers a metadata listener + * + * @param onMetadata the listener to register + * @return the same instance + */ + public Whatsapp addMetadataListener(OnMetadata onMetadata) { + return addListener(onMetadata); + } + + /** + * Registers a new contact listener + * + * @param onNewContact the listener to register + * @return the same instance + */ + public Whatsapp addNewContactListener(OnNewContact onNewContact) { + return addListener(onNewContact); + } + + /** + * Registers a new message listener + * + * @param onNewMessage the listener to register + * @return the same instance + */ + public Whatsapp addNewChatMessageListener(OnNewMessage onNewMessage) { + return addListener(onNewMessage); + } + + /** + * Registers a new status listener + * + * @param onNewMediaStatus the listener to register + * @return the same instance + */ + public Whatsapp addNewStatusListener(OnNewStatus onNewMediaStatus) { + return addListener(onNewMediaStatus); + } + + /** + * Registers a received node listener + * + * @param onNodeReceived the listener to register + * @return the same instance + */ + public Whatsapp addNodeReceivedListener(OnNodeReceived onNodeReceived) { + return addListener(onNodeReceived); + } + + /** + * Registers a sent node listener + * + * @param onNodeSent the listener to register + * @return the same instance + */ + public Whatsapp addNodeSentListener(OnNodeSent onNodeSent) { + return addListener(onNodeSent); + } + + /** + * Registers a setting listener + * + * @param onSetting the listener to register + * @return the same instance + */ + public Whatsapp addSettingListener(OnSetting onSetting) { + return addListener(onSetting); + } + + /** + * Registers a status listener + * + * @param onMediaStatus the listener to register + * @return the same instance + */ + public Whatsapp addMediaStatusListener(OnStatus onMediaStatus) { + return addListener(onMediaStatus); + } + + /** + * Registers an event listener + * + * @param onSocketEvent the listener to register + * @return the same instance + */ + public Whatsapp addSocketEventListener(OnSocketEvent onSocketEvent) { + return addListener(onSocketEvent); + } + + /** + * Registers an action listener + * + * @param onAction the listener to register + * @return the same instance + */ + public Whatsapp addActionListener(OnWhatsappAction onAction) { + return addListener(onAction); + } + + /** + * Registers a sync progress listener + * + * @param onSyncProgress the listener to register + * @return the same instance + */ + public Whatsapp addHistorySyncProgressListener(OnHistorySyncProgress onSyncProgress) { + return addListener(onSyncProgress); + } + + /** + * Registers a chat recent messages listener + * + * @param onChatRecentMessages the listener to register + * @return the same instance + */ + public Whatsapp addChatMessagesSyncListener(OnWhatsappChatMessagesSync onChatRecentMessages) { + return addListener(onChatRecentMessages); + } + + /** + * Registers a contact presence listener + * + * @param onContactPresence the listener to register + * @return the same instance + */ + public Whatsapp addContactPresenceListener(OnWhatsappContactPresence onContactPresence) { + return addListener(onContactPresence); + } + + /** + * Registers a contacts listener + * + * @param onContacts the listener to register + * @return the same instance + */ + public Whatsapp addContactsListener(OnWhatsappContacts onContacts) { + return addListener(onContacts); + } + + /** + * Registers a message status listener + * + * @param onMessageStatus the listener to register + * @return the same instance + */ + public Whatsapp addMessageStatusListener(OnWhatsappMessageStatus onMessageStatus) { + return addListener(onMessageStatus); + } + + /** + * Registers a disconnected listener + * + * @param onDisconnected the listener to register + * @return the same instance + */ + public Whatsapp addDisconnectedListener(OnWhatsappDisconnected onDisconnected) { + return addListener(onDisconnected); + } + + /** + * Registers a features listener + * + * @param onFeatures the listener to register + * @return the same instance + */ + public Whatsapp addFeaturesListener(OnWhatsappFeatures onFeatures) { + return addListener(onFeatures); + } + + /** + * Registers a logged in listener + * + * @param onLoggedIn the listener to register + * @return the same instance + */ + public Whatsapp addLoggedInListener(OnWhatsappLoggedIn onLoggedIn) { + return addListener(onLoggedIn); + } + + /** + * Registers a message deleted listener + * + * @param onMessageDeleted the listener to register + * @return the same instance + */ + public Whatsapp addMessageDeletedListener(OnWhatsappMessageDeleted onMessageDeleted) { + return addListener(onMessageDeleted); + } + + /** + * Registers a metadata listener + * + * @param onMetadata the listener to register + * @return the same instance + */ + public Whatsapp addMetadataListener(OnWhatsappMetadata onMetadata) { + return addListener(onMetadata); + } + + /** + * Registers a new message listener + * + * @param onNewMessage the listener to register + * @return the same instance + */ + public Whatsapp addNewChatMessageListener(OnWhatsappNewMessage onNewMessage) { + return addListener(onNewMessage); + } + + /** + * Registers a new status listener + * + * @param onNewStatus the listener to register + * @return the same instance + */ + public Whatsapp addNewStatusListener(OnWhatsappNewStatus onNewStatus) { + return addListener(onNewStatus); + } + + /** + * Registers a received node listener + * + * @param onNodeReceived the listener to register + * @return the same instance + */ + public Whatsapp addNodeReceivedListener(OnWhatsappNodeReceived onNodeReceived) { + return addListener(onNodeReceived); + } + + /** + * Registers a sent node listener + * + * @param onNodeSent the listener to register + * @return the same instance + */ + public Whatsapp addNodeSentListener(OnWhatsappNodeSent onNodeSent) { + return addListener(onNodeSent); + } + + /** + * Registers a setting listener + * + * @param onSetting the listener to register + * @return the same instance + */ + public Whatsapp addSettingListener(OnWhatsappSetting onSetting) { + return addListener(onSetting); + } + + /** + * Registers a status listener + * + * @param onStatus the listener to register + * @return the same instance + */ + public Whatsapp addMediaStatusListener(OnWhatsappMediaStatus onStatus) { + return addListener(onStatus); + } + + /** + * Registers an event listener + * + * @param onSocketEvent the listener to register + * @return the same instance + */ + public Whatsapp addSocketEventListener(OnWhatsappSocketEvent onSocketEvent) { + return addListener(onSocketEvent); + } + + /** + * Registers a sync progress listener + * + * @param onSyncProgress the listener to register + * @return the same instance + */ + public Whatsapp addHistorySyncProgressListener(OnWhatsappHistorySyncProgress onSyncProgress) { + return addListener(onSyncProgress); + } + + /** + * Registers a message reply listener + * + * @param onMessageReply the listener to register + * @return the same instance + */ + public Whatsapp addMessageReplyListener(OnWhatsappMessageReply onMessageReply) { + return addListener(onMessageReply); + } + + /** + * Registers a message reply listener for a specific message + * + * @param info the non-null target message + * @param onMessageReply the non-null listener + */ + public Whatsapp addMessageReplyListener(ChatMessageInfo info, OnMessageReply onMessageReply) { + return addMessageReplyListener(info.id(), onMessageReply); + } + + /** + * Registers a message reply listener + * + * @param onMessageReply the listener to register + * @return the same instance + */ + public Whatsapp addMessageReplyListener(OnMessageReply onMessageReply) { + return addListener(onMessageReply); + } + + /** + * Registers a message reply listener for a specific message + * + * @param info the non-null target message + * @param onMessageReply the non-null listener + */ + public Whatsapp addMessageReplyListener(ChatMessageInfo info, OnWhatsappMessageReply onMessageReply) { + return addMessageReplyListener(info.id(), onMessageReply); + } + + /** + * Registers a message reply listener for a specific message + * + * @param id the non-null id of the target message + * @param onMessageReply the non-null listener + */ + public Whatsapp addMessageReplyListener(String id, OnMessageReply onMessageReply) { + return addMessageReplyListener((info, quoted) -> { + if (!info.id().equals(id)) { + return; + } + + onMessageReply.onMessageReply(info, quoted); + }); + } + + /** + * Registers a message reply listener for a specific message + * + * @param id the non-null id of the target message + * @param onMessageReply the non-null listener + */ + public Whatsapp addMessageReplyListener(String id, OnWhatsappMessageReply onMessageReply) { + return addMessageReplyListener(((whatsapp, info, quoted) -> { + if (!info.id().equals(id)) { + return; + } + + onMessageReply.onMessageReply(whatsapp, info, quoted); + })); + } + + /** + * Registers a name change listener + * + * @param onUserNameChanged the non-null listener + */ + public Whatsapp addNameChangedListener(OnUserNameChanged onUserNameChanged) { + return addListener(onUserNameChanged); + } + + /** + * Registers a name change listener + * + * @param onNameChange the non-null listener + */ + public Whatsapp addNameChangedListener(OnWhatsappNameChanged onNameChange) { + return addListener(onNameChange); + } + + /** + * Registers a status change listener + * + * @param onUserAboutChanged the non-null listener + */ + public Whatsapp addAboutChangedListener(OnUserAboutChanged onUserAboutChanged) { + return addListener(onUserAboutChanged); + } + + /** + * Registers a status change listener + * + * @param onUserStatusChange the non-null listener + */ + public Whatsapp addAboutChangedListener(OnWhatsappAboutChanged onUserStatusChange) { + return addListener(onUserStatusChange); + } + + /** + * Registers a picture change listener + * + * @param onProfilePictureChanged the non-null listener + */ + public Whatsapp addUserPictureChangedListener(OnProfilePictureChanged onProfilePictureChanged) { + return addListener(onProfilePictureChanged); + } + + /** + * Registers a picture change listener + * + * @param onUserPictureChange the non-null listener + */ + public Whatsapp addUserPictureChangedListener(OnWhatsappProfilePictureChanged onUserPictureChange) { + return addListener(onUserPictureChange); + } + + /** + * Registers a profile picture listener + * + * @param onContactPictureChanged the non-null listener + */ + public Whatsapp addContactPictureChangedListener(OnContactPictureChanged onContactPictureChanged) { + return addListener(onContactPictureChanged); + } + + /** + * Registers a profile picture listener + * + * @param onProfilePictureChange the non-null listener + */ + public Whatsapp addContactPictureChangedListener(OnWhatsappContactPictureChanged onProfilePictureChange) { + return addListener(onProfilePictureChange); + } + + /** + * Registers a group picture listener + * + * @param onGroupPictureChange the non-null listener + */ + public Whatsapp addGroupPictureChangedListener(OnGroupPictureChange onGroupPictureChange) { + return addListener(onGroupPictureChange); + } + + /** + * Registers a group picture listener + * + * @param onGroupPictureChange the non-null listener + */ + public Whatsapp addGroupPictureChangedListener(OnWhatsappGroupPictureChange onGroupPictureChange) { + return addListener(onGroupPictureChange); + } + + /** + * Registers a contact blocked listener + * + * @param onContactBlocked the non-null listener + */ + public Whatsapp addContactBlockedListener(OnContactBlocked onContactBlocked) { + return addListener(onContactBlocked); + } + + /** + * Registers a contact blocked listener + * + * @param onContactBlocked the non-null listener + */ + public Whatsapp addContactBlockedListener(OnWhatsappContactBlocked onContactBlocked) { + return addListener(onContactBlocked); + } + + /** + * Registers a privacy setting changed listener + * + * @param onPrivacySettingChanged the listener to register + * @return the same instance + */ + public Whatsapp addPrivacySettingChangedListener(OnPrivacySettingChanged onPrivacySettingChanged) { + return addListener(onPrivacySettingChanged); + } + + + /** + * Registers a privacy setting changed listener + * + * @param onWhatsappPrivacySettingChanged the listener to register + * @return the same instance + */ + public Whatsapp addPrivacySettingChangedListener(OnWhatsappPrivacySettingChanged onWhatsappPrivacySettingChanged) { + return addListener(onWhatsappPrivacySettingChanged); + } + + /** + * Registers a companion devices changed listener + * + * @param onLinkedDevices the listener to register + * @return the same instance + */ + public Whatsapp addLinkedDevicesListener(OnLinkedDevices onLinkedDevices) { + return addListener(onLinkedDevices); + } + + /** + * Registers a companion devices changed listener + * + * @param onWhatsappLinkedDevices the listener to register + * @return the same instance + */ + public Whatsapp addLinkedDevicesListener(OnWhatsappLinkedDevices onWhatsappLinkedDevices) { + return addListener(onWhatsappLinkedDevices); + } + + /** + * Registers a registration code listener for the mobile api + * + * @param onRegistrationCode the listener to register + * @return the same instance + */ + public Whatsapp addRegistrationCodeListener(OnRegistrationCode onRegistrationCode) { + return addListener(onRegistrationCode); + } + + /** + * Registers a registration code listener for the mobile api + * + * @param onWhatsappRegistrationCode the listener to register + * @return the same instance + */ + public Whatsapp addRegistrationCodeListener(OnWhatsappRegistrationCode onWhatsappRegistrationCode) { + return addListener(onWhatsappRegistrationCode); + } + + /** + * Registers a call listener + * + * @param onCall the listener to register + * @return the same instance + */ + public Whatsapp addCallListener(OnCall onCall) { + return addListener(onCall); + } + + /** + * Registers a call listener + * + * @param onWhatsappCall the listener to register + * @return the same instance + */ + public Whatsapp addCallListener(OnWhatsappCall onWhatsappCall) { + return addListener(onWhatsappCall); + } + + private Jid jidOrThrowError() { + return store().jid() + .orElseThrow(() -> new IllegalStateException("The session isn't connected")); + } + + public Whatsapp setResponse(RegistrationResponse response) { + this.response = response; + return this; + } + + public RegistrationResponse getResponse() { + return response; + } +} diff --git a/src/main/java/it/auties/whatsapp/api/WhatsappCustomBuilder.java b/src/main/java/it/auties/whatsapp/api/WhatsappCustomBuilder.java new file mode 100644 index 000000000..4183c502e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/api/WhatsappCustomBuilder.java @@ -0,0 +1,68 @@ +package it.auties.whatsapp.api; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.util.Validate; + +import java.util.Objects; +import java.util.concurrent.ExecutorService; + +public class WhatsappCustomBuilder { + private Store store; + private Keys keys; + private ErrorHandler errorHandler; + private WebVerificationHandler webVerificationHandler; + private ExecutorService socketExecutor; + + WhatsappCustomBuilder() { + + } + + public WhatsappCustomBuilder store(Store store) { + this.store = store; + return this; + } + + public WhatsappCustomBuilder keys(Keys keys) { + this.keys = keys; + return this; + } + + public WhatsappCustomBuilder errorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + return this; + } + + public WhatsappCustomBuilder webVerificationSupport(WebVerificationHandler webVerificationHandler) { + this.webVerificationHandler = webVerificationHandler; + return this; + } + + public WhatsappCustomBuilder socketExecutor(ExecutorService socketExecutor) { + this.socketExecutor = socketExecutor; + return this; + } + + public Whatsapp build() { + Validate.isTrue(Objects.equals(store.uuid(), keys.uuid()), "UUID mismatch: %s != %s", store.uuid(), keys.uuid()); + var knownInstance = Whatsapp.getInstanceByUuid(store.uuid()); + if (knownInstance.isPresent()) { + return knownInstance.get(); + } + + var checkedSupport = getWebVerificationMethod(store, keys, webVerificationHandler); + return new Whatsapp(store, keys, errorHandler, checkedSupport, socketExecutor); + } + + private static WebVerificationHandler getWebVerificationMethod(Store store, Keys keys, WebVerificationHandler webVerificationHandler) { + if (store.clientType() != ClientType.WEB) { + return null; + } + + if (!keys.registered() && webVerificationHandler == null) { + return QrHandler.toTerminal(); + } + + return webVerificationHandler; + } +} diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java b/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java new file mode 100644 index 000000000..b96fae2c8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/binary/BinaryDecoder.java @@ -0,0 +1,164 @@ +package it.auties.whatsapp.binary; + +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; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static it.auties.whatsapp.binary.BinaryTag.*; + +public final class BinaryDecoder implements AutoCloseable { + private final DataInputStream dataInputStream; + private boolean closed; + public BinaryDecoder(byte[] buffer) { + var token = buffer[0] & 2; + if (token == 0) { + this.dataInputStream = new DataInputStream(new ByteArrayInputStream(buffer, 1, buffer.length - 1)); + }else { + this.dataInputStream = new DataInputStream(new ByteArrayInputStream(BytesHelper.decompress(buffer, 1, buffer.length - 1))); + } + } + + public Node decode() throws IOException { + if(closed) { + throw new IllegalStateException("The encoder is closed"); + } + + var token = dataInputStream.readUnsignedByte(); + var size = readSize(token); + Validate.isTrue(size != 0, "Cannot decode node with empty body"); + var description = readString(); + var attrs = readAttributes(size); + return size % 2 != 0 ? Node.of(description, attrs) : Node.of(description, attrs, read(false)); + } + + private String readString() throws IOException { + var read = read(true); + if (read instanceof String string) { + return string; + } + + throw new IllegalArgumentException("Strict decoding failed: expected string, got %s with type %s" + .formatted(read, read == null ? null : read.getClass().getName())); + } + + private List readList(int size) throws IOException { + var results = new ArrayList(); + for (int index = 0; index < size; index++) { + results.add(decode()); + } + + return results; + } + + private String readString(List permitted, int start, int end) throws IOException { + var string = new char[2 * end - start]; + for(var index = 0; index < string.length - 1; index += 2) { + readChar(permitted, string, index); + } + if (start != 0) { + string[string.length - 1] = permitted.get(dataInputStream.readUnsignedByte() >>> 4); + } + + return String.valueOf(string); + } + + private void readChar(List permitted, char[] string, int index) throws IOException { + var token = dataInputStream.readUnsignedByte(); + string[index] = permitted.get(token >>> 4); + string[index + 1] = permitted.get(15 & token); + } + + private Object read(boolean parseBytes) throws IOException { + var tag = dataInputStream.readUnsignedByte(); + return switch (of(tag)) { + case LIST_EMPTY -> null; + case COMPANION_JID -> readCompanionJid(); + case LIST_8 -> readList(dataInputStream.readUnsignedByte()); + case LIST_16 -> readList(dataInputStream.readUnsignedShort()); + case JID_PAIR -> readJidPair(); + case HEX_8 -> readHexString(); + case BINARY_8 -> readString(dataInputStream.readUnsignedByte(), parseBytes); + case BINARY_20 -> readString(readString20Length(), parseBytes); + case BINARY_32 -> readString(dataInputStream.readUnsignedShort(), parseBytes); + case NIBBLE_8 -> readNibble(); + default -> readStringFromToken(tag); + }; + } + + private int readString20Length() throws IOException { + return ((15 & dataInputStream.readUnsignedByte()) << 16) + + ((dataInputStream.readUnsignedByte()) << 8) + + (dataInputStream.readUnsignedByte()); + } + + private String readStringFromToken(int token) throws IOException { + if (token < DICTIONARY_0.data() || token > DICTIONARY_3.data()) { + return BinaryTokens.SINGLE_BYTE.get(token - 1); + } + + var delta = (BinaryTokens.DOUBLE_BYTE.size() / 4) * (token - DICTIONARY_0.data()); + return BinaryTokens.DOUBLE_BYTE.get(dataInputStream.readUnsignedByte() + delta); + } + + private String readNibble() throws IOException { + var number = dataInputStream.readUnsignedByte(); + return readString(BinaryTokens.NUMBERS, number >>> 7, 127 & number); + } + + private Object readString(int size, boolean parseBytes) throws IOException { + var data = new byte[size]; + dataInputStream.readFully(data); + return parseBytes ? new String(data, StandardCharsets.UTF_8) : data; + } + + private String readHexString() throws IOException { + var number = dataInputStream.readUnsignedByte(); + return readString(BinaryTokens.HEX, number >>> 7, 127 & number); + } + + private Jid readJidPair() throws IOException { + return switch (read(true)) { + case String encoded -> Jid.of(encoded, JidServer.of(readString())); + case null -> Jid.ofServer(JidServer.of(readString())); + default -> throw new RuntimeException("Invalid jid type"); + }; + } + + private Jid readCompanionJid() throws IOException { + var agent = dataInputStream.readUnsignedByte(); + var device = dataInputStream.readUnsignedByte(); + var user = readString(); + return new Jid(user, JidServer.WHATSAPP, device == 0 ? null : device, agent == 0 ? null : agent); + } + + private int readSize(int token) throws IOException { + return LIST_8.contentEquals(token) ? dataInputStream.readUnsignedByte() : dataInputStream.readUnsignedShort(); + } + + private Map readAttributes(int size) throws IOException { + var map = new HashMap(); + for (var pair = size - 1; pair > 1; pair -= 2) { + var key = readString(); + var value = read(true); + map.put(key, value); + } + return map; + } + + @Override + public void close() throws IOException { + this.closed = true; + dataInputStream.close(); + } +} diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java b/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java new file mode 100644 index 000000000..3f13759e6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/binary/BinaryEncoder.java @@ -0,0 +1,269 @@ +package it.auties.whatsapp.binary; + +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.node.Node; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static it.auties.whatsapp.binary.BinaryTag.*; + +public final class BinaryEncoder implements AutoCloseable { + private static final int UNSIGNED_BYTE_MAX_VALUE = 256; + private static final int UNSIGNED_SHORT_MAX_VALUE = 65536; + private static final int INT_20_MAX_VALUE = 1048576; + + private final ByteArrayOutputStream byteArrayOutputStream; + private final DataOutputStream dataOutputStream; + private final List singleByteTokens; + private final List doubleByteTokens; + private boolean closed; + + public BinaryEncoder() { + this(BinaryTokens.SINGLE_BYTE, BinaryTokens.DOUBLE_BYTE); + } + + public BinaryEncoder(List singleByteTokens, List doubleByteTokens) { + this.byteArrayOutputStream = new ByteArrayOutputStream(); + this.dataOutputStream = new DataOutputStream(byteArrayOutputStream); + this.singleByteTokens = singleByteTokens; + this.doubleByteTokens = doubleByteTokens; + } + + public byte[] encode(Node node) throws IOException { + if(closed) { + throw new IllegalStateException("The encoder is closed"); + } + + dataOutputStream.write(0); + writeNode(node); + return byteArrayOutputStream.toByteArray(); + } + + private void writeString(String input, BinaryTag token) throws IOException { + dataOutputStream.write(token.data()); + writeStringLength(input); + for (int charCode = 0, index = 0; index < input.length(); index++) { + var stringCodePoint = Character.codePointAt(input, index); + var binaryCodePoint = getStringCodePoint(token, stringCodePoint); + + if (index % 2 != 0) { + dataOutputStream.write(charCode |= binaryCodePoint); + continue; + } + + charCode = binaryCodePoint << 4; + if (index != input.length() - 1) { + continue; + } + + dataOutputStream.write(charCode |= 15); + } + } + + private int getStringCodePoint(BinaryTag token, int codePoint) { + if (codePoint >= 48 && codePoint <= 57) { + return codePoint - 48; + } + + if (token == NIBBLE_8 && codePoint == 45) { + return 10; + } + + if (token == NIBBLE_8 && codePoint == 46) { + return 11; + } + + if (token == HEX_8 && codePoint >= 65 && codePoint <= 70) { + return codePoint - 55; + } + + throw new IllegalArgumentException("Cannot parse codepoint %s with token %s".formatted(codePoint, token)); + } + + private void writeStringLength(String input) throws IOException { + var roundedLength = (int) Math.ceil(input.length() / 2F); + if (input.length() % 2 == 1) { + dataOutputStream.write(roundedLength | 128); + return; + } + + dataOutputStream.write(roundedLength); + } + + private void writeLong(long input) throws IOException { + if (input < UNSIGNED_BYTE_MAX_VALUE) { + dataOutputStream.write(BINARY_8.data()); + dataOutputStream.write((int) input); + return; + } + + if (input < INT_20_MAX_VALUE) { + dataOutputStream.write(BINARY_20.data()); + dataOutputStream.write((int) ((input >>> 16) & 255)); + dataOutputStream.write((int) ((input >>> 8) & 255)); + dataOutputStream.write((int) (255 & input)); + return; + } + + dataOutputStream.write(BINARY_32.data()); + dataOutputStream.writeLong(input); + } + + private void writeString(String input) throws IOException { + if (input.isEmpty()) { + dataOutputStream.write(BINARY_8.data()); + dataOutputStream.write(LIST_EMPTY.data()); + return; + } + + var tokenIndex = singleByteTokens.indexOf(input); + if (tokenIndex != -1) { + dataOutputStream.write(tokenIndex + 1); + return; + } + + if (writeDoubleByteString(input)) { + return; + } + + var length = length(input); + if (length < 128 && !BinaryTokens.anyMatch(input, BinaryTokens.NUMBERS_REGEX)) { + writeString(input, NIBBLE_8); + return; + } + + if (length < 128 && !BinaryTokens.anyMatch(input, BinaryTokens.HEX_REGEX)) { + writeString(input, HEX_8); + return; + } + + writeLong(length); + dataOutputStream.write(input.getBytes(StandardCharsets.UTF_8)); + } + + private boolean writeDoubleByteString(String input) throws IOException { + if (!doubleByteTokens.contains(input)) { + return false; + } + + var index = doubleByteTokens.indexOf(input); + dataOutputStream.write(doubleByteStringTag(index).data()); + dataOutputStream.write(index % (doubleByteTokens.size() / 4)); + return true; + } + + private BinaryTag doubleByteStringTag(int index) { + return switch (index / (doubleByteTokens.size() / 4)) { + case 0 -> DICTIONARY_0; + case 1 -> DICTIONARY_1; + case 2 -> DICTIONARY_2; + case 3 -> DICTIONARY_3; + default -> throw new IllegalArgumentException("Cannot find tag for quadrant %s".formatted(index)); + }; + } + + private void writeNode(Node input) throws IOException { + if (input.description().equals("0")) { + dataOutputStream.write(LIST_8.data()); + dataOutputStream.write(LIST_EMPTY.data()); + return; + } + + writeInt(input.size()); + writeString(input.description()); + writeAttributes(input); + if (input.hasContent()) { + write(input.content()); + } + } + + private void writeAttributes(Node input) throws IOException { + for (var entry : input.attributes().toMap().entrySet()) { + writeString(entry.getKey()); + write(entry.getValue()); + } + } + + private void writeInt(int size) throws IOException { + if (size < UNSIGNED_BYTE_MAX_VALUE) { + dataOutputStream.write(LIST_8.data()); + dataOutputStream.write(size); + return; + } + + if (size < UNSIGNED_SHORT_MAX_VALUE) { + dataOutputStream.write(LIST_16.data()); + dataOutputStream.writeShort(size); + return; + } + + throw new IllegalArgumentException("Cannot write int %s: overflow".formatted(size)); + } + + private void write(Object input) throws IOException { + switch (input) { + case null -> dataOutputStream.write(LIST_EMPTY.data()); + case String str -> writeString(str); + case Boolean bool -> writeString(Boolean.toString(bool)); + case Number number -> writeString(number.toString()); + case byte[] bytes -> writeBytes(bytes); + case Jid jid -> writeJid(jid); + case Collection collection -> writeList(collection); + case Enum serializable -> writeString(Objects.toString(serializable)); + case Node node -> + throw new IllegalArgumentException("Invalid payload type(nodes should be wrapped by a collection): %s".formatted(input)); + default -> + throw new IllegalArgumentException("Invalid payload type(%s): %s".formatted(input.getClass().getName(), input)); + } + } + + private void writeList(Collection collection) throws IOException { + writeInt(collection.size()); + for (var entry : collection) { + if (entry instanceof Node node) { + writeNode(node); + } + } + } + + private void writeBytes(byte[] bytes) throws IOException { + writeLong(bytes.length); + dataOutputStream.write(bytes); + } + + private void writeJid(Jid jid) throws IOException { + if (jid.isCompanion()) { + dataOutputStream.write(COMPANION_JID.data()); + dataOutputStream.write(jid.agent()); + dataOutputStream.write(jid.device()); + writeString(jid.user()); + return; + } + + dataOutputStream.write(JID_PAIR.data()); + if (jid.user() != null) { + writeString(jid.user()); + writeString(jid.server().address()); + return; + } + + dataOutputStream.write(LIST_EMPTY.data()); + writeString(jid.server().address()); + } + + private int length(String input) { + return input.getBytes(StandardCharsets.UTF_8).length; + } + + @Override + public void close() throws IOException { + this.closed = true; + dataOutputStream.close(); + } +} diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryTag.java b/src/main/java/it/auties/whatsapp/binary/BinaryTag.java new file mode 100644 index 000000000..5aa3e2d61 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/binary/BinaryTag.java @@ -0,0 +1,42 @@ +package it.auties.whatsapp.binary; + +import java.util.Arrays; + +public enum BinaryTag { + UNKNOWN(-1), + LIST_EMPTY(0), + STREAM_END(2), + DICTIONARY_0(236), + DICTIONARY_1(237), + DICTIONARY_2(238), + DICTIONARY_3(239), + COMPANION_JID(247), + LIST_8(248), + LIST_16(249), + JID_PAIR(250), + HEX_8(251), + BINARY_8(252), + BINARY_20(253), + BINARY_32(254), + NIBBLE_8(255), + SINGLE_BYTE_MAX(256), + PACKED_MAX(254); + + private final int data; + + BinaryTag(int data) { + this.data = data; + } + + public static BinaryTag of(int data) { + return Arrays.stream(values()).filter(entry -> entry.data() == data).findAny().orElse(UNKNOWN); + } + + public boolean contentEquals(int number) { + return number == this.data(); + } + + public int data() { + return this.data; + } +} diff --git a/src/main/java/it/auties/whatsapp/binary/BinaryTokens.java b/src/main/java/it/auties/whatsapp/binary/BinaryTokens.java new file mode 100644 index 000000000..0f20c47ed --- /dev/null +++ b/src/main/java/it/auties/whatsapp/binary/BinaryTokens.java @@ -0,0 +1,1206 @@ +package it.auties.whatsapp.binary; + +import it.auties.whatsapp.model.companion.CompanionProperty; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public final class BinaryTokens { + public static final List SINGLE_BYTE = List.of("xmlstreamstart", "xmlstreamend", "s.whatsapp.net", "type", "participant", "from", "receipt", "id", "notification", "disappearing_mode", "status", "jid", "broadcast", "user", "devices", "device_hash", "to", "offline", "message", "result", "class", "xmlns", "duration", "notify", "iq", "t", "ack", "g.us", "enc", "urn:xmpp:whatsapp:push", "presence", "config_value", "picture", "verified_name", "config_code", "key-index-list", "contact", "mediatype", "routing_info", "edge_routing", "get", "read", "urn:xmpp:ping", "fallback_hostname", "0", "chatstate", "business_hours_config", "unavailable", "download_buckets", "skmsg", "verified_level", "composing", "handshake", "device-list", "media", "text", "fallback_ip4", "media_conn", "device", "creation", "location", "config", "item", "fallback_ip6", "count", "w:profile:picture", "image", "business", "2", "hostname", "call-creator", "display_name", "relaylatency", "platform", "abprops", "success", "msg", "offline_preview", "prop", "key-index", "v", "day_of_week", "pkmsg", "version", "1", "ping", "w:p", "download", "video", "set", "specific_hours", "props", "primary", "unknown", "hash", "commerce_experience", "last", "subscribe", "max_buckets", "call", "profile", "member_since_text", "close_time", "call-id", "sticker", "mode", "participants", "value", "query", "profile_options", "open_time", "code", "list", "host", "ts", "contacts", "upload", "lid", "preview", "update", "usync", "w:stats", "delivery", "auth_ttl", "context", "fail", "cart_enabled", "appdata", "category", "atn", "direct_connection", "decrypt-fail", "relay_id", "mmg-fallback.whatsapp.net", "target", "available", "name", "last_id", "mmg.whatsapp.net", "categories", "401", "is_new", "index", "tctoken", "ip4", "token_id", "latency", "recipient", "edit", "ip6", "add", "thumbnail-document", "26", "paused", "true", "identity", "stream:error", "key", "sidelist", "background", "audio", "3", "thumbnail-image", "biz-cover-photo", "cat", "gcm", "thumbnail-video", "error", "auth", "deny", "serial", "in", "registration", "thumbnail-link", "remove", "00", "gif", "thumbnail-gif", "tag", "capability", "multicast", "item-not-found", "description", "business_hours", "config_expo_key", "md-app-state", "expiration", "fallback", "ttl", "300", "md-msg-hist", "device_orientation", "out", "w:m", "open_24h", "side_list", "token", "inactive", "01", "document", "te2", "played", "encrypt", "msgr", "hide", "direct_path", "12", "state", "not-authorized", "url", "terminate", "signature", "status-revoke-delay", "02", "te", "linked_accounts", "trusted_contact", "timezone", "ptt", "kyc-id", "privacy_token", "readreceipts", "appointment_only", "address", "expected_ts", "privacy", "7", "android", "interactive", "device-identity", "enabled", "attribute_padding", "1080", "03", "screen_height"); + + public static final List DOUBLE_BYTE = List.of("read-self", "active", "fbns", "protocol", "reaction", "screen_width", "heartbeat", "deviceid", "2:47DEQpj8", "uploadfieldstat", "voip_settings", "retry", "priority", "longitude", "conflict", "false", "ig_professional", "replaced", "preaccept", "cover_photo", "uncompressed", "encopt", "ppic", "04", "passive", "status-revoke-drop", "keygen", "540", "offer", "rate", "opus", "latitude", "w:gp2", "ver", "4", "business_profile", "medium", "sender", "prev_v_id", "email", "website", "invited", "sign_credential", "05", "transport", "skey", "reason", "peer_abtest_bucket", "America/Sao_Paulo", "appid", "refresh", "100", "06", "404", "101", "104", "107", "102", "109", "103", "member_add_mode", "105", "transaction-id", "110", "106", "outgoing", "108", "111", "tokens", "followers", "ig_handle", "self_pid", "tue", "dec", "thu", "joinable", "peer_pid", "mon", "features", "wed", "peer_device_presence", "pn", "delete", "07", "fri", "audio_duration", "admin", "connected", "delta", "rcat", "disable", "collection", "08", "480", "sat", "phash", "all", "invite", "accept", "critical_unblock_low", "group_update", "signed_credential", "blinded_credential", "eph_setting", "net", "09", "background_location", "refresh_id", "Asia/Kolkata", "privacy_mode_ts", "account_sync", "voip_payload_type", "service_areas", "acs_public_key", "v_id", "0a", "fallback_class", "relay", "actual_actors", "metadata", "w:biz", "5", "connected-limit", "notice", "0b", "host_storage", "fb_page", "subject", "privatestats", "invis", "groupadd", "010", "note.m4r", "uuid", "0c", "8000", "sun", "372", "1020", "stage", "1200", "720", "canonical", "fb", "011", "video_duration", "0d", "1140", "superadmin", "012", "Opening.m4r", "keystore_attestation", "dleq_proof", "013", "timestamp", "ab_key", "w:sync:app:state", "0e", "vertical", "600", "p_v_id", "6", "likes", "014", "500", "1260", "creator", "0f", "rte", "destination", "group", "group_info", "syncd_anti_tampering_fatal_exception_enabled", "015", "dl_bw", "Asia/Jakarta", "vp8/h.264", "online", "1320", "fb:multiway", "10", "timeout", "016", "nse_retry", "urn:xmpp:whatsapp:dirty", "017", "a_v_id", "web_shops_chat_header_button_enabled", "nse_call", "inactive-upgrade", "none", "web", "groups", "2250", "mms_hot_content_timespan_in_seconds", "contact_blacklist", "nse_read", "suspended_group_deletion_notification", "binary_version", "018", "https://www.whatsapp.com/otp/copy/", "reg_push", "shops_hide_catalog_attachment_entrypoint", "server_sync", ".", "ephemeral_messages_allowed_values", "019", "mms_vcache_aggregation_enabled", "iphone", "America/Argentina/Buenos_Aires", "01a", "mms_vcard_autodownload_size_kb", "nse_ver", "shops_header_dropdown_menu_item", "dhash", "catalog_status", "communities_mvp_new_iqs_serverprop", "blocklist", "default", "11", "ephemeral_messages_enabled", "01b", "original_dimensions", "8", "mms4_media_retry_notification_encryption_enabled", "mms4_server_error_receipt_encryption_enabled", "original_image_url", "sync", "multiway", "420", "companion_enc_static", "shops_profile_drawer_entrypoint", "01c", "vcard_as_document_size_kb", "status_video_max_duration", "request_image_url", "01d", "regular_high", "s_t", "abt", "share_ext_min_preliminary_image_quality", "01e", "32", "syncd_key_rotation_enabled", "data_namespace", "md_downgrade_read_receipts2", "patch", "polltype", "ephemeral_messages_setting", "userrate", "15", "partial_pjpeg_bw_threshold", "played-self", "catalog_exists", "01f", "mute_v2", "reject", "dirty", "announcement", "020", "13", "9", "status_video_max_bitrate", "fb:thrift_iq", "offline_batch", "022", "full", "ctwa_first_business_reply_logging", "h.264", "smax_id", "group_description_length", "https://www.whatsapp.com/otp/code", "status_image_max_edge", "smb_upsell_business_profile_enabled", "021", "web_upgrade_to_md_modal", "14", "023", "s_o", "smaller_video_thumbs_status_enabled", "media_max_autodownload", "960", "blocking_status", "peer_msg", "joinable_group_call_client_version", "group_call_video_maximization_enabled", "return_snapshot", "high", "America/Mexico_City", "entry_point_block_logging_enabled", "pop", "024", "1050", "16", "1380", "one_tap_calling_in_group_chat_size", "regular_low", "inline_joinable_education_enabled", "hq_image_max_edge", "locked", "America/Bogota", "smb_biztools_deeplink_enabled", "status_image_quality", "1088", "025", "payments_upi_intent_transaction_limit", "voip", "w:g2", "027", "md_pin_chat_enabled", "026", "multi_scan_pjpeg_download_enabled", "shops_product_grid", "transaction_id", "ctwa_context_enabled", "20", "fna", "hq_image_quality", "alt_jpeg_doc_detection_quality", "group_call_max_participants", "pkey", "America/Belem", "image_max_kbytes", "web_cart_v1_1_order_message_changes_enabled", "ctwa_context_enterprise_enabled", "urn:xmpp:whatsapp:account", "840", "Asia/Kuala_Lumpur", "max_participants", "video_remux_after_repair_enabled", "stella_addressbook_restriction_type", "660", "900", "780", "context_menu_ios13_enabled", "mute-state", "ref", "payments_request_messages", "029", "frskmsg", "vcard_max_size_kb", "sample_buffer_gif_player_enabled", "match_last_seen", "510", "4983", "video_max_bitrate", "028", "w:comms:chat", "17", "frequently_forwarded_max", "groups_privacy_blacklist", "Asia/Karachi", "02a", "web_download_document_thumb_mms_enabled", "02b", "hist_sync", "biz_block_reasons_version", "1024", "18", "web_is_direct_connection_for_plm_transparent", "view_once_write", "file_max_size", "paid_convo_id", "online_privacy_setting", "video_max_edge", "view_once_read", "enhanced_storage_management", "multi_scan_pjpeg_encoding_enabled", "ctwa_context_forward_enabled", "video_transcode_downgrade_enable", "template_doc_mime_types", "hq_image_bw_threshold", "30", "body", "u_aud_limit_sil_restarts_ctrl", "other", "participating", "w:biz:directory", "1110", "vp8", "4018", "meta", "doc_detection_image_max_edge", "image_quality", "1170", "02c", "smb_upsell_chat_banner_enabled", "key_expiry_time_second", "pid", "stella_interop_enabled", "19", "linked_device_max_count", "md_device_sync_enabled", "02d", "02e", "360", "enhanced_block_enabled", "ephemeral_icon_in_forwarding", "paid_convo_status", "gif_provider", "project_name", "server-error", "canonical_url_validation_enabled", "wallpapers_v2", "syncd_clear_chat_delete_chat_enabled", "medianotify", "02f", "shops_required_tos_version", "vote", "reset_skey_on_id_change", "030", "image_max_edge", "multicast_limit_global", "ul_bw", "21", "25", "5000", "poll", "570", "22", "031", "1280", "WhatsApp", "032", "bloks_shops_enabled", "50", "upload_host_switching_enabled", "web_ctwa_context_compose_enabled", "ptt_forwarded_features_enabled", "unblocked", "partial_pjpeg_enabled", "fbid:devices", "height", "ephemeral_group_query_ts", "group_join_permissions", "order", "033", "alt_jpeg_status_quality", "migrate", "popular-bank", "win_uwp_deprecation_killswitch_enabled", "web_download_status_thumb_mms_enabled", "blocking", "url_text", "035", "web_forwarding_limit_to_groups", "1600", "val", "1000", "syncd_msg_date_enabled", "bank-ref-id", "max_subject", "payments_web_enabled", "web_upload_document_thumb_mms_enabled", "size", "request", "ephemeral", "24", "receipt_agg", "ptt_remember_play_position", "sampling_weight", "enc_rekey", "mute_always", "037", "034", "23", "036", "action", "click_to_chat_qr_enabled", "width", "disabled", "038", "md_blocklist_v2", "played_self_enabled", "web_buttons_message_enabled", "flow_id", "clear", "450", "fbid:thread", "bloks_session_state", "America/Lima", "attachment_picker_refresh", "download_host_switching_enabled", "1792", "u_aud_limit_sil_restarts_test2", "custom_urls", "device_fanout", "optimistic_upload", "2000", "key_cipher_suite", "web_smb_upsell_in_biz_profile_enabled", "e", "039", "siri_post_status_shortcut", "pair-device", "lg", "lc", "stream_attribution_url", "model", "mspjpeg_phash_gen", "catalog_send_all", "new_multi_vcards_ui", "share_biz_vcard_enabled", "-", "clean", "200", "md_blocklist_v2_server", "03b", "03a", "web_md_migration_experience", "ptt_conversation_waveform", "u_aud_limit_sil_restarts_test1", "64", "ptt_playback_speed_enabled", "web_product_list_message_enabled", "paid_convo_ts", "27", "manufacturer", "psp-routing", "grp_uii_cleanup", "ptt_draft_enabled", "03c", "business_initiated", "web_catalog_products_onoff", "web_upload_link_thumb_mms_enabled", "03e", "mediaretry", "35", "hfm_string_changes", "28", "America/Fortaleza", "max_keys", "md_mhfs_days", "streaming_upload_chunk_size", "5541", "040", "03d", "2675", "03f", "...", "512", "mute", "48", "041", "alt_jpeg_quality", "60", "042", "md_smb_quick_reply", "5183", "c", "1343", "40", "1230", "043", "044", "mms_cat_v1_forward_hot_override_enabled", "user_notice", "ptt_waveform_send", "047", "Asia/Calcutta", "250", "md_privacy_v2", "31", "29", "128", "md_messaging_enabled", "046", "crypto", "690", "045", "enc_iv", "75", "failure", "ptt_oot_playback", "AIzaSyDR5yfaG7OG8sMTUj8kfQEb8T9pN8BM6Lk", "w", "048", "2201", "web_large_files_ui", "Asia/Makassar", "812", "status_collapse_muted", "1334", "257", "2HP4dm", "049", "patches", "1290", "43cY6T", "America/Caracas", "web_sticker_maker", "campaign", "ptt_pausable_enabled", "33", "42", "attestation", "biz", "04b", "query_linked", "s", "125", "04a", "810", "availability", "1411", "responsiveness_v2_m1", "catalog_not_created", "34", "America/Santiago", "1465", "enc_p", "04d", "status_info", "04f", "key_version", "..", "04c", "04e", "md_group_notification", "1598", "1215", "web_cart_enabled", "37", "630", "1920", "2394", "-1", "vcard", "38", "elapsed", "36", "828", "peer", "pricing_category", "1245", "invalid", "stella_ios_enabled", "2687", "45", "1528", "39", "u_is_redial_audio_1104_ctrl", "1025", "1455", "58", "2524", "2603", "054", "bsp_system_message_enabled", "web_pip_redesign", "051", "verify_apps", "1974", "1272", "1322", "1755", "052", "70", "050", "1063", "1135", "1361", "80", "1096", "1828", "1851", "1251", "1921", "key_config_id", "1254", "1566", "1252", "2525", "critical_block", "1669", "max_available", "w:auth:backup:token", "product", "2530", "870", "1022", "participant_uuid", "web_cart_on_off", "1255", "1432", "1867", "41", "1415", "1440", "240", "1204", "1608", "1690", "1846", "1483", "1687", "1749", "69", "url_number", "053", "1325", "1040", "365", "59", "Asia/Riyadh", "1177", "test_recommended", "057", "1612", "43", "1061", "1518", "1635", "055", "1034", "1375", "750", "1430", "event_code", "1682", "503", "55", "865", "78", "1309", "1365", "44", "America/Guayaquil", "535", "LIMITED", "1377", "1613", "1420", "1599", "1822", "05a", "1681", "password", "1111", "1214", "1376", "1478", "47", "1082", "4282", "Europe/Istanbul", "1307", "46", "058", "1124", "256", "rate-overlimit", "retail", "u_a_socket_err_fix_succ_test", "1292", "1370", "1388", "520", "861", "psa", "regular", "1181", "1766", "05b", "1183", "1213", "1304", "1537", "1724", "profile_picture", "1071", "1314", "1605", "407", "990", "1710", "746", "pricing_model", "056", "059", "061", "1119", "6027", "65", "877", "1607", "05d", "917", "seen", "1516", "49", "470", "973", "1037", "1350", "1394", "1480", "1796", "keys", "794", "1536", "1594", "2378", "1333", "1524", "1825", "116", "309", "52", "808", "827", "909", "495", "1660", "361", "957", "google", "1357", "1565", "1967", "996", "1775", "586", "736", "1052", "1670", "bank", "177", "1416", "2194", "2222", "1454", "1839", "1275", "53", "997", "1629", "6028", "smba", "1378", "1410", "05c", "1849", "727", "create", "1559", "536", "1106", "1310", "1944", "670", "1297", "1316", "1762", "en", "1148", "1295", "1551", "1853", "1890", "1208", "1784", "7200", "05f", "178", "1283", "1332", "381", "643", "1056", "1238", "2024", "2387", "179", "981", "1547", "1705", "05e", "290", "903", "1069", "1285", "2436", "062", "251", "560", "582", "719", "56", "1700", "2321", "325", "448", "613", "777", "791", "51", "488", "902", "Asia/Almaty", "is_hidden", "1398", "1527", "1893", "1999", "2367", "2642", "237", "busy", "065", "067", "233", "590", "993", "1511", "54", "723", "860", "363", "487", "522", "605", "995", "1321", "1691", "1865", "2447", "2462", "NON_TRANSACTIONAL", "433", "871", "432", "1004", "1207", "2032", "2050", "2379", "2446", "279", "636", "703", "904", "248", "370", "691", "700", "1068", "1655", "2334", "060", "063", "364", "533", "534", "567", "1191", "1210", "1473", "1827", "069", "701", "2531", "514", "prev_dhash", "064", "496", "790", "1046", "1139", "1505", "1521", "1108", "207", "544", "637", "final", "1173", "1293", "1694", "1939", "1951", "1993", "2353", "2515", "504", "601", "857", "modify", "spam_request", "p_121_aa_1101_test4", "866", "1427", "1502", "1638", "1744", "2153", "068", "382", "725", "1704", "1864", "1990", "2003", "Asia/Dubai", "508", "531", "1387", "1474", "1632", "2307", "2386", "819", "2014", "066", "387", "1468", "1706", "2186", "2261", "471", "728", "1147", "1372", "1961"); + + public static final int DICTIONARY_VERSION = 3; + + public static final List NUMBERS = List.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', '�', '�', '�', '�'); + + public static final List HEX = List.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'); + + public static final String NUMBERS_REGEX = "[^0-9.-]+?"; + + public static final String HEX_REGEX = "[^0-9A-F]+?"; + + public static final Map PROPERTIES; + + static { + var properties = new HashMap(); + properties.put(1719, new CompanionProperty("order_details_total_order_minimum_value", 1719, 1, 1)); + properties.put(1684, new CompanionProperty("order_details_total_maximum_value", 1684, 5.0E8, 5.0E8)); + properties.put(1683, new CompanionProperty("order_details_total_minimum_value", 1683, 0, 0)); + properties.put(3240, new CompanionProperty("order_messages_ephemeral_exception_enabled", 3240, false, true)); + properties.put(233, new CompanionProperty("in_app_support_v2_enabled", 233, false, false)); + properties.put(379, new CompanionProperty("in_app_support_v2_locale_langs", 379, "", "")); + properties.put(390, new CompanionProperty("in_app_support_v2_numbers", 390, "", "")); + properties.put(1031, new CompanionProperty("in_app_support_v2_number_prefixes", 1031, "15517868", "15517868")); + properties.put(4799, new CompanionProperty("in_app_support_capi_number_prefixes", 4799, "155178684", "155178684")); + properties.put(819, new CompanionProperty("in_app_support_v2_jump_to_group", 819, false, false)); + properties.put(974, new CompanionProperty("in_app_support_v2_jump_to_group_wait_time_in_ms", 974, 5000.0, 5000.0)); + properties.put(2765, new CompanionProperty("quick_mute_enabled", 2765, false, false)); + properties.put(308, new CompanionProperty("groups_dogfooding_ui", 308, false, false)); + properties.put(309, new CompanionProperty("md_icdc_enabled", 309, false, false)); + properties.put(310, new CompanionProperty("md_icdc_hash_length", 310, 10, 10)); + properties.put(361, new CompanionProperty("played_self_enabled", 361, false, false)); + properties.put(407, new CompanionProperty("ephemeral_24h_duration", 407, false, true)); + properties.put(536, new CompanionProperty("disappearing_mode", 536, false, false)); + properties.put(605, new CompanionProperty("payments_expressive_backgrounds_enabled", 605, false, true)); + properties.put(432, new CompanionProperty("ephemeral_allow_group_members", 432, false, true)); + properties.put(470, new CompanionProperty("business_profile_refresh_m1_enabled", 470, false, true)); + properties.put(730, new CompanionProperty("num_days_key_index_list_expiration", 730, 35, 35)); + properties.put(731, new CompanionProperty("num_days_before_device_expiry_check", 731, 7, 7)); + properties.put(1098, new CompanionProperty("media_reupload_limit_mb", 1098, 100, 100)); + properties.put(1961, new CompanionProperty("portrait_thumb_enabled_chat", 1961, false, true)); + properties.put(1962, new CompanionProperty("portrait_thumb_enabled_status", 1962, false, true)); + properties.put(4787, new CompanionProperty("channels_video_limit_mb", 4787, 16, 16)); + properties.put(3185, new CompanionProperty("default_video_limit_mb", 3185, 16, 64)); + properties.put(4155, new CompanionProperty("default_video_limit_mb_newsletter", 4155, 16, 16)); + properties.put(3656, new CompanionProperty("default_gif_limit_mb", 3656, 16, 64)); + properties.put(3657, new CompanionProperty("default_audio_limit_mb", 3657, 16, 64)); + properties.put(3660, new CompanionProperty("default_media_limit_mb", 3660, 16, 64)); + properties.put(3934, new CompanionProperty("hd_video_label_enabled", 3934, false, true)); + properties.put(3935, new CompanionProperty("per_send_hd_video_setting_enabled", 3935, false, true)); + properties.put(4138, new CompanionProperty("per_send_hd_video_setting_for_groups_enabled", 4138, false, true)); + properties.put(3936, new CompanionProperty("hd_video_min_streaming_bandwidth", 3936, 150, 150)); + properties.put(4152, new CompanionProperty("hd_video_show_data_warning_dialog", 4152, false, true)); + properties.put(4153, new CompanionProperty("hd_video_data_warning_max_mb", 4153, 64, 64)); + properties.put(4171, new CompanionProperty("hd_video_definition_min_edge", 4171, 720, 720)); + properties.put(4172, new CompanionProperty("hd_video_definition_max_edge", 4172, 864, 864)); + properties.put(4175, new CompanionProperty("hd_video_definition_min_edge_with_max_edge", 4175, 480, 480)); + properties.put(535, new CompanionProperty("message_level_reporting", 535, false, true)); + properties.put(636, new CompanionProperty("native_shop_preview_enabled", 636, false, true)); + properties.put(736, new CompanionProperty("sync_archive_v2_setting", 736, false, false)); + properties.put(637, new CompanionProperty("ptt_conversation_waveform", 637, false, true)); + properties.put(746, new CompanionProperty("ptt_waveform_send", 746, false, true)); + properties.put(753, new CompanionProperty("adv_v2_m4_m5", 753, false, false)); + properties.put(903, new CompanionProperty("adv_v2_m6", 903, false, false)); + properties.put(777, new CompanionProperty("ptt_draft_enabled", 777, false, true)); + properties.put(871, new CompanionProperty("ptt_pausable_enabled", 871, false, true)); + properties.put(791, new CompanionProperty("tos_3_client_gating_enabled", 791, false, false)); + properties.put(877, new CompanionProperty("tos_client_state_fetch_enabled", 877, false, false)); + properties.put(908, new CompanionProperty("tos_client_state_fetch_iteration", 908, 0, 0)); + properties.put(1105, new CompanionProperty("country_client_gating_enabled", 1105, false, false)); + properties.put(1035, new CompanionProperty("system_msg_numbers_fb_branded", 1035, "16505434800,16503130062,16507885324,16508620604,16504228206,447710173736,16315551023,16505361212,16508129150,16315555102,16315558723,16505212669,16507885280,19032707825,0", "16505434800,16503130062,16507885324,16508620604,16504228206,447710173736,16315551023,16505361212,16508129150,16315555102,16315558723,16505212669,16507885280,19032707825,0")); + properties.put(1036, new CompanionProperty("system_msg_numbers_fb_inc", 1036, "", "")); + properties.put(1190, new CompanionProperty("log_clock_skew", 1190, false, false)); + properties.put(794, new CompanionProperty("trusted_contacts", 794, false, false)); + properties.put(995, new CompanionProperty("trusted_contacts_sender", 995, false, false)); + properties.put(922, new CompanionProperty("trusted_contacts_ti", 922, false, false)); + properties.put(865, new CompanionProperty("tctoken_duration", 865, 604800, 604800)); + properties.put(909, new CompanionProperty("tctoken_num_buckets", 909, 4, 4)); + properties.put(996, new CompanionProperty("tctoken_duration_sender", 996, 604800, 604800)); + properties.put(997, new CompanionProperty("tctoken_num_buckets_sender", 997, 4, 4)); + properties.put(827, new CompanionProperty("reactions_receive", 827, false, true)); + properties.put(828, new CompanionProperty("reactions_send", 828, false, true)); + properties.put(1150, new CompanionProperty("reactions_announcement_only", 1150, false, false)); + properties.put(987, new CompanionProperty("reaction_cleanup_days", 987, 31, 31)); + properties.put(1605, new CompanionProperty("reactions_chat_preview", 1605, false, true)); + properties.put(1361, new CompanionProperty("reactions_animations", 1361, false, true)); + properties.put(1485, new CompanionProperty("reactions_animations_simple", 1485, false, true)); + properties.put(861, new CompanionProperty("md_migration_experience", 861, 2, 2)); + properties.put(869, new CompanionProperty("web_abprop_direct_connection_md", 869, false, true)); + properties.put(907, new CompanionProperty("media_upload_prekeys_fetch_enabled", 907, false, true)); + properties.put(1828, new CompanionProperty("reactions_panel_prekeys_fetch_enabled", 1828, false, true)); + properties.put(1455, new CompanionProperty("status_quick_reply_enabled", 1455, false, true)); + properties.put(1974, new CompanionProperty("status_quick_reply_receiver_changes_enabled", 1974, false, true)); + properties.put(952, new CompanionProperty("ptt_remember_play_position", 952, false, true)); + properties.put(957, new CompanionProperty("banned_shops_ux_enabled", 957, false, true)); + properties.put(973, new CompanionProperty("group_suspend_v0_enabled", 973, false, true)); + properties.put(3181, new CompanionProperty("expiring_groups_enabled", 3181, false, false)); + properties.put(3864, new CompanionProperty("community_breakout_groups_enabled", 3864, false, true)); + properties.put(3795, new CompanionProperty("parent_group_directory_enabled", 3795, false, true)); + properties.put(5109, new CompanionProperty("parent_group_join_request_system_enabled", 5109, false, false)); + properties.put(4654, new CompanionProperty("parent_group_member_can_add_enabled", 4654, false, false)); + properties.put(5385, new CompanionProperty("parent_group_member_can_add_default_everyone_enabled", 5385, false, false)); + properties.put(982, new CompanionProperty("parent_group_view_enabled", 982, false, true)); + properties.put(1173, new CompanionProperty("parent_group_create_enabled", 1173, false, true)); + properties.put(1228, new CompanionProperty("parent_group_query_ts", 1228, 0, 0)); + properties.put(1238, new CompanionProperty("parent_group_link_limit", 1238, 100, 100)); + properties.put(3054, new CompanionProperty("allow_subgroup_admin_to_unlink", 3054, false, true)); + properties.put(3246, new CompanionProperty("community_creation_no_add_groups_screen", 3246, false, true)); + properties.put(2774, new CompanionProperty("community_announcement_group_size_limit", 2774, 5000.0, 5000.0)); + properties.put(5656, new CompanionProperty("parent_group_announcement_comments_banner", 5656, false, true)); + properties.put(3121, new CompanionProperty("community_announcement_improvement_m1", 3121, false, true)); + properties.put(3239, new CompanionProperty("community_announcement_improvement_m2", 3239, false, true)); + properties.put(3380, new CompanionProperty("community_announcement_improvement_m3", 3380, false, true)); + properties.put(4053, new CompanionProperty("community_creation_nux_always", 4053, false, false)); + properties.put(4071, new CompanionProperty("community_creation_nux_count", 4071, 1, 1)); + properties.put(3738, new CompanionProperty("community_subgroup_switcher_entrypoint_enabled", 3738, false, true)); + properties.put(3078, new CompanionProperty("community_subgroup_icon_variant", 3078, 0, 2)); + properties.put(4160, new CompanionProperty("community_subgroup_identity_v2", 4160, false, true)); + properties.put(5046, new CompanionProperty("community_history_setting_receive", 5046, false, false)); + properties.put(5191, new CompanionProperty("community_history_setting_send", 5191, false, false)); + properties.put(5192, new CompanionProperty("community_history_receive", 5192, false, false)); + properties.put(5193, new CompanionProperty("community_history_send", 5193, false, false)); + properties.put(1990, new CompanionProperty("parent_group_link_limit_community_creation", 1990, 10, 20)); + properties.put(1655, new CompanionProperty("parent_group_admins_limit", 1655, 20, 20)); + properties.put(2205, new CompanionProperty("parent_group_view_enabled_for_smb_on_web", 2205, false, true)); + properties.put(2206, new CompanionProperty("parent_group_create_enabled_for_smb_on_web", 2206, false, true)); + properties.put(2356, new CompanionProperty("parent_group_create_privacy", 2356, false, true)); + properties.put(2382, new CompanionProperty("parent_group_min_participants_for_group_entry_point", 2382, 20, 1)); + properties.put(2436, new CompanionProperty("parent_group_tap_to_request_enabled", 2436, false, true)); + properties.put(2446, new CompanionProperty("parent_group_tap_to_add_enabled", 2446, false, true)); + properties.put(2447, new CompanionProperty("parent_group_no_disclaimer", 2447, false, true)); + properties.put(3147, new CompanionProperty("parent_group_subgroup_filter", 3147, false, false)); + properties.put(3023, new CompanionProperty("community_groups_navigation", 3023, false, true)); + properties.put(3748, new CompanionProperty("community_chat_list_tabs", 3748, false, false)); + properties.put(3167, new CompanionProperty("parent_group_no_subgroup_requirement", 3167, false, true)); + properties.put(1864, new CompanionProperty("community_admin_promotion_one_time_prompt", 1864, false, false)); + properties.put(2307, new CompanionProperty("document_preview_caption_changes_enabled", 2307, false, true)); + properties.put(1040, new CompanionProperty("forwarded_ptt_ui_enabled", 1040, false, true)); + properties.put(1054, new CompanionProperty("shops_storefront_url_format", 1054, "https://www.facebook.com/%s/shop/", "https://www.facebook.com/%s/shop/")); + properties.put(1135, new CompanionProperty("message_count_logging_md_enabled", 1135, false, false)); + properties.put(2430, new CompanionProperty("url_send_receive_logging_enabled", 2430, false, true)); + properties.put(2431, new CompanionProperty("inline_video_playback_additional_logging_enabled", 2431, false, true)); + properties.put(1064, new CompanionProperty("dev_prop_string", 1064, "", "")); + properties.put(1065, new CompanionProperty("dev_prop_boolean", 1065, false, false)); + properties.put(1066, new CompanionProperty("dev_prop_int", 1066, 0, 0)); + properties.put(1067, new CompanionProperty("dev_prop_float", 1067, 0, 0)); + properties.put(3077, new CompanionProperty("disable_status_to_non_sub", 3077, false, false)); + properties.put(1107, new CompanionProperty("order_details_from_cart_enabled", 1107, false, true)); + properties.put(1176, new CompanionProperty("order_details_custom_item_enabled", 1176, false, true)); + properties.put(1212, new CompanionProperty("order_details_from_catalog_enabled", 1212, false, true)); + properties.put(1187, new CompanionProperty("md_app_state_critical_data_processing_logging", 1187, false, true)); + properties.put(1221, new CompanionProperty("md_app_state_report_md_sync_mutation_stats", 1221, false, true)); + properties.put(1188, new CompanionProperty("order_management_enabled", 1188, false, false)); + properties.put(1204, new CompanionProperty("growth_lock_v0_enabled", 1204, false, true)); + properties.put(1287, new CompanionProperty("smart_filters_enabled_consumer", 1287, false, true)); + properties.put(3554, new CompanionProperty("inbox_management_filters_m2", 3554, false, false)); + properties.put(4991, new CompanionProperty("enable_spam_report_iq_with_privacy_token", 4991, false, true)); + properties.put(4992, new CompanionProperty("enable_privacy_token_with_timestamp", 4992, false, true)); + properties.put(1517, new CompanionProperty("md_offline_v2_m2_enabled", 1517, 10, 10)); + properties.put(1533, new CompanionProperty("profile_photo_rings_for_status_enabled", 1533, false, true)); + properties.put(1534, new CompanionProperty("dc_edit_postcode_by_default_enabled", 1534, false, false)); + properties.put(2614, new CompanionProperty("media_picker_select_limit", 2614, 30, 30)); + properties.put(2693, new CompanionProperty("media_picker_select_limit_new", 2693, 30, 30)); + properties.put(1608, new CompanionProperty("chatlist_filters_v1", 1608, false, false)); + properties.put(1653, new CompanionProperty("community_suspend_v0_enabled", 1653, false, true)); + properties.put(1777, new CompanionProperty("is_meta_employee_or_internal_tester", 1777, false, false)); + properties.put(1838, new CompanionProperty("disable_auto_download", 1838, false, false)); + properties.put(2154, new CompanionProperty("community_tab_m2", 2154, false, true)); + properties.put(2281, new CompanionProperty("gif_autoplay_enabled", 2281, false, false)); + properties.put(3682, new CompanionProperty("gif_min_play_loops", 3682, 1, 1)); + properties.put(3683, new CompanionProperty("gif_max_play_loops", 3683, 3, 3)); + properties.put(3684, new CompanionProperty("gif_max_play_duration", 3684, 5, 5)); + properties.put(1868, new CompanionProperty("web_send_only_active_receipts", 1868, false, true)); + properties.put(2461, new CompanionProperty("num_days_hosted_device_signed_identity_signature_expiration", 2461, 90, 90)); + properties.put(2521, new CompanionProperty("cag_member_key_rotation_optimization", 2521, false, false)); + properties.put(2540, new CompanionProperty("elevated_push_names_v2_enabled", 2540, false, false)); + properties.put(2763, new CompanionProperty("elevated_push_names_v2_m1_follow_up_enabled", 2763, false, false)); + properties.put(2904, new CompanionProperty("elevated_push_names_v2_m2_enabled", 2904, false, false)); + properties.put(2588, new CompanionProperty("smb_capi_coexistence_enabled", 2588, false, true)); + properties.put(2633, new CompanionProperty("smb_client_side_linkshim_enabled", 2633, true, true)); + properties.put(2508, new CompanionProperty("web_non_blocking_offline_resume_max_message_count", 2508, 1000.0, 1000.0)); + properties.put(1809, new CompanionProperty("web_unified_flow", 1809, 0, 0)); + properties.put(2634, new CompanionProperty("smb_client_side_linkshim_signed_regexp", 2634, "https:\\/\\/n\\.wl\\.co\\/[^/]*\\/[^/]*\\/(.*)$", "https:\\/\\/n\\.wl\\.co\\/[^/]*\\/[^/]*\\/(.*)$")); + properties.put(2639, new CompanionProperty("placeholder_message_key_hash_logging", 2639, false, true)); + properties.put(2795, new CompanionProperty("use_appdata_stanza_on_receiver", 2795, false, false)); + properties.put(2796, new CompanionProperty("use_appdata_stanza_on_sender", 2796, false, false)); + properties.put(2814, new CompanionProperty("web_lazy_pull", 2814, false, false)); + properties.put(3806, new CompanionProperty("msgd_drop_device_notifications", 3806, false, false)); + properties.put(3061, new CompanionProperty("media_large_file_awareness_popup_enabled", 3061, false, true)); + properties.put(3115, new CompanionProperty("media_large_file_awareness_popup_file_size_in_MB", 3115, 2048, 2048)); + properties.put(3069, new CompanionProperty("send_cag_member_revokes_as_GDM", 3069, true, true)); + properties.put(3079, new CompanionProperty("parent_group_remove_orphaned_members", 3079, false, true)); + properties.put(3292, new CompanionProperty("community_rich_system_message_enabled", 3292, false, false)); + properties.put(3097, new CompanionProperty("group_mentions_in_cag", 3097, false, true)); + properties.put(4087, new CompanionProperty("group_mentions_in_subgroups", 4087, false, true)); + properties.put(3267, new CompanionProperty("parent_group_home_header_actions_enabled", 3267, false, false)); + properties.put(3191, new CompanionProperty("non_blocking_resume_from_open_tab_enabled", 3191, false, false)); + properties.put(3622, new CompanionProperty("non_blocking_resume_from_open_tab_signal_enabled", 3622, false, false)); + properties.put(3247, new CompanionProperty("smb_catalog_messages_download_thumbnail_on_receiver_enabled", 3247, false, false)); + properties.put(3280, new CompanionProperty("send_extended_nack_enabled", 3280, false, false)); + properties.put(3741, new CompanionProperty("send_message_drop_nack_enabled", 3741, false, false)); + properties.put(4213, new CompanionProperty("send_message_drop_old_couter_nack_enabled", 4213, false, false)); + properties.put(3154, new CompanionProperty("parent_group_enhanced_description_enabled", 3154, false, true)); + properties.put(3616, new CompanionProperty("parent_group_info_updates_enabled", 3616, false, false)); + properties.put(3488, new CompanionProperty("noyb_opt_out_flag", 3488, false, false)); + properties.put(3664, new CompanionProperty("service_improvement_opt_out_flag", 3664, false, false)); + properties.put(3058, new CompanionProperty("wa_ctwa_web_entrypoint_home_header_enabled", 3058, false, false)); + properties.put(3095, new CompanionProperty("wa_ctwa_web_entrypoint_home_header_dropdown_enabled", 3095, false, false)); + properties.put(3096, new CompanionProperty("wa_ctwa_web_entrypoint_home_banner_enabled", 3096, false, false)); + properties.put(3242, new CompanionProperty("wa_ctwa_web_entrypoint_home_icon_tooltip_enabled", 3242, false, false)); + properties.put(3293, new CompanionProperty("wa_ctwa_web_entrypoint_pageless_enabled", 3293, false, false)); + properties.put(3376, new CompanionProperty("wa_ctwa_web_entrypoint_manage_ads_home_header_dropdown_enabled", 3376, false, false)); + properties.put(3294, new CompanionProperty("wa_ctwa_web_fetch_linked_accounts_enabled", 3294, false, false)); + properties.put(3695, new CompanionProperty("report_to_admin_kill_switch", 3695, false, true)); + properties.put(3696, new CompanionProperty("report_to_admin_enabled", 3696, false, true)); + properties.put(3829, new CompanionProperty("parent_group_allow_member_added_groups_m1", 3829, false, true)); + properties.put(4530, new CompanionProperty("parent_group_allow_member_added_groups_default_on_creation", 4530, false, true)); + properties.put(4184, new CompanionProperty("parent_group_allow_member_added_groups_m2", 4184, false, true)); + properties.put(5077, new CompanionProperty("parent_group_allow_member_suggest_existing_m3_sender", 5077, false, true)); + properties.put(5078, new CompanionProperty("parent_group_allow_member_suggest_existing_m3_receiver", 5078, false, true)); + properties.put(5562, new CompanionProperty("events_create", 5562, false, false)); + properties.put(5563, new CompanionProperty("events_view", 5563, false, false)); + properties.put(3224, new CompanionProperty("abort_building_e2e_proto_on_error", 3224, false, true)); + properties.put(4055, new CompanionProperty("abort_decrypting_e2e_on_error", 4055, false, true)); + properties.put(3966, new CompanionProperty("community_shorter_group_creation_enabled", 3966, false, false)); + properties.put(5103, new CompanionProperty("community_stacked_squircle_enabled", 5103, false, false)); + properties.put(5665, new CompanionProperty("community_general_chat_notification_followup_enabled", 5665, false, false)); + properties.put(5169, new CompanionProperty("community_navigate_to_unread_subgroup_enabled", 5169, false, false)); + properties.put(4003, new CompanionProperty("community_navigation", 4003, false, false)); + properties.put(4852, new CompanionProperty("community_examples", 4852, false, true)); + properties.put(5453, new CompanionProperty("community_general_chat_create_enabled", 5453, false, true)); + properties.put(5021, new CompanionProperty("community_general_chat_UI_enabled", 5021, false, true)); + properties.put(5144, new CompanionProperty("community_general_chat_max_auto_add_users", 5144, 1024, 1024)); + properties.put(4010, new CompanionProperty("bonsai_enabled", 4010, false, false)); + properties.put(5362, new CompanionProperty("bonsai_entry_point_enabled", 5362, false, false)); + properties.put(5459, new CompanionProperty("bonsai_waitlist_enabled", 5459, false, false)); + properties.put(4165, new CompanionProperty("bonsai_receiver_enabled", 4165, false, false)); + properties.put(5246, new CompanionProperty("bonsai_inline_feedback_enabled", 5246, false, false)); + properties.put(4416, new CompanionProperty("bonsai_ptt_enabled", 4416, false, false)); + properties.put(4417, new CompanionProperty("bonsai_update_interval", 4417, 86400, 86400)); + properties.put(5413, new CompanionProperty("bonsai_waitlist_update_interval", 5413, 21600, 21600)); + properties.put(4532, new CompanionProperty("bonsai_avatar_enabled", 4532, false, false)); + properties.put(4736, new CompanionProperty("bonsai_ti_timeout_duration_ms", 4736, 10000.0, 10000.0)); + properties.put(4974, new CompanionProperty("bonsai_word_streaming_enabled", 4974, false, false)); + properties.put(5150, new CompanionProperty("bonsai_streaming_chunk_latency", 5150, 0, 0)); + properties.put(5268, new CompanionProperty("bonsai_streaming_line_count_for_pinning", 5268, 4, 4)); + properties.put(5283, new CompanionProperty("bonsai_carousel_enabled", 5283, false, true)); + properties.put(5637, new CompanionProperty("bonsai_english_only", 5637, false, false)); + properties.put(4206, new CompanionProperty("web_mediaretry_notification_nack_enabled", 4206, false, false)); + properties.put(4274, new CompanionProperty("bot_response_futureproof_message_enabled", 4274, false, true)); + properties.put(4836, new CompanionProperty("low_cache_hit_rate_media_types", 4836, "ptt,audio,document,ppic", "ptt,audio,document,ppic")); + properties.put(2898, new CompanionProperty("wa_ctwa_web_thread_ad_attribution_enabled", 2898, false, false)); + properties.put(1495, new CompanionProperty("wa_ctwa_ads_action_banner_enabled", 1495, false, true)); + properties.put(4021, new CompanionProperty("wa_ctwa_ads_action_banner_enabled_web", 4021, false, true)); + properties.put(4022, new CompanionProperty("wa_ctwa_action_banner_logging_enabled_web", 4022, false, true)); + properties.put(1841, new CompanionProperty("ctwa_data_max_length", 1841, 768, 768)); + properties.put(1866, new CompanionProperty("wa_ctwa_action_banner_logging_enabled", 1866, false, true)); + properties.put(2487, new CompanionProperty("wa_ctwa_web_dc_logging_enabled", 2487, false, false)); + properties.put(2934, new CompanionProperty("ctwa_smb_data_sharing_consent", 2934, false, true)); + properties.put(5615, new CompanionProperty("ctwa_smb_data_sharing_settings_killswitch", 5615, false, false)); + properties.put(3331, new CompanionProperty("ctwa_smb_data_sharing_opt_in_cool_off_period", 3331, 259200, 259200)); + properties.put(2935, new CompanionProperty("ctwa_consumer_data_sharing_consent", 2935, false, true)); + properties.put(2936, new CompanionProperty("mark_as_action", 2936, false, true)); + properties.put(3017, new CompanionProperty("pairless_logging_attribution_window", 3017, 7, 7)); + properties.put(3169, new CompanionProperty("wa_biz_tool_logging_improvement", 3169, false, true)); + properties.put(3793, new CompanionProperty("ctwa_additional_label_event_logging_enabled", 3793, false, true)); + properties.put(4761, new CompanionProperty("ctwa_enhanced_label_logging", 4761, false, true)); + properties.put(5151, new CompanionProperty("ctwa_clear_tracking", 5151, false, false)); + properties.put(4542, new CompanionProperty("in_app_comms_manage_ads_web_banner_campaign_enabled", 4542, false, true)); + properties.put(4427, new CompanionProperty("business_tool_enhanced_logging", 4427, false, false)); + properties.put(4796, new CompanionProperty("ctwa_value_holdout_h2_23_enabled", 4796, false, false)); + properties.put(5009, new CompanionProperty("smb_labels_ctwa_data_sharing", 5009, false, true)); + properties.put(5324, new CompanionProperty("smb_message_labels_ctwa_data_sharing", 5324, false, true)); + properties.put(5463, new CompanionProperty("smb_label_improvements_m2", 5463, false, true)); + properties.put(5554, new CompanionProperty("ctwa_manage_ads_tab_web", 5554, false, true)); + properties.put(5671, new CompanionProperty("ctwa_quick_reply_labels", 5671, false, false)); + properties.put(5719, new CompanionProperty("smb_business_action_bar_enabled", 5719, false, false)); + properties.put(1912, new CompanionProperty("ig_reels_music_attribution", 1912, false, true)); + properties.put(2167, new CompanionProperty("video_stream_buffering_ui_enabled", 2167, false, true)); + properties.put(3068, new CompanionProperty("original_quality_image_min_edge", 3068, 2560, 2560)); + properties.put(3306, new CompanionProperty("original_quality_data_warning_max_mb", 3306, 16, 16)); + properties.put(3307, new CompanionProperty("original_quality_show_data_warning_dialog", 3307, false, true)); + properties.put(3613, new CompanionProperty("original_quality_minimum_elements_to_show_data_warning_dialog", 3613, 20, 20)); + properties.put(2915, new CompanionProperty("maximum_group_size_for_rcat", 2915, 100, 100)); + properties.put(2957, new CompanionProperty("web_youtube_rcat_consumption_enabled", 2957, false, true)); + properties.put(3044, new CompanionProperty("web_youtube_rcat_chat_generation_enabled", 3044, false, true)); + properties.put(5178, new CompanionProperty("force_transcode_videos", 5178, false, false)); + properties.put(5179, new CompanionProperty("force_transcode_photos", 5179, false, false)); + properties.put(3273, new CompanionProperty("autodownload_update_in_group_chat", 3273, true, true)); + properties.put(5517, new CompanionProperty("autodownload_update_in_one_one_chat", 5517, false, true)); + properties.put(3116, new CompanionProperty("enable_receiving_hd_photo_quality", 3116, false, true)); + properties.put(3322, new CompanionProperty("enable_days_since_receive_logging", 3322, false, true)); + properties.put(3490, new CompanionProperty("additional_pre_logging_enabled", 3490, false, true)); + properties.put(3820, new CompanionProperty("client_message_id_media_download_log_enabled", 3820, false, true)); + properties.put(3491, new CompanionProperty("media_sender_client_logging_enabled", 3491, false, true)); + properties.put(3349, new CompanionProperty("hqp_log_enabled", 3349, false, true)); + properties.put(3455, new CompanionProperty("web_fix_media_conn_block_rule_parsing", 3455, false, false)); + properties.put(3522, new CompanionProperty("youtube_inline_playback_killswitch", 3522, false, false)); + properties.put(3787, new CompanionProperty("media_engagement_logging_enabled", 3787, false, false)); + properties.put(3844, new CompanionProperty("show_bottom_sheet_gallery", 3844, false, true)); + properties.put(4538, new CompanionProperty("max_pixels_size_allowed_for_image", 4538, 921600, 921600)); + properties.put(4631, new CompanionProperty("fun_stickers_locale_langs", 4631, "en", "en")); + properties.put(4643, new CompanionProperty("fun_stickers_phase2_enabled", 4643, false, false)); + properties.put(5582, new CompanionProperty("enable_media_view_reply", 5582, false, true)); + properties.put(1522, new CompanionProperty("status_inline_link_preview_enabled", 1522, false, true)); + properties.put(1851, new CompanionProperty("text_status_url_logging_enabled", 1851, false, true)); + properties.put(1852, new CompanionProperty("status_reaction_emojis", 1852, "[128525, 128514, 128558, 128546, 128591, 128079, 127881, 128175]", "[128525, 128514, 128558, 128546, 128591, 128079, 127881, 128175]")); + properties.put(1859, new CompanionProperty("status_reply_received_logging_enabled", 1859, false, true)); + properties.put(2032, new CompanionProperty("status_caption_link_detection_enabled", 2032, false, true)); + properties.put(2086, new CompanionProperty("status_view_error_type_logging_enabled", 2086, true, true)); + properties.put(2039, new CompanionProperty("status_from_me_unseen_enabled", 2039, false, true)); + properties.put(451, new CompanionProperty("smb_collections_enabled", 451, false, true)); + properties.put(582, new CompanionProperty("consumer_collections_enabled", 582, false, true)); + properties.put(724, new CompanionProperty("smb_collections_appeal_flow_enabled", 724, false, false)); + properties.put(1074, new CompanionProperty("smb_multi_device_awareness", 1074, false, true)); + properties.put(875, new CompanionProperty("smb_quick_replies_v2_enabled", 875, false, false)); + properties.put(1003, new CompanionProperty("smb_ecommerce_compliance_india_m4", 1003, false, true)); + properties.put(1192, new CompanionProperty("smb_ecommerce_compliance_india_m4_5", 1192, false, true)); + properties.put(1015, new CompanionProperty("smart_filters_enabled", 1015, false, true)); + properties.put(1022, new CompanionProperty("btm_threads_logging_enabled", 1022, false, true)); + properties.put(1034, new CompanionProperty("native_commerce_threads_logging_enabled", 1034, false, true)); + properties.put(1168, new CompanionProperty("threads_logging_observe_list_enabled", 1168, false, true)); + properties.put(1203, new CompanionProperty("smb_hide_unsupported_currency_price", 1203, false, true)); + properties.put(1215, new CompanionProperty("hyperlinked_phone_numbers_enabled", 1215, false, false)); + properties.put(1229, new CompanionProperty("smb_catkit_query_version", 1229, 1, 1)); + properties.put(1263, new CompanionProperty("smb_phase_out_not_a_business", 1263, false, true)); + properties.put(1771, new CompanionProperty("smb_phase_out_not_a_business_V2", 1771, false, true)); + properties.put(1251, new CompanionProperty("smb_threads_logging_enabled", 1251, false, true)); + properties.put(1252, new CompanionProperty("smb_click_to_chat_logging_enabled", 1252, false, true)); + properties.put(1253, new CompanionProperty("smb_broadcast_logging_enabled", 1253, false, true)); + properties.put(1254, new CompanionProperty("smb_status_logging_enabled", 1254, false, true)); + properties.put(1255, new CompanionProperty("smb_biz_profile_logging_enabled", 1255, false, true)); + properties.put(1256, new CompanionProperty("smb_registration_flow_logging_enabled", 1256, false, true)); + properties.put(1272, new CompanionProperty("btm_qpl_enabled", 1272, false, true)); + properties.put(1913, new CompanionProperty("smb_temp_cover_photo_privacy_messaging", 1913, false, true)); + properties.put(1949, new CompanionProperty("show_shops_sunset_banner", 1949, false, true)); + properties.put(3961, new CompanionProperty("vname_logging_and_debugging", 3961, true, true)); + properties.put(3969, new CompanionProperty("verified_business_numbers", 3969, "{}", """ + {"paytm":[917531875318, 919004990049]}""")); + properties.put(4006, new CompanionProperty("verified_business_numbers_for_business_name_update", 4006, "", "917531875318,919004990049")); + properties.put(5001, new CompanionProperty("vname_cert_deprecation", 5001, false, true)); + properties.put(5383, new CompanionProperty("enable_coex_system_message", 5383, false, true)); + properties.put(212, new CompanionProperty("qpl_enabled", 212, false, true)); + properties.put(215, new CompanionProperty("qpl_upload_delay", 215, 1440, 1)); + properties.put(466, new CompanionProperty("qpl_sampling_as_string", 466, """ + json:{"sampling":[]}""", """ + json:{"sampling":[]}""")); + properties.put(1223, new CompanionProperty("qpl_initial_upload_delay", 1223, 5, 1)); + properties.put(1570, new CompanionProperty("is_meta_employee", 1570, false, false)); + properties.put(383, new CompanionProperty("should_deregister_on_syncd_fatal", 383, true, true)); + properties.put(559, new CompanionProperty("group_catch_up", 559, false, false)); + properties.put(591, new CompanionProperty("web_abprop_ctwa_context_compose_enabled", 591, false, false)); + properties.put(592, new CompanionProperty("web_abprop_group_description_length", 592, 0, 0)); + properties.put(593, new CompanionProperty("web_abprop_ephemeral_messages_allowed_values", 593, "604800", "604800")); + properties.put(584, new CompanionProperty("web_abprop_collections_display", 584, false, false)); + properties.put(2312, new CompanionProperty("multi_select_from_chat_list", 2312, false, true)); + properties.put(585, new CompanionProperty("web_abprop_collections_management", 585, false, false)); + properties.put(600, new CompanionProperty("web_abprop_drop_full_history_sync", 600, false, false)); + properties.put(710, new CompanionProperty("web_abprop_business_profile_incomplete_nux_banner", 710, false, false)); + properties.put(711, new CompanionProperty("web_abprop_product_catalog_nux_banner", 711, false, false)); + properties.put(712, new CompanionProperty("web_abprop_click_nux_banner_migration", 712, false, false)); + properties.put(717, new CompanionProperty("web_abprop_ecommerce_compliance_india", 717, false, false)); + properties.put(826, new CompanionProperty("web_abprop_edit_ecommerce_compliance_india", 826, false, false)); + properties.put(726, new CompanionProperty("drop_last_name", 726, false, false)); + properties.put(734, new CompanionProperty("web_abprop_catalog_icon_on_top_bar", 734, false, false)); + properties.put(741, new CompanionProperty("web_abprop_collections_nux_banner", 741, false, false)); + properties.put(760, new CompanionProperty("nfm_rendering_enabled", 760, false, false)); + properties.put(761, new CompanionProperty("web_abprop_nux_cart_interstitial", 761, false, false)); + properties.put(763, new CompanionProperty("web_abprop_business_profile_refresh_status_enabled", 763, false, false)); + properties.put(764, new CompanionProperty("web_abprop_business_profile_refresh_linked_account_enabled", 764, false, false)); + properties.put(765, new CompanionProperty("web_abprop_business_profile_refresh_edit_cover_photo_enabled", 765, false, false)); + properties.put(766, new CompanionProperty("web_abprop_business_profile_refresh_cover_photo_view_enabled", 766, false, false)); + properties.put(809, new CompanionProperty("elevated_important_msg", 809, false, false)); + properties.put(837, new CompanionProperty("web_privacy_settings", 837, false, false)); + properties.put(1226, new CompanionProperty("web_privacy_settings_v2", 1226, false, false)); + properties.put(873, new CompanionProperty("web_status_psa", 873, false, false)); + properties.put(1095, new CompanionProperty("web_status_psa_history_sync", 1095, false, false)); + properties.put(1195, new CompanionProperty("web_2fa", 1195, false, false)); + properties.put(887, new CompanionProperty("web_abprop_stateful_enumeration_enabled", 887, true, true)); + properties.put(894, new CompanionProperty("web_abprop_block_catalog_creation_ecommerce_compliance_india", 894, false, false)); + properties.put(930, new CompanionProperty("web_sticker_store", 930, true, true)); + properties.put(937, new CompanionProperty("web_proactive_prekeys_fetch_group_size_limit", 937, 0, 0)); + properties.put(962, new CompanionProperty("web_favorite_stickers", 962, false, false)); + properties.put(984, new CompanionProperty("web_orchestrator_enabled_version", 984, "bucket", "bucket")); + properties.put(1033, new CompanionProperty("web_wam_v5_enabled", 1033, false, false)); + properties.put(1114, new CompanionProperty("web_ps_v3_enabled", 1114, false, false)); + properties.put(1053, new CompanionProperty("web_shop_storefront_message", 1053, false, false)); + properties.put(1078, new CompanionProperty("web_identity_store_cache", 1078, false, false)); + properties.put(1086, new CompanionProperty("web_abprop_large_files_encryption_optimization", 1086, false, false)); + properties.put(1099, new CompanionProperty("web_send_invisible_msg_to_new_groups", 1099, false, false)); + properties.put(1100, new CompanionProperty("web_send_invisible_msg_min_group_size", 1100, 128, 128)); + properties.put(1945, new CompanionProperty("web_send_invisible_msg_max_group_size", 1945, 1024, 1024)); + properties.put(1171, new CompanionProperty("web_init_chat_batch_size", 1171, 100, 100)); + properties.put(1172, new CompanionProperty("web_init_chat_max_unread_message_count", 1172, 0, 0)); + properties.put(1174, new CompanionProperty("web_abprop_skip_file_copy_on_attach", 1174, false, false)); + properties.put(1179, new CompanionProperty("reaction_history_sync", 1179, false, false)); + properties.put(1189, new CompanionProperty("web_abprop_screen_sharing_enabled", 1189, false, false)); + properties.put(1205, new CompanionProperty("web_graphql_for_catalog_m1", 1205, false, false)); + properties.put(1224, new CompanionProperty("web_adaptive_offline_resume_enabled", 1224, false, false)); + properties.put(1225, new CompanionProperty("web_wa_signal_enabled", 1225, false, false)); + properties.put(1232, new CompanionProperty("web_gdpr_request_account_info_enabled", 1232, false, false)); + properties.put(1247, new CompanionProperty("web_abprop_document_resume_upload", 1247, false, false)); + properties.put(1759, new CompanionProperty("more_reactions_option_desktop_beta_rollout", 1759, true, true)); + properties.put(1796, new CompanionProperty("reactions_keyboard_hides_three_flags_desktop_beta_rollout", 1796, false, false)); + properties.put(1329, new CompanionProperty("web_rotate_sender_key_if_sent", 1329, false, false)); + properties.put(1383, new CompanionProperty("web_lru_cache_purge_logic_refactor", 1383, false, false)); + properties.put(1367, new CompanionProperty("companion_min_versions", 1367, "json:[]", "json:[]")); + properties.put(1368, new CompanionProperty("comparion_force_upgrade", 1368, false, false)); + properties.put(1351, new CompanionProperty("web_abprop_business_profile_refresh_linked_accounts_killswitch", 1351, false, false)); + properties.put(1355, new CompanionProperty("web_default_pull_mode_enabled", 1355, false, false)); + properties.put(1371, new CompanionProperty("web_abprop_chatd_login_cookie_enabled", 1371, false, false)); + properties.put(1373, new CompanionProperty("web_prekeys_fetch_first_batch_size", 1373, 0, 0)); + properties.put(1379, new CompanionProperty("md_app_state_gate_D34336913", 1379, false, false)); + properties.put(1385, new CompanionProperty("web_address_capture_message_enabled", 1385, false, false)); + properties.put(1400, new CompanionProperty("syncd_periodic_sync_days", 1400, 0, 0)); + properties.put(1401, new CompanionProperty("web_enable_hyperlinked_phone_numbers_ps_logging", 1401, false, false)); + properties.put(1451, new CompanionProperty("web_get_maybe_me_user_optimization_enabled", 1451, false, false)); + properties.put(1461, new CompanionProperty("web_should_fatal_on_missing_patch", 1461, true, true)); + properties.put(1479, new CompanionProperty("web_reactions_send_desktop_beta_rollout", 1479, true, true)); + properties.put(1481, new CompanionProperty("web_abprop_remove_uploaded_files", 1481, false, false)); + properties.put(1496, new CompanionProperty("web_abprop_remove_downloaded_files", 1496, false, false)); + properties.put(2879, new CompanionProperty("web_killswitch_s310872_mitigation", 2879, false, false)); + properties.put(1507, new CompanionProperty("web_new_rich_text_input", 1507, true, true)); + properties.put(1513, new CompanionProperty("web_syncd_max_mutations_to_process_during_resume", 1513, 1000.0, 1000.0)); + properties.put(1593, new CompanionProperty("reactions_skin_tone_aggregation", 1593, false, false)); + properties.put(1623, new CompanionProperty("message_quick_reply", 1623, false, false)); + properties.put(1659, new CompanionProperty("web_quantity_controls_enabled", 1659, false, false)); + properties.put(1633, new CompanionProperty("web_unified_message_processing_enabled", 1633, false, false)); + properties.put(1643, new CompanionProperty("web_push_notifications", 1643, false, true)); + properties.put(3868, new CompanionProperty("web_push_notifications_super_users", 3868, false, false)); + properties.put(1676, new CompanionProperty("web_notification_settings_v2", 1676, false, true)); + properties.put(1675, new CompanionProperty("web_abprop_device_agnostic_voip", 1675, false, false)); + properties.put(1680, new CompanionProperty("web_abprop_screen_lock_enabled", 1680, false, false)); + properties.put(4790, new CompanionProperty("web_abprop_screen_lock_show_learn_more_link", 4790, false, false)); + properties.put(1726, new CompanionProperty("web_command_palette", 1726, true, true)); + properties.put(1745, new CompanionProperty("web_group_profile_editor", 1745, true, true)); + properties.put(4901, new CompanionProperty("web_group_profile_picture_stickers_label_fix", 4901, false, true)); + properties.put(1751, new CompanionProperty("web_quick_reply_authoring", 1751, false, false)); + properties.put(1752, new CompanionProperty("web_accidental_delete_for_me", 1752, true, true)); + properties.put(1753, new CompanionProperty("web_abprop_core_wam_runtime", 1753, false, false)); + properties.put(1757, new CompanionProperty("web_profile_picture_db_cache_disabled", 1757, false, false)); + properties.put(1773, new CompanionProperty("web_offline_resume_qpl_enabled", 1773, false, false)); + properties.put(1802, new CompanionProperty("web_offline_resume_m3_enabled", 1802, false, false)); + properties.put(1808, new CompanionProperty("web_syncd_fatal_fields_from_L1104589PRV2", 1808, false, false)); + properties.put(1816, new CompanionProperty("web_media_editor_blur_tool", 1816, true, true)); + properties.put(1824, new CompanionProperty("web_abprop_mute_notifications_on_app_focus", 1824, false, true)); + properties.put(2533, new CompanionProperty("web_auto_mute_256_groups_confirmation", 2533, false, false)); + properties.put(1850, new CompanionProperty("web_multi_skin_toned_emoji_picker", 1850, false, false)); + properties.put(1894, new CompanionProperty("web_message_send_cache_warming_up", 1894, false, true)); + properties.put(2801, new CompanionProperty("web_message_send_precalculate_icdc", 2801, false, true)); + properties.put(1902, new CompanionProperty("web_ptt_streamer_upload", 1902, false, true)); + properties.put(1910, new CompanionProperty("web_prekey_fetch_cache_warming_up", 1910, false, true)); + properties.put(1911, new CompanionProperty("web_history_sync_ui", 1911, false, false)); + properties.put(1932, new CompanionProperty("web_abprop_emoji_experimental_api", 1932, false, false)); + properties.put(1959, new CompanionProperty("web_new_media_caption_input", 1959, true, true)); + properties.put(1964, new CompanionProperty("web_chatlist_toggle", 1964, false, true)); + properties.put(1985, new CompanionProperty("web_electron_deprecation_windows_sideload_stage1_awareness", 1985, false, false)); + properties.put(1986, new CompanionProperty("web_electron_deprecation_windows_sideload_stage2_compatible_expiry_kickoff", 1986, false, false)); + properties.put(1987, new CompanionProperty("web_electron_deprecation_windows_sideload_stage2_compatible_expiry_delay", 1987, 0, 0)); + properties.put(1988, new CompanionProperty("web_electron_deprecation_windows_sideload_stage2_incompatible_expiry_kickoff", 1988, false, false)); + properties.put(1989, new CompanionProperty("web_electron_deprecation_windows_sideload_stage2_incompatible_expiry_delay", 1989, 0, 0)); + properties.put(5101, new CompanionProperty("web_electron_deprecation_mac_appstore_stage_1_awareness", 5101, false, false)); + properties.put(5018, new CompanionProperty("web_electron_deprecation_mac_sideload_stage_1_awareness", 5018, false, false)); + properties.put(5294, new CompanionProperty("web_electron_deprecation_mac_sideload_stage_1_bbar_dismiss_duration_days", 5294, 7, 7)); + properties.put(5019, new CompanionProperty("web_electron_deprecation_mac_sideload_stage_2_expiry_kickoff", 5019, false, false)); + properties.put(5020, new CompanionProperty("web_electron_deprecation_mac_sideload_stage_2_expiry_delay", 5020, 0, 0)); + properties.put(5364, new CompanionProperty("web_electron_deprecation_mac_appstore_stage_2_expiry_kickoff", 5364, false, false)); + properties.put(5365, new CompanionProperty("web_electron_deprecation_mac_appstore_stage_2_expiry_delay", 5365, 0, 0)); + properties.put(2016, new CompanionProperty("web_message_list_a11y_redesign", 2016, true, true)); + properties.put(2018, new CompanionProperty("web_enable_profile_pic_thumb_db_caching", 2018, false, false)); + properties.put(4327, new CompanionProperty("web_enable_profile_pic_thumb_download_over_mms4", 4327, false, false)); + properties.put(2056, new CompanionProperty("web_enable_biz_catalog_view_ps_logging", 2056, true, true)); + properties.put(2063, new CompanionProperty("web_abprop_media_links_docs_search", 2063, false, false)); + properties.put(2179, new CompanionProperty("web_poll_creation_desktop_beta_rollout", 2179, false, false)); + properties.put(2181, new CompanionProperty("web_poll_receiving_desktop_beta_rollout", 2181, false, false)); + properties.put(2210, new CompanionProperty("web_file_streaming_upload", 2210, false, false)); + properties.put(2220, new CompanionProperty("web_new_group_member_search", 2220, false, false)); + properties.put(2264, new CompanionProperty("web_max_contacts_to_show_common_groups", 2264, 10, 10)); + properties.put(2268, new CompanionProperty("web_max_found_common_groups_displayed", 2268, 15, 15)); + properties.put(2231, new CompanionProperty("web_fp_reparsing_for_non_add_ons", 2231, false, false)); + properties.put(2280, new CompanionProperty("web_message_custom_aria_label", 2280, false, false)); + properties.put(2294, new CompanionProperty("web_message_list_a11y_redesign_beta_only", 2294, true, true)); + properties.put(4768, new CompanionProperty("web_search_by_type_date_infra", 4768, false, false)); + properties.put(4769, new CompanionProperty("web_search_by_type_enabled", 4769, false, false)); + properties.put(4770, new CompanionProperty("web_search_by_date_enabled", 4770, false, false)); + properties.put(2303, new CompanionProperty("web_poll_spam_report", 2303, false, false)); + properties.put(2322, new CompanionProperty("web_electron_active_reload", 2322, true, true)); + properties.put(2348, new CompanionProperty("desktop_upsell_win_butterbar", 2348, false, false)); + properties.put(2349, new CompanionProperty("desktop_upsell_win_ctas", 2349, false, false)); + properties.put(2725, new CompanionProperty("desktop_upsell_win_dropdown_btn", 2725, false, false)); + properties.put(4802, new CompanionProperty("desktop_upsell_win_temporary_ctas", 4802, false, false)); + properties.put(4803, new CompanionProperty("desktop_upsell_mac_temporary_ctas", 4803, false, false)); + properties.put(4804, new CompanionProperty("desktop_upsell_win_permanent_ctas", 4804, false, false)); + properties.put(4805, new CompanionProperty("desktop_upsell_mac_permanent_ctas", 4805, false, false)); + properties.put(5145, new CompanionProperty("desktop_upsell_win_cta_chatlist_dropdown", 5145, false, false)); + properties.put(5146, new CompanionProperty("desktop_upsell_win_cta_chatlist_toastbar", 5146, false, false)); + properties.put(5147, new CompanionProperty("desktop_upsell_win_cta_search_results_toastbar", 5147, false, false)); + properties.put(5148, new CompanionProperty("desktop_upsell_win_cta_call_btn", 5148, false, false)); + properties.put(5149, new CompanionProperty("desktop_upsell_win_cta_intro_panel", 5149, false, false)); + properties.put(2486, new CompanionProperty("documents_with_captions_send_desktop_beta_rollout", 2486, false, false)); + properties.put(2512, new CompanionProperty("profile_photo_rings_for_status_on_web_enabled", 2512, false, true)); + properties.put(2513, new CompanionProperty("voice_status_receipt_on_web_enabled", 2513, false, true)); + properties.put(2534, new CompanionProperty("web_crypto_library_enabled", 2534, false, false)); + properties.put(5320, new CompanionProperty("web_crypto_library_verification_enabled", 5320, false, false)); + properties.put(4583, new CompanionProperty("web_crypto_library_with_queues_enabled", 4583, false, false)); + properties.put(2543, new CompanionProperty("group_chat_profile_pictures_enabled_web_beta_rollout", 2543, true, true)); + properties.put(2545, new CompanionProperty("web_message_plugin_backend_registration_enabled", 2545, false, false)); + properties.put(2549, new CompanionProperty("query_verified_name_when_msg_differs", 2549, true, true)); + properties.put(2555, new CompanionProperty("web_media_auto_download_enabled", 2555, false, true)); + properties.put(2556, new CompanionProperty("web_media_auto_download_desktop_beta_enabled", 2556, false, true)); + properties.put(2566, new CompanionProperty("link_preview_wait_time", 2566, 7, 7)); + properties.put(2622, new CompanionProperty("web_screen_lock_max_retries", 2622, 10, 10)); + properties.put(2664, new CompanionProperty("forward_media_with_caption_desktop_beta_rollout", 2664, true, true)); + properties.put(2708, new CompanionProperty("web_new_status_reply_input", 2708, true, true)); + properties.put(2715, new CompanionProperty("web_display_name_for_enterprise_biz_vlevel_low_killswitch", 2715, false, false)); + properties.put(2716, new CompanionProperty("web_display_name_for_biz_vlevel_low_killswitch", 2716, true, true)); + properties.put(2793, new CompanionProperty("web_message_plugin_frontend_registration_enabled", 2793, false, false)); + properties.put(3081, new CompanionProperty("external_beta_can_join", 3081, false, true)); + properties.put(3031, new CompanionProperty("web_native_fetch_media_download", 3031, false, false)); + properties.put(3042, new CompanionProperty("web_image_max_edge", 3042, 1600, 1600)); + properties.put(3204, new CompanionProperty("web_image_max_hd_edge", 3204, 2560, 2560)); + properties.put(3118, new CompanionProperty("enable_logging_multi_select_from_chat_list", 3118, false, true)); + properties.put(3133, new CompanionProperty("web_store_quota_manager_enabled", 3133, false, false)); + properties.put(4670, new CompanionProperty("web_chat_with_unknown_contacts", 4670, false, false)); + properties.put(3134, new CompanionProperty("web_browser_quota_threshold", 3134, 100, 100)); + properties.put(3135, new CompanionProperty("web_browser_min_storage_quota", 3135, 5, 5)); + properties.put(3136, new CompanionProperty("web_original_photo_quality_upload_enabled", 3136, false, false)); + properties.put(3152, new CompanionProperty("web_deprecate_mms4_hash_based_download", 3152, false, true)); + properties.put(3164, new CompanionProperty("web_md5_message_key", 3164, false, true)); + properties.put(3729, new CompanionProperty("web_sha256_message_key", 3729, true, true)); + properties.put(3234, new CompanionProperty("web_e2e_backfill_expire_time", 3234, 5, 60)); + properties.put(3279, new CompanionProperty("web_message_table_index_rowid_optimization", 3279, false, false)); + properties.put(3350, new CompanionProperty("wds_radius_and_casing", 3350, false, true)); + properties.put(4032, new CompanionProperty("web_attach_menu_redesign", 4032, false, true)); + properties.put(3420, new CompanionProperty("web_expression_panels", 3420, false, false)); + properties.put(3600, new CompanionProperty("can_support_web_column_packing", 3600, false, false)); + properties.put(3970, new CompanionProperty("web_column_data_serialization_enabled", 3970, false, false)); + properties.put(3973, new CompanionProperty("column_serialization_perf_impact_test", 3973, false, false)); + properties.put(3723, new CompanionProperty("web_message_edit_receive_desktop_beta_rollout", 3723, false, false)); + properties.put(3724, new CompanionProperty("web_message_edit_send_desktop_beta_rollout", 3724, false, false)); + properties.put(3883, new CompanionProperty("web_message_edit_processing_reply_messages", 3883, true, true)); + properties.put(3728, new CompanionProperty("web_message_processing_cache_size", 3728, 400, 400)); + properties.put(3779, new CompanionProperty("web_encryption_failed_message_resend", 3779, false, false)); + properties.put(3818, new CompanionProperty("append_message_when_forwarding_media_desktop_beta", 3818, false, false)); + properties.put(3890, new CompanionProperty("web_client_pull_timeout_ms", 3890, 10000.0, 10000.0)); + properties.put(3892, new CompanionProperty("web_socket_reconnect_enabled", 3892, false, false)); + properties.put(4019, new CompanionProperty("web_outgoing_message_validation_list", 4019, "[]", "[]")); + properties.put(4024, new CompanionProperty("web_device_sync_manager_enabled", 4024, false, false)); + properties.put(4453, new CompanionProperty("web_device_sync_manager_group_enabled", 4453, false, false)); + properties.put(4125, new CompanionProperty("web_draft_message_enabled", 4125, false, false)); + properties.put(4149, new CompanionProperty("history_sync_loop_interval_ms", 4149, 20000.0, 20000.0)); + properties.put(4364, new CompanionProperty("history_sync_on_demand_failure_limit", 4364, 10, 10)); + properties.put(4365, new CompanionProperty("history_sync_on_demand_cooldown_sec", 4365, 7200, 7200)); + properties.put(4366, new CompanionProperty("history_sync_on_demand_request_send_killswitch", 4366, true, true)); + properties.put(4390, new CompanionProperty("flattened_reactions_collection", 4390, false, false)); + properties.put(4403, new CompanionProperty("web_offline_notification_priority", 4403, false, false)); + properties.put(4404, new CompanionProperty("web_status_posting_enabled", 4404, false, false)); + properties.put(4475, new CompanionProperty("unified_pin_addon_infra_enabled", 4475, false, false)); + properties.put(4483, new CompanionProperty("web_enable_open_tab_pre_ack", 4483, false, false)); + properties.put(4669, new CompanionProperty("web_improved_text_tool_enabled", 4669, false, false)); + properties.put(4681, new CompanionProperty("web_internal_in_app_bug_reporting_enable", 4681, false, false)); + properties.put(4726, new CompanionProperty("web_sticker_suggestions_enable", 4726, false, false)); + properties.put(4724, new CompanionProperty("web_enable_capi_support_chat", 4724, false, false)); + properties.put(4792, new CompanionProperty("web_device_switching", 4792, false, true)); + properties.put(4904, new CompanionProperty("web_initial_sync_encrypted_msgs_storing", 4904, false, false)); + properties.put(4951, new CompanionProperty("web_expression_panels_mitigations", 4951, false, true)); + properties.put(4952, new CompanionProperty("web_animate_messages", 4952, false, false)); + properties.put(5523, new CompanionProperty("web_animate_new_messages", 5523, false, true)); + properties.put(4973, new CompanionProperty("web_improved_message_composer_enabled", 4973, false, false)); + properties.put(5079, new CompanionProperty("web_preload_chat_messages", 5079, false, true)); + properties.put(5080, new CompanionProperty("web_anyone_can_add_group_setting_enabled", 5080, false, true)); + properties.put(5100, new CompanionProperty("web_command_palette_plugins", 5100, false, false)); + properties.put(5106, new CompanionProperty("web_noncritical_history_sync_message_processing_break_iteration", 5106, 100, 100)); + properties.put(5110, new CompanionProperty("web_tc_token_db_read_enabled", 5110, false, false)); + properties.put(5164, new CompanionProperty("web_invalid_message_count_validation", 5164, false, true)); + properties.put(5165, new CompanionProperty("web_invalid_media_message_validation", 5165, false, true)); + properties.put(5271, new CompanionProperty("web_offline_dynamic_batch_size_enabled", 5271, false, true)); + properties.put(5297, new CompanionProperty("web_offline_dynamic_batch_config", 5297, """ + {"denominator": 2}""", """ + {"denominator": 2}""")); + properties.put(5291, new CompanionProperty("web_history_sync_notification_handling_queue_v2", 5291, false, false)); + properties.put(5346, new CompanionProperty("web_evolve_about_receive_enabled", 5346, false, false)); + properties.put(5347, new CompanionProperty("web_evolve_about_send_enabled", 5347, false, false)); + properties.put(5388, new CompanionProperty("web_abort_building_e2e_proto_on_error", 5388, false, true)); + properties.put(5389, new CompanionProperty("web_abort_decrypting_e2e_on_error", 5389, false, true)); + properties.put(5410, new CompanionProperty("web_offline_progress_toastbar", 5410, false, true)); + properties.put(5447, new CompanionProperty("web_quoted_generate_msg_data", 5447, false, false)); + properties.put(5461, new CompanionProperty("web_resume_optimized_read_receipt_send_active_chat", 5461, false, true)); + properties.put(5502, new CompanionProperty("web_resume_optimized_read_receipt_send_interval", 5502, 500, 500)); + properties.put(5520, new CompanionProperty("web_pre_acks_m2_enabled", 5520, false, false)); + properties.put(5521, new CompanionProperty("web_pre_acks_m3_enabled", 5521, false, false)); + properties.put(5564, new CompanionProperty("web_push_notifications_receipt_handling_enabled", 5564, false, false)); + properties.put(5565, new CompanionProperty("desktop_upsell_mac_cta_chatlist_dropdown", 5565, false, false)); + properties.put(5566, new CompanionProperty("desktop_upsell_mac_cta_chatlist_toastbar", 5566, false, false)); + properties.put(5567, new CompanionProperty("desktop_upsell_mac_cta_search_results_toastbar", 5567, false, false)); + properties.put(5568, new CompanionProperty("desktop_upsell_mac_cta_call_btn", 5568, false, false)); + properties.put(5569, new CompanionProperty("desktop_upsell_mac_cta_intro_panel", 5569, false, false)); + properties.put(5677, new CompanionProperty("web_add_non_contacts_to_groups_enabled", 5677, false, false)); + properties.put(5680, new CompanionProperty("web_resume_optimized_message_post_processing_enabled", 5680, false, true)); + properties.put(5708, new CompanionProperty("web_biz_tools_on_navbar_enabled", 5708, false, false)); + properties.put(315, new CompanionProperty("stop_abprops_traffic_in_serverprops_response", 315, false, false)); + properties.put(3689, new CompanionProperty("chat_upsell_for_1on1_invites", 3689, false, false)); + properties.put(4118, new CompanionProperty("ugc_participant_limit", 4118, 5, 5)); + properties.put(4441, new CompanionProperty("anyone_can_add_to_groups_by_default", 4441, false, false)); + properties.put(4593, new CompanionProperty("group_join_request_on_by_default", 4593, false, false)); + properties.put(1825, new CompanionProperty("group_chat_profile_pictures_enabled", 1825, false, false)); + properties.put(3261, new CompanionProperty("group_chat_profile_pictures_v2_enabled", 3261, false, false)); + properties.put(3523, new CompanionProperty("unified_user_profile_navigation_enabled", 3523, false, false)); + properties.put(4215, new CompanionProperty("view_all_replies_enabled", 4215, false, false)); + properties.put(4545, new CompanionProperty("top_menu_redesign_enabled", 4545, false, false)); + properties.put(3010, new CompanionProperty("ugr_enabled", 3010, false, true)); + properties.put(3011, new CompanionProperty("ugc_enabled", 3011, false, true)); + properties.put(5002, new CompanionProperty("ug_chat_banner_enabled", 5002, false, true)); + properties.put(5016, new CompanionProperty("ug_chat_banner_visibility_max_seconds", 5016, 432000.0, 432000.0)); + properties.put(5119, new CompanionProperty("ug_chat_banner_visibility_min_seconds", 5119, 0, 0)); + properties.put(3088, new CompanionProperty("reword_subject_to_group_name_enabled", 3088, false, true)); + properties.put(1693, new CompanionProperty("commerce_metadata_supported_business", 1693, "18785550326,918591749310,917977079770,12245555037,5515997781156,5511989238421,555191894444,905333860133,908502213040,5511916282555,555139214004,555198849745,551147664020,622150851766,551121038525", "18785550326,447766028329,918591749310,917977079770,12245555037,5515997781156,5511989238421,555191894444,905333860133,908502213040,5511916282555,555139214004,555198849745,551147664020,622150851766")); + properties.put(1607, new CompanionProperty("in_app_survey_phone_numbers", 1607, "16508638904", "16508638904")); + properties.put(1595, new CompanionProperty("order_details_payment_instructions_enabled", 1595, false, true)); + properties.put(455, new CompanionProperty("enable_biz_activity_report_request", 455, false, false)); + properties.put(464, new CompanionProperty("plm_products_max_batch_fetch_size", 464, 18, 18)); + properties.put(550, new CompanionProperty("enable_granular_reject_reasons", 550, false, false)); + properties.put(604, new CompanionProperty("elevating_profile_names_enabled", 604, false, false)); + properties.put(689, new CompanionProperty("enable_group_profile_editor", 689, false, false)); + properties.put(690, new CompanionProperty("csat_message_rating", 690, false, true)); + properties.put(810, new CompanionProperty("facebook_link_preview_use_thumbnail", 810, false, true)); + properties.put(838, new CompanionProperty("tam_attachment_cache_compaction_enabled", 838, false, false)); + properties.put(853, new CompanionProperty("business_threads_logging_enabled", 853, false, false)); + properties.put(904, new CompanionProperty("private_stats_biz_view_logging_enabled", 904, false, false)); + properties.put(2367, new CompanionProperty("group_join_request_m0_anyone_can_join", 2367, false, false)); + properties.put(1727, new CompanionProperty("group_join_request_m1", 1727, false, false)); + properties.put(1728, new CompanionProperty("group_join_request_m2", 1728, false, false)); + properties.put(4727, new CompanionProperty("parent_group_announcement_comments_enabled", 4727, false, false)); + properties.put(5141, new CompanionProperty("parent_group_announcement_comments_receiver_enabled", 5141, false, false)); + properties.put(5660, new CompanionProperty("parent_group_announcement_comments_sender_use_lid", 5660, true, true)); + properties.put(4728, new CompanionProperty("parent_group_announcement_comments_participant_limit", 4728, 1024, 1024)); + properties.put(4729, new CompanionProperty("parent_group_announcement_comment_subscription_enabled", 4729, false, false)); + properties.put(1887, new CompanionProperty("group_join_request_m2_setting", 1887, false, false)); + properties.put(2913, new CompanionProperty("group_join_request_m2_logging", 2913, false, false)); + properties.put(2418, new CompanionProperty("group_join_request_m2_max_pending_participants_limit", 2418, 2, 2)); + properties.put(2369, new CompanionProperty("group_join_request_m3", 2369, false, false)); + properties.put(3451, new CompanionProperty("group_join_request_m3_sort_by_time", 3451, false, false)); + properties.put(3571, new CompanionProperty("group_join_request_m3_invited_tab", 3571, false, false)); + properties.put(3895, new CompanionProperty("group_join_request_m3_groups_in_common", 3895, false, false)); + properties.put(3452, new CompanionProperty("group_join_request_m3_banner", 3452, false, false)); + properties.put(5212, new CompanionProperty("group_join_request_m3_push_notification", 5212, false, false)); + properties.put(3382, new CompanionProperty("group_join_request_optional_message_soak", 3382, false, false)); + properties.put(3383, new CompanionProperty("group_join_request_can_view_optional_message", 3383, false, false)); + properties.put(3384, new CompanionProperty("group_join_request_can_send_optional_message", 3384, false, false)); + properties.put(2376, new CompanionProperty("group_join_request_m2_pushname", 2376, false, true)); + properties.put(2449, new CompanionProperty("group_join_request_m2_banner_on_conversation", 2449, false, false)); + properties.put(2749, new CompanionProperty("group_invite_new_bottom_sheet_enabled", 2749, true, true)); + properties.put(1967, new CompanionProperty("note_to_self", 1967, false, true)); + properties.put(2630, new CompanionProperty("note_to_self_entry_point", 2630, false, true)); + properties.put(1011, new CompanionProperty("no_delete_message_time_limit", 1011, false, false)); + properties.put(1333, new CompanionProperty("sender_revoke_window_sender", 1333, false, true)); + properties.put(1334, new CompanionProperty("sender_revoke_window_receiver", 1334, false, true)); + properties.put(1335, new CompanionProperty("sender_revoke_ui", 1335, false, true)); + properties.put(1177, new CompanionProperty("admin_revoke_receiver", 1177, false, true)); + properties.put(1292, new CompanionProperty("admin_revoke_sender", 1292, false, true)); + properties.put(1245, new CompanionProperty("admin_revoke_history_sync_consumer", 1245, false, true)); + properties.put(1865, new CompanionProperty("revokes_logging_unsampled", 1865, true, true)); + properties.put(3138, new CompanionProperty("pinned_messages_m0", 3138, false, false)); + properties.put(3139, new CompanionProperty("pinned_messages_m1_receiver", 3139, false, false)); + properties.put(5474, new CompanionProperty("pinned_messsages_m1_receiver_first_time_server_ts_storage", 5474, false, false)); + properties.put(3140, new CompanionProperty("pinned_messages_m1_sender", 3140, false, false)); + properties.put(3813, new CompanionProperty("pinned_messages_m1_sender_debug_expiry_duration_secs", 3813, 86400, 86400)); + properties.put(4432, new CompanionProperty("pinned_messages_sender_short_expiry_durations_enabled", 4432, false, false)); + properties.put(3732, new CompanionProperty("pinned_messages_m2_pin_max", 3732, 1, 1)); + properties.put(3141, new CompanionProperty("pinned_messages_m2", 3141, false, false)); + properties.put(1021, new CompanionProperty("admin_hfm_toggle", 1021, false, false)); + properties.put(1082, new CompanionProperty("csat_message_trigger", 1082, false, true)); + properties.put(1096, new CompanionProperty("graphql_privacy_imp_m1", 1096, false, false)); + properties.put(1104, new CompanionProperty("lthash_check_hours", 1104, 0, 0)); + properties.put(1133, new CompanionProperty("interactive_message_native_flow_killswitch", 1133, false, false)); + properties.put(1185, new CompanionProperty("sender_key_expired_logging_enabled", 1185, false, false)); + properties.put(1861, new CompanionProperty("group_size_bypassing_sampling", 1861, 100000.0, 100000.0)); + properties.put(1304, new CompanionProperty("group_size_limit", 1304, 257, 257)); + properties.put(2334, new CompanionProperty("v_id_deprecation_enabled", 2334, false, true)); + properties.put(2757, new CompanionProperty("proactive_distribute_sender_keys_enabled", 2757, false, true)); + properties.put(2860, new CompanionProperty("minimum_percentage_to_proactive_distribute_sender_keys", 2860, 200, 50)); + properties.put(1538, new CompanionProperty("address_message_native_flow_killswitch", 1538, false, false)); + properties.put(1319, new CompanionProperty("commerce_sanctioned", 1319, false, false)); + properties.put(1320, new CompanionProperty("commerce_bloks_apps_mapping", 1320, """ + {"address_message":{"app_id":"com.bloks.www.whatsapp.commerce.address_message","expiration_secs":300,"version":"1.5","supported_businesses":["+918591749310","+917977079770","+12165552716","+918591749310","+917977079770","+919324433533","+917669800185","+919355081749","+917217010106","+912248913727","+912068135414","+918368818019","+917827971992","+917827971988","+911244632002","+919999006542","+917982465931","+911244632030","+918920528558","+911244632026","+918920530301","+15550083895","+12995550004","+6589523673","+6597685939","+6580536071","+6531631404","+6590834813","+6588867112","+16615555837","+12765985268","+18055908026"]},"galaxy_message":{"flow_message_version":{"1":{"min_android_app_supported_version":"2.22.21","min_ios_app_supported_version":"2.22.16"}},"app_id":"com.bloks.www.whatsapp.commerce.galaxy_message","expiration_secs":86400,"version":"1.0","flows":{"5315848498536354":{"supported_businesses":["18785550326","19505550093","18055555085","12115551400","12165554570"]},"384213690506206":{"supported_businesses":["13072224829","908502213040"]},"785254429343710":{"supported_businesses":["13072224829","908502213040"]},"552092896712166":{"supported_businesses":["13072224829","908502213040"]},"659207712435246":{"supported_businesses":["13072224829","908502213040"]},"1218944301990105":{"supported_businesses":["13072224829","908502213040"]},"842529276647219":{"supported_businesses":["908502419528","905333860133"]},"2135286959994016":{"supported_businesses":["908502419528","905333860133"]},"465280328842503":{"supported_businesses":["908502419528","905333860133"]},"554437403152809":{"supported_businesses":["908502419528","905333860133"]},"1503880053408592":{"supported_businesses":["908502419528","905333860133"]},"1177261906521760":{"supported_businesses":["908502419528","905333860133"]},"5199590820090002":{"supported_businesses":["5511989238421"]},"615215783523200":{"supported_businesses":["5511989238421"]},"1160930701174631":{"supported_businesses":["5511989238421","555191894444"]},"2934205950056123":{"supported_businesses":["5511916282555"]},"5324889264212944":{"supported_businesses":["5511916282555"]},"3301029236883120":{"supported_businesses":["555139214004","555198849745"]},"774830743793476":{"supported_businesses":["555139214004","555198849745"]},"1493489641166601":{"supported_businesses":["555139214004","555198849745"]},"1115920052387436":{"supported_businesses":["555139214004","555198849745"]},"611775360605929":{"supported_businesses":["551147664020","551121038525"]},"1283565282457467":{"supported_businesses":["551147664020","551121038525"]},"673695173931335":{"supported_businesses":["551147664020","551121038525"]},"508459817855605":{"supported_businesses":["442034673249","447418310027"]},"639247544356777":{"supported_businesses":["442034673249","447418310027","622150851766"]},"2679509568858534":{"supported_businesses":["442034673249","447418310027","622150851766"]}}}}""", """ + {"address_message":{"app_id":"com.bloks.www.whatsapp.commerce.address_message","expiration_secs":300,"version":"1.5","supported_businesses":["+918591749310","+917977079770","+12165552716","+918591749310","+917977079770","+919324433533","+917669800185","+919355081749","+917217010106","+912248913727","+912068135414","+918368818019","+917827971992","+917827971988","+911244632002","+919999006542","+917982465931","+911244632030","+918920528558","+911244632026","+918920530301","+15550083895","+12995550004","+6589523673","+6597685939","+6580536071","+6531631404","+6590834813","+6588867112","+16615555837","+12765985268","+18055908026"]},"galaxy_message":{"flow_message_version":{"1":{"min_android_app_supported_version":"2.22.21","min_ios_app_supported_version":"2.22.16"}},"app_id":"com.bloks.www.whatsapp.commerce.galaxy_message","expiration_secs":86400,"version":"1.0","flows":{"5315848498536354":{"supported_businesses":["18785550326","19505550093","18055555085","12115551400","12165554570"]},"384213690506206":{"supported_businesses":["13072224829","908502213040"]},"785254429343710":{"supported_businesses":["13072224829","908502213040"]},"552092896712166":{"supported_businesses":["13072224829","908502213040"]},"659207712435246":{"supported_businesses":["13072224829","908502213040"]},"1218944301990105":{"supported_businesses":["13072224829","908502213040"]},"842529276647219":{"supported_businesses":["908502419528","905333860133"]},"2135286959994016":{"supported_businesses":["908502419528","905333860133"]},"465280328842503":{"supported_businesses":["908502419528","905333860133"]},"554437403152809":{"supported_businesses":["908502419528","905333860133"]},"1503880053408592":{"supported_businesses":["908502419528","905333860133"]},"1177261906521760":{"supported_businesses":["908502419528","905333860133"]},"5199590820090002":{"supported_businesses":["5511989238421"]},"615215783523200":{"supported_businesses":["5511989238421"]},"1160930701174631":{"supported_businesses":["5511989238421","555191894444"]},"2934205950056123":{"supported_businesses":["5511916282555"]},"5324889264212944":{"supported_businesses":["5511916282555"]},"3301029236883120":{"supported_businesses":["555139214004","555198849745"]},"774830743793476":{"supported_businesses":["555139214004","555198849745"]},"1493489641166601":{"supported_businesses":["555139214004","555198849745"]},"1115920052387436":{"supported_businesses":["555139214004","555198849745"]},"611775360605929":{"supported_businesses":["551147664020","551121038525"]},"1283565282457467":{"supported_businesses":["551147664020","551121038525"]},"673695173931335":{"supported_businesses":["551147664020","551121038525"]},"508459817855605":{"supported_businesses":["442034673249","447418310027"]},"639247544356777":{"supported_businesses":["442034673249","447418310027","622150851766"]},"2679509568858534":{"supported_businesses":["442034673249","447418310027","622150851766"]}}}}""")); + properties.put(1327, new CompanionProperty("graphql_privacy_imp_m2", 1327, false, false)); + properties.put(1343, new CompanionProperty("nux_sync", 1343, false, true)); + properties.put(1377, new CompanionProperty("in_app_survey_enabled", 1377, false, true)); + properties.put(1394, new CompanionProperty("poll_creation_enabled", 1394, false, false)); + properties.put(1395, new CompanionProperty("poll_receiving_enabled", 1395, false, false)); + properties.put(2737, new CompanionProperty("poll_receiving_cag_enabled", 2737, false, false)); + properties.put(1406, new CompanionProperty("poll_name_length", 1406, 255, 255)); + properties.put(1407, new CompanionProperty("poll_option_length", 1407, 100, 100)); + properties.put(1408, new CompanionProperty("poll_option_count", 1408, 12, 12)); + properties.put(1409, new CompanionProperty("poll_offline_accuracy", 1409, 30, 30)); + properties.put(1410, new CompanionProperty("poll_cleanup_days", 1410, 31, 31)); + properties.put(1541, new CompanionProperty("poll_vote_processing_enabled", 1541, false, false)); + properties.put(1948, new CompanionProperty("poll_result_details_view_enabled", 1948, true, true)); + properties.put(2194, new CompanionProperty("poll_creation_one_on_one_chats_enabled", 2194, false, false)); + properties.put(2738, new CompanionProperty("poll_creation_cag_enabled", 2738, false, false)); + properties.put(2390, new CompanionProperty("poll_a11y_enabled", 2390, false, true)); + properties.put(2728, new CompanionProperty("enable_status_reporting", 2728, false, true)); + properties.put(1415, new CompanionProperty("group_suspend_v1_enabled", 1415, false, true)); + properties.put(2057, new CompanionProperty("group_suspend_appeal_include_entity_id_enabled", 2057, false, true)); + properties.put(2290, new CompanionProperty("block_from_chat_list", 2290, false, true)); + properties.put(2818, new CompanionProperty("community_reporting_ui_upsell_exit", 2818, true, true)); + properties.put(1417, new CompanionProperty("smb_product_price_label", 1417, "control", "control")); + properties.put(1435, new CompanionProperty("interactive_response_message_killswitch", 1435, false, false)); + properties.put(1436, new CompanionProperty("interactive_response_message_native_flow_killswitch", 1436, false, false)); + properties.put(1464, new CompanionProperty("biz_api_voip_enabled", 1464, false, false)); + properties.put(1480, new CompanionProperty("quantity_controls_enabled", 1480, false, true)); + properties.put(1514, new CompanionProperty("catalog_categories_enabled", 1514, false, true)); + properties.put(1518, new CompanionProperty("disappearing_messages_chat_picker", 1518, false, false)); + properties.put(1322, new CompanionProperty("more_reactions_option", 1322, false, false)); + properties.put(1792, new CompanionProperty("reactions_keyboard_hides_three_flags", 1792, false, false)); + properties.put(2170, new CompanionProperty("send_reaction_from_details_pane", 2170, false, false)); + properties.put(1527, new CompanionProperty("silent_group_exit", 1527, false, true)); + properties.put(1528, new CompanionProperty("silent_group_exit_past_participants", 1528, false, true)); + properties.put(1597, new CompanionProperty("silent_group_exit_dialog", 1597, false, true)); + properties.put(1598, new CompanionProperty("silent_group_exit_sync", 1598, false, true)); + properties.put(1613, new CompanionProperty("silent_group_exit_db", 1613, false, true)); + properties.put(1600, new CompanionProperty("order_details_quick_pay", 1600, """ + {"allowed_product_type":"none"}""", """ + {"allowed_product_type":"none"}""")); + properties.put(1599, new CompanionProperty("incentive_program_logging_enabled", 1599, false, true)); + properties.put(1612, new CompanionProperty("md_syncd_24_hour_time_format_sync_enabled", 1612, false, false)); + properties.put(2734, new CompanionProperty("md_link_device_with_phone_number_enabled", 2734, false, false)); + properties.put(3693, new CompanionProperty("md_link_device_with_phone_number_force_enabled", 3693, false, false)); + properties.put(1660, new CompanionProperty("send_cart_cta_long_button_enabled", 1660, true, true)); + properties.put(2153, new CompanionProperty("send_cart_cta_long_button_alternative_text_type", 2153, 0, 0)); + properties.put(1678, new CompanionProperty("product_search_m1_enabled", 1678, false, true)); + properties.put(1688, new CompanionProperty("smb_catalog_collections_reordering_enabled", 1688, true, true)); + properties.put(1794, new CompanionProperty("smb_catalog_collection_items_reordering_enabled", 1794, true, true)); + properties.put(1707, new CompanionProperty("is_message_secret_enabled", 1707, false, true)); + properties.put(1749, new CompanionProperty("documents_with_captions_receive", 1749, false, true)); + properties.put(1750, new CompanionProperty("documents_with_captions_send", 1750, false, true)); + properties.put(1763, new CompanionProperty("external_payments_supported_business", 1763, "+917000770007", "+918369150604,+917000770007")); + properties.put(1766, new CompanionProperty("active_cart_discovery_enabled", 1766, false, true)); + properties.put(1767, new CompanionProperty("order_details_payment_options", 1767, """ + {"payment_options":[{"type":"JioPay","url_regex_list":["^https://www.jio.com/.*$","^https://t.jio/.*$","^http://tiny.jio.com/.*$"],"title":{"name":"jiopay_title","default_text":"Pay on Jio.com"},"subtitle":{"name":"jiopay_subtitle","default_text":"Go to Jio.com website"},"button":{"name":"jiopay_button","default_text":"Proceed to Jio.com"}}]}""", """ + {"payment_options":[{"type":"JioPay","url_regex_list":["^https://www.jio.com/.*$","^https://t.jio/.*$","^http://tiny.jio.com/.*$"],"title":{"name":"jiopay_title","default_text":"Pay on Jio.com"},"subtitle":{"name":"jiopay_subtitle","default_text":"Go to Jio.com website"},"button":{"name":"jiopay_button","default_text":"Proceed to Jio.com"}}]}""")); + properties.put(3014, new CompanionProperty("order_details_payment_protection_link", 3014, "https://faq.whatsapp.com/725152392426717", "https://faq.whatsapp.com/725152392426717")); + properties.put(1829, new CompanionProperty("recent_sticker_rollout_phase", 1829, 0, 0)); + properties.put(1844, new CompanionProperty("enable_client_chat_psa", 1844, false, true)); + properties.put(3182, new CompanionProperty("enable_chat_psa_auto_play_videos", 3182, false, true)); + properties.put(4033, new CompanionProperty("enable_chat_psa_forwards", 4033, false, true)); + properties.put(4659, new CompanionProperty("enable_clear_formatted_preview", 4659, false, false)); + properties.put(1846, new CompanionProperty("direct_connection_business_numbers", 1846, "16005554444,918591749310,917977079770", "16005554444,918591749310,917977079770")); + properties.put(1853, new CompanionProperty("forward_media_with_captions", 1853, false, false)); + properties.put(3177, new CompanionProperty("append_message_when_forwarding_media", 3177, false, false)); + properties.put(3875, new CompanionProperty("append_message_when_forwarding_media_without_caption", 3875, false, false)); + properties.put(4036, new CompanionProperty("view_all_replies", 4036, false, false)); + properties.put(1867, new CompanionProperty("share_phone_number_on_cart_send_to_direct_connection_biz_enabled", 1867, true, true)); + properties.put(1875, new CompanionProperty("voice_status_receipt_enabled", 1875, true, true)); + properties.put(1921, new CompanionProperty("admin_include_message_secret_in_cag", 1921, true, true)); + properties.put(1993, new CompanionProperty("md_syncd_primary_version_sync_enabled", 1993, false, false)); + properties.put(2003, new CompanionProperty("product_catalog_qpl_logging_enabled", 2003, false, true)); + properties.put(2007, new CompanionProperty("syncd_do_not_fatal_on_snapshot_mac_mismatch_in_patches", 2007, false, false)); + properties.put(2014, new CompanionProperty("graphql_locale_remapping", 2014, "{}", "{}")); + properties.put(2024, new CompanionProperty("product_catalog_qpl_direct_connection_status_logging_enabled", 2024, false, true)); + properties.put(2155, new CompanionProperty("favorite_sticker_rmr_sync_enabled", 2155, false, false)); + properties.put(2156, new CompanionProperty("web_link_preview_sync_enabled", 2156, false, true)); + properties.put(2189, new CompanionProperty("message_edit_receive", 2189, false, true)); + properties.put(2190, new CompanionProperty("message_edit_send", 2190, false, true)); + properties.put(3686, new CompanionProperty("caption_edit_receive", 3686, false, false)); + properties.put(3687, new CompanionProperty("caption_edit_send", 3687, false, false)); + properties.put(2983, new CompanionProperty("message_edit_window_duration_seconds", 2983, 1200, 1200)); + properties.put(3272, new CompanionProperty("message_edit_client_entry_point_limit_seconds", 3272, 900, 900)); + properties.put(4325, new CompanionProperty("message_edit_bubble_animation", 4325, false, false)); + properties.put(2193, new CompanionProperty("prekey_fetch_iq_for_missing_devices_enabled", 2193, false, false)); + properties.put(2306, new CompanionProperty("extensions_message_support_version", 2306, """ + {"1":{"min_android_app_supported_version":"2.22.21"},"2":{"min_android_app_supported_version":"2.22.23.11","min_ios_app_supported_version":"2.23.18.15"},"3":{"min_android_app_supported_version":"2.23.17.10","min_ios_app_supported_version":"2.23.18.15"}}""", """ + {"1":{"min_android_app_supported_version":"2.22.21"},"2":{"min_android_app_supported_version":"2.22.23","min_ios_app_supported_version":"2.23.18.15"},"3":{"min_android_app_supported_version":"2.23.17","min_ios_app_supported_version":"2.23.18.15"}}""")); + properties.put(2374, new CompanionProperty("block_from_notification", 2374, false, true)); + properties.put(2378, new CompanionProperty("four_reactions_in_bubble_enabled", 2378, false, true)); + properties.put(2522, new CompanionProperty("block_entry_point_logging_enabled", 2522, false, true)); + properties.put(2573, new CompanionProperty("non_message_data_request_logging_enabled", 2573, false, true)); + properties.put(2661, new CompanionProperty("polls_fast_follow_enabled", 2661, true, true)); + properties.put(2720, new CompanionProperty("poll_chatlist_preview_enabled", 2720, false, true)); + properties.put(2662, new CompanionProperty("polls_search_support_enabled", 2662, false, true)); + properties.put(2914, new CompanionProperty("attachment_tray_logging_enabled", 2914, false, true)); + properties.put(2663, new CompanionProperty("polls_reply_support_enabled", 2663, false, true)); + properties.put(3050, new CompanionProperty("polls_single_option_control_enabled", 3050, false, true)); + properties.put(3433, new CompanionProperty("polls_single_option_sender_control_enabled", 3433, false, true)); + properties.put(3434, new CompanionProperty("polls_single_option_reciever_control_enabled", 3434, true, true)); + properties.put(3437, new CompanionProperty("polls_single_option_receiver_control_enabled", 3437, true, true)); + properties.put(3158, new CompanionProperty("polls_notification_enabled", 3158, false, false)); + properties.put(2890, new CompanionProperty("ptt_transcription_enabled", 2890, false, true)); + properties.put(3223, new CompanionProperty("attach_menu_redesign_enabled", 3223, false, false)); + properties.put(3858, new CompanionProperty("ts_navigation_community_enabled", 3858, false, false)); + properties.put(3859, new CompanionProperty("ts_bit_array_enabled", 3859, false, false)); + properties.put(4928, new CompanionProperty("ts_external_enabled", 4928, false, false)); + properties.put(4929, new CompanionProperty("ts_surface_killswitch", 4929, 0, 0)); + properties.put(3860, new CompanionProperty("ts_session_duration_ms", 3860, 600000.0, 600000.0)); + properties.put(2776, new CompanionProperty("fullscreen_animation_for_keyword", 2776, false, false)); + properties.put(2777, new CompanionProperty("syncd_additional_mutations_count", 2777, 1, 1)); + properties.put(2811, new CompanionProperty("mpm_nfm_enabled", 2811, true, true)); + properties.put(2813, new CompanionProperty("interactive_template_enabled", 2813, true, true)); + properties.put(2871, new CompanionProperty("inapp_banner_client_enabled", 2871, false, true)); + properties.put(3712, new CompanionProperty("quick_promotion_banner_client_enabled", 3712, false, false)); + properties.put(2885, new CompanionProperty("extensions_template_killswitch", 2885, false, false)); + properties.put(2891, new CompanionProperty("biz_extensions_metadata_cache_ttl_minutes", 2891, 1440, 1440)); + properties.put(2892, new CompanionProperty("biz_extensions_metadata_ban_ttl_minutes", 2892, 525600, 525600)); + properties.put(2895, new CompanionProperty("utm_tracking_enabled", 2895, false, false)); + properties.put(2896, new CompanionProperty("utm_tracking_expiration_hours", 2896, 24, 24)); + properties.put(2909, new CompanionProperty("mpm_nfm_forwarding_enabled", 2909, false, false)); + properties.put(2990, new CompanionProperty("url_hsm_redesign_enabled", 2990, true, true)); + properties.put(2994, new CompanionProperty("button_url_hsm_redesign_enabled", 2994, true, true)); + properties.put(2945, new CompanionProperty("is_internal_tester", 2945, false, true)); + properties.put(3032, new CompanionProperty("report_string_comprehension", 3032, false, true)); + properties.put(3128, new CompanionProperty("alt_device_linking_enabled", 3128, false, false)); + properties.put(3155, new CompanionProperty("mute_dialog_description", 3155, false, true)); + properties.put(3156, new CompanionProperty("mute_always_show_notification_action", 3156, false, true)); + properties.put(3192, new CompanionProperty("extensions_graphql_cta_disable", 3192, "2498088", "2498088")); + properties.put(3198, new CompanionProperty("recent_emojis_sync", 3198, false, false)); + properties.put(3301, new CompanionProperty("syncd_report_key_stats", 3301, false, true)); + properties.put(3337, new CompanionProperty("history_sync_on_demand", 3337, false, false)); + properties.put(3642, new CompanionProperty("history_sync_on_demand_time_boundary_days", 3642, 365, 365)); + properties.put(3811, new CompanionProperty("history_sync_on_demand_message_count", 3811, 50, 50)); + properties.put(3882, new CompanionProperty("history_sync_on_demand_timeout_ms", 3882, 10000.0, 10000.0)); + properties.put(4135, new CompanionProperty("history_sync_on_demand_with_android_beta", 4135, false, false)); + properties.put(3354, new CompanionProperty("ptv_sending_enabled", 3354, false, true)); + properties.put(3355, new CompanionProperty("ptv_receiving_enabled", 3355, false, true)); + properties.put(3356, new CompanionProperty("ptv_max_duration_seconds", 3356, 60, 60)); + properties.put(3482, new CompanionProperty("ptv_autoplay_enabled", 3482, true, true)); + properties.put(3483, new CompanionProperty("ptv_autoplay_loop_limit", 3483, 3, 3)); + properties.put(4548, new CompanionProperty("ptv_nux_enabled", 4548, false, true)); + properties.put(4549, new CompanionProperty("ptv_button_persistence_enabled", 4549, false, false)); + properties.put(5292, new CompanionProperty("ptv_button_tooltip_animation_enabled", 5292, false, true)); + properties.put(5317, new CompanionProperty("ptv_button_animation_enabled", 5317, false, true)); + properties.put(5384, new CompanionProperty("ptt_button_toggle_cooldown", 5384, 0, 0)); + properties.put(5386, new CompanionProperty("ptv_button_reset_minimize_threshold", 5386, -1, -1)); + properties.put(5412, new CompanionProperty("ptv_recording_countdown_interval", 5412, 500, 500)); + properties.put(5418, new CompanionProperty("ptv_setting", 5418, false, false)); + properties.put(5419, new CompanionProperty("ptv_setting_sends_threshold", 5419, -1, -1)); + properties.put(5483, new CompanionProperty("ptv_setting_duration_threshold_seconds", 5483, 604800, 604800)); + properties.put(5507, new CompanionProperty("ptv_button_redesign_version", 5507, 0, 0)); + properties.put(3444, new CompanionProperty("template_button_improvements_on", 3444, false, true)); + properties.put(3536, new CompanionProperty("qp_campaign_client_enabled", 3536, false, false)); + properties.put(4200, new CompanionProperty("qp_push_notifications_enabled", 4200, false, false)); + properties.put(3575, new CompanionProperty("animated_emojis_enabled", 3575, false, false)); + properties.put(3579, new CompanionProperty("placeholder_message_resend", 3579, false, false)); + properties.put(3630, new CompanionProperty("is_coupon_button_enabled", 3630, false, true)); + properties.put(3631, new CompanionProperty("coupon_copy_button_url", 3631, "https://www.whatsapp.com/coupon?code=", "https://www.whatsapp.com/coupon?code=")); + properties.put(4693, new CompanionProperty("is_lto_offer_enabled", 4693, false, true)); + properties.put(5073, new CompanionProperty("lto_offer_media_aspect_ratio", 5073, 0.8, 0.8)); + properties.put(3639, new CompanionProperty("placeholder_message_resend_maximum_days_limit", 3639, 14, 14)); + properties.put(3644, new CompanionProperty("placeholder_chat_open_group_fetch", 3644, false, false)); + properties.put(3749, new CompanionProperty("placeholder_chat_open_group_fetch_size_limit", 3749, 33, 65)); + properties.put(3665, new CompanionProperty("high_quality_link_preview_enabled", 3665, false, true)); + properties.put(3690, new CompanionProperty("orders_expansion_receiver_countries_allowed", 3690, "", "")); + properties.put(3750, new CompanionProperty("retry_receipt_error_code_enabled", 3750, false, true)); + properties.put(3771, new CompanionProperty("orders_expansion_paying_enabled", 3771, false, false)); + properties.put(4089, new CompanionProperty("cag_message_edit_receive", 4089, false, false)); + properties.put(4090, new CompanionProperty("cag_message_edit_send", 4090, false, false)); + properties.put(4091, new CompanionProperty("broadcast_message_edit_receive", 4091, false, false)); + properties.put(4092, new CompanionProperty("broadcast_message_edit_send", 4092, false, false)); + properties.put(4093, new CompanionProperty("expanded_text_formatting_enabled", 4093, false, false)); + properties.put(4150, new CompanionProperty("support_ticket_data_collection_improvements", 4150, false, false)); + properties.put(4205, new CompanionProperty("link_preview_shimmer_enabled", 4205, false, false)); + properties.put(4233, new CompanionProperty("member_name_tag_enabled", 4233, false, true)); + properties.put(4242, new CompanionProperty("support_ticket_stop_uploading_device_logs", 4242, false, false)); + properties.put(4345, new CompanionProperty("community_subgroup_join_from_system_message_enabled", 4345, false, true)); + properties.put(4668, new CompanionProperty("carousel_message_client_enabled", 4668, false, true)); + properties.put(5542, new CompanionProperty("enable_carousel_message_client_logging", 5542, false, true)); + properties.put(4697, new CompanionProperty("internal_bug_reporting_v1_enabled", 4697, false, false)); + properties.put(4849, new CompanionProperty("wae_metadata_integrity_timeout_minutes", 4849, 5, 5)); + properties.put(4893, new CompanionProperty("row_buyer_order_revamp_m0_enabled", 4893, false, true)); + properties.put(5518, new CompanionProperty("row_buyer_order_revamp_m0_nux_banner_enabled", 5518, false, true)); + properties.put(4905, new CompanionProperty("post_status_in_companion", 4905, false, false)); + properties.put(4921, new CompanionProperty("evolve_about_m1_enabled", 4921, false, false)); + properties.put(4979, new CompanionProperty("community_members_bottomsheet_enabled", 4979, false, false)); + properties.put(5000, new CompanionProperty("community_members_bottomsheet_post_creation_enabled", 5000.0, false, false)); + properties.put(5114, new CompanionProperty("buyer_initiated_order_request_variant_enabled", 5114, false, false)); + properties.put(5124, new CompanionProperty("im_nfm_dynamic_message_killswitch", 5124, false, false)); + properties.put(5171, new CompanionProperty("inbox_filters_enabled", 5171, false, false)); + properties.put(5172, new CompanionProperty("inbox_filters_favorites_enabled", 5172, false, false)); + properties.put(5173, new CompanionProperty("inbox_filters_custom_filters_enabled", 5173, false, false)); + properties.put(5190, new CompanionProperty("seller_orders_management_revamp", 5190, false, false)); + properties.put(5247, new CompanionProperty("extensions_central_config_killswitch", 5247, false, false)); + properties.put(5333, new CompanionProperty("extensions_geoblocking_enabled", 5333, false, true)); + properties.put(5553, new CompanionProperty("support_ticket_device_log_retention_period_days", 5553, 3, 3)); + properties.put(5587, new CompanionProperty("bot_3p_enabled", 5587, false, false)); + properties.put(5610, new CompanionProperty("companion_biz_label_sync_enabled", 5610, false, false)); + properties.put(5626, new CompanionProperty("saga_enabled", 5626, false, false)); + properties.put(5694, new CompanionProperty("companion_biz_quick_reply_sync_enabled", 5694, false, false)); + properties.put(2575, new CompanionProperty("project_pdf_enabled", 2575, false, true)); + properties.put(3479, new CompanionProperty("pdf_auto_start_interval_seconds", 3479, 86400, 30)); + properties.put(4100, new CompanionProperty("pdf_external_deeplink_enabled", 4100, false, true)); + properties.put(4679, new CompanionProperty("pdf_client_driven_rollout_enabled", 4679, false, true)); + properties.put(4680, new CompanionProperty("pdf_max_download_jitter_time_seconds", 4680, 180, 180)); + properties.put(4779, new CompanionProperty("pdf_md_support_enabled", 4779, false, true)); + properties.put(618, new CompanionProperty("client_group_participants_limit", 618, 257, 257)); + properties.put(812, new CompanionProperty("payment_stickers_render_enabled", 812, false, false)); + properties.put(4257, new CompanionProperty("cart_order_creation_shortcut_enabled", 4257, false, false)); + properties.put(3744, new CompanionProperty("payments_merchant_global_orders_value_props_banner_enabled", 3744, false, true)); + properties.put(5255, new CompanionProperty("payments_merchant_global_orders_value_props_banner_logging_enabled", 5255, true, true)); + properties.put(4144, new CompanionProperty("payments_br_installment_buyer_learn_more_link", 4144, "https://faq.whatsapp.com/1134168457974360", "https://faq.whatsapp.com/1134168457974360")); + properties.put(4145, new CompanionProperty("ipayments_br_installment_seller_learn_more_link", 4145, "https://faq.whatsapp.com/253337763937767", "https://faq.whatsapp.com/253337763937767")); + properties.put(4254, new CompanionProperty("payments_br_installment_seller_learn_more_link", 4254, "https://faq.whatsapp.com/253337763937767", "https://faq.whatsapp.com/253337763937767")); + properties.put(5574, new CompanionProperty("seller_order_payment_request_enabled", 5574, false, false)); + properties.put(5575, new CompanionProperty("buyer_order_payment_request_enabled", 5575, false, false)); + properties.put(3051, new CompanionProperty("payments_link_to_lite_consumer_enabled", 3051, false, true)); + properties.put(4248, new CompanionProperty("payments_br_content_optimization_variant", 4248, 0, 0)); + properties.put(4781, new CompanionProperty("payments_br_pix_phase_1_seller_enabled", 4781, false, false)); + properties.put(4976, new CompanionProperty("payments_br_info_architecture_orders_hub_enabled", 4976, false, false)); + properties.put(5414, new CompanionProperty("payments_info_architecture_orders_hub_enabled", 5414, false, false)); + properties.put(808, new CompanionProperty("privacy_allow_contacts_except", 808, false, false)); + properties.put(1063, new CompanionProperty("primary_feature_sync", 1063, false, true)); + properties.put(1071, new CompanionProperty("privacy_narrative_v1", 1071, false, false)); + properties.put(1309, new CompanionProperty("add_dm_to_chat_overflow_menu", 1309, false, false)); + properties.put(1352, new CompanionProperty("keep_in_chat_receiver", 1352, false, false)); + properties.put(1353, new CompanionProperty("keep_in_chat_sender", 1353, false, false)); + properties.put(2005, new CompanionProperty("keep_in_chat_ui_content", 2005, false, false)); + properties.put(1673, new CompanionProperty("kic_orphan_cleanup_days", 1673, 31, 31)); + properties.put(2844, new CompanionProperty("supports_keep_in_chat_in_cag", 2844, true, true)); + properties.put(4042, new CompanionProperty("kic_msg_send_expiry_sec", 4042, 86400, 86400)); + properties.put(1397, new CompanionProperty("ddm_reversed_options", 1397, false, false)); + properties.put(1645, new CompanionProperty("qm_lean_msg", 1645, false, false)); + properties.put(1429, new CompanionProperty("pnh_historical_mapping_retention_seconds", 1429, 7776000.0, 7776000.0)); + properties.put(1437, new CompanionProperty("trusted_contacts_reciprocity", 1437, false, false)); + properties.put(1566, new CompanionProperty("trusted_contacts_chat_state_optimization", 1566, "old", "old")); + properties.put(1687, new CompanionProperty("trusted_contacts_op", 1687, false, true)); + properties.put(1670, new CompanionProperty("dm_updated_system_message", 1670, false, true)); + properties.put(1698, new CompanionProperty("keep_in_chat_undo_duration_limit", 1698, 2592000.0, 2592000.0)); + properties.put(1710, new CompanionProperty("view_once_sp_receiver", 1710, false, false)); + properties.put(1711, new CompanionProperty("view_once_sp_sender", 1711, false, false)); + properties.put(1823, new CompanionProperty("pnh_ctwa", 1823, false, true)); + properties.put(2245, new CompanionProperty("pnh_indicator", 2245, false, true)); + properties.put(1892, new CompanionProperty("usync_lid", 1892, false, false)); + properties.put(3062, new CompanionProperty("pnh_pn_for_lid_chat_sync", 3062, false, true)); + properties.put(2751, new CompanionProperty("pnh_identity_verification_v3", 2751, false, false)); + properties.put(3070, new CompanionProperty("share_own_pn_sync", 3070, false, true)); + properties.put(3481, new CompanionProperty("pnh_companion_history_sync_lid_chat", 3481, false, true)); + properties.put(2304, new CompanionProperty("pnh_cag_upgrade", 2304, 0, 0)); + properties.put(2035, new CompanionProperty("cag_reactions_receive", 2035, false, false)); + properties.put(2036, new CompanionProperty("cag_reactions_send", 2036, false, false)); + properties.put(2346, new CompanionProperty("pnh_cag_show_masked_members", 2346, false, false)); + properties.put(1970, new CompanionProperty("calling_privacy_caller_offer", 1970, true, true)); + properties.put(1971, new CompanionProperty("calling_privacy_caller_send_token", 1971, true, true)); + properties.put(1972, new CompanionProperty("calling_privacy_callee", 1972, true, true)); + properties.put(3624, new CompanionProperty("group_add_ack_server", 3624, true, true)); + properties.put(2433, new CompanionProperty("pnh_cag_future_proof_banner", 2433, false, false)); + properties.put(2479, new CompanionProperty("pnh_split_threads_detection", 2479, false, false)); + properties.put(3691, new CompanionProperty("pnh_ctwa_mat_crashlog", 3691, false, false)); + properties.put(2507, new CompanionProperty("pnh_group_lid", 2507, 0, 0)); + properties.put(2561, new CompanionProperty("out_of_sync_disappearing_messages_logging", 2561, false, true)); + properties.put(2597, new CompanionProperty("dm_chat_picker_v2", 2597, false, true)); + properties.put(3305, new CompanionProperty("dm_additional_durations", 3305, false, false)); + properties.put(5309, new CompanionProperty("dm_initiator_trigger", 5309, false, true)); + properties.put(2714, new CompanionProperty("ephemeral_sync_response", 2714, false, false)); + properties.put(2919, new CompanionProperty("dmcp_manage_storage_LAUNCH", 2919, false, true)); + properties.put(2800, new CompanionProperty("settings_search", 2800, false, false)); + properties.put(2802, new CompanionProperty("enable_soox_message_receiving", 2802, false, false)); + properties.put(4922, new CompanionProperty("soox_long_press_duration_ms", 4922, 500, 500)); + properties.put(2832, new CompanionProperty("enable_soox_message_sending", 2832, false, false)); + properties.put(2939, new CompanionProperty("pnh_split_thread_case1_detection", 2939, false, true)); + properties.put(2962, new CompanionProperty("pnh_cag_block_lid_in_limbo", 2962, true, true)); + properties.put(3103, new CompanionProperty("prekey_fetch_iq_pnh_lid_enabled", 3103, false, false)); + properties.put(3366, new CompanionProperty("persisted_profile_name", 3366, false, false)); + properties.put(3458, new CompanionProperty("pnh_identity_verification_v3_pn_generation", 3458, false, false)); + properties.put(3469, new CompanionProperty("pnh_1on1_lid_expected", 3469, false, true)); + properties.put(3519, new CompanionProperty("allow_lid_contacts_storage", 3519, false, false)); + properties.put(3751, new CompanionProperty("allow_lid_contacts_new_1on1_chat", 3751, false, false)); + properties.put(3752, new CompanionProperty("allow_lid_contacts_add_to_group", 3752, false, false)); + properties.put(3762, new CompanionProperty("allow_lid_contacts_calling", 3762, false, false)); + properties.put(3763, new CompanionProperty("allow_lid_contacts_privacy_settings", 3763, false, false)); + properties.put(3789, new CompanionProperty("allow_share_lid_contacts_vcard", 3789, false, false)); + properties.put(3790, new CompanionProperty("allow_parse_lid_contacts_vcard", 3790, false, false)); + properties.put(3603, new CompanionProperty("rabbit_enabled", 3603, false, false)); + properties.put(3872, new CompanionProperty("pnh_prevent_undefined_lid_chat_origin", 3872, false, false)); + properties.put(3962, new CompanionProperty("first_message_experience", 3962, false, false)); + properties.put(5263, new CompanionProperty("first_message_experience_v2", 5263, false, false)); + properties.put(4314, new CompanionProperty("privacy_tips_killswitch", 4314, false, false)); + properties.put(3995, new CompanionProperty("privacy_tips_groups_build", 3995, false, false)); + properties.put(3996, new CompanionProperty("privacy_tips_callers_build", 3996, false, false)); + properties.put(3997, new CompanionProperty("privacy_tips_status_build", 3997, false, false)); + properties.put(3998, new CompanionProperty("privacy_tips_profile_build", 3998, false, false)); + properties.put(3999, new CompanionProperty("unified_e2ee_copy_build", 3999, false, false)); + properties.put(4000, new CompanionProperty("unified_e2ee_ui_build", 4000.0, false, false)); + properties.put(4869, new CompanionProperty("unified_e2ee_copy_launch", 4869, false, false)); + properties.put(4870, new CompanionProperty("unified_e2ee_ui_launch", 4870, false, false)); + properties.put(5111, new CompanionProperty("unified_e2ee_bottomsheet", 5111, false, false)); + properties.put(5112, new CompanionProperty("unified_e2ee_security_page", 5112, false, false)); + properties.put(5113, new CompanionProperty("unified_e2ee_backup_page", 5113, false, false)); + properties.put(4131, new CompanionProperty("dm_reliability_refactor", 4131, false, false)); + properties.put(5580, new CompanionProperty("dm_reliability_logging", 5580, false, false)); + properties.put(4178, new CompanionProperty("pnh_1on1_report_lid_message_send", 4178, false, false)); + properties.put(4214, new CompanionProperty("privacy_tip_expiration_min", 4214, 10080, 10080)); + properties.put(4297, new CompanionProperty("soox_media_send_keep_old_button", 4297, false, false)); + properties.put(4495, new CompanionProperty("pnh_cag_disable_reactions_group_size", 4495, 10000.0, 10000.0)); + properties.put(5056, new CompanionProperty("pnh_cag_disable_polls_group_size", 5056, 10000.0, 10000.0)); + properties.put(4745, new CompanionProperty("username_creation", 4745, false, false)); + properties.put(4746, new CompanionProperty("username_contact_display", 4746, false, false)); + properties.put(4747, new CompanionProperty("username_change", 4747, false, false)); + properties.put(4748, new CompanionProperty("username_1on1_chat", 4748, false, false)); + properties.put(4749, new CompanionProperty("username_group_participants", 4749, false, false)); + properties.put(5290, new CompanionProperty("username_usync", 5290, false, false)); + properties.put(5189, new CompanionProperty("system_msg_update", 5189, false, false)); + properties.put(5513, new CompanionProperty("soox_vo_moving_hide_learn_more", 5513, true, false)); + properties.put(864, new CompanionProperty("sticker_md_favorite_stickers_enabled", 864, false, false)); + properties.put(1469, new CompanionProperty("smb_orange_enabled", 1469, false, false)); + properties.put(1483, new CompanionProperty("smb_melon_display_enabled", 1483, false, false)); + properties.put(1484, new CompanionProperty("smb_melon_management_enabled", 1484, false, false)); + properties.put(1525, new CompanionProperty("call_only_primary_device_limit_exceeded", 1525, false, false)); + properties.put(1591, new CompanionProperty("smb_premium_md_limit_perf_tracker_enabled", 1591, false, true)); + properties.put(1583, new CompanionProperty("smb_billing_enabled", 1583, false, false)); + properties.put(1619, new CompanionProperty("smb_billing_premium_access_config", 1619, "", "")); + properties.put(1672, new CompanionProperty("smb_billing_logging_enabled", 1672, false, true)); + properties.put(1669, new CompanionProperty("smb_melon_logging_enabled", 1669, false, true)); + properties.put(1701, new CompanionProperty("smb_dcp_enabled", 1701, false, false)); + properties.put(1849, new CompanionProperty("smb_custom_url_display_v2_enabled", 1849, false, true)); + properties.put(1438, new CompanionProperty("smb_multi_device_agents_enabled", 1438, false, true)); + properties.put(1981, new CompanionProperty("smb_multi_device_message_attribution_enabled", 1981, false, true)); + properties.put(1671, new CompanionProperty("smb_multi_device_agents_logging_enabled", 1671, false, true)); + properties.put(1897, new CompanionProperty("smb_multi_device_agents_logging_V2_enabled", 1897, false, true)); + properties.put(1798, new CompanionProperty("smb_md_agent_chat_assignment_enabled", 1798, false, true)); + properties.put(2157, new CompanionProperty("smb_md_agent_chat_assignment_system_messages_enabled", 2157, false, true)); + properties.put(2709, new CompanionProperty("smb_md_agent_chat_assignment_system_messages_logging_v2_enabled", 2709, false, true)); + properties.put(2778, new CompanionProperty("smb_md_agent_chat_assignment_system_messages_chats_reorder_enabled", 2778, false, true)); + properties.put(2787, new CompanionProperty("smb_md_agent_chat_assignment_chats_reorder_on_chat_assignment_enabled", 2787, false, true)); + properties.put(2788, new CompanionProperty("smb_md_agent_chat_assignment_chats_reorder_on_chat_unassignment_enabled", 2788, false, true)); + properties.put(2207, new CompanionProperty("smb_md_agent_chat_assignment_nux_impressions", 2207, 0, 3)); + properties.put(2976, new CompanionProperty("smb_md_agent_chat_assignment_chat_list_new_label_enabled", 2976, false, true)); + properties.put(2320, new CompanionProperty("coex_biz_states_sys_msg_enabled", 2320, false, true)); + properties.put(2582, new CompanionProperty("smb_biz_profile_custom_url", 2582, false, false)); + properties.put(2583, new CompanionProperty("smb_biz_profile_custom_url_notifications", 2583, false, false)); + properties.put(2908, new CompanionProperty("smb_md_agent_chat_assignment_notifications_enabled", 2908, false, true)); + properties.put(3046, new CompanionProperty("smb_marketing_messages_enabled", 3046, false, true)); + properties.put(3113, new CompanionProperty("smb_marketing_messages_product_ids", 3113, "", "")); + properties.put(3124, new CompanionProperty("smb_rambutan_enabled", 3124, false, true)); + properties.put(3125, new CompanionProperty("smb_rambutan_product_ids", 3125, "", "")); + properties.put(4005, new CompanionProperty("smb_premium_messages_spam_report_enabled", 4005, false, true)); + properties.put(4596, new CompanionProperty("web_premium_messages_interactivity_rendering_enabled", 4596, false, true)); + properties.put(4657, new CompanionProperty("smb_premium_messages_click_logging_enabled", 4657, false, true)); + properties.put(4942, new CompanionProperty("smba_premium_messages_interactivity_catalog_cta_consumer_enabled", 4942, false, true)); + properties.put(4957, new CompanionProperty("smb_premium_messages_interactivity_catalog_cta_consumer_enabled", 4957, false, true)); + properties.put(5044, new CompanionProperty("smb_premium_messages_url_cta_alert_dialog_enabled", 5044, true, true)); + properties.put(5318, new CompanionProperty("premium_blue_enabled", 5318, false, false)); + properties.put(5276, new CompanionProperty("blue_enabled", 5276, false, false)); + properties.put(5295, new CompanionProperty("blue_education_enabled", 5295, false, false)); + properties.put(5277, new CompanionProperty("newsletter_blue_enabled", 5277, false, false)); + properties.put(5296, new CompanionProperty("newsletter_blue_education_enabled", 5296, false, false)); + properties.put(2249, new CompanionProperty("mex_phase3_enabled", 2249, false, false)); + properties.put(2250, new CompanionProperty("mex_phase3_status_flags", 2250, 0, 0)); + properties.put(3604, new CompanionProperty("mex_newsletter_killswitch", 3604, false, false)); + properties.put(5437, new CompanionProperty("mex_native_client_enabled", 5437, false, false)); + properties.put(3605, new CompanionProperty("mex_newsletter_flags", 3605, 0, 0)); + properties.put(2980, new CompanionProperty("groove_enabled_web", 2980, false, false)); + properties.put(3385, new CompanionProperty("newsletter_enabled", 3385, false, false)); + properties.put(3020, new CompanionProperty("newsletter_enabled_web", 3020, false, false)); + properties.put(3148, new CompanionProperty("newsletter_reporting_enabled", 3148, false, true)); + properties.put(3149, new CompanionProperty("newsletter_suspend_enabled", 3149, false, true)); + properties.put(4219, new CompanionProperty("channels_restricted_updates_enabled", 4219, false, true)); + properties.put(5161, new CompanionProperty("channels_geosuspend_enabled", 5161, false, true)); + properties.put(5216, new CompanionProperty("channels_geosuspend_admin_alerts_enabled", 5216, false, true)); + properties.put(5621, new CompanionProperty("channel_info_admin_metadata_fetching_enabled", 5621, false, true)); + properties.put(3209, new CompanionProperty("allow_nl_linkpreview", 3209, true, true)); + properties.put(3607, new CompanionProperty("newsletter_creation_enabled", 3607, false, false)); + properties.put(3778, new CompanionProperty("newsletter_media_autodownload_mode", 3778, 3, 3)); + properties.put(4369, new CompanionProperty("newsletter_media_autodownload_jitter_multiplier", 4369, 5000.0, 1000.0)); + properties.put(4370, new CompanionProperty("newsletter_media_autodownload_queue_max_concurrency", 4370, 5, 5)); + properties.put(4479, new CompanionProperty("newsletter_media_priority_queue_incoming_max_size", 4479, 32, 32)); + properties.put(3617, new CompanionProperty("nl_df_gid", 3617, "", "")); + properties.put(3618, new CompanionProperty("nl_crt_df_gid", 3618, "", "120363080354356818")); + properties.put(3810, new CompanionProperty("newsletter_tos_notice_id", 3810, "20601216", "20601216")); + properties.put(5448, new CompanionProperty("newsletter_tos_notice_version", 5448, 1, 1)); + properties.put(3834, new CompanionProperty("newsletter_creation_tos_id", 3834, "20601217", "20601217")); + properties.put(5449, new CompanionProperty("newsletter_creation_tos_version", 5449, "20601217", "20601217")); + properties.put(5456, new CompanionProperty("newsletter_creation_tos_version_v2", 5456, 1, 1)); + properties.put(3835, new CompanionProperty("newsletter_creation_nux_id", 3835, "20601218", "20601218")); + properties.put(5450, new CompanionProperty("newsletter_creation_nux_version", 5450, "20601218", "20601218")); + properties.put(5457, new CompanionProperty("newsletter_creation_nux_version_v2", 5457, 1, 1)); + properties.put(5597, new CompanionProperty("newsletter_tos_notice_id_smb_web", 5597, "20601216", "20601216")); + properties.put(5598, new CompanionProperty("newsletter_creation_tos_id_smb_web", 5598, "20601217", "20601217")); + properties.put(3877, new CompanionProperty("channels_enabled", 3877, 0, 0)); + properties.put(3878, new CompanionProperty("channels_creation_enabled", 3878, 0, 2)); + properties.put(3879, new CompanionProperty("channels_directory_enabled", 3879, 0, 2)); + properties.put(4356, new CompanionProperty("channels_recommended_enabled", 4356, 0, 2)); + properties.put(4590, new CompanionProperty("channels_view_counts_enabled", 4590, false, true)); + properties.put(4721, new CompanionProperty("channel_view_counts_enabled", 4721, 0, 3)); + properties.put(4648, new CompanionProperty("channel_views_duration_milliseconds", 4648, 1000.0, 1000.0)); + properties.put(4722, new CompanionProperty("channel_playable_message_views_duration_milliseconds", 4722, 3000.0, 3000.0)); + properties.put(4271, new CompanionProperty("channels_recommended_cache_ttl_ms", 4271, 6.048E8, 8.64E7)); + properties.put(3880, new CompanionProperty("show_channels_not_available_dialog", 3880, false, true)); + properties.put(3900, new CompanionProperty("newsletter_supported_message_types", 3900, """ + {"supported": [1, 2, 3, 9]}""", """ + {"supported": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]}""")); + properties.put(3919, new CompanionProperty("channel_supported_message_types", 3919, "1, 2, 3, 9, 10", "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15")); + properties.put(4282, new CompanionProperty("directory_sort_kill_switch", 4282, true, true)); + properties.put(4283, new CompanionProperty("directory_search_kill_switch", 4283, true, true)); + properties.put(5303, new CompanionProperty("channels_directory_v2_cache_ttl_ms", 5303, 7200000.0, 3600000.0)); + properties.put(5304, new CompanionProperty("channels_directory_v2_cache_refresh_interval_ms", 5304, 1800000.0, 600000.0)); + properties.put(4306, new CompanionProperty("channel_reactions_enabled", 4306, false, true)); + properties.put(4633, new CompanionProperty("channel_reactions_sending_enabled", 4633, false, true)); + properties.put(4887, new CompanionProperty("channel_reactions_settings_enabled", 4887, false, true)); + properties.put(5274, new CompanionProperty("channel_reactions_settings_none_option_enabled", 5274, false, false)); + properties.put(5185, new CompanionProperty("channel_reactions_sender_list_enabled", 5185, false, true)); + properties.put(4699, new CompanionProperty("reactions_allowlisted_channels", 4699, "", "120363144141162927,120363160538286018")); + properties.put(4307, new CompanionProperty("channel_follower_list_enabled", 4307, false, true)); + properties.put(5182, new CompanionProperty("channels_followers_list_cache_refresh_seconds", 5182, 60, 60)); + properties.put(5217, new CompanionProperty("channels_followers_list_cache_refresh_milliseconds", 5217, 60000.0, 60000.0)); + properties.put(4308, new CompanionProperty("recommended_channels_cache_max_ttl", 4308, 0, 0)); + properties.put(4309, new CompanionProperty("recommended_channels_background_refresh", 4309, 1.44E7, 1800000.0)); + properties.put(4326, new CompanionProperty("channel_pull_message_updates_threshold_seconds", 4326, 120, 120)); + properties.put(4338, new CompanionProperty("channel_forward_to_chat_enabled", 4338, false, true)); + properties.put(4860, new CompanionProperty("channel_forward_to_chat_link_enabled", 4860, false, true)); + properties.put(4357, new CompanionProperty("channels_waitlist_enabled", 4357, false, true)); + properties.put(4506, new CompanionProperty("channels_updates_tab_logging_enabled", 4506, false, true)); + properties.put(4632, new CompanionProperty("channels_waitlist_logging_enabled", 4632, false, true)); + properties.put(4635, new CompanionProperty("channels_dyi_enabled", 4635, false, true)); + properties.put(4866, new CompanionProperty("channels_dyi_max_file_size_in_bytes_warning_threshold", 4866, 1.0E9, 1.0E9)); + properties.put(5488, new CompanionProperty("channels_dyi_logging_enabled", 5488, false, true)); + properties.put(4644, new CompanionProperty("channel_forward_to_chat_v2_enabled", 4644, false, false)); + properties.put(4653, new CompanionProperty("channels_large_number_format_enabled", 4653, false, true)); + properties.put(4682, new CompanionProperty("channel_forward_to_chat_v2_message_navigation_enabled", 4682, false, true)); + properties.put(4683, new CompanionProperty("channel_forward_to_chat_v2_new_ui_enabled", 4683, false, true)); + properties.put(4684, new CompanionProperty("channels_view_counts_display_to_followers_enabled", 4684, false, true)); + properties.put(4760, new CompanionProperty("channels_send_view_receipt_enabled", 4760, false, true)); + properties.put(4782, new CompanionProperty("channels_forwarding_logging_enabled", 4782, false, true)); + properties.put(4783, new CompanionProperty("channels_directory_logging_enabled", 4783, false, true)); + properties.put(4784, new CompanionProperty("channels_creation_logging_enabled", 4784, false, true)); + properties.put(5015, new CompanionProperty("channels_filter_out_subscribed_in_directory_null_state", 5015, false, true)); + properties.put(5040, new CompanionProperty("ts_navigation_channels_enabled", 5040, false, true)); + properties.put(5262, new CompanionProperty("channel_core_event_logging_enabled", 5262, false, true)); + properties.put(5041, new CompanionProperty("channel_link_in_nav_bar_enabled", 5041, false, true)); + properties.put(5096, new CompanionProperty("channels_recommended_v2_ui_enabled", 5096, false, true)); + properties.put(5464, new CompanionProperty("channels_recommended_v2_recently_followed_channels_below_enabled", 5464, false, true)); + properties.put(5126, new CompanionProperty("channels_directory_v2_enabled", 5126, false, true)); + properties.put(5127, new CompanionProperty("channels_directory_v2_filter_types", 5127, "", "1, 2, 3, 4, 5, 6")); + properties.put(5158, new CompanionProperty("channels_admin_context_card_enabled", 5158, false, true)); + properties.put(5174, new CompanionProperty("channels_message_edit_enabled", 5174, false, true)); + properties.put(5188, new CompanionProperty("channels_message_link_enabled", 5188, false, true)); + properties.put(5203, new CompanionProperty("channels_directory_fetch_limit", 5203, 50, 50)); + properties.put(5204, new CompanionProperty("channels_directory_search_debounce_ms", 5204, 250, 250)); + properties.put(5205, new CompanionProperty("recommended_channels_fetch_limit", 5205, 20, 20)); + properties.put(5208, new CompanionProperty("channels_edit_backwards_compatibility", 5208, false, true)); + properties.put(5287, new CompanionProperty("channels_hide_news_url_preview", 5287, false, true)); + properties.put(5402, new CompanionProperty("large_number_format_uses_generic_plural", 5402, true, true)); + properties.put(5471, new CompanionProperty("channels_directory_v2_logging_enabled", 5471, false, true)); + properties.put(5491, new CompanionProperty("channels_share_link_logging_enabled", 5491, false, true)); + properties.put(5492, new CompanionProperty("channels_forward_logging_v2_enabled", 5492, false, true)); + properties.put(5487, new CompanionProperty("channels_directory_pagination_enabled", 5487, false, true)); + properties.put(5489, new CompanionProperty("updates_tab_open_channels_fields_logging_enabled", 5489, false, true)); + properties.put(5494, new CompanionProperty("channels_max_messages_batch_pull", 5494, 100, 100)); + properties.put(5511, new CompanionProperty("channels_hq_link_preview", 5511, false, false)); + properties.put(5533, new CompanionProperty("channels_poll_creation_enabled", 5533, false, true)); + properties.put(5534, new CompanionProperty("channels_poll_single_option_control_enable", 5534, false, true)); + properties.put(5551, new CompanionProperty("channels_web_bootstrap_timeout_enabled", 5551, false, true)); + properties.put(5625, new CompanionProperty("channels_media_cache_setting_enabled", 5625, false, false)); + properties.put(5643, new CompanionProperty("channels_send_album_enabled", 5643, false, true)); + properties.put(5646, new CompanionProperty("channels_message_loading_indicators_enabled", 5646, false, true)); + properties.put(3710, new CompanionProperty("otp_ttl_inject_receipt_enabled", 3710, false, true)); + properties.put(3827, new CompanionProperty("unified_otp_copy_code_url", 3827, "https://www.whatsapp.com/otp/copy/", "https://www.whatsapp.com/otp/copy/")); + properties.put(3828, new CompanionProperty("unified_otp_retriever_url", 3828, "https://www.whatsapp.com/otp/code", "https://www.whatsapp.com/otp/code")); + properties.put(4330, new CompanionProperty("web_otp_copy_code_disabled", 4330, false, false)); + properties.put(3514, new CompanionProperty("lid_groups_ougtoing_explict_address_mode", 3514, false, true)); + properties.put(3615, new CompanionProperty("lid_groups_outgoing_explict_address_mode", 3615, false, true)); + properties.put(3645, new CompanionProperty("lid_groups_new_group_creation", 3645, false, false)); + properties.put(3688, new CompanionProperty("lid_groups_handle_server_addressing_mode", 3688, false, false)); + properties.put(3876, new CompanionProperty("lid_groups_create_lid_individual_chats", 3876, false, false)); + properties.put(3803, new CompanionProperty("lid_groups_outgoing_explicit_address_mode", 3803, false, true)); + properties.put(3804, new CompanionProperty("lid_groups_aggregate_participant_change_system_message", 3804, false, false)); + properties.put(4162, new CompanionProperty("lid_groups_message_send_validation", 4162, false, true)); + properties.put(4476, new CompanionProperty("pnh_copy_identity_keys_and_devices", 4476, false, true)); + properties.put(4533, new CompanionProperty("pnh_sync_identity_keys_and_devices", 4533, false, true)); + properties.put(5555, new CompanionProperty("invalid_hosted_companion_nack_enabled", 5555, false, true)); + properties.put(5623, new CompanionProperty("lid_outgoing_msg_attach_meta_tag", 5623, false, false)); + properties.put(3180, new CompanionProperty("group_suspend_v2_enabled", 3180, false, true)); + properties.put(3988, new CompanionProperty("enable_status_report_and_block", 3988, false, true)); + properties.put(5245, new CompanionProperty("report_block_classification_logging_enabled", 5245, false, true)); + properties.put(5716, new CompanionProperty("rt_validate_message_type", 5716, false, false)); + properties.put(5717, new CompanionProperty("rt_send_reporting_tag", 5717, false, false)); + properties.put(5718, new CompanionProperty("rt_receive_reporting_tag", 5718, false, false)); + properties.put(3471, new CompanionProperty("df_config", 3471, "", "")); + properties.put(3472, new CompanionProperty("df_enabled", 3472, false, false)); + properties.put(4873, new CompanionProperty("wabai_message_rendering_enabled", 4873, false, false)); + properties.put(5215, new CompanionProperty("wabai_message_feedback_enabled", 5215, false, false)); + properties.put(5224, new CompanionProperty("wabai_marketing_message_content_gen_enabled", 5224, false, false)); + properties.put(5330, new CompanionProperty("ctwa_content_gen_enabled", 5330, false, false)); + //noinspection Java9CollectionFactory + PROPERTIES = Collections.unmodifiableMap(properties); + } + + public static boolean anyMatch(String input, String regex) { + return Pattern.compile(regex) + .matcher(input) + .results() + .findAny() + .isPresent(); + } +} diff --git a/src/main/java/it/auties/whatsapp/controller/Controller.java b/src/main/java/it/auties/whatsapp/controller/Controller.java new file mode 100644 index 000000000..f1d5e5e4a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/Controller.java @@ -0,0 +1,169 @@ +package it.auties.whatsapp.controller; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.api.ClientType; +import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.util.Json; +import it.auties.whatsapp.util.ProtobufUuidMixin; + +import java.util.*; + +/** + * This interface represents is implemented by all WhatsappWeb4J's controllers. It provides an easy + * way to store IDs and serialize said class. + */ +@SuppressWarnings("unused") +public abstract sealed class Controller> implements ProtobufMessage permits Store, Keys { + /** + * The id of this controller + */ + @ProtobufProperty(index = 1, type = ProtobufType.STRING, mixin = ProtobufUuidMixin.class) + protected final UUID uuid; + + /** + * The phone number of the associated companion + */ + @ProtobufProperty(index = 2, type = ProtobufType.UINT64) + private PhoneNumber phoneNumber; + + /** + * The serializer instance to use + */ + @JsonIgnore + protected ControllerSerializer serializer; + + /** + * The client type + */ + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + protected final ClientType clientType; + + /** + * A list of alias for the controller, can be used in place of UUID1 + */ + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + protected final Collection alias; + + public Controller(UUID uuid, PhoneNumber phoneNumber, ControllerSerializer serializer, ClientType clientType, Collection alias) { + this.uuid = Objects.requireNonNull(uuid, "Missing uuid"); + this.phoneNumber = phoneNumber; + this.serializer = serializer; + this.clientType = clientType; + this.alias = Objects.requireNonNullElseGet(alias, ArrayList::new); + } + + /** + * Serializes this object + * + * @param async whether the operation should be executed asynchronously + */ + public abstract void serialize(boolean async); + + /** + * Disposes this object + */ + public abstract void dispose(); + + public UUID uuid() { + return uuid; + } + + public ClientType clientType() { + return this.clientType; + } + + /** + * Returns the phone number of this controller + * + * @return an optional + */ + public Optional phoneNumber() { + return Optional.ofNullable(phoneNumber); + } + + /** + * Sets the phone number used by this session + * + * @return the same instance + */ + @SuppressWarnings("unchecked") + public T setPhoneNumber(PhoneNumber phoneNumber) { + this.phoneNumber = phoneNumber; + serializer.linkMetadata(this); + return (T) this; + } + + /** + * Returns the serializer + * + * @return a non-null serializer + */ + public ControllerSerializer serializer() { + return serializer; + } + + /** + * Sets the serializer of this controller + * + * @param serializer a serializer + * @return the same instance + */ + @SuppressWarnings("unchecked") + public T setSerializer(ControllerSerializer serializer) { + this.serializer = serializer; + return (T) this; + } + + /** + * Returns an immutable collection of alias + * + * @return an immutable collection + */ + public Collection alias() { + return Collections.unmodifiableCollection(alias); + } + + /** + * Adds an alias to this controller + * + * @param entry the non-null alias to add + */ + public void addAlias(String entry) { + alias.add(entry); + } + + /** + * Removes an alias to this controller + * + * @param entry the non-null alias to remove + */ + public void removeAlias(String entry) { + alias.remove(entry); + } + + /** + * Removes all alias from this controller + */ + public void removeAlias() { + alias.clear(); + } + + /** + * Converts this controller to a json. Useful when debugging. + * + * @return a non-null string + */ + public String toJson() { + return Json.writeValueAsString(this, true); + } + + /** + * Deletes the current session + */ + public void deleteSession() { + serializer.deleteSession(this); + } +} diff --git a/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java new file mode 100644 index 000000000..790ac015a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/ControllerSerializer.java @@ -0,0 +1,246 @@ +package it.auties.whatsapp.controller; + +import it.auties.whatsapp.api.ClientType; +import it.auties.whatsapp.model.mobile.PhoneNumber; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +/** + * This interface provides a standardized way to serialize a session + */ +@SuppressWarnings("unused") +public interface ControllerSerializer { + /** + * Returns the default serializer + * This implementation uses .proto files compressed using gzip + * + * @return a serializer + */ + static ControllerSerializer discarding() { + return DiscardingControllerSerializer.singleton(); + } + + /** + * Returns the default serializer + * This implementation uses .proto files compressed using gzip + * + * @return a serializer + */ + static ControllerSerializer toProtobuf() { + return ProtobufControllerSerializer.ofDefaultPath(); + } + + /** + * Returns the default serializer + * This implementation uses .proto files compressed using gzip + * + * @param baseDirectory the directory where all the sessions should be saved + * @return a serializer + */ + static ControllerSerializer toProtobuf(Path baseDirectory) { + return ProtobufControllerSerializer.of(baseDirectory); + } + + /** + * Returns all the known IDs + * + * @param type the non-null type of client + * @return a non-null linked list + */ + LinkedList listIds(ClientType type); + + /** + * Returns all the known IDs + * + * @param type the non-null type of client + * @return a non-null linked list + */ + LinkedList listPhoneNumbers(ClientType type); + + /** + * Creates a fresh pair of store and keys + * + * @param uuid the non-null uuid + * @param phoneNumber the nullable phone number + * @param alias the nullable alias + * @param clientType the non-null client type + * @return a non-null store-keys pair + */ + default StoreKeysPair newStoreKeysPair(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType) { + var store = Store.newStore(uuid, phoneNumber, alias, clientType); + store.setSerializer(this); + linkMetadata(store); + var keys = Keys.newKeys(uuid, phoneNumber, alias, clientType); + keys.setSerializer(this); + serializeKeys(keys, true); + return new StoreKeysPair(store, keys); + } + + /** + * Deserializes a store-keys pair from a list of possible identifiers + * + * @param uuid the nullable identifying unique id + * @param phoneNumber the nullable identifying phone number + * @param alias the nullable identifying alias + * @param clientType the non-null client type + * @return an optional store-keys pair + */ + default Optional deserializeStoreKeysPair(UUID uuid, Long phoneNumber, String alias, ClientType clientType) { + if (uuid != null) { + var store = deserializeStore(clientType, uuid); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, uuid); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + if (phoneNumber != null) { + var store = deserializeStore(clientType, phoneNumber); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, phoneNumber); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + if (alias != null) { + var store = deserializeStore(clientType, alias); + if(store.isEmpty()) { + return Optional.empty(); + } + + store.get().setSerializer(this); + attributeStore(store.get()); + var keys = deserializeKeys(clientType, alias); + if(keys.isEmpty()) { + return Optional.empty(); + } + + keys.get().setSerializer(this); + return Optional.of(new StoreKeysPair(store.get(), keys.get())); + } + + return Optional.empty(); + } + + /** + * Serializes the keys + * + * @param keys the non-null keys to serialize + * @param async whether the operation should be executed asynchronously + */ + CompletableFuture serializeKeys(Keys keys, boolean async); + + /** + * Serializes the store + * + * @param store the non-null store to serialize + * @param async whether the operation should be executed asynchronously + */ + CompletableFuture serializeStore(Store store, boolean async); + + /** + * Serializes the keys + * + * @param type the non-null type of client + * @param id the id of the keys + * @return a non-null keys + */ + Optional deserializeKeys(ClientType type, UUID id); + + /** + * Serializes the keys + * + * @param type the non-null type of client + * @param phoneNumber the phone number of the keys + * @return a non-null keys + */ + Optional deserializeKeys(ClientType type, long phoneNumber); + + + /** + * Serializes the keys + * + * @param type the non-null type of client + * @param alias the alias number of the keys + * @return a non-null keys + */ + Optional deserializeKeys(ClientType type, String alias); + + /** + * Serializes the store + * + * @param type the non-null type of client + * @param id the id of the store + * @return a non-null store + */ + Optional deserializeStore(ClientType type, UUID id); + + /** + * Serializes the store + * + * @param type the non-null type of client + * @param phoneNumber the phone number of the store + * @return a non-null store + */ + Optional deserializeStore(ClientType type, long phoneNumber); + + /** + * Serializes the store + * + * @param type the non-null type of client + * @param alias the alias of the store + * @return a non-null store + */ + Optional deserializeStore(ClientType type, String alias); + + /** + * Deletes a session + * + * @param controller the non-null controller + */ + void deleteSession(Controller controller); + + /** + * Creates a link between the session and its metadata, usually phone number and alias + * + * @param controller a non-null controller + */ + default void linkMetadata(Controller controller) { + + } + + /** + * Attributes the store asynchronously. This method is optionally used to load asynchronously + * heavy data such as chats while the socket is connecting. If implemented, cache the returning + * newsletters because the method may be called multiple times. + * + * @param store the non-null store to attribute + * @return a completable newsletters + */ + default CompletableFuture attributeStore(Store store) { + return CompletableFuture.completedFuture(null); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/controller/DiscardingControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/DiscardingControllerSerializer.java new file mode 100644 index 000000000..a045b02fd --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/DiscardingControllerSerializer.java @@ -0,0 +1,78 @@ +package it.auties.whatsapp.controller; + +import it.auties.whatsapp.api.ClientType; +import it.auties.whatsapp.model.mobile.PhoneNumber; + +import java.util.LinkedList; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +class DiscardingControllerSerializer implements ControllerSerializer { + private static final DiscardingControllerSerializer SINGLETON = new DiscardingControllerSerializer(); + private static final LinkedList EMPTY_IDS = new LinkedList<>(); + private static final LinkedList EMPTY_PHONE_NUMBERS = new LinkedList<>(); + + public static ControllerSerializer singleton() { + return SINGLETON; + } + + @Override + public LinkedList listIds(ClientType type) { + return EMPTY_IDS; + } + + @Override + public LinkedList listPhoneNumbers(ClientType type) { + return EMPTY_PHONE_NUMBERS; + } + + @Override + public Optional deserializeStoreKeysPair(UUID uuid, Long phoneNumber, String alias, ClientType clientType) { + return Optional.empty(); + } + + @Override + public CompletableFuture serializeKeys(Keys keys, boolean async) { + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture serializeStore(Store store, boolean async) { + return CompletableFuture.completedFuture(null); + } + + @Override + public Optional deserializeKeys(ClientType type, UUID id) { + return Optional.empty(); + } + + @Override + public Optional deserializeKeys(ClientType type, long phoneNumber) { + return Optional.empty(); + } + + @Override + public Optional deserializeKeys(ClientType type, String alias) { + return Optional.empty(); + } + + @Override + public Optional deserializeStore(ClientType type, UUID id) { + return Optional.empty(); + } + + @Override + public Optional deserializeStore(ClientType type, long phoneNumber) { + return Optional.empty(); + } + + @Override + public Optional deserializeStore(ClientType type, String alias) { + return Optional.empty(); + } + + @Override + public void deleteSession(Controller controller) { + + } +} diff --git a/src/main/java/it/auties/whatsapp/controller/Keys.java b/src/main/java/it/auties/whatsapp/controller/Keys.java new file mode 100644 index 000000000..14b6b1ee4 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/Keys.java @@ -0,0 +1,685 @@ +package it.auties.whatsapp.controller; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.api.ClientType; +import it.auties.whatsapp.model.companion.CompanionHashState; +import it.auties.whatsapp.model.companion.CompanionPatch; +import it.auties.whatsapp.model.companion.CompanionSyncKey; +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; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.signal.keypair.SignalPreKeyPair; +import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair; +import it.auties.whatsapp.model.signal.sender.SenderKeyName; +import it.auties.whatsapp.model.signal.sender.SenderKeyRecord; +import it.auties.whatsapp.model.signal.sender.SenderPreKeys; +import it.auties.whatsapp.model.signal.session.Session; +import it.auties.whatsapp.model.signal.session.SessionAddress; +import it.auties.whatsapp.model.sync.AppStateSyncKey; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.Clock; +import it.auties.whatsapp.util.KeyHelper; +import it.auties.whatsapp.util.ProtobufUuidMixin; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNullElseGet; + +/** + * This controller holds the cryptographic-related data regarding a WhatsappWeb session + */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public final class Keys extends Controller implements ProtobufMessage { + /** + * The client id + */ + @ProtobufProperty(index = 5, type = ProtobufType.INT32) + final Integer registrationId; + + /** + * The secret key pair used for buffer messages + */ + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + final SignalKeyPair noiseKeyPair; + + /** + * The ephemeral key pair + */ + @ProtobufProperty(index = 7, type = ProtobufType.OBJECT) + final SignalKeyPair ephemeralKeyPair; + + /** + * The signed identity key + */ + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + final SignalKeyPair identityKeyPair; + + /** + * The companion secret key + */ + @ProtobufProperty(index = 9, type = ProtobufType.OBJECT) + SignalKeyPair companionKeyPair; + + /** + * The signed pre key + */ + @ProtobufProperty(index = 10, type = ProtobufType.OBJECT) + final SignalSignedKeyPair signedKeyPair; + + /** + * The signed key of the companion's device + * This value will be null until it gets synced by whatsapp + */ + @ProtobufProperty(index = 11, type = ProtobufType.BYTES) + byte[] signedKeyIndex; + + /** + * The timestampSeconds of the signed key companion's device + */ + @ProtobufProperty(index = 12, type = ProtobufType.UINT64) + Long signedKeyIndexTimestamp; + + /** + * Whether these keys have generated pre keys assigned to them + */ + @ProtobufProperty(index = 13, type = ProtobufType.OBJECT) + final List preKeys; + + /** + * The phone id for the mobile api + */ + @ProtobufProperty(index = 14, type = ProtobufType.STRING) + final String fdid; + + /** + * The device id for the mobile api + */ + @ProtobufProperty(index = 15, type = ProtobufType.BYTES) + final byte[] deviceId; + + @ProtobufProperty(index = 26, type = ProtobufType.STRING, mixin = ProtobufUuidMixin.class) + final UUID advertisingId; + + /** + * The recovery token for the mobile api + */ + @ProtobufProperty(index = 16, type = ProtobufType.BYTES) + final byte[] identityId; + + /** + * The bytes of the encoded {@link SignedDeviceIdentityHMAC} received during the auth process + */ + @ProtobufProperty(index = 17, type = ProtobufType.OBJECT) + SignedDeviceIdentity companionIdentity; + + /** + * Sender keys for signal implementation + */ + @ProtobufProperty(index = 18, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final Map senderKeys; + + /** + * App state keys + */ + @ProtobufProperty(index = 19, type = ProtobufType.OBJECT) + final List appStateKeys; + + /** + * Sessions map + */ + @ProtobufProperty(index = 20, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final Map sessions; + + /** + * Hash state + */ + @ProtobufProperty(index = 21, type = ProtobufType.OBJECT) + final List hashStates; + + + @ProtobufProperty(index = 22, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final Map groupsPreKeys; + + /** + * Whether the client was registered + */ + @ProtobufProperty(index = 23, type = ProtobufType.BOOL) + boolean registered; + + /** + * Whether the client has already sent its business certificate (mobile api only) + */ + @ProtobufProperty(index = 24, type = ProtobufType.BOOL) + boolean businessCertificate; + + /** + * Whether the client received the initial app sync (web api only) + */ + @ProtobufProperty(index = 25, type = ProtobufType.BOOL) + boolean initialAppSync; + + /** + * Write counter for IV + */ + @JsonIgnore + final AtomicLong writeCounter; + + /** + * Read counter for IV + */ + @JsonIgnore + final AtomicLong readCounter; + + /** + * Session dependent keys to write and read cyphered messages + */ + @JsonIgnore + byte[] writeKey, readKey; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Keys(UUID uuid, PhoneNumber phoneNumber, ClientType clientType, Collection alias, Integer registrationId, SignalKeyPair noiseKeyPair, SignalKeyPair ephemeralKeyPair, SignalKeyPair identityKeyPair, SignalKeyPair companionKeyPair, SignalSignedKeyPair signedKeyPair, byte[] signedKeyIndex, Long signedKeyIndexTimestamp, List preKeys, String fdid, byte[] deviceId, UUID advertisingId, byte[] identityId, SignedDeviceIdentity companionIdentity, Map senderKeys, List appStateKeys, Map sessions, List hashStates, Map groupsPreKeys, boolean registered, boolean businessCertificate, boolean initialAppSync) { + super(uuid, phoneNumber, null, clientType, alias); + this.registrationId = Objects.requireNonNullElseGet(registrationId, KeyHelper::registrationId); + this.noiseKeyPair = Objects.requireNonNull(noiseKeyPair, "Missing noise keypair"); + this.ephemeralKeyPair = Objects.requireNonNullElseGet(ephemeralKeyPair, SignalKeyPair::random); + this.identityKeyPair = Objects.requireNonNull(identityKeyPair, "Missing identity keypair"); + this.companionKeyPair = Objects.requireNonNullElseGet(companionKeyPair, SignalKeyPair::random); + this.signedKeyPair = Objects.requireNonNullElseGet(signedKeyPair, () -> SignalSignedKeyPair.of(this.registrationId, identityKeyPair)); + this.signedKeyIndex = signedKeyIndex; + this.signedKeyIndexTimestamp = signedKeyIndexTimestamp; + this.preKeys = Objects.requireNonNullElseGet(preKeys, ArrayList::new); + this.fdid = Objects.requireNonNullElseGet(fdid, KeyHelper::fdid); + this.deviceId = Objects.requireNonNullElseGet(deviceId, KeyHelper::deviceId); + this.advertisingId = Objects.requireNonNullElseGet(advertisingId, UUID::randomUUID); + this.identityId = Objects.requireNonNull(identityId, "Missing identity id"); + this.companionIdentity = companionIdentity; + this.senderKeys = Objects.requireNonNullElseGet(senderKeys, ConcurrentHashMap::new); + this.appStateKeys = Objects.requireNonNullElseGet(appStateKeys, ArrayList::new); + this.sessions = Objects.requireNonNullElseGet(sessions, ConcurrentHashMap::new); + this.hashStates = Objects.requireNonNullElseGet(hashStates, ArrayList::new); + this.groupsPreKeys = Objects.requireNonNullElseGet(groupsPreKeys, ConcurrentHashMap::new); + this.registered = registered; + this.businessCertificate = businessCertificate; + this.initialAppSync = initialAppSync; + this.writeCounter = new AtomicLong(); + this.readCounter = new AtomicLong(); + } + + public static Keys newKeys(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType) { + return new KeysBuilder() + .uuid(uuid) + .phoneNumber(PhoneNumber.ofNullable(phoneNumber).orElse(null)) + .alias(alias) + .clientType(clientType) + .registrationId(KeyHelper.registrationId()) + .noiseKeyPair(SignalKeyPair.random()) + .identityKeyPair(SignalKeyPair.random()) + .identityId(KeyHelper.identityId()) + .build(); + } + + /** + * Returns the encoded id + * + * @return a non-null byte array + */ + public byte[] encodedRegistrationId() { + return BytesHelper.intToBytes(registrationId(), 4); + } + + /** + * Clears the signal keys associated with this object + */ + public void clearReadWriteKey() { + this.writeKey = null; + this.writeCounter.set(0); + this.readCounter.set(0); + } + + /** + * Checks if the client sent pre keys to the server + * + * @return true if the client sent pre keys to the server + */ + public boolean hasPreKeys() { + return !preKeys.isEmpty(); + } + + /** + * Queries the first {@link SenderKeyRecord} that matches {@code name} + * + * @param name the non-null name to search + * @return a non-null SenderKeyRecord + */ + public SenderKeyRecord findSenderKeyByName(SenderKeyName name) { + return requireNonNullElseGet(senderKeys.get(name), () -> { + var record = new SenderKeyRecord(); + senderKeys.put(name, record); + return record; + }); + } + + /** + * Queries the {@link Session} that matches {@code address} + * + * @param address the non-null address to search + * @return a non-null Optional SessionRecord + */ + public Optional findSessionByAddress(SessionAddress address) { + return Optional.ofNullable(sessions.get(address)); + } + + /** + * Queries the trusted key that matches {@code id} + * + * @param id the id to search + * @return a non-null signed key pair + * @throws IllegalArgumentException if no element can be found + */ + public Optional findSignedKeyPairById(int id) { + return id == signedKeyPair.id() ? Optional.of(signedKeyPair) : Optional.empty(); + } + + /** + * Queries the trusted key that matches {@code id} + * + * @param id the non-null id to search + * @return a non-null pre key + */ + public Optional findPreKeyById(Integer id) { + return id == null ? Optional.empty() : preKeys.stream().filter(preKey -> preKey.id() == id).findFirst(); + } + + /** + * Queries the app state key that matches {@code id} + * + * @param jid the non-null jid of the app key + * @param id the non-null id to search + * @return a non-null Optional app state dataSync key + */ + public Optional findAppKeyById(Jid jid, byte[] id) { + return appStateKeys.stream() + .filter(preKey -> Objects.equals(preKey.companion(), jid)) + .map(CompanionSyncKey::keys) + .flatMap(Collection::stream) + .filter(preKey -> preKey.keyId() != null && Arrays.equals(preKey.keyId().keyId(), id)) + .findFirst(); + } + + /** + * Queries the hash state that matches {@code name}. Otherwise, creates a new one. + * + * @param device the non-null device + * @param patchType the non-null name to search + * @return a non-null hash state + */ + public Optional findHashStateByName(Jid device, PatchType patchType) { + return hashStates.stream() + .filter(hashState -> Objects.equals(hashState.companion(), device) && hashState.state().type() == patchType) + .findFirst() + .map(CompanionPatch::state); + } + + /** + * Checks whether {@code identityKey} is trusted for {@code address} + * + * @param address the non-null address + * @param identityKey the nullable identity key + * @return true if any match is found + */ + public boolean hasTrust(SessionAddress address, byte[] identityKey) { + return true; // At least for now + } + + /** + * Checks whether a session already whatsappOldEligible for the given address + * + * @param address the address to check + * @return true if a session for that address already whatsappOldEligible + */ + public boolean hasSession(SessionAddress address) { + return sessions.containsKey(address); + } + + /** + * Adds the provided address and record to the known sessions + * + * @param address the non-null address + * @param record the non-null record + * @return this + */ + public Keys putSession(SessionAddress address, Session record) { + sessions.put(address, record); + return this; + } + + /** + * Adds the provided hash state to the known ones + * + * @param device the non-null device + * @param state the non-null hash state + * @return this + */ + public Keys putState(Jid device, CompanionHashState state) { + var hashState = new CompanionPatch(device, state); + hashStates.add(hashState); + return this; + } + + /** + * Adds the provided keys to the app state keys + * + * @param jid the non-null jid of the app key + * @param keys the keys to add + * @return this + */ + public Keys addAppKeys(Jid jid, Collection keys) { + appStateKeys.stream() + .filter(preKey -> Objects.equals(preKey.companion(), jid)) + .findFirst() + .ifPresentOrElse(key -> key.keys().addAll(keys), () -> { + var syncKey = new CompanionSyncKey(jid, new LinkedList<>(keys)); + appStateKeys.add(syncKey); + }); + return this; + } + + /** + * Get any available app key + * + * @return a non-null app key + */ + public AppStateSyncKey getLatestAppKey(Jid jid) { + return getAppKeys(jid).getLast(); + } + + /** + * Get any available app key + * + * @return a non-null app key + */ + public LinkedList getAppKeys(Jid jid) { + return appStateKeys.stream() + .filter(preKey -> Objects.equals(preKey.companion(), jid)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Missing keys")) + .keys(); + } + + /** + * Adds the provided pre key to the pre keys + * + * @param preKey the key to add + * @return this + */ + public Keys addPreKey(SignalPreKeyPair preKey) { + preKeys.add(preKey); + return this; + } + + /** + * Returns write counter + * + * @param increment whether the counter should be incremented after the call + * @return an unsigned long + */ + public long writeCounter(boolean increment) { + return increment ? writeCounter.getAndIncrement() : writeCounter.get(); + } + + /** + * Returns read counter + * + * @param increment whether the counter should be incremented after the call + * @return an unsigned long + */ + public long readCounter(boolean increment) { + return increment ? readCounter.getAndIncrement() : readCounter.get(); + } + + /** + * Returns the id of the last available pre key + * + * @return an integer + */ + public int lastPreKeyId() { + return preKeys.isEmpty() ? 0 : preKeys.getLast().id(); + } + + /** + * This function sets the companionIdentity field to the value of the companionIdentity parameter, + * serializes the object, and returns the object. + * + * @param companionIdentity The identity of the companion device. + * @return The object itself. + */ + public Keys companionIdentity(SignedDeviceIdentity companionIdentity) { + this.companionIdentity = companionIdentity; + return this; + } + + /** + * Returns the companion identity of this session + * Only available for web sessions + * + * @return an optional + */ + public Optional companionIdentity() { + return Optional.ofNullable(companionIdentity); + } + + /** + * Returns all the registered pre keys + * + * @return a non-null collection + */ + public Collection preKeys() { + return Collections.unmodifiableList(preKeys); + } + + public void addRecipientWithPreKeys(Jid group, Jid recipient) { + var preKeys = groupsPreKeys.get(group); + if (preKeys != null) { + preKeys.addPreKey(recipient); + return; + } + + var newPreKeys = new SenderPreKeys(); + newPreKeys.addPreKey(recipient); + groupsPreKeys.put(group, newPreKeys); + } + + public void addRecipientsWithPreKeys(Jid group, Collection recipients) { + var preKeys = groupsPreKeys.get(group); + if (preKeys != null) { + preKeys.addPreKeys(recipients); + return; + } + + var newPreKeys = new SenderPreKeys(); + newPreKeys.addPreKeys(recipients); + groupsPreKeys.put(group, newPreKeys); + } + + public boolean hasGroupKeys(Jid group, Jid recipient) { + var preKeys = groupsPreKeys.get(group); + return preKeys != null && preKeys.contains(recipient); + } + + @Override + public void dispose() { + serialize(false); + } + + @Override + public void serialize(boolean async) { + serializer.serializeKeys(this, async); + } + + public int registrationId() { + return this.registrationId; + } + + public SignalKeyPair noiseKeyPair() { + return this.noiseKeyPair; + } + + public SignalKeyPair ephemeralKeyPair() { + return this.ephemeralKeyPair; + } + + public SignalKeyPair identityKeyPair() { + return this.identityKeyPair; + } + + public SignalSignedKeyPair signedKeyPair() { + return this.signedKeyPair; + } + + public Optional signedKeyIndex() { + return Optional.ofNullable(signedKeyIndex); + } + + public OptionalLong signedKeyIndexTimestamp() { + return Clock.parseTimestamp(signedKeyIndexTimestamp); + } + + public SignalKeyPair companionKeyPair() { + return this.companionKeyPair; + } + + public String fdid() { + return this.fdid; + } + + public byte[] deviceId() { + return this.deviceId; + } + + public UUID advertisingId() { + return this.advertisingId; + } + + public byte[] identityId() { + return this.identityId; + } + + public Map senderKeys() { + return this.senderKeys; + } + + public List appStateKeys() { + return appStateKeys; + } + + public Map sessions() { + return this.sessions; + } + + public List hashStates() { + return hashStates; + } + + public Map groupsPreKeys() { + return this.groupsPreKeys; + } + + public boolean registered() { + return this.registered; + } + + public boolean businessCertificate() { + return this.businessCertificate; + } + + public boolean initialAppSync() { + return this.initialAppSync; + } + + public AtomicLong writeCounter() { + return this.writeCounter; + } + + public AtomicLong readCounter() { + return this.readCounter; + } + + public Optional writeKey() { + return Optional.ofNullable(this.writeKey); + } + + public Optional readKey() { + return Optional.ofNullable(this.readKey); + } + + public Keys setCompanionKeyPair(SignalKeyPair companionKeyPair) { + this.companionKeyPair = companionKeyPair; + return this; + } + + public Keys setSignedKeyIndex(byte[] signedKeyIndex) { + this.signedKeyIndex = signedKeyIndex; + return this; + } + + public Keys setSignedKeyIndexTimestamp(Long signedKeyIndexTimestamp) { + this.signedKeyIndexTimestamp = signedKeyIndexTimestamp; + return this; + } + + public Keys setCompanionIdentity(SignedDeviceIdentity companionIdentity) { + this.companionIdentity = companionIdentity; + return this; + } + + public Keys setRegistered(boolean registered) { + this.registered = registered; + return this; + } + + public Keys setBusinessCertificate(boolean businessCertificate) { + this.businessCertificate = businessCertificate; + return this; + } + + public Keys setInitialAppSync(boolean initialAppSync) { + this.initialAppSync = initialAppSync; + return this; + } + + public Keys setWriteKey(byte[] writeKey) { + this.writeKey = writeKey; + return this; + } + + public Keys setReadKey(byte[] readKey) { + this.readKey = readKey; + return this; + } + + /** + * Six part keys representation + * + * @return a string + */ + @Override + public String toString() { + var cryptographicKeys = Stream.of(noiseKeyPair.publicKey(), noiseKeyPair.privateKey(), identityKeyPair.publicKey(), identityKeyPair.privateKey(), identityId()) + .map(Base64.getEncoder()::encodeToString) + .collect(Collectors.joining(",")); + return phoneNumber() + .map(phoneNumber -> phoneNumber + "," + cryptographicKeys) + .orElse(cryptographicKeys); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/controller/ProtobufControllerSerializer.java b/src/main/java/it/auties/whatsapp/controller/ProtobufControllerSerializer.java new file mode 100644 index 000000000..09d8f1fee --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/ProtobufControllerSerializer.java @@ -0,0 +1,801 @@ +package it.auties.whatsapp.controller; + +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.chat.ChatSpec; +import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.ContextualMessage; +import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.newsletter.NewsletterSpec; +import it.auties.whatsapp.model.sync.HistorySyncMessage; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +class ProtobufControllerSerializer implements ControllerSerializer { + private static final Path DEFAULT_SERIALIZER_PATH = Path.of(System.getProperty("user.home") + "/.cobalt/"); + private static final String CHAT_PREFIX = "chat_"; + private static final String NEWSLETTER_PREFIX = "newsletter_"; + private static final String STORE_NAME = "store.proto"; + private static final String KEYS_NAME = "keys.proto"; + + private static final Map serializers = new ConcurrentHashMap<>(); + private final Path baseDirectory; + private final ConcurrentMap> attributeStoreSerializers; + private LinkedList cachedUuids; + private LinkedList cachedPhoneNumbers; + + static { + serializers.put(DEFAULT_SERIALIZER_PATH, new ProtobufControllerSerializer(DEFAULT_SERIALIZER_PATH)); + } + + public static ControllerSerializer ofDefaultPath() { + return Objects.requireNonNull(serializers.get(DEFAULT_SERIALIZER_PATH)); + } + + public static ControllerSerializer of(Path baseDirectory) { + var known = serializers.get(baseDirectory); + if(known != null) { + return known; + } + + var result = new ProtobufControllerSerializer(baseDirectory); + serializers.put(baseDirectory, result); + return result; + } + + private ProtobufControllerSerializer(Path baseDirectory) { + this.baseDirectory = baseDirectory; + this.attributeStoreSerializers = new ConcurrentHashMap<>(); + } + + @Override + public LinkedList listIds(ClientType type) { + if (cachedUuids != null) { + return new ImmutableLinkedList<>(cachedUuids); + } + + var directory = getHome(type); + if (Files.notExists(directory)) { + return ImmutableLinkedList.empty(); + } + + 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)); + } catch (IOException exception) { + return ImmutableLinkedList.empty(); + } + } + + @Override + public LinkedList listPhoneNumbers(ClientType type) { + if (cachedPhoneNumbers != null) { + return new ImmutableLinkedList<>(cachedPhoneNumbers); + } + + var directory = getHome(type); + if (Files.notExists(directory)) { + return ImmutableLinkedList.empty(); + } + + 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)); + } catch (IOException exception) { + return ImmutableLinkedList.empty(); + } + } + + private FileTime getLastModifiedTime(Path path) { + try { + return Files.getLastModifiedTime(path); + } catch (IOException exception) { + return FileTime.fromMillis(0); + } + } + + private Optional parsePathAsId(Path file) { + try { + return Optional.of(UUID.fromString(file.getFileName().toString())); + } catch (IllegalArgumentException ignored) { + return Optional.empty(); + } + } + + private Optional parsePathAsPhoneNumber(Path file) { + try { + var longValue = Long.parseLong(file.getFileName().toString()); + return PhoneNumber.ofNullable(longValue); + } catch (IllegalArgumentException ignored) { + return Optional.empty(); + } + } + + @Override + public CompletableFuture serializeKeys(Keys keys, boolean async) { + if (cachedUuids != null && !cachedUuids.contains(keys.uuid())) { + cachedUuids.add(keys.uuid()); + } + + var outputFile = getSessionFile(keys.clientType(), keys.uuid().toString(), KEYS_NAME); + if (async) { + return CompletableFuture.runAsync(() -> writeFile(KeysSpec.encode(keys), KEYS_NAME, outputFile)) + .exceptionallyAsync(this::onError); + } + + writeFile(KeysSpec.encode(keys), KEYS_NAME, outputFile); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture serializeStore(Store store, boolean async) { + if (cachedUuids != null && !cachedUuids.contains(store.uuid())) { + cachedUuids.add(store.uuid()); + } + + var phoneNumber = store.phoneNumber().orElse(null); + if (cachedPhoneNumbers != null && !cachedPhoneNumbers.contains(phoneNumber)) { + cachedPhoneNumbers.add(phoneNumber); + } + + var task = attributeStoreSerializers.get(store.uuid()); + if (task != null && !task.isDone()) { + return task; + } + + var chatsFutures = serializeChatsAsync(store); + var newslettersFutures = serializeNewslettersAsync(store); + var dependableFutures = Stream.of(chatsFutures, newslettersFutures) + .flatMap(Arrays::stream) + .toArray(CompletableFuture[]::new); + var result = CompletableFuture.allOf(dependableFutures).thenRunAsync(() -> { + var storePath = getSessionFile(store, STORE_NAME); + writeFile(StoreSpec.encode(store), STORE_NAME, storePath); + }); + if (async) { + return result; + } + + result.join(); + return CompletableFuture.completedFuture(null); + } + + private CompletableFuture[] serializeChatsAsync(Store store) { + return store.chats() + .stream() + .map(chat -> serializeChatAsync(store, chat)) + .toArray(CompletableFuture[]::new); + } + + private CompletableFuture serializeChatAsync(Store store, Chat chat) { + if (!chat.hasUpdate()) { + return CompletableFuture.completedFuture(null); + } + + var fileName = CHAT_PREFIX + chat.jid() + ".proto"; + var outputFile = getSessionFile(store, fileName); + return CompletableFuture.runAsync(() -> writeFile(ChatSpec.encode(chat), fileName, outputFile)) + .exceptionallyAsync(this::onError); + } + + private Void onError(Throwable error) { + var logger = System.getLogger("Serializer"); + logger.log(System.Logger.Level.ERROR, error); + return null; + } + + private CompletableFuture[] serializeNewslettersAsync(Store store) { + return store.newsletters() + .stream() + .map(newsletter -> serializeNewsletterAsync(store, newsletter)) + .toArray(CompletableFuture[]::new); + } + + private CompletableFuture serializeNewsletterAsync(Store store, Newsletter newsletter) { + var fileName = NEWSLETTER_PREFIX + newsletter.jid() + ".proto"; + var outputFile = getSessionFile(store, fileName); + return CompletableFuture.runAsync(() -> writeFile(NewsletterSpec.encode(newsletter), fileName, outputFile)); + } + + private void writeFile(byte[] object, String fileName, Path outputFile) { + try { + var tempFile = Files.createTempFile(fileName, ".tmp"); + try (var tempFileOutputStream = new GZIPOutputStream(Files.newOutputStream(tempFile))) { + tempFileOutputStream.write(object); + Files.move(tempFile, outputFile, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException exception) { + throw new UncheckedIOException("Cannot write file", exception); + } + } + + @Override + public Optional deserializeKeys(ClientType type, UUID id) { + return deserializeKeysFromId(type, id.toString()); + } + + @Override + public Optional deserializeKeys(ClientType type, String alias) { + var file = getSessionDirectory(type, alias); + if (Files.notExists(file)) { + return Optional.empty(); + } + + try { + return deserializeKeysFromId(type, Files.readString(file)); + } catch (IOException exception) { + return Optional.empty(); + } + } + + @Override + public Optional deserializeKeys(ClientType type, long phoneNumber) { + var file = getSessionDirectory(type, String.valueOf(phoneNumber)); + if (Files.notExists(file)) { + return Optional.empty(); + } + + try { + return deserializeKeysFromId(type, Files.readString(file)); + } catch (IOException exception) { + return Optional.empty(); + } + } + + private Optional deserializeKeysFromId(ClientType type, String id) { + var path = getSessionFile(type, id, "keys.proto"); + try (var input = new GZIPInputStream(Files.newInputStream(path))) { + return Optional.of(KeysSpec.decode(input.readAllBytes())); + } catch (IOException exception) { + return Optional.empty(); + } + } + + @Override + public Optional deserializeStore(ClientType type, UUID id) { + return deserializeStoreFromId(type, id.toString()); + } + + @Override + public Optional deserializeStore(ClientType type, String alias) { + var file = getSessionDirectory(type, alias); + if (Files.notExists(file)) { + return Optional.empty(); + } + + try { + return deserializeStoreFromId(type, Files.readString(file)); + } catch (IOException exception) { + return Optional.empty(); + } + } + + @Override + public Optional deserializeStore(ClientType type, long phoneNumber) { + var file = getSessionDirectory(type, String.valueOf(phoneNumber)); + if (Files.notExists(file)) { + return Optional.empty(); + } + + try { + return deserializeStoreFromId(type, Files.readString(file)); + } catch (IOException exception) { + return Optional.empty(); + } + } + + private Optional deserializeStoreFromId(ClientType type, String id) { + var path = getSessionFile(type, id, "store.proto"); + if (Files.notExists(path)) { + return Optional.empty(); + } + + try (var input = new GZIPInputStream(Files.newInputStream(path))) { + return Optional.of(StoreSpec.decode(input.readAllBytes())); + } catch (IOException exception) { + return Optional.empty(); + } + } + + @Override + public CompletableFuture attributeStore(Store store) { + var oldTask = attributeStoreSerializers.get(store.uuid()); + if (oldTask != null) { + return oldTask; + } + var directory = getSessionDirectory(store.clientType(), store.uuid().toString()); + if (Files.notExists(directory)) { + return CompletableFuture.completedFuture(null); + } + try (var walker = Files.walk(directory)) { + var futures = walker.map(entry -> handleStoreFile(store, entry)) + .filter(Objects::nonNull) + .toArray(CompletableFuture[]::new); + var result = CompletableFuture.allOf(futures) + .thenRun(() -> attributeStoreContextualMessages(store)); + attributeStoreSerializers.put(store.uuid(), result); + return result; + } catch (IOException exception) { + return CompletableFuture.failedFuture(exception); + } + } + + // Do this after we have all the chats, or it won't work for obvious reasons + private void attributeStoreContextualMessages(Store store) { + store.chats() + .stream() + .flatMap(chat -> chat.messages().stream()) + .forEach(message -> attributeStoreContextualMessage(store, message)); + } + + private void attributeStoreContextualMessage(Store store, HistorySyncMessage message) { + message.messageInfo() + .message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .ifPresent(contextInfo -> attributeStoreContextInfo(store, contextInfo)); + } + + private void attributeStoreContextInfo(Store store, ContextInfo contextInfo) { + contextInfo.quotedMessageChatJid() + .flatMap(store::findChatByJid) + .ifPresent(contextInfo::setQuotedMessageChat); + } + + private CompletableFuture handleStoreFile(Store store, Path entry) { + return switch (FileType.of(entry)) { + case NEWSLETTER -> CompletableFuture.runAsync(() -> deserializeNewsletter(store, entry)) + .exceptionallyAsync(this::onError); + case CHAT -> CompletableFuture.runAsync(() -> deserializeChat(store, entry)) + .exceptionallyAsync(this::onError); + case UNKNOWN -> null; + }; + } + + private enum FileType { + UNKNOWN(null), + CHAT(CHAT_PREFIX), + NEWSLETTER(NEWSLETTER_PREFIX); + + private final String prefix; + + FileType(String prefix) { + this.prefix = prefix; + } + + private static FileType of(Path path) { + return Arrays.stream(values()) + .filter(entry -> entry.prefix() != null && path.getFileName().toString().startsWith(entry.prefix())) + .findFirst() + .orElse(UNKNOWN); + } + + private String prefix() { + return prefix; + } + } + + @Override + public void deleteSession(Controller controller) { + try { + var folderPath = getSessionDirectory(controller.clientType(), controller.uuid().toString()); + delete(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); + } + } + + private void delete(Path path) throws IOException { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + @Override + public void linkMetadata(Controller controller) { + controller.phoneNumber() + .ifPresent(phoneNumber -> linkToUuid(controller.clientType(), controller.uuid(), phoneNumber.toString())); + controller.alias() + .forEach(alias -> linkToUuid(controller.clientType(), controller.uuid(), alias)); + } + + private void linkToUuid(ClientType type, UUID uuid, String string) { + try { + var link = getSessionDirectory(type, string); + Files.writeString(link, uuid.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException ignored) { + + } + } + + private void deserializeChat(Store store, Path chatFile) { + try (var input = new GZIPInputStream(Files.newInputStream(chatFile))) { + var chat = ChatSpec.decode(input.readAllBytes()); + for (var message : chat.messages()) { + message.messageInfo().setChat(chat); + } + store.addChatDirect(chat); + } catch (IOException exception) { + store.addChatDirect(rescueChat(chatFile)); + } + } + + private Chat rescueChat(Path entry) { + try { + Files.deleteIfExists(entry); + } catch (IOException ignored) { + + } + var chatName = entry.getFileName().toString() + .replaceFirst(CHAT_PREFIX, "") + .replace(".proto", "") + .replaceAll("~~", ":"); + return new ChatBuilder() + .jid(Jid.of(chatName)) + .build(); + } + + private void deserializeNewsletter(Store store, Path newsletterFile) { + try (var input = new GZIPInputStream(Files.newInputStream(newsletterFile))) { + var newsletter = NewsletterSpec.decode(input.readAllBytes()); + for (var message : newsletter.messages()) { + message.setNewsletter(newsletter); + } + store.addNewsletter(newsletter); + } catch (IOException exception) { + store.addNewsletter(rescueNewsletter(newsletterFile)); + } + } + + private Newsletter rescueNewsletter(Path entry) { + try { + Files.deleteIfExists(entry); + } catch (IOException ignored) { + + } + var newsletterName = entry.getFileName().toString() + .replaceFirst(CHAT_PREFIX, "") + .replace(".proto", "") + .replaceAll("~~", ":"); + return new Newsletter(Jid.of(newsletterName), null, null, null); + } + + private Path getHome(ClientType type) { + return baseDirectory.resolve(type == ClientType.MOBILE ? "mobile" : "web"); + } + + private Path getSessionDirectory(ClientType clientType, String path) { + try { + var result = getHome(clientType).resolve(path); + Files.createDirectories(result.getParent()); + return result; + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } + } + + private Path getSessionFile(Store store, String fileName) { + 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) { + try { + var result = getSessionDirectory(clientType, uuid).resolve(fileName); + Files.createDirectories(result.getParent()); + return result; + } catch (IOException exception) { + throw new UncheckedIOException("Cannot create directory", exception); + } + } + + private static class ImmutableLinkedList extends LinkedList { + @SuppressWarnings({"rawtypes", "unchecked"}) + private static final ImmutableLinkedList EMPTY = new ImmutableLinkedList(new LinkedList()); + + private final LinkedList delegate; + + @SuppressWarnings("unchecked") + private static ImmutableLinkedList empty() { + return EMPTY; + } + + private ImmutableLinkedList(LinkedList delegate) { + this.delegate = delegate; + } + + @Override + public E getFirst() { + return delegate.getFirst(); + } + + @Override + public E getLast() { + return delegate.getLast(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public E get(int index) { + return delegate.get(index); + } + + @Override + public int indexOf(Object o) { + return delegate.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + @Override + public E peek() { + return delegate.peek(); + } + + @Override + public E element() { + return delegate.element(); + } + + @Override + public E poll() { + return delegate.poll(); + } + + @Override + public boolean offer(E e) { + return delegate.offer(e); + } + + @Override + public boolean offerFirst(E e) { + return delegate.offerFirst(e); + } + + @Override + public boolean offerLast(E e) { + return delegate.offerLast(e); + } + + @Override + public E peekFirst() { + return delegate.peekFirst(); + } + + @Override + public E peekLast() { + return delegate.peekLast(); + } + + @Override + public E pollFirst() { + return delegate.pollFirst(); + } + + @Override + public E pollLast() { + return delegate.pollLast(); + } + + @Override + public void push(E e) { + delegate.push(e); + } + + @Override + public E pop() { + return delegate.pop(); + } + + @Override + public ListIterator listIterator(int index) { + return delegate.listIterator(index); + } + + @Override + public Iterator descendingIterator() { + return delegate.descendingIterator(); + } + + @SuppressWarnings("MethodDoesntCallSuperMethod") + @Override + public Object clone() { + return delegate.clone(); + } + + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return delegate.toArray(a); + } + + @Override + public Spliterator spliterator() { + return delegate.spliterator(); + } + + @Override + public LinkedList reversed() { + return delegate.reversed(); + } + + @Override + public void replaceAll(UnaryOperator operator) { + delegate.replaceAll(operator); + } + + @Override + public void sort(Comparator c) { + delegate.sort(c); + } + + @Override + public T[] toArray(IntFunction generator) { + return delegate.toArray(generator); + } + + @Override + public Stream stream() { + return delegate.stream(); + } + + @Override + public Stream parallelStream() { + return delegate.parallelStream(); + } + + @Override + public void forEach(Consumer action) { + delegate.forEach(action); + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, E element) { + throw new UnsupportedOperationException(); + } + + @Override + public void addLast(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public void addFirst(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public E remove() { + throw new UnsupportedOperationException(); + } + + @Override + public E removeFirst() { + throw new UnsupportedOperationException(); + } + + @Override + public E removeLast() { + throw new UnsupportedOperationException(); + } + + @Override + protected void removeRange(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeFirstOccurrence(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public E remove(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeLastOccurrence(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public E set(int index, E element) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/controller/Store.java b/src/main/java/it/auties/whatsapp/controller/Store.java new file mode 100644 index 000000000..dfb49fc70 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/Store.java @@ -0,0 +1,1518 @@ +package it.auties.whatsapp.controller; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.api.ClientType; +import it.auties.whatsapp.api.TextPreviewSetting; +import it.auties.whatsapp.api.WebHistoryLength; +import it.auties.whatsapp.listener.Listener; +import it.auties.whatsapp.model.business.BusinessCategory; +import it.auties.whatsapp.model.call.Call; +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.chat.ChatBuilder; +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.info.ChatMessageInfo; +import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.info.MessageStatusInfo; +import it.auties.whatsapp.model.info.NewsletterMessageInfo; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.jid.JidProvider; +import it.auties.whatsapp.model.jid.JidServer; +import it.auties.whatsapp.model.media.MediaConnection; +import it.auties.whatsapp.model.message.model.ChatMessageKey; +import it.auties.whatsapp.model.message.model.ContextualMessage; +import it.auties.whatsapp.model.mobile.CountryLocale; +import it.auties.whatsapp.model.mobile.PhoneNumber; +import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.newsletter.NewsletterName; +import it.auties.whatsapp.model.node.Node; +import it.auties.whatsapp.model.privacy.PrivacySettingEntry; +import it.auties.whatsapp.model.privacy.PrivacySettingType; +import it.auties.whatsapp.model.signal.auth.UserAgent.ReleaseChannel; +import it.auties.whatsapp.model.signal.auth.Version; +import it.auties.whatsapp.model.sync.HistorySyncMessage; +import it.auties.whatsapp.registration.WhatsappMetadata; +import it.auties.whatsapp.socket.SocketRequest; +import it.auties.whatsapp.util.*; + +import java.net.URI; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentHashMap.KeySetView; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This controller holds the user-related data regarding a WhatsappWeb session + */ +@SuppressWarnings({"unused", "UnusedReturnValue"}) +public final class Store extends Controller implements ProtobufMessage { + /** + * The version used by this session + */ + @ProtobufProperty(index = 5, type = ProtobufType.STRING, mixin = ProtobufUriMixin.class) + URI proxy; + + /** + * The version used by this session + */ + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT, overrideType = Version.class) + FutureReference version; + + /** + * Whether this account is online for other users + */ + @ProtobufProperty(index = 7, type = ProtobufType.BOOL) + boolean online; + + /** + * The locale of the user linked to this account + */ + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + CountryLocale locale; + + /** + * The name of the user linked to this account. This field will be null while the user hasn't + * logged in yet. Assumed to be non-null otherwise. + */ + @ProtobufProperty(index = 9, type = ProtobufType.STRING) + String name; + + /** + * The name of the user linked to this account. This field will be null while the user hasn't + * logged in yet. Assumed to be non-null otherwise. + */ + @ProtobufProperty(index = 40, type = ProtobufType.STRING) + String verifiedName; + + /** + * The address of this account, if it's a business account + */ + @ProtobufProperty(index = 10, type = ProtobufType.STRING) + String businessAddress; + + /** + * The longitude of this account's location, if it's a business account + */ + @ProtobufProperty(index = 11, type = ProtobufType.DOUBLE) + Double businessLongitude; + + /** + * The latitude of this account's location, if it's a business account + */ + @ProtobufProperty(index = 12, type = ProtobufType.DOUBLE) + Double businessLatitude; + + /** + * The description of this account, if it's a business account + */ + @ProtobufProperty(index = 13, type = ProtobufType.STRING) + String businessDescription; + + /** + * The website of this account, if it's a business account + */ + @ProtobufProperty(index = 14, type = ProtobufType.STRING) + String businessWebsite; + + /** + * The email of this account, if it's a business account + */ + @ProtobufProperty(index = 15, type = ProtobufType.STRING) + String businessEmail; + + /** + * The category of this account, if it's a business account + */ + @ProtobufProperty(index = 16, type = ProtobufType.OBJECT) + BusinessCategory businessCategory; + + /** + * The hash of the companion associated with this session + */ + @ProtobufProperty(index = 17, type = ProtobufType.STRING) + String deviceHash; + + /** + * A map of all the devices that the companion has associated using WhatsappWeb + * The key here is the index of the device's key + * The value is the device's companion jid + */ + @ProtobufProperty(index = 18, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.INT32) + LinkedHashMap linkedDevicesKeys; + + /** + * The profile picture of the user linked to this account. This field will be null while the user + * hasn't logged in yet. This field can also be null if no image was set. + */ + @ProtobufProperty(index = 19, type = ProtobufType.STRING, mixin = ProtobufUriMixin.class) + URI profilePicture; + + /** + * The status of the user linked to this account. + * This field will be null while the user hasn't logged in yet. + * Assumed to be non-null otherwise. + */ + @ProtobufProperty(index = 20, type = ProtobufType.STRING) + String about; + + /** + * The user linked to this account. This field will be null while the user hasn't logged in yet. + */ + @ProtobufProperty(index = 21, type = ProtobufType.STRING) + Jid jid; + + /** + * The lid user linked to this account. This field will be null while the user hasn't logged in yet. + */ + @ProtobufProperty(index = 22, type = ProtobufType.STRING) + Jid lid; + + /** + * The non-null map of properties received by whatsapp + */ + @ProtobufProperty(index = 23, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.STRING) + final ConcurrentHashMap properties; + + /** + * The non-null map of chats + */ + @JsonIgnore + final ConcurrentHashMap chats; + + /** + * The non-null map of contacts + */ + @ProtobufProperty(index = 24, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final ConcurrentHashMap contacts; + + /** + * The non-null list of status messages + */ + @ProtobufProperty(index = 25, type = ProtobufType.OBJECT) + final KeySetView status; + + /** + * The non-null map of newsletters + */ + @JsonIgnore + final ConcurrentHashMap newsletters; + + /** + * The non-null map of privacy settings + */ + @ProtobufProperty(index = 26, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final ConcurrentHashMap privacySettings; + + /** + * The non-null map of calls + */ + @ProtobufProperty(index = 27, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final ConcurrentHashMap calls; + + /** + * Whether chats should be unarchived if a new message arrives + */ + @ProtobufProperty(index = 28, type = ProtobufType.BOOL) + boolean unarchiveChats; + + /** + * Whether the twenty-hours format is being used by the client + */ + @ProtobufProperty(index = 29, type = ProtobufType.BOOL) + boolean twentyFourHourFormat; + + /** + * The non-null list of requests that were sent to Whatsapp. They might or might not be waiting + * for a newsletters + */ + @JsonIgnore + final ConcurrentHashMap requests; + + /** + * The non-null list of replies waiting to be fulfilled + */ + @JsonIgnore + final ConcurrentHashMap> replyHandlers; + + /** + * The non-null list of listeners + */ + @JsonIgnore + final KeySetView listeners; + + /** + * The request tag, used to create messages + */ + @JsonIgnore + final String tag; + + /** + * The timestampSeconds in seconds for the initialization of this object + */ + @ProtobufProperty(index = 30, type = ProtobufType.UINT64) + final long initializationTimeStamp; + + /** + * The media connection associated with this store + */ + @JsonIgnore + MediaConnection mediaConnection; + + /** + * The media connection latch associated with this store + */ + @JsonIgnore + final CountDownLatch mediaConnectionLatch; + + /** + * The request tag, used to create messages + */ + @ProtobufProperty(index = 31, type = ProtobufType.OBJECT) + ChatEphemeralTimer newChatsEphemeralTimer; + + /** + * The setting to use when generating previews for text messages that contain links + */ + @ProtobufProperty(index = 32, type = ProtobufType.OBJECT) + TextPreviewSetting textPreviewSetting; + + /** + * Describes how much chat history Whatsapp should send + */ + @ProtobufProperty(index = 33, type = ProtobufType.OBJECT) + WebHistoryLength historyLength; + + /** + * Whether listeners should be automatically scanned and registered or not + */ + @ProtobufProperty(index = 34, type = ProtobufType.BOOL) + boolean autodetectListeners; + + /** + * Whether the listeners that were automatically scanned should be cached + */ + @ProtobufProperty(index = 35, type = ProtobufType.BOOL) + boolean cacheDetectedListeners; + + /** + * Whether updates about the presence of the session should be sent automatically to Whatsapp + * For example, when the bot is started, the status of the companion is changed to available if this option is enabled + * If this option is enabled, the companion will not receive notifications because the bot will instantly read them + */ + @ProtobufProperty(index = 36, type = ProtobufType.BOOL) + boolean automaticPresenceUpdates; + + /** + * The release channel to use when connecting to Whatsapp + * This should allow the use of beta features + */ + @ProtobufProperty(index = 37, type = ProtobufType.OBJECT) + ReleaseChannel releaseChannel; + + /** + * Metadata about the device that is being simulated for Whatsapp + */ + @ProtobufProperty(index = 38, type = ProtobufType.OBJECT) + CompanionDevice device; + + /** + * Whether the mac of every app state request should be checked + */ + @ProtobufProperty(index = 39, type = ProtobufType.BOOL) + boolean checkPatchMacs; + + /** + * All args constructor + */ + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Store(UUID uuid, PhoneNumber phoneNumber, ClientType clientType, Collection alias, URI proxy, FutureReference version, boolean online, CountryLocale locale, String name, String verifiedName, String businessAddress, Double businessLongitude, Double businessLatitude, String businessDescription, String businessWebsite, String businessEmail, BusinessCategory businessCategory, String deviceHash, LinkedHashMap linkedDevicesKeys, URI profilePicture, String about, Jid jid, Jid lid, ConcurrentHashMap properties, ConcurrentHashMap contacts, KeySetView status, ConcurrentHashMap privacySettings, ConcurrentHashMap calls, boolean unarchiveChats, boolean twentyFourHourFormat, Long initializationTimeStamp, ChatEphemeralTimer newChatsEphemeralTimer, TextPreviewSetting textPreviewSetting, WebHistoryLength historyLength, Boolean autodetectListeners, Boolean cacheDetectedListeners, Boolean automaticPresenceUpdates, ReleaseChannel releaseChannel, CompanionDevice device, boolean checkPatchMacs) { + super(uuid, phoneNumber, null, clientType, alias); + if (proxy != null) { + ProxyAuthenticator.register(proxy); + } + + this.proxy = proxy; + this.version = version; + this.online = online; + this.locale = locale; + this.name = Objects.requireNonNullElse(name, Specification.Whatsapp.DEFAULT_NAME); + this.verifiedName = verifiedName; + this.businessAddress = businessAddress; + this.businessLongitude = businessLongitude; + this.businessLatitude = businessLatitude; + this.businessDescription = businessDescription; + this.businessWebsite = businessWebsite; + this.businessEmail = businessEmail; + this.businessCategory = businessCategory; + this.deviceHash = deviceHash; + this.linkedDevicesKeys = Objects.requireNonNullElseGet(linkedDevicesKeys, LinkedHashMap::new); + this.profilePicture = profilePicture; + this.about = about; + this.jid = jid; + this.lid = lid; + this.properties = Objects.requireNonNullElseGet(properties, ConcurrentHashMap::new); + this.chats = new ConcurrentHashMap<>(); + this.contacts = Objects.requireNonNullElseGet(contacts, ConcurrentHashMap::new); + this.status = Objects.requireNonNullElseGet(status, ConcurrentHashMap::newKeySet); + this.newsletters = new ConcurrentHashMap<>(); + this.privacySettings = Objects.requireNonNullElseGet(privacySettings, ConcurrentHashMap::new); + this.calls = Objects.requireNonNullElseGet(calls, ConcurrentHashMap::new); + this.unarchiveChats = unarchiveChats; + this.twentyFourHourFormat = twentyFourHourFormat; + this.requests = new ConcurrentHashMap<>(); + this.replyHandlers = new ConcurrentHashMap<>(); + this.listeners = ConcurrentHashMap.newKeySet(); + this.tag = HexFormat.of().formatHex(BytesHelper.random(1)); + this.initializationTimeStamp = Objects.requireNonNullElseGet(initializationTimeStamp, Clock::nowSeconds); + this.mediaConnectionLatch = new CountDownLatch(1); + this.newChatsEphemeralTimer = Objects.requireNonNullElse(newChatsEphemeralTimer, ChatEphemeralTimer.OFF); + this.textPreviewSetting = Objects.requireNonNullElse(textPreviewSetting, TextPreviewSetting.ENABLED_WITH_INFERENCE); + this.historyLength = Objects.requireNonNullElseGet(historyLength, WebHistoryLength::standard); + this.autodetectListeners = Objects.requireNonNullElse(autodetectListeners, true); + this.cacheDetectedListeners = Objects.requireNonNullElse(cacheDetectedListeners, true); + this.automaticPresenceUpdates = Objects.requireNonNullElse(automaticPresenceUpdates, true); + this.releaseChannel = Objects.requireNonNullElse(releaseChannel, ReleaseChannel.RELEASE); + this.device = device; + this.checkPatchMacs = checkPatchMacs; + } + + public static Store newStore(UUID uuid, Long phoneNumber, Collection alias, ClientType clientType) { + return new StoreBuilder() + .uuid(uuid) + .phoneNumber(phoneNumber != null ? PhoneNumber.of(phoneNumber) : null) + .device(CompanionDevice.ios(false)) + .clientType(clientType) + .alias(alias) + .name(Specification.Whatsapp.DEFAULT_NAME) + .jid(phoneNumber != null ? Jid.of(phoneNumber) : null) + .build(); + } + + /** + * Queries the first contact whose jid is equal to {@code jid} + * + * @param jid the jid to search + * @return a non-null optional + */ + public Optional findContactByJid(JidProvider jid) { + if (jid == null) { + return Optional.empty(); + } + + if (jid instanceof Contact contact) { + return Optional.of(contact); + } + + return Optional.ofNullable(contacts.get(jid.toJid())); + } + + /** + * Queries the first contact whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null optional + */ + public Optional findContactByName(String name) { + return findContactsStream(name).findAny(); + } + + private Stream findContactsStream(String name) { + return name == null ? Stream.empty() : contacts().parallelStream() + .filter(contact -> contact.fullName().filter(name::equals).isPresent() || contact.chosenName().filter(name::equals).isPresent() || contact.shortName().filter(name::equals).isPresent()); + } + + /** + * Returns all the contacts + * + * @return an immutable collection + */ + public Collection contacts() { + return Collections.unmodifiableCollection(contacts.values()); + } + + /** + * Queries every contact whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null immutable set + */ + public Set findContactsByName(String name) { + return findContactsStream(name).collect(Collectors.toUnmodifiableSet()); + } + + /** + * Queries the first message whose id matches the one provided in the specified chat + * + * @param key the key to search + * @return a non-null optional + */ + public Optional findMessageByKey(ChatMessageKey key) { + return Optional.ofNullable(key) + .map(ChatMessageKey::chatJid) + .flatMap(this::findChatByJid) + .flatMap(chat -> findMessageById(chat, key.id())); + } + + /** + * Queries the first message whose id matches the one provided in the specified chat or newsletter + * + * @param provider the chat to search in + * @param id the jid to search + * @return a non-null optional + */ + public Optional> findMessageById(JidProvider provider, String id) { + if (provider == null || id == null) { + return Optional.empty(); + } + + return switch (provider) { + case Chat chat -> findMessageById(chat, id); + case Newsletter newsletter -> findMessageById(newsletter, id); + case Contact contact -> findChatByJid(contact.jid()) + .flatMap(chat -> findMessageById(chat, id)); + case Jid contactJid -> switch (contactJid.type()) { + case NEWSLETTER -> findNewsletterByJid(contactJid) + .flatMap(newsletter -> findMessageById(newsletter, id)); + case STATUS -> status.stream() + .filter(entry -> Objects.equals(entry.chatJid(), provider.toJid()) && Objects.equals(entry.id(), id)) + .findFirst(); + default -> findChatByJid(contactJid) + .flatMap(chat -> findMessageById(chat, id)); + }; + }; + } + + /** + * Queries the first message whose id matches the one provided in the specified newsletter + * + * @param newsletter newsletter chat to search in + * @param id the jid to search + * @return a non-null optional + */ + public Optional findMessageById(Newsletter newsletter, String id) { + return newsletter.messages() + .parallelStream() + .filter(entry -> Objects.equals(id, entry.id()) || Objects.equals(id, String.valueOf(entry.serverId()))) + .findFirst(); + } + + + /** + * Queries the first message whose id matches the one provided in the specified chat + * + * @param chat the chat to search in + * @param id the jid to search + * @return a non-null optional + */ + public Optional findMessageById(Chat chat, String id) { + return chat.messages() + .parallelStream() + .map(HistorySyncMessage::messageInfo) + .filter(message -> Objects.equals(message.key().id(), id)) + .findAny(); + } + + /** + * Queries the first chat whose jid is equal to {@code jid} + * + * @param jid the jid to search + * @return a non-null optional + */ + public Optional findChatByJid(JidProvider jid) { + if (jid == null) { + return Optional.empty(); + } + + if (jid instanceof Chat chat) { + return Optional.of(chat); + } + + return Optional.ofNullable(chats.get(jid.toJid())); + } + + /** + * Queries the first newsletter whose jid is equal to {@code jid} + * + * @param jid the jid to search + * @return a non-null optional + */ + public Optional findNewsletterByJid(JidProvider jid) { + if (jid == null) { + return Optional.empty(); + } + + if (jid instanceof Newsletter newsletter) { + return Optional.of(newsletter); + } + + return Optional.ofNullable(newsletters.get(jid.toJid())); + } + + /** + * Queries the first chat whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null optional + */ + public Optional findChatByName(String name) { + return findChatsByNameStream(name).findAny(); + } + + /** + * Queries the first newsletter whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null optional + */ + public Optional findNewsletterByName(String name) { + return findNewslettersByNameStream(name).findAny(); + } + + + private Stream findChatsByNameStream(String name) { + return name == null ? Stream.empty() : chats.values() + .parallelStream() + .filter(chat -> chat.name().equalsIgnoreCase(name)); + } + + private Stream findNewslettersByNameStream(String name) { + return name == null ? Stream.empty() : newsletters.values() + .parallelStream() + .filter(newsletter -> name.equalsIgnoreCase(newsletter.metadata().name().map(NewsletterName::text).orElse(null))); + } + + /** + * Queries the first chat that matches the provided function + * + * @param function the non-null filter + * @return a non-null optional + */ + public Optional findChatBy(Function function) { + return chats.values().parallelStream() + .filter(function::apply) + .findFirst(); + } + + /** + * Queries the first newsletter that matches the provided function + * + * @param function the non-null filter + * @return a non-null optional + */ + public Optional findNewsletterBy(Function function) { + return newsletters.values() + .parallelStream() + .filter(function::apply) + .findFirst(); + } + + /** + * Queries every chat whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null immutable set + */ + public Set findChatsByName(String name) { + return findChatsByNameStream(name) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Queries every newsletter whose name is equal to {@code name} + * + * @param name the name to search + * @return a non-null immutable set + */ + public Set findNewslettersByName(String name) { + return findNewslettersByNameStream(name) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Queries the first chat that matches the provided function + * + * @param function the non-null filter + * @return a non-null optional + */ + public Set findChatsBy(Function function) { + return chats.values() + .stream() + .filter(function::apply) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Returns all the status + * + * @return an immutable collection + */ + public Collection status() { + return Collections.unmodifiableCollection(status); + } + + /** + * Returns all the newsletters + * + * @return an immutable collection + */ + public Collection newsletters() { + return Collections.unmodifiableCollection(newsletters.values()); + } + + /** + * Queries all the status of a contact + * + * @param jid the sender of the status + * @return a non-null immutable list + */ + public Collection findStatusBySender(JidProvider jid) { + return status.stream() + .filter(entry -> Objects.equals(entry.chatJid(), jid)) + .toList(); + } + + /** + * Queries the first request whose id equals the one stored by the newsletters and, if any is found, + * it completes it + * + * @param response the newsletters to complete the request with + * @param exceptionally whether the newsletters is erroneous + * @return a boolean + */ + public boolean resolvePendingRequest(Node response, boolean exceptionally) { + return findPendingRequest(response.id()).map(request -> deleteAndComplete(request, response, exceptionally)) + .isPresent(); + } + + /** + * Queries the first request whose id is equal to {@code id} + * + * @param id the id to search, can be null + * @return a non-null optional + */ + @SuppressWarnings("ClassEscapesDefinedScope") + public Optional findPendingRequest(String id) { + return id == null ? Optional.empty() : Optional.ofNullable(requests.get(id)); + } + + private SocketRequest deleteAndComplete(SocketRequest request, Node response, boolean exceptionally) { + if (request.complete(response, exceptionally)) { + requests.remove(request.id()); + } + + return request; + } + + /** + * Clears all the data that this object holds and closes the pending requests + */ + public void resolveAllPendingRequests() { + requests.values().forEach(request -> request.complete(null, false)); + } + + /** + * Returns an immutable collection of pending requests + * + * @return a non-null collection + */ + @SuppressWarnings("ClassEscapesDefinedScope") + public Collection pendingRequests() { + return Collections.unmodifiableCollection(requests.values()); + } + + /** + * Queries the first reply waiting and completes it with the input message + * + * @param response the newsletters to complete the reply with + * @return a boolean + */ + public boolean resolvePendingReply(ChatMessageInfo response) { + return response.message() + .contentWithContext() + .flatMap(ContextualMessage::contextInfo) + .flatMap(ContextInfo::quotedMessageId) + .map(id -> { + var future = replyHandlers.remove(id); + if (future == null) { + return false; + } + + future.complete(response); + return true; + }) + .orElse(false); + } + + /** + * Adds a chat in memory + * + * @param chatJid the chat to add + * @return the input chat + */ + public Chat addNewChat(Jid chatJid) { + var chat = new ChatBuilder() + .jid(chatJid) + .build(); + addChat(chat); + return chat; + } + + /** + * Adds a chat in memory + * + * @param chat the chat to add + * @return the old chat, if present + */ + public Optional addChat(Chat chat) { + if (chat.hasName() && chat.jid().hasServer(JidServer.WHATSAPP)) { + var contact = findContactByJid(chat.jid()) + .orElseGet(() -> addContact(new Contact(chat.jid()))); + contact.setFullName(chat.name()); + } + var oldChat = chats.get(chat.jid()); + if (oldChat != null) { + if (oldChat.hasName() && !chat.hasName()) { + chat.setName(oldChat.name()); // Coming from contact actions + } + joinMessages(chat, oldChat); + } + return addChatDirect(chat); + } + + private void joinMessages(Chat chat, Chat oldChat) { + var newChatTimestamp = chat.newestMessage() + .map(message -> message.timestampSeconds().orElse(0L)) + .orElse(0L); + var oldChatTimestamp = oldChat.newestMessage() + .map(message -> message.timestampSeconds().orElse(0L)) + .orElse(0L); + if (newChatTimestamp <= oldChatTimestamp) { + chat.addMessages(oldChat.messages()); + return; + } + chat.addOldMessages(chat.messages()); + } + + /** + * Adds a chat in memory without executing any check + * + * @param chat the chat to add + * @return the old chat, if present + */ + public Optional addChatDirect(Chat chat) { + return Optional.ofNullable(chats.put(chat.jid(), chat)); + } + + /** + * Adds a contact in memory + * + * @param jid the contact to add + * @return the input contact + */ + public Contact addContact(Jid jid) { + return addContact(new Contact(jid)); + } + + /** + * Adds a contact in memory + * + * @param contact the contact to add + * @return the input contact + */ + public Contact addContact(Contact contact) { + contacts.put(contact.jid(), contact); + return contact; + } + + /** + * Adds a newsletter in memory + * + * @param newsletter the newsletter to add + * @return the old newsletter, if present + */ + public Optional addNewsletter(Newsletter newsletter) { + return Optional.ofNullable(newsletters.put(newsletter.jid(), newsletter)); + } + + /** + * Removes a chat from memory + * + * @param chatJid the chat to remove + * @return the chat that was deleted wrapped by an optional + */ + public Optional removeChat(JidProvider chatJid) { + return Optional.ofNullable(chats.remove(chatJid.toJid())); + } + + /** + * Removes a newsletter from memory + * + * @param newsletterJid the newsletter to remove + * @return the newsletter that was deleted wrapped by an optional + */ + public Optional removeNewsletter(JidProvider newsletterJid) { + return Optional.ofNullable(newsletters.remove(newsletterJid.toJid())); + } + + /** + * Removes a contact from memory + * + * @param contactJid the contact to remove + * @return the contact that was deleted wrapped by an optional + */ + public Optional removeContact(JidProvider contactJid) { + return Optional.ofNullable(contacts.remove(contactJid.toJid())); + } + + /** + * Returns the chats pinned to the top sorted new to old + * + * @return a non-null list of chats + */ + public List pinnedChats() { + return chats.values() + .parallelStream() + .filter(Chat::isPinned) + .sorted(Comparator.comparingLong(Chat::pinnedTimestampSeconds).reversed()) + .toList(); + } + + /** + * Returns all the starred messages + * + * @return a non-null list of messages + */ + public List starredMessages() { + return chats().parallelStream().map(Chat::starredMessages).flatMap(Collection::stream).toList(); + } + + /** + * Returns all the chats sorted from newest to oldest + * + * @return an immutable collection + */ + public List chats() { + return chats.values() + .stream() + .sorted(Comparator.comparingLong(Chat::timestampSeconds).reversed()) + .toList(); + } + + /** + * Returns the non-null map of properties received by whatsapp + * + * @return an unmodifiable map + */ + public Map properties() { + return Collections.unmodifiableMap(properties); + } + + public void addProperties(Map properties) { + this.properties.putAll(properties); + } + + /** + * The media connection associated with this store + * + * @return the media connection + */ + public MediaConnection mediaConnection() { + return mediaConnection(Duration.ofMinutes(1)); + } + + /** + * The media connection associated with this store + * + * @param timeout the non-null timeout for the connection to be filled + * @return the media connection + */ + public MediaConnection mediaConnection(Duration timeout) { + try { + var result = mediaConnectionLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS); + if (!result) { + throw new RuntimeException("Cannot get media connection"); + } + return mediaConnection; + } catch (InterruptedException exception) { + throw new RuntimeException("Cannot lock on media connection", exception); + } + } + + /** + * Writes a media connection + * + * @param mediaConnection a media connection + * @return the same instance + */ + public Store setMediaConnection(MediaConnection mediaConnection) { + this.mediaConnection = mediaConnection; + mediaConnectionLatch.countDown(); + return this; + } + + /** + * Returns all the blocked contacts + * + * @return an immutable collection + */ + public Collection blockedContacts() { + return contacts().stream().filter(Contact::blocked).toList(); + } + + /** + * Adds a status to this store + * + * @param info the non-null status to add + * @return the same instance + */ + public Store addStatus(ChatMessageInfo info) { + status.add(info); + return this; + } + + /** + * Adds a request to this store + * + * @param request the non-null request to add + * @return the non-null completable newsletters of the request + */ + @SuppressWarnings("ClassEscapesDefinedScope") + public CompletableFuture addRequest(SocketRequest request) { + if (request.id() == null) { + return CompletableFuture.completedFuture(null); + } + + requests.put(request.id(), request); + return request.future(); + } + + /** + * Adds a replay handler to this store + * + * @param messageId the non-null message id to listen for + * @return the non-null completable newsletters of the reply handler + */ + public CompletableFuture addPendingReply(String messageId) { + var result = new CompletableFuture(); + replyHandlers.put(messageId, result); + return result; + } + + /** + * Returns the profile picture of this user if present + * + * @return an optional uri + */ + public Optional profilePicture() { + return Optional.ofNullable(profilePicture); + } + + /** + * Queries all the privacy settings + * + * @return a non-null list + */ + public Collection privacySettings() { + return privacySettings.values(); + } + + /** + * Queries the privacy setting entry for the type + * + * @param type a non-null type + * @return a non-null entry + */ + public PrivacySettingEntry findPrivacySetting(PrivacySettingType type) { + return privacySettings.get(type.name()); + } + + /** + * Sets the privacy setting entry for a type + * + * @param type a non-null type + * @param entry the non-null entry + * @return the old privacy setting entry + */ + public PrivacySettingEntry addPrivacySetting(PrivacySettingType type, PrivacySettingEntry entry) { + return privacySettings.put(type.name(), entry); + } + + /** + * Returns an unmodifiable map that contains every companion associated using Whatsapp web mapped to its key index + * + * @return an unmodifiable map + */ + public Map linkedDevicesKeys() { + return Collections.unmodifiableMap(linkedDevicesKeys); + } + + + /** + * Returns an unmodifiable list that contains the devices associated using Whatsapp web to this session's companion + * + * @return an unmodifiable list + */ + public Collection linkedDevices() { + return Collections.unmodifiableCollection(linkedDevicesKeys.keySet()); + } + + /** + * Registers a new companion + * Only use this method in the mobile api + * + * @param companion a non-null companion + * @param keyId the id of its key + * @return the nullable old key + */ + public Optional addLinkedDevice(Jid companion, int keyId) { + return Optional.ofNullable(linkedDevicesKeys.put(companion, keyId)); + } + + /** + * Removes a companion + * Only use this method in the mobile api + * + * @param companion a non-null companion + * @return the nullable old key + */ + public Optional removeLinkedCompanion(Jid companion) { + return Optional.ofNullable(linkedDevicesKeys.remove(companion)); + } + + /** + * Removes all linked companion + */ + public void removeLinkedCompanions() { + linkedDevicesKeys.clear(); + } + + /** + * Returns an immutable collection of listeners + * + * @return a non-null collection + */ + public Collection listeners() { + return Collections.unmodifiableSet(listeners); + } + + /** + * Registers a listener + * + * @param listener the listener to register + * @return the same instance + */ + public Store addListener(Listener listener) { + listeners.add(listener); + return this; + } + + /** + * Registers a collection of listeners + * + * @param listeners the listeners to register + * @return the same instance + */ + public Store addListeners(Collection listeners) { + this.listeners.addAll(listeners); + return this; + } + + /** + * Removes a listener + * + * @param listener the listener to remove + * @return the same instance + */ + public Store removeListener(Listener listener) { + listeners.remove(listener); + return this; + } + + /** + * Removes all listeners + * + * @return the same instance + */ + public Store removeListener() { + listeners.clear(); + return this; + } + + /** + * Sets the proxy used by this session + * + * @return the same instance + */ + public Store setProxy(URI proxy) { + if (proxy != null && proxy.getUserInfo() != null) { + ProxyAuthenticator.register(proxy); + } else if (proxy == null && this.proxy != null) { + ProxyAuthenticator.unregister(this.proxy); + } + + this.proxy = proxy; + return this; + } + + /** + * Returns the proxy used by this session + * + * @return a non-null optional + */ + public Optional proxy() { + return Optional.ofNullable(proxy); + } + + /** + * The address of this account, if it's a business account + * + * @return an optional + */ + public Optional businessAddress() { + return Optional.ofNullable(businessAddress); + } + + /** + * The longitude of this account's location, if it's a business account + * + * @return an optional + */ + public Optional businessLongitude() { + return Optional.ofNullable(businessLongitude); + } + + /** + * The latitude of this account's location, if it's a business account + * + * @return an optional + */ + public Optional businessLatitude() { + return Optional.ofNullable(businessLatitude); + } + + /** + * The description of this account, if it's a business account + * + * @return an optional + */ + public Optional businessDescription() { + return Optional.ofNullable(businessDescription); + } + + /** + * The website of this account, if it's a business account + * + * @return an optional + */ + public Optional businessWebsite() { + return Optional.ofNullable(businessWebsite); + } + + /** + * The email of this account, if it's a business account + * + * @return an optional + */ + public Optional businessEmail() { + return Optional.ofNullable(businessEmail); + } + + /** + * The category of this account, if it's a business account + * + * @return an optional + */ + public Optional businessCategory() { + return Optional.ofNullable(businessCategory); + } + + public void dispose() { + serialize(false); + serializer.linkMetadata(this); + mediaConnectionLatch.countDown(); + } + + @Override + public void serialize(boolean async) { + serializer.serializeStore(this, async); + } + + /** + * Adds a call to the store + * + * @param call a non-null call + * @return the old value associated with {@link Call#id()} + */ + public Optional addCall(Call call) { + return Optional.ofNullable(calls.put(call.id(), call)); + } + + /** + * Finds a call by id + * + * @param callId the id of the call, can be null + * @return an optional + */ + public Optional findCallById(String callId) { + return callId == null ? Optional.empty() : Optional.ofNullable(calls.get(callId)); + } + + public String tag() { + return tag; + } + + @JsonGetter("version") + public Version version() { + if(version == null) { + this.version = new FutureReference<>(null, () -> WhatsappMetadata.getVersion(device.platform())); + } + + return version.value(); + } + + public boolean online() { + return this.online; + } + + public Optional locale() { + return Optional.ofNullable(this.locale); + } + + public String name() { + return name; + } + + public Optional deviceHash() { + return Optional.ofNullable(this.deviceHash); + } + + public Optional about() { + return Optional.ofNullable(this.about); + } + + public Optional jid() { + return Optional.ofNullable(this.jid); + } + + public Optional lid() { + return Optional.ofNullable(this.lid); + } + + public boolean unarchiveChats() { + return this.unarchiveChats; + } + + public boolean twentyFourHourFormat() { + return this.twentyFourHourFormat; + } + + public long initializationTimeStamp() { + return this.initializationTimeStamp; + } + + public ChatEphemeralTimer newChatsEphemeralTimer() { + return this.newChatsEphemeralTimer; + } + + public TextPreviewSetting textPreviewSetting() { + return this.textPreviewSetting; + } + + public WebHistoryLength historyLength() { + return this.historyLength; + } + + public boolean autodetectListeners() { + return this.autodetectListeners; + } + + public boolean cacheDetectedListeners() { + return cacheDetectedListeners; + } + + public boolean automaticPresenceUpdates() { + return this.automaticPresenceUpdates; + } + + public ReleaseChannel releaseChannel() { + return this.releaseChannel; + } + + public CompanionDevice device() { + return device; + } + + public boolean checkPatchMacs() { + return this.checkPatchMacs; + } + + public Map calls() { + return Collections.unmodifiableMap(calls); + } + + public Store setOnline(boolean online) { + this.online = online; + return this; + } + + public Store setLocale(CountryLocale locale) { + this.locale = locale; + return this; + } + + public Store setName(String name) { + this.name = name; + return this; + } + + public Store setBusinessAddress(String businessAddress) { + this.businessAddress = businessAddress; + return this; + } + + public Store setBusinessLongitude(Double businessLongitude) { + this.businessLongitude = businessLongitude; + return this; + } + + public Store setBusinessLatitude(Double businessLatitude) { + this.businessLatitude = businessLatitude; + return this; + } + + public Store setBusinessDescription(String businessDescription) { + this.businessDescription = businessDescription; + return this; + } + + public Store setBusinessWebsite(String businessWebsite) { + this.businessWebsite = businessWebsite; + return this; + } + + public Store setBusinessEmail(String businessEmail) { + this.businessEmail = businessEmail; + return this; + } + + public Store setBusinessCategory(BusinessCategory businessCategory) { + this.businessCategory = businessCategory; + return this; + } + + public Store setDeviceHash(String deviceHash) { + this.deviceHash = deviceHash; + return this; + } + + public Store setLinkedDevicesKeys(LinkedHashMap linkedDevicesKeys) { + this.linkedDevicesKeys = linkedDevicesKeys; + return this; + } + + public Store setProfilePicture(URI profilePicture) { + this.profilePicture = profilePicture; + return this; + } + + public Store setAbout(String about) { + this.about = about; + return this; + } + + public Store setJid(Jid jid) { + this.jid = jid; + return this; + } + + public Store setLid(Jid lid) { + this.lid = lid; + return this; + } + + public Store setUnarchiveChats(boolean unarchiveChats) { + this.unarchiveChats = unarchiveChats; + return this; + } + + public Store setTwentyFourHourFormat(boolean twentyFourHourFormat) { + this.twentyFourHourFormat = twentyFourHourFormat; + return this; + } + + public Store setNewChatsEphemeralTimer(ChatEphemeralTimer newChatsEphemeralTimer) { + this.newChatsEphemeralTimer = newChatsEphemeralTimer; + return this; + } + + public Store setTextPreviewSetting(TextPreviewSetting textPreviewSetting) { + this.textPreviewSetting = textPreviewSetting; + return this; + } + + public Store setHistoryLength(WebHistoryLength historyLength) { + this.historyLength = historyLength; + return this; + } + + public Store setAutodetectListeners(boolean autodetectListeners) { + this.autodetectListeners = autodetectListeners; + return this; + } + + public void setCacheDetectedListeners(boolean cacheDetectedListeners) { + this.cacheDetectedListeners = cacheDetectedListeners; + } + + public Store setAutomaticPresenceUpdates(boolean automaticPresenceUpdates) { + this.automaticPresenceUpdates = automaticPresenceUpdates; + return this; + } + + public Store setReleaseChannel(ReleaseChannel releaseChannel) { + this.releaseChannel = releaseChannel; + return this; + } + + public Store setDevice(CompanionDevice device) { + if(Objects.equals(device(), device)) { + return this; + } + + Objects.requireNonNull(device, "The device cannot be null"); + this.device = device; + this.version = new FutureReference<>(null, () -> WhatsappMetadata.getVersion(device.platform())); + return this; + } + + public Store setCheckPatchMacs(boolean checkPatchMacs) { + this.checkPatchMacs = checkPatchMacs; + return this; + } + + public Store setVersion(Version version) { + this.version.setValue(version); + return this; + } + + public Optional verifiedName() { + return Optional.ofNullable(verifiedName); + } + + public Store setVerifiedName(String verifiedName) { + this.verifiedName = verifiedName; + return this; + } +} diff --git a/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java b/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java new file mode 100644 index 000000000..45f1bac6b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/controller/StoreKeysPair.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.controller; + +import it.auties.whatsapp.util.Validate; + +import java.util.Objects; + +/** + * A pair of Store and Keys with the same uuid + */ +public record StoreKeysPair(Store store, Keys keys) { + public StoreKeysPair { + Validate.isTrue(Objects.equals(store.uuid(), keys.uuid()), "UUID mismatch between store and keys"); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/AesCbc.java b/src/main/java/it/auties/whatsapp/crypto/AesCbc.java new file mode 100644 index 000000000..bc86c9287 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/AesCbc.java @@ -0,0 +1,52 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.Validate; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +public final class AesCbc { + private static final String AES_CBC = "AES/CBC/PKCS5Padding"; + private static final String AES = "AES"; + private static final int AES_BLOCK_SIZE = 16; + + public static byte[] encryptAndPrefix(byte[] plaintext, byte[] key) { + var iv = BytesHelper.random(AES_BLOCK_SIZE); + var encrypted = encrypt(iv, plaintext, key); + return BytesHelper.concat(iv, encrypted); + } + + public static byte[] encrypt(byte[] iv, byte[] plaintext, byte[] key) { + try { + var cipher = Cipher.getInstance(AES_CBC); + var keySpec = new SecretKeySpec(key, AES); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv)); + return cipher.doFinal(plaintext); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("Cannot encrypt data", exception); + } + } + + public static byte[] decrypt(byte[] encrypted, byte[] key) { + var iv = Arrays.copyOfRange(encrypted, 0, 16); + var encryptedNoIv = Arrays.copyOfRange(encrypted, iv.length, encrypted.length); + return decrypt(iv, encryptedNoIv, key); + } + + public static byte[] decrypt(byte[] iv, byte[] encrypted, byte[] key) { + try { + Validate.isTrue(iv.length == AES_BLOCK_SIZE, "Invalid iv size: expected %s, got %s", AES_BLOCK_SIZE, iv.length); + Validate.isTrue(encrypted.length % AES_BLOCK_SIZE == 0, "Invalid encrypted size"); + var cipher = Cipher.getInstance(AES_CBC); + var keySpec = new SecretKeySpec(key, AES); + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv)); + return cipher.doFinal(encrypted); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("Cannot encrypt data", exception); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/AesGcm.java b/src/main/java/it/auties/whatsapp/crypto/AesGcm.java new file mode 100644 index 000000000..338da2581 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/AesGcm.java @@ -0,0 +1,74 @@ +package it.auties.whatsapp.crypto; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; + +public final class AesGcm { + private static final int NONCE = 128; + + private AesGcm() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + public static byte[] encrypt(long iv, byte[] input, byte[] key) { + return encrypt(iv, input, key, null); + } + + public static byte[] encrypt(long iv, byte[] input, byte[] key, byte[] additionalData) { + return cipher(toIv(iv), input, key, additionalData, true); + } + + private static byte[] cipher(byte[] iv, byte[] input, byte[] key, byte[] additionalData, boolean encrypt) { + try { + var cipher = new GCMBlockCipher(new AESEngine()); + var parameters = new AEADParameters(new KeyParameter(key), NONCE, iv, additionalData); + cipher.init(encrypt, parameters); + var outputLength = cipher.getOutputSize(input.length); + var output = new byte[outputLength]; + var outputOffset = cipher.processBytes(input, 0, input.length, output, 0); + cipher.doFinal(output, outputOffset); + return output; + } catch (InvalidCipherTextException exception) { + throw new RuntimeException("Cannot %s data".formatted(encrypt ? "encrypt" : "decrypt"), exception); + } + } + + private static byte[] toIv(long iv) { + var byteArrayOutputStream = new ByteArrayOutputStream(); + try(var dataOutputStream = new DataOutputStream(byteArrayOutputStream)) { + dataOutputStream.write(new byte[4]); + dataOutputStream.writeLong(iv); + return byteArrayOutputStream.toByteArray(); + }catch (IOException exception) { + throw new UncheckedIOException(exception); + } + } + + public static byte[] decrypt(long iv, byte[] input, byte[] key) { + return decrypt(iv, input, key, null); + } + + public static byte[] decrypt(long iv, byte[] input, byte[] key, byte[] additionalData) { + return cipher(toIv(iv), input, key, additionalData, false); + } + + public static byte[] encrypt(byte[] iv, byte[] input, byte[] key, byte[] additionalData) { + return cipher(iv, input, key, additionalData, true); + } + + public static byte[] encrypt(byte[] iv, byte[] input, byte[] key) { + return cipher(iv, input, key, null, true); + } + + public static byte[] decrypt(byte[] iv, byte[] input, byte[] key, byte[] additionalData) { + return cipher(iv, input, key, additionalData, false); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/CipheredMessageResult.java b/src/main/java/it/auties/whatsapp/crypto/CipheredMessageResult.java new file mode 100644 index 000000000..5e00332e3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/CipheredMessageResult.java @@ -0,0 +1,5 @@ +package it.auties.whatsapp.crypto; + +public record CipheredMessageResult(byte[] message, String type) { + +} diff --git a/src/main/java/it/auties/whatsapp/crypto/GroupBuilder.java b/src/main/java/it/auties/whatsapp/crypto/GroupBuilder.java new file mode 100644 index 000000000..9eaddd466 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/GroupBuilder.java @@ -0,0 +1,24 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.signal.message.SignalDistributionMessage; +import it.auties.whatsapp.model.signal.sender.SenderKeyName; +import it.auties.whatsapp.util.KeyHelper; + +public record GroupBuilder(Keys keys) { + public byte[] createOutgoing(SenderKeyName name) { + var record = keys.findSenderKeyByName(name); + if (record.isEmpty()) { + record.addState(KeyHelper.senderKeyId(), SignalKeyPair.random(), 0, KeyHelper.senderKey()); + } + var state = record.firstState(); + var message = new SignalDistributionMessage(state.id(), state.chainKey().iteration(), state.chainKey().seed(), state.signingKey().encodedPublicKey()); + return message.serialized(); + } + + public void createIncoming(SenderKeyName name, SignalDistributionMessage message) { + var record = keys.findSenderKeyByName(name); + record.addState(message.id(), message.signingKey(), message.iteration(), message.chainKey()); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java b/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java new file mode 100644 index 000000000..3f09bd0c8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/GroupCipher.java @@ -0,0 +1,55 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.model.signal.message.SenderKeyMessage; +import it.auties.whatsapp.model.signal.sender.SenderKeyName; +import it.auties.whatsapp.model.signal.sender.SenderKeyState; +import it.auties.whatsapp.model.signal.sender.SenderMessageKey; +import it.auties.whatsapp.util.Specification.Signal; + +import java.util.NoSuchElementException; + +public record GroupCipher(SenderKeyName name, Keys keys) { + public CipheredMessageResult encrypt(byte[] data) { + if (data == null) { + return new CipheredMessageResult(null, Signal.UNAVAILABLE); + } + + var currentState = keys.findSenderKeyByName(name).firstState(); + var messageKey = currentState.chainKey().toMessageKey(); + var ciphertext = AesCbc.encrypt(messageKey.iv(), data, messageKey.cipherKey()); + var senderKeyMessage = new SenderKeyMessage(currentState.id(), messageKey.iteration(), ciphertext, currentState.signingKey().privateKey()); + var next = currentState.chainKey().next(); + currentState.setChainKey(next); + return new CipheredMessageResult(senderKeyMessage.serialized(), Signal.SKMSG); + } + + public byte[] decrypt(byte[] data) { + var record = keys.findSenderKeyByName(name); + var senderKeyMessage = SenderKeyMessage.ofSerialized(data); + var senderKeyStates = record.findStatesById(senderKeyMessage.id()); + for (var senderKeyState : senderKeyStates) { + try { + var senderKey = getSenderKey(senderKeyState, senderKeyMessage.iteration()); + return AesCbc.decrypt(senderKey.iv(), senderKeyMessage.cipherText(), senderKey.cipherKey()); + } catch (Throwable ignored) { + } + } + throw new RuntimeException("Cannot decode message with any session"); + } + + private SenderMessageKey getSenderKey(SenderKeyState senderKeyState, int iteration) { + if (senderKeyState.chainKey().iteration() > iteration) { + return senderKeyState.findSenderMessageKey(iteration) + .orElseThrow(() -> new NoSuchElementException("Received message with old counter: got %s, expected more than %s".formatted(iteration, senderKeyState.chainKey() + .iteration()))); + } + var lastChainKey = senderKeyState.chainKey(); + while (lastChainKey.iteration() < iteration) { + senderKeyState.addSenderMessageKey(lastChainKey.toMessageKey()); + lastChainKey = lastChainKey.next(); + } + senderKeyState.setChainKey(lastChainKey.next()); + return lastChainKey.toMessageKey(); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/Hkdf.java b/src/main/java/it/auties/whatsapp/crypto/Hkdf.java new file mode 100644 index 000000000..9d6940aaa --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/Hkdf.java @@ -0,0 +1,86 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.Validate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import static it.auties.whatsapp.util.Specification.Signal.KEY_LENGTH; + +public final class Hkdf { + private static final int ITERATION_START_OFFSET = 1; // v3 + private static final int HASH_OUTPUT_SIZE = 32; + private static final byte[] DEFAULT_SALT = new byte[HASH_OUTPUT_SIZE]; + private static final String HMAC_SHA_256 = "HmacSHA256"; + + public static byte[][] deriveSecrets(byte[] input, byte[] info) { + return deriveSecrets(input, info, 3); + } + + public static byte[][] deriveSecrets(byte[] input, byte[] info, int chunks) { + return deriveSecrets(input, DEFAULT_SALT, info, chunks); + } + + public static byte[][] deriveSecrets(byte[] input, byte[] salt, byte[] info, int chunks) { + Validate.isTrue(salt.length == KEY_LENGTH, "Incorrect salt codeLength: %s", salt.length); + Validate.isTrue(chunks >= 1 && chunks <= 3, "Incorrect number of chunks: %s", chunks); + var prk = Hmac.calculateSha256(input, salt); + var result = BytesHelper.concat(new byte[KEY_LENGTH], info, new byte[]{1}); + var signed = new byte[chunks][]; + var key = Arrays.copyOfRange(result, KEY_LENGTH, result.length); + var first = Hmac.calculateSha256(key, prk); + signed[0] = first; + if (chunks > 1) { + System.arraycopy(first, 0, result, 0, first.length); + result[result.length - 1] = 2; + signed[1] = Hmac.calculateSha256(result, prk); + } + if (chunks > 2) { + System.arraycopy(signed[1], 0, result, 0, signed[1].length); + result[result.length - 1] = 3; + signed[2] = Hmac.calculateSha256(result, prk); + } + return signed; + } + + public static byte[][] deriveSecrets(byte[] input, byte[] salt, byte[] info) { + return deriveSecrets(input, salt, info, 3); + } + + public static byte[] extractAndExpand(byte[] key, byte[] info, int outputLength) { + return extractAndExpand(key, DEFAULT_SALT, info, outputLength); + } + + public static byte[] extractAndExpand(byte[] key, byte[] salt, byte[] info, int outputLength) { + return expand(Hmac.calculateSha256(key, salt), info, outputLength); + } + + private static byte[] expand(byte[] prk, byte[] info, int outputSize) { + try { + var iterations = (int) Math.ceil((double) outputSize / (double) HASH_OUTPUT_SIZE); + var mixin = new byte[0]; + var results = new ByteArrayOutputStream(); + for (var index = ITERATION_START_OFFSET; index < iterations + ITERATION_START_OFFSET; index++) { + var mac = Mac.getInstance(HMAC_SHA_256); + mac.init(new SecretKeySpec(prk, HMAC_SHA_256)); + mac.update(mixin); + if (info != null) { + mac.update(info); + } + mac.update((byte) index); + var stepResult = mac.doFinal(); + var stepSize = Math.min(outputSize, stepResult.length); + results.write(stepResult, 0, stepSize); + mixin = stepResult; + outputSize -= stepSize; + } + return results.toByteArray(); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("Cannot expand data", exception); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/Hmac.java b/src/main/java/it/auties/whatsapp/crypto/Hmac.java new file mode 100644 index 000000000..b1bf4ec7e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/Hmac.java @@ -0,0 +1,29 @@ +package it.auties.whatsapp.crypto; + + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.GeneralSecurityException; + +public final class Hmac { + private static final String HMAC_SHA_256 = "HmacSHA256"; + private static final String HMAC_SHA_512 = "HmacSHA512"; + + public static byte[] calculateSha256(byte[] plain, byte[] key) { + return calculate(HMAC_SHA_256, plain, key); + } + + private static byte[] calculate(String algorithm, byte[] plain, byte[] key) { + try { + var localMac = Mac.getInstance(algorithm); + localMac.init(new SecretKeySpec(key, algorithm)); + return localMac.doFinal(plain); + } catch (GeneralSecurityException exception) { + throw new IllegalArgumentException("Cannot calculate hmac", exception); + } + } + + public static byte[] calculateSha512(byte[] plain, byte[] key) { + return calculate(HMAC_SHA_512, plain, key); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/LTHash.java b/src/main/java/it/auties/whatsapp/crypto/LTHash.java new file mode 100644 index 000000000..3abfc50e0 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/LTHash.java @@ -0,0 +1,78 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.model.companion.CompanionHashState; +import it.auties.whatsapp.model.sync.RecordSync; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class LTHash { + private static final int EXPAND_SIZE = 128; + public static final String SALT = "WhatsApp Patch Integrity"; + + private final byte[] salt; + + private final byte[] hash; + + private final Map indexValueMap; + + private final List add, subtract; + + public LTHash(CompanionHashState hash) { + this.salt = SALT.getBytes(StandardCharsets.UTF_8); + this.hash = hash.hash(); + this.indexValueMap = new HashMap<>(hash.indexValueMap()); + this.add = new ArrayList<>(); + this.subtract = new ArrayList<>(); + } + + public void mix(byte[] indexMac, byte[] valueMac, RecordSync.Operation operation) { + var indexMacBase64 = Base64.getEncoder().encodeToString(indexMac); + var prevOp = indexValueMap.get(indexMacBase64); + if (operation == RecordSync.Operation.REMOVE) { + if (prevOp == null) { + return; + } + indexValueMap.remove(indexMacBase64, prevOp); + } else { + add.add(valueMac); + indexValueMap.put(indexMacBase64, valueMac); + } + if (prevOp != null) { + subtract.add(prevOp); + } + } + + public Result finish() { + var subtracted = perform(hash, false); + var added = perform(subtracted, true); + return new Result(added, indexValueMap); + } + + private byte[] perform(byte[] input, boolean sum) { + for (var item : sum ? add : subtract) { + input = perform(input, item, sum); + } + return input; + } + + private byte[] perform(byte[] input, byte[] buffer, boolean sum) { + var expanded = Hkdf.extractAndExpand(buffer, salt, EXPAND_SIZE); + var eRead = ByteBuffer.wrap(input).order(ByteOrder.LITTLE_ENDIAN); + var tRead = ByteBuffer.wrap(expanded).order(ByteOrder.LITTLE_ENDIAN); + var write = ByteBuffer.allocate(input.length).order(ByteOrder.LITTLE_ENDIAN); + for (var index = 0; index < input.length; index += 2) { + var first = Short.toUnsignedInt(eRead.getShort(index)); + var second = Short.toUnsignedInt(tRead.getShort(index)); + write.putShort(index, (short) (sum ? first + second : first - second)); + } + var result = new byte[input.length]; + write.get(result); + return result; + } + + public record Result(byte[] hash, Map indexValueMap) { + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/MD5.java b/src/main/java/it/auties/whatsapp/crypto/MD5.java new file mode 100644 index 000000000..3a9452574 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/MD5.java @@ -0,0 +1,24 @@ +package it.auties.whatsapp.crypto; + + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class MD5 { + private static final String MD5 = "MD5"; + + public static byte[] calculate(String data) { + return calculate(data.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] calculate(byte[] data) { + try { + var digest = MessageDigest.getInstance(MD5); + digest.update(data); + return digest.digest(); + } catch (NoSuchAlgorithmException exception) { + throw new UnsupportedOperationException("Missing md5 implementation", exception); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java b/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java new file mode 100644 index 000000000..ff33c3683 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/SessionBuilder.java @@ -0,0 +1,131 @@ +package it.auties.whatsapp.crypto; + +import it.auties.curve25519.Curve25519; +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair; +import it.auties.whatsapp.model.signal.message.SignalPreKeyMessage; +import it.auties.whatsapp.model.signal.session.*; +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.KeyHelper; +import it.auties.whatsapp.util.Specification.Signal; +import it.auties.whatsapp.util.Validate; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +public record SessionBuilder(SessionAddress address, Keys keys) { + public void createOutgoing(int id, byte[] identityKey, SignalSignedKeyPair signedPreKey, SignalSignedKeyPair preKey) { + Validate.isTrue(keys.hasTrust(address, identityKey), "Untrusted key", SecurityException.class); + Validate.isTrue(Curve25519.verifySignature(KeyHelper.withoutHeader(identityKey), signedPreKey.keyPair() + .encodedPublicKey(), signedPreKey.signature()), "Signature mismatch", SecurityException.class); + var baseKey = SignalKeyPair.random(); + var state = createState(true, + baseKey, + null, + identityKey, + preKey == null ? null : preKey.keyPair().encodedPublicKey(), + signedPreKey.keyPair().encodedPublicKey(), + id, + Signal.CURRENT_VERSION + ); + var pendingPreKey = new SessionPreKey( + preKey == null ? null : preKey.id(), + baseKey.encodedPublicKey(), + signedPreKey.id() + ); + state.pendingPreKey(pendingPreKey); + keys.findSessionByAddress(address) + .map(Session::closeCurrentState) + .orElseGet(this::createSession) + .addState(state); + } + + public SessionState createState(boolean isInitiator, SignalKeyPair ourEphemeralKey, SignalKeyPair ourSignedKey, byte[] theirIdentityPubKey, byte[] theirEphemeralPubKey, byte[] theirSignedPubKey, int registrationId, int version) { + if (isInitiator) { + Validate.isTrue(ourSignedKey == null, "Our signed key should be null"); + ourSignedKey = ourEphemeralKey; + } else { + Validate.isTrue(theirSignedPubKey == null, "Their signed public key should be null"); + theirSignedPubKey = theirEphemeralPubKey; + } + var signedSecret = Curve25519.sharedKey(KeyHelper.withoutHeader(theirSignedPubKey), keys.identityKeyPair() + .privateKey()); + var identitySecret = Curve25519.sharedKey(KeyHelper.withoutHeader(theirIdentityPubKey), ourSignedKey.privateKey()); + var signedIdentitySecret = Curve25519.sharedKey(KeyHelper.withoutHeader(theirSignedPubKey), ourSignedKey.privateKey()); + var ephemeralSecret = theirEphemeralPubKey == null || ourEphemeralKey == null ? null : Curve25519.sharedKey(KeyHelper.withoutHeader(theirEphemeralPubKey), ourEphemeralKey.privateKey()); + var sharedSecret = createStateSecret(isInitiator, signedSecret, identitySecret, signedIdentitySecret, ephemeralSecret); + var masterKey = Hkdf.deriveSecrets(sharedSecret, "WhisperText".getBytes(StandardCharsets.UTF_8)); + var state = createState(isInitiator, ourEphemeralKey, ourSignedKey, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKey, registrationId, version, masterKey); + return isInitiator ? calculateSendingRatchet(state, theirSignedPubKey) : state; + } + + private Session createSession() { + var session = new Session(); + keys.putSession(address, session); + return session; + } + + private byte[] createStateSecret(boolean isInitiator, byte[] signedSecret, byte[] identitySecret, byte[] signedIdentitySecret, byte[] ephemeralSecret) { + var header = new byte[32]; + Arrays.fill(header, (byte) 0xff); + return BytesHelper.concat( + header, + isInitiator ? signedSecret : identitySecret, + isInitiator ? identitySecret : signedSecret, + signedIdentitySecret, + ephemeralSecret + ); + } + + private SessionState createState(boolean isInitiator, SignalKeyPair ourEphemeralKey, SignalKeyPair ourSignedKey, byte[] theirIdentityPubKey, byte[] theirEphemeralPubKey, byte[] theirSignedPubKey, int registrationId, int version, byte[][] masterKey) { + return new SessionState( + version, + registrationId, + isInitiator ? ourEphemeralKey.encodedPublicKey() : theirEphemeralPubKey, + theirIdentityPubKey, + new ConcurrentHashMap<>(), + masterKey[0], + null, + isInitiator ? SignalKeyPair.random() : ourSignedKey, + Objects.requireNonNull(theirSignedPubKey), + 0, + false + ); + } + + private SessionState calculateSendingRatchet(SessionState state, byte[] theirSignedPubKey) { + var initSecret = Curve25519.sharedKey(KeyHelper.withoutHeader(theirSignedPubKey), state.ephemeralKeyPair() + .privateKey()); + var initKey = Hkdf.deriveSecrets(initSecret, state.rootKey(), "WhisperRatchet".getBytes(StandardCharsets.UTF_8)); + var key = state.ephemeralKeyPair().encodedPublicKey(); + var chain = new SessionChain(-1, initKey[1]); + return state.addChain(key, chain).rootKey(initKey[0]); + } + + public void createIncoming(Session session, SignalPreKeyMessage message) { + Validate.isTrue(keys.hasTrust(address, message.identityKey()), "Untrusted key", SecurityException.class); + if (session.hasState(message.version(), message.baseKey())) { + return; + } + var preKeyPair = keys.findPreKeyById(message.preKeyId()) + .orElse(null); + var signedPreKeyPair = keys.findSignedKeyPairById(message.signedPreKeyId()) + .orElseThrow(() -> new NoSuchElementException("Cannot find signed pre key with id %s".formatted(message.signedPreKeyId()))); + session.closeCurrentState(); + var nextState = createState( + false, + preKeyPair != null ? preKeyPair.toGenericKeyPair() : null, + signedPreKeyPair == null ? null : signedPreKeyPair.toGenericKeyPair(), + message.identityKey(), + message.baseKey(), + null, + message.registrationId(), + message.version() + ); + session.addState(nextState); + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java new file mode 100644 index 000000000..bf001540c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/SessionCipher.java @@ -0,0 +1,194 @@ +package it.auties.whatsapp.crypto; + +import it.auties.whatsapp.controller.Keys; +import it.auties.whatsapp.exception.HmacValidationException; +import it.auties.whatsapp.model.signal.keypair.SignalKeyPair; +import it.auties.whatsapp.model.signal.message.SignalMessage; +import it.auties.whatsapp.model.signal.message.SignalMessageSpec; +import it.auties.whatsapp.model.signal.message.SignalPreKeyMessage; +import it.auties.whatsapp.model.signal.session.Session; +import it.auties.whatsapp.model.signal.session.SessionAddress; +import it.auties.whatsapp.model.signal.session.SessionChain; +import it.auties.whatsapp.model.signal.session.SessionState; +import it.auties.whatsapp.util.BytesHelper; +import it.auties.whatsapp.util.KeyHelper; +import it.auties.whatsapp.util.Specification.Signal; +import it.auties.whatsapp.util.Validate; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.function.Supplier; + +import static it.auties.curve25519.Curve25519.sharedKey; +import static it.auties.whatsapp.util.Specification.Signal.*; + +public record SessionCipher(SessionAddress address, Keys keys) { + public CipheredMessageResult encrypt(byte[] data) { + if (data == null) { + return new CipheredMessageResult(null, Signal.UNAVAILABLE); + } + var currentState = loadSession().currentState() + .orElseThrow(() -> new NoSuchElementException("Missing session for address %s".formatted(address))); + Validate.isTrue(keys.hasTrust(address, currentState.remoteIdentityKey()), "Untrusted key", SecurityException.class); + var chain = currentState.findChain(currentState.ephemeralKeyPair().encodedPublicKey()) + .orElseThrow(() -> new NoSuchElementException("Missing chain for %s".formatted(address))); + fillMessageKeys(chain, chain.counter().get() + 1); + var currentKey = chain.messageKeys().get(chain.counter().get()); + var secrets = Hkdf.deriveSecrets(currentKey, "WhisperMessageKeys".getBytes(StandardCharsets.UTF_8)); + chain.messageKeys().remove(chain.counter().get()); + var iv = Arrays.copyOf(secrets[2], IV_LENGTH); + var encrypted = AesCbc.encrypt(iv, data, secrets[0]); + var encryptedMessageType = getMessageType(currentState); + var encryptedMessage = encrypt(currentState, chain, secrets[1], encrypted); + return new CipheredMessageResult(encryptedMessage, encryptedMessageType); + } + + private String getMessageType(SessionState currentState) { + return currentState.hasPreKey() ? Signal.PKMSG : Signal.MSG; + } + + private byte[] encrypt(SessionState state, SessionChain chain, byte[] key, byte[] encrypted) { + var message = new SignalMessage(state.ephemeralKeyPair().encodedPublicKey(), chain.counter().get(), state.previousCounter(), encrypted); + message.setSignature(createMessageSignature(state, key, message)); + if (!state.hasPreKey()) { + return message.serialized(); + } + + var preKeyMessage = new SignalPreKeyMessage( + state.pendingPreKey().preKeyId(), + state.pendingPreKey().baseKey(), + keys.identityKeyPair().encodedPublicKey(), + message.serialized(), + keys.registrationId(), + state.pendingPreKey().signedKeyId() + ); + return preKeyMessage.serialized(); + } + + private byte[] createMessageSignature(SessionState state, byte[] key, SignalMessage message) { + var encodedMessage = BytesHelper.concat( + message.serializedVersion(), + SignalMessageSpec.encode(message) + ); + var macInput = BytesHelper.concat( + keys.identityKeyPair().encodedPublicKey(), + state.remoteIdentityKey(), + encodedMessage + ); + var sha256 = Hmac.calculateSha256(macInput, key); + return Arrays.copyOfRange(sha256, 0, MAC_LENGTH); + } + + private void fillMessageKeys(SessionChain chain, int counter) { + if (chain.counter().get() >= counter) { + return; + } + Validate.isTrue(counter - chain.counter() + .get() <= MAX_MESSAGES, "Message overflow: expected <= %s, got %s", MAX_MESSAGES, counter - chain.counter() + .get()); + Validate.isTrue(chain.key().get() != null, "Closed chain"); + var messagesHmac = Hmac.calculateSha256(new byte[]{1}, chain.key().get()); + chain.messageKeys().put(chain.counter().get() + 1, messagesHmac); + var keyHmac = Hmac.calculateSha256(new byte[]{2}, chain.key().get()); + chain.key().set(keyHmac); + chain.counter().getAndIncrement(); + fillMessageKeys(chain, counter); + } + + public byte[] decrypt(SignalPreKeyMessage message) { + var session = loadSession(this::createSession); + var builder = new SessionBuilder(address, keys); + builder.createIncoming(session, message); + var state = session.findState(message.version(), message.baseKey()) + .orElseThrow(() -> new NoSuchElementException("Missing state")); + return decrypt(message.signalMessage(), state); + } + + private Optional createSession() { + var newSession = new Session(); + keys.putSession(address, newSession); + return Optional.of(newSession); + } + + public byte[] decrypt(SignalMessage message) { + return loadSession().states() + .stream() + .map(state -> tryDecrypt(message, state)) + .flatMap(Optional::stream) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Cannot decrypt message: no suitable session found")); + } + + private Optional tryDecrypt(SignalMessage message, SessionState state) { + try { + Validate.isTrue(keys.hasTrust(address, state.remoteIdentityKey()), "Untrusted key"); + return Optional.of(decrypt(message, state)); + } catch (Throwable throwable) { + return Optional.empty(); + } + } + + private byte[] decrypt(SignalMessage message, SessionState state) { + maybeStepRatchet(message, state); + var chain = state.findChain(message.ephemeralPublicKey()) + .orElseThrow(() -> new NoSuchElementException("Invalid chain")); + fillMessageKeys(chain, message.counter()); + Validate.isTrue(chain.hasMessageKey(message.counter()), "Key used already or never filled"); + var messageKey = chain.messageKeys().get(message.counter()); + var secrets = Hkdf.deriveSecrets(messageKey, "WhisperMessageKeys".getBytes(StandardCharsets.UTF_8)); + var hmacValue = BytesHelper.concat( + state.remoteIdentityKey(), + keys.identityKeyPair().encodedPublicKey(), + message.serialized() + ); + var hmacInput = Arrays.copyOfRange(hmacValue, 0, hmacValue.length - MAC_LENGTH); + var hmacSha256 = Hmac.calculateSha256(hmacInput, secrets[1]); + var hmac = Arrays.copyOf(hmacSha256, MAC_LENGTH); + Validate.isTrue(Arrays.equals(message.signature(), hmac), "message_decryption", HmacValidationException.class); + var iv = Arrays.copyOf(secrets[2], IV_LENGTH); + var plaintext = AesCbc.decrypt(iv, message.ciphertext(), secrets[0]); + state.pendingPreKey(null); + return plaintext; + } + + private void maybeStepRatchet(SignalMessage message, SessionState state) { + if (state.hasChain(message.ephemeralPublicKey())) { + return; + } + var previousRatchet = state.findChain(state.lastRemoteEphemeralKey()); + previousRatchet.ifPresent(chain -> { + fillMessageKeys(chain, state.previousCounter()); + chain.key().set(null); + }); + calculateRatchet(message, state, false); + var previousCounter = state.findChain(state.ephemeralKeyPair().encodedPublicKey()); + previousCounter.ifPresent(chain -> { + state.previousCounter(chain.counter().get()); + state.removeChain(state.ephemeralKeyPair().encodedPublicKey()); + }); + state.ephemeralKeyPair(SignalKeyPair.random()); + calculateRatchet(message, state, true); + state.lastRemoteEphemeralKey(message.ephemeralPublicKey()); + } + + private void calculateRatchet(SignalMessage message, SessionState state, boolean sending) { + var sharedSecret = sharedKey(KeyHelper.withoutHeader(message.ephemeralPublicKey()), state.ephemeralKeyPair() + .privateKey()); + var masterKey = Hkdf.deriveSecrets(sharedSecret, state.rootKey(), "WhisperRatchet".getBytes(StandardCharsets.UTF_8), 2); + var chainKey = sending ? state.ephemeralKeyPair().encodedPublicKey() : message.ephemeralPublicKey(); + state.addChain(chainKey, new SessionChain(-1, masterKey[1])); + state.rootKey(masterKey[0]); + } + + private Session loadSession() { + return loadSession(() -> keys.findSessionByAddress(new SessionAddress(address.name(), 0))); + } + + private Session loadSession(Supplier> defaultSupplier) { + return keys.findSessionByAddress(address) + .or(defaultSupplier) + .orElseThrow(() -> new NoSuchElementException("Missing session for: %s".formatted(address))); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/crypto/Sha1.java b/src/main/java/it/auties/whatsapp/crypto/Sha1.java new file mode 100644 index 000000000..ca206001a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/Sha1.java @@ -0,0 +1,24 @@ +package it.auties.whatsapp.crypto; + + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class Sha1 { + private static final String SHA_1 = "SHA-1"; + + public static byte[] calculate(String data) { + return calculate(data.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] calculate(byte[] data) { + try { + var digest = MessageDigest.getInstance(SHA_1); + digest.update(data); + return digest.digest(); + } catch (NoSuchAlgorithmException exception) { + throw new UnsupportedOperationException("Missing sha1 implementation"); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/crypto/Sha256.java b/src/main/java/it/auties/whatsapp/crypto/Sha256.java new file mode 100644 index 000000000..668eba33d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/crypto/Sha256.java @@ -0,0 +1,24 @@ +package it.auties.whatsapp.crypto; + + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class Sha256 { + private static final String SHA_256 = "SHA-256"; + + public static byte[] calculate(String data) { + return calculate(data.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] calculate(byte[] data) { + try { + var digest = MessageDigest.getInstance(SHA_256); + digest.update(data); + return digest.digest(); + } catch (NoSuchAlgorithmException exception) { + throw new UnsupportedOperationException("Missing sha256 implementation"); + } + } +} diff --git a/src/main/java/it/auties/whatsapp/exception/HmacValidationException.java b/src/main/java/it/auties/whatsapp/exception/HmacValidationException.java new file mode 100644 index 000000000..a5292b599 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/exception/HmacValidationException.java @@ -0,0 +1,11 @@ +package it.auties.whatsapp.exception; + + +/** + * An unchecked exception that is thrown when a hmac signature cannot be validated + */ +public class HmacValidationException extends SecurityException { + public HmacValidationException(String location) { + super(location); + } +} diff --git a/src/main/java/it/auties/whatsapp/exception/RegistrationException.java b/src/main/java/it/auties/whatsapp/exception/RegistrationException.java new file mode 100644 index 000000000..0d2ba8e75 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/exception/RegistrationException.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.exception; + +import it.auties.whatsapp.model.response.RegistrationResponse; + +import java.util.Optional; + +/** + * This exception is thrown when a phone number cannot be registered by the Whatsapp API + */ +public class RegistrationException extends RuntimeException { + private final RegistrationResponse erroneousResponse; + + public RegistrationException(RegistrationResponse erroneousResponse, String message) { + super(message); + this.erroneousResponse = erroneousResponse; + } + + public Optional erroneousResponse() { + return Optional.ofNullable(erroneousResponse); + } +} diff --git a/src/main/java/it/auties/whatsapp/exception/RequestException.java b/src/main/java/it/auties/whatsapp/exception/RequestException.java new file mode 100644 index 000000000..b2e948926 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/exception/RequestException.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.exception; + +/** + * This exception is thrown when a request cannot be sent to Whatsapp's socket + */ +public class RequestException extends RuntimeException { + public RequestException(String message, Throwable cause) { + super(message, cause); + } + + public RequestException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/it/auties/whatsapp/listener/Listener.java b/src/main/java/it/auties/whatsapp/listener/Listener.java new file mode 100644 index 000000000..f4ff1dacc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/Listener.java @@ -0,0 +1,661 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.DisconnectReason; +import it.auties.whatsapp.api.SocketEvent; +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.model.action.Action; +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.ContactStatus; +import it.auties.whatsapp.model.info.ChatMessageInfo; +import it.auties.whatsapp.model.info.MessageIndexInfo; +import it.auties.whatsapp.model.info.MessageInfo; +import it.auties.whatsapp.model.info.QuotedMessageInfo; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.mobile.CountryLocale; +import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.node.Node; +import it.auties.whatsapp.model.privacy.PrivacySettingEntry; +import it.auties.whatsapp.model.setting.Setting; + +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * This interface can be used to listen for events fired when new information is sent by + * WhatsappWeb's socket. A listener can be registered manually using + * {@link Whatsapp#addListener(Listener)}. Otherwise, it can be registered by annotating it with the + * {@link RegisterListener} annotation. To disable the latter, check out + * {@link Store#autodetectListeners()}. + */ +@SuppressWarnings("unused") +public interface Listener { + /** + * Called when the socket sends a node to Whatsapp + * + * @param whatsapp an instance to the calling api + * @param outgoing the non-null node that was just sent + */ + default void onNodeSent(Whatsapp whatsapp, Node outgoing) { + } + + /** + * Called when the socket sends a node to Whatsapp + * + * @param outgoing the non-null node that was just sent + */ + default void onNodeSent(Node outgoing) { + } + + /** + * Called when the socket receives a node from Whatsapp + * + * @param whatsapp an instance to the calling api + * @param incoming the non-null node that was just received + */ + default void onNodeReceived(Whatsapp whatsapp, Node incoming) { + } + + /** + * Called when the socket receives a node from Whatsapp + * + * @param incoming the non-null node that was just received + */ + default void onNodeReceived(Node incoming) { + } + + /** + * Called when the socket successfully establishes a connection and logs in into an account. When + * this event is called, any data, including chats and contact, is not guaranteed to be already in + * memory. Instead, {@link OnChats#onChats(Whatsapp, Collection)} ()} and + * {@link OnContacts#onContacts(Whatsapp, Collection)} ()} should be used. + * + * @param whatsapp an instance to the calling api + */ + default void onLoggedIn(Whatsapp whatsapp) { + } + + /** + * Called when the socket successfully establishes a connection and logs in into an account. When + * this event is called, any data, including chats and contact, is not guaranteed to be already in + * memory. Instead, {@link Listener#onChats(Collection)} and + * {@link Listener#onContacts(Collection)} should be used. + */ + default void onLoggedIn() { + } + + /** + * Called when an updated list of properties is received. This method is called both when a + * connection is established with WhatsappWeb and when new props are available. In the latter case + * though, this object should be considered as partial and is guaranteed to contain only updated + * entries. + * + * @param whatsapp an instance to the calling api + * @param metadata the updated list of properties + */ + default void onMetadata(Whatsapp whatsapp, Map metadata) { + } + + /** + * Called when an updated list of properties is received. This method is called both when a + * connection is established with WhatsappWeb and when new props are available. In the latter case + * though, this object should be considered as partial and is guaranteed to contain only updated + * entries. + * + * @param metadata the updated list of properties + */ + default void onMetadata(Map metadata) { + } + + /** + * Called when the socket successfully disconnects from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param reason the errorReason why the session was disconnected + */ + default void onDisconnected(Whatsapp whatsapp, DisconnectReason reason) { + } + + + /** + * Called when the socket successfully disconnects from WhatsappWeb's Socket + * + * @param reason the errorReason why the session was disconnected + */ + default void onDisconnected(DisconnectReason reason) { + } + + /** + * Called when the socket receives a sync from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param action the sync that was executed + * @param messageIndexInfo the data about this action + */ + default void onAction(Whatsapp whatsapp, Action action, MessageIndexInfo messageIndexInfo) { + } + + /** + * Called when the socket receives a sync from Whatsapp. + * + * @param action the sync that was executed + * @param messageIndexInfo the data about this action + */ + default void onAction(Action action, MessageIndexInfo messageIndexInfo) { + } + + /** + * Called when the socket receives a setting change from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param setting the setting that was toggled + */ + default void onSetting(Whatsapp whatsapp, Setting setting) { + } + + /** + * Called when the socket receives a setting change from Whatsapp. + * + * @param setting the setting that was toggled + */ + default void onSetting(Setting setting) { + } + + /** + * Called when the socket receives new features from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param features the non-null features that were sent + */ + default void onFeatures(Whatsapp whatsapp, List features) { + } + + + /** + * Called when the socket receives new features from Whatsapp. + * + * @param features the non-null features that were sent + */ + default void onFeatures(List features) { + } + + /** + * Called when the socket receives all the contacts from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param contacts the contacts + */ + default void onContacts(Whatsapp whatsapp, Collection contacts) { + } + + /** + * Called when the socket receives all the contacts from WhatsappWeb's Socket + * + * @param contacts the contacts + */ + default void onContacts(Collection contacts) { + } + + /** + * Called when the socket receives an update regarding the presence of a contact + * + * @param whatsapp an instance to the calling api + * @param chat the chat 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, 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 jid the contact that this update regards + * @param status the new status of the contact + */ + default void onContactPresence(Chat chat, Jid jid, ContactStatus status) { + } + + /** + * Called when the socket receives all the chats from WhatsappWeb's Socket. When this event is + * fired, it is guaranteed that all metadata excluding messages will be present. If you also need + * the messages to be loaded, please refer to {@link Listener#onChatMessagesSync(Chat, boolean)}. + * Particularly old chats may come later through + * {@link Listener#onChatMessagesSync(Chat, boolean)} + * + * @param whatsapp an instance to the calling api + * @param chats the chats + */ + default void onChats(Whatsapp whatsapp, Collection chats) { + } + + /** + * Called when the socket receives all the chats from WhatsappWeb's Socket. When this event is + * fired, it is guaranteed that all metadata excluding messages will be present. To access this + * data use {@link Store#chats()}. If you also need the messages to be loaded, please refer to + * {@link Listener#onChatMessagesSync(Chat, boolean)}. Particularly old chats may come later + * through {@link Listener#onChatMessagesSync(Chat, boolean)}. + * + * @param chats the chats + */ + default void onChats(Collection chats) { + } + + + /** + * Called when the socket receives all the newsletters from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param newsletters the newsletters + */ + default void onNewsletters(Whatsapp whatsapp, Collection newsletters) { + } + + /** + * Called when the socket receives all the newsletters from WhatsappWeb's Socket + * + * @param newsletters the newsletters + */ + default void onNewsletters(Collection newsletters) { + } + + /** + * Called when the socket receives the messages for a chat. This method is only called when the QR + * is first scanned and history is being synced. From all subsequent runs, the messages will + * already in the chat on startup. + * + * @param whatsapp an instance to the calling api + * @param chat the chat + * @param last whether the messages in this chat are complete or there are more coming + */ + default void onChatMessagesSync(Whatsapp whatsapp, Chat chat, boolean last) { + } + + /** + * Called when the socket receives the message for a chat This method is only called when the QR + * is first scanned and history is being synced. From all subsequent runs, the messages will + * already in the chat on startup. + * + * @param chat the chat + * @param last whether the messages in this chat are complete or there are more coming + */ + default void onChatMessagesSync(Chat chat, boolean last) { + } + + /** + * Called when the socket receives the sync percentage for the full or recent chunk of messages. + * This method is only called when the QR is first scanned and history is being synced. + * + * @param percentage the percentage synced up to now + * @param recent whether the sync is about the recent messages or older messages + */ + default void onHistorySyncProgress(int percentage, boolean recent) { + } + + /** + * Called when the socket receives the sync percentage for the full or recent chunk of messages. + * This method is only called when the QR is first scanned and history is being synced. + * + * @param whatsapp an instance to the calling api + * @param percentage the percentage synced up to now + * @param recent whether the sync is about the recent messages or older messages + */ + default void onHistorySyncProgress(Whatsapp whatsapp, int percentage, boolean recent) { + } + + /** + * Called when a new message is received in a chat + * + * @param whatsapp an instance to the calling api + * @param info the message that was sent + */ + default void onNewMessage(Whatsapp whatsapp, MessageInfo info) { + } + + /** + * Called when a new message is received in a chat + * + * @param info the message that was sent + */ + default void onNewMessage(MessageInfo info) { + } + + /** + * Called when a message is deleted + * + * @param whatsapp an instance to the calling api + * @param info the message that was deleted + * @param everyone whether this message was deleted by you only for yourself or whether the + * message was permanently removed + */ + default void onMessageDeleted(Whatsapp whatsapp, MessageInfo info, boolean everyone) { + } + + /** + * Called when a message is deleted + * + * @param info the message that was deleted + * @param everyone whether this message was deleted by you only for yourself or whether the + * message was permanently removed + */ + default void onMessageDeleted(MessageInfo info, boolean everyone) { + } + + /** + * Called when the status of a message changes inside a chat + * + * @param whatsapp an instance to the calling api + * @param info the message whose status changed + */ + default void onMessageStatus(Whatsapp whatsapp, MessageInfo info) { + } + + /** + * Called when the status of a message changes inside a chat + * + * @param info the message whose status changed + */ + default void onMessageStatus(MessageInfo info) { + } + + + /** + * Called when the socket receives all the status updated from WhatsappWeb's Socket. + * + * @param whatsapp an instance to the calling api + * @param status the status + */ + default void onStatus(Whatsapp whatsapp, Collection status) { + } + + /** + * Called when the socket receives all the status updated from WhatsappWeb's Socket. + * + * @param status the status + */ + default void onStatus(Collection status) { + } + + /** + * Called when the socket receives a new status from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param status the new status message + */ + default void onNewStatus(Whatsapp whatsapp, ChatMessageInfo status) { + } + + /** + * Called when the socket receives a new status from WhatsappWeb's Socket + * + * @param status the new status message + */ + default void onNewStatus(ChatMessageInfo status) { + } + + /** + * Called when an event regarding the underlying is fired + * + * @param whatsapp an instance to the calling api + * @param event the event + */ + default void onSocketEvent(Whatsapp whatsapp, SocketEvent event) { + } + + /** + * Called when an event regarding the underlying is fired + * + * @param event the event + */ + default void onSocketEvent(SocketEvent event) { + } + + /** + * Called when a message answers a previous message + * + * @param response the response + * @param quoted the quoted message + */ + default void onMessageReply(ChatMessageInfo response, QuotedMessageInfo quoted) { + } + + /** + * Called when a message answers a previous message + * + * @param whatsapp an instance to the calling api + * @param response the response + * @param quoted the quoted message + */ + default void onMessageReply(Whatsapp whatsapp, ChatMessageInfo response, QuotedMessageInfo quoted) { + } + + /** + * Called when a contact's profile picture changes + * + * @param contact the contact whose pic changed + */ + default void onProfilePictureChanged(Contact contact) { + } + + + /** + * Called when a contact's profile picture changes + * + * @param whatsapp an instance to the calling api + * @param contact the contact whose pic changed + */ + default void onProfilePictureChanged(Whatsapp whatsapp, Contact contact) { + } + + /** + * Called when a group's picture changes + * + * @param group the group whose pic changed + */ + default void onGroupPictureChanged(Chat group) { + } + + /** + * Called when a group's picture changes + * + * @param whatsapp an instance to the calling api + * @param group the group whose pic changed + */ + default void onGroupPictureChanged(Whatsapp whatsapp, Chat group) { + } + + /** + * Called when the companion's name changes + * + * @param oldName the non-null old name + * @param newName the non-null new name + */ + default void onNameChanged(String oldName, String newName) { + } + + /** + * Called when the companion's name changes + * + * @param whatsapp an instance to the calling api + * @param oldName the non-null old name + * @param newName the non-null new name + */ + default void onNameChanged(Whatsapp whatsapp, String oldName, String newName) { + } + + /** + * Called when the companion's about changes + * + * @param oldAbout the non-null old about + * @param newAbout the non-null new about + */ + default void onAboutChanged(String oldAbout, String newAbout) { + } + + /** + * Called when the companion's about changes + * + * @param whatsapp an instance to the calling api + * @param oldAbout the non-null old about + * @param newAbout the non-null new about + */ + default void onAboutChanged(Whatsapp whatsapp, String oldAbout, String newAbout) { + } + + /** + * Called when the companion's picture changes + * + * @param oldPicture the non-null old picture + * @param newPicture the non-null new picture + */ + default void onProfilePictureChanged(URI oldPicture, URI newPicture) { + } + + /** + * Called when the companion's picture changes + * + * @param whatsapp an instance to the calling api + * @param oldPicture the non-null old picture + * @param newPicture the non-null new picture + */ + default void onProfilePictureChanged(Whatsapp whatsapp, URI oldPicture, URI newPicture) { + } + + /** + * Called when the companion's locale changes + * + * @param oldLocale the non-null old locale + * @param newLocale the non-null new picture + */ + default void onLocaleChanged(CountryLocale oldLocale, CountryLocale newLocale) { + } + + /** + * Called when the companion's locale changes + * + * @param whatsapp an instance to the calling api + * @param oldLocale the non-null old locale + * @param newLocale the non-null new picture + */ + default void onLocaleChanged(Whatsapp whatsapp, CountryLocale oldLocale, CountryLocale newLocale) { + } + + /** + * Called when a contact is blocked or unblocked + * + * @param contact the non-null contact + */ + default void onContactBlocked(Contact contact) { + } + + /** + * Called when a contact is blocked or unblocked + * + * @param whatsapp an instance to the calling api + * @param contact the non-null contact + */ + default void onContactBlocked(Whatsapp whatsapp, Contact contact) { + } + + /** + * Called when the socket receives a new contact + * + * @param whatsapp an instance to the calling api + * @param contact the new contact + */ + default void onNewContact(Whatsapp whatsapp, Contact contact) { + } + + /** + * Called when the socket receives a new contact + * + * @param contact the new contact + */ + default void onNewContact(Contact contact) { + } + + /** + * Called when a privacy setting is modified + * + * @param whatsapp an instance to the calling api + * @param oldPrivacyEntry the old entry + * @param newPrivacyEntry the new entry + */ + default void onPrivacySettingChanged(Whatsapp whatsapp, PrivacySettingEntry oldPrivacyEntry, PrivacySettingEntry newPrivacyEntry) { + + } + + /** + * Called when a privacy setting is modified + * + * @param oldPrivacyEntry the old entry + * @param newPrivacyEntry the new entry + */ + default void onPrivacySettingChanged(PrivacySettingEntry oldPrivacyEntry, PrivacySettingEntry newPrivacyEntry) { + + } + + /** + * Called when the list of companion devices is updated + * + * @param whatsapp an instance to the calling api + * @param devices the non-null devices + */ + default void onLinkedDevices(Whatsapp whatsapp, Collection devices) { + + } + + /** + * Called when the list of companion devices is updated + * + * @param devices the non-null devices + */ + default void onLinkedDevices(Collection devices) { + + } + + /** + * Called when an OTP is requested from a new device + * Only works on the mobile API + * + * @param code the registration code + */ + default void onRegistrationCode(long code) { + + } + + /** + * Called when an OTP is requested from a new device + * Only works on the mobile API + * + * @param whatsapp an instance to the calling api + * @param code the registration code + */ + default void onRegistrationCode(Whatsapp whatsapp, long code) { + + } + + /** + * Called when a phone call arrives + * + * @param call the non-null phone call + */ + default void onCall(Call call) { + + } + + /** + * Called when a phone call arrives + * + * @param whatsapp an instance to the calling api + * @param call the non-null phone call + */ + default void onCall(Whatsapp whatsapp, Call call) { + + } +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnAction.java b/src/main/java/it/auties/whatsapp/listener/OnAction.java new file mode 100644 index 000000000..675f3785b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnAction.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.action.Action; +import it.auties.whatsapp.model.info.MessageIndexInfo; + +public interface OnAction extends Listener { + /** + * Called when the socket receives a sync from Whatsapp. + * + * @param action the sync that was executed + * @param messageIndexInfo the data about this action + */ + @Override + void onAction(Action action, MessageIndexInfo messageIndexInfo); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnCall.java b/src/main/java/it/auties/whatsapp/listener/OnCall.java new file mode 100644 index 000000000..0653e5865 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnCall.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.call.Call; + +public interface OnCall extends Listener { + /** + * Called when a phone call arrives + * + * @param call the non-null phone call + */ + @Override + void onCall(Call call); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnChatMessagesSync.java b/src/main/java/it/auties/whatsapp/listener/OnChatMessagesSync.java new file mode 100644 index 000000000..6ac9d371c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnChatMessagesSync.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.chat.Chat; + +public interface OnChatMessagesSync extends Listener { + /** + * Called when the socket receives the recent message for a chat + * + * @param chat the chat + * @param last whether the messages in this chat are complete or there are more coming + */ + @Override + void onChatMessagesSync(Chat chat, boolean last); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnChats.java b/src/main/java/it/auties/whatsapp/listener/OnChats.java new file mode 100644 index 000000000..136e965a6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnChats.java @@ -0,0 +1,20 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.controller.Store; +import it.auties.whatsapp.model.chat.Chat; + +import java.util.Collection; + +public interface OnChats extends Listener { + /** + * Called when the socket receives all the chats from WhatsappWeb's WebSocket. When this event is + * fired, it is guaranteed that all metadata excluding messages will be present. To access this + * data use {@link Store#chats()}. If you also need the messages to be loaded, please refer to + * {@link Listener#onChatMessagesSync(Chat, boolean)}. Particularly old chats may come later + * through {@link Listener#onChatMessagesSync(Chat, boolean)}. + * + * @param chats the chats + */ + @Override + void onChats(Collection chats); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnContactBlocked.java b/src/main/java/it/auties/whatsapp/listener/OnContactBlocked.java new file mode 100644 index 000000000..9ff2d27cb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnContactBlocked.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.contact.Contact; + +public interface OnContactBlocked extends Listener { + /** + * Called when a contact is blocked or unblocked + * + * @param contact the non-null contact + */ + @Override + void onContactBlocked(Contact contact); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnContactPictureChanged.java b/src/main/java/it/auties/whatsapp/listener/OnContactPictureChanged.java new file mode 100644 index 000000000..f742918a5 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnContactPictureChanged.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.contact.Contact; + +public interface OnContactPictureChanged extends Listener { + /** + * Called when a contact's profile picture changes + * + * @param contact the contact whose pic changed + */ + @Override + void onProfilePictureChanged(Contact contact); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java new file mode 100644 index 000000000..5f3ffbc6a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnContactPresence.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.jid.Jid; + +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 jid the contact that this update regards + * @param status the new status of the contact + */ + @Override + void onContactPresence(Chat chat, Jid jid, ContactStatus status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnContacts.java b/src/main/java/it/auties/whatsapp/listener/OnContacts.java new file mode 100644 index 000000000..e40c359d3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnContacts.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + + +import it.auties.whatsapp.model.contact.Contact; + +import java.util.Collection; + +public interface OnContacts extends Listener { + /** + * Called when the socket receives all the contacts from WhatsappWeb's WebSocket + * + * @param contacts the contacts + */ + @Override + void onContacts(Collection contacts); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnDisconnected.java b/src/main/java/it/auties/whatsapp/listener/OnDisconnected.java new file mode 100644 index 000000000..cf2010ca8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnDisconnected.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.DisconnectReason; + +public interface OnDisconnected extends Listener { + /** + * Called when the socket successfully disconnects from WhatsappWeb's WebSocket + * + * @param reason the errorReason why the session was disconnected + */ + @Override + void onDisconnected(DisconnectReason reason); +} + diff --git a/src/main/java/it/auties/whatsapp/listener/OnFeatures.java b/src/main/java/it/auties/whatsapp/listener/OnFeatures.java new file mode 100644 index 000000000..8d02ebb29 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnFeatures.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import java.util.List; + +public interface OnFeatures extends Listener { + /** + * Called when the socket receives new features from Whatsapp. + * + * @param features the non-null features that were sent + */ + @Override + void onFeatures(List features); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnGroupPictureChange.java b/src/main/java/it/auties/whatsapp/listener/OnGroupPictureChange.java new file mode 100644 index 000000000..b12b15aff --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnGroupPictureChange.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.chat.Chat; + +public interface OnGroupPictureChange extends Listener { + /** + * Called when a group's picture changes + * + * @param group the group whose pic changed + */ + @Override + void onGroupPictureChanged(Chat group); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnHistorySyncProgress.java b/src/main/java/it/auties/whatsapp/listener/OnHistorySyncProgress.java new file mode 100644 index 000000000..2b926c7ad --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnHistorySyncProgress.java @@ -0,0 +1,12 @@ +package it.auties.whatsapp.listener; + +public interface OnHistorySyncProgress extends Listener { + /** + * Called when the socket receives the sync percentage for the full or recent chunk of messages. + * This method is only called when the QR is first scanned and history is being synced. + * + * @param percentage the percentage synced up to now + * @param recent whether the sync is about the recent messages or older messages + */ + void onHistorySyncProgress(int percentage, boolean recent); +} \ 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 new file mode 100644 index 000000000..94e8edf1e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnLinkedDevices.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.jid.Jid; + +import java.util.Collection; + +public interface OnLinkedDevices extends Listener { + /** + * Called when the list of companion devices is updated + * + * @param devices the non-null devices + */ + @Override + void onLinkedDevices(Collection devices); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnLoggedIn.java b/src/main/java/it/auties/whatsapp/listener/OnLoggedIn.java new file mode 100644 index 000000000..76a40b17b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnLoggedIn.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import java.util.Collection; + +public interface OnLoggedIn extends Listener { + /** + * Called when the socket successfully establishes a connection and logs in into an account. When + * this event is called, any data, including chats and contact, is not guaranteed to be already in + * memory. Instead, {@link OnChats#onChats(Collection)} ()} and + * {@link OnContacts#onContacts(Collection)} ()} should be used. + */ + @Override + void onLoggedIn(); +} + diff --git a/src/main/java/it/auties/whatsapp/listener/OnMessageDeleted.java b/src/main/java/it/auties/whatsapp/listener/OnMessageDeleted.java new file mode 100644 index 000000000..e8abf25f1 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnMessageDeleted.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnMessageDeleted extends Listener { + /** + * Called when a message is deleted + * + * @param info the message that was deleted + * @param everyone whether this message was deleted by you only for yourself or whether the + * message was permanently removed + */ + @Override + void onMessageDeleted(MessageInfo info, boolean everyone); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnMessageReply.java b/src/main/java/it/auties/whatsapp/listener/OnMessageReply.java new file mode 100644 index 000000000..062cfd22c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnMessageReply.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.ChatMessageInfo; +import it.auties.whatsapp.model.info.QuotedMessageInfo; + +public interface OnMessageReply extends Listener { + /** + * Called when a message answers a previous message + * + * @param info the answer message + * @param quoted the quoted message + */ + @Override + void onMessageReply(ChatMessageInfo info, QuotedMessageInfo quoted); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnMessageStatus.java b/src/main/java/it/auties/whatsapp/listener/OnMessageStatus.java new file mode 100644 index 000000000..290b05d16 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnMessageStatus.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnMessageStatus extends Listener { + /** + * Called when the status of a message changes + * + * @param info the message whose status changed + */ + @Override + void onMessageStatus(MessageInfo info); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnMetadata.java b/src/main/java/it/auties/whatsapp/listener/OnMetadata.java new file mode 100644 index 000000000..157958386 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnMetadata.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import java.util.Map; + +public interface OnMetadata extends Listener { + /** + * Called when an updated list of properties is received. This method is called both when a + * connection is established with WhatsappWeb and when new props are available. In the latter case + * though, this object should be considered as partial and is guaranteed to contain only updated + * entries. + * + * @param metadata the updated list of properties + */ + @Override + void onMetadata(Map metadata); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewContact.java b/src/main/java/it/auties/whatsapp/listener/OnNewContact.java new file mode 100644 index 000000000..ba4adbf3c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNewContact.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.contact.Contact; + +public interface OnNewContact extends Listener { + /** + * Called when the socket receives a new contact. There isn't an overloaded method with a Whatsapp + * parameter due to technical limitations. + * + * @param contact the new contact + */ + @Override + void onNewContact(Contact contact); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewMessage.java b/src/main/java/it/auties/whatsapp/listener/OnNewMessage.java new file mode 100644 index 000000000..b0043f524 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNewMessage.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnNewMessage extends Listener { + /** + * Called when a new message is received in a chat + * + * @param info the message that was sent + */ + @Override + void onNewMessage(MessageInfo info); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewStatus.java b/src/main/java/it/auties/whatsapp/listener/OnNewStatus.java new file mode 100644 index 000000000..d58fd8903 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNewStatus.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.ChatMessageInfo; + +public interface OnNewStatus extends Listener { + /** + * Called when the socket receives a new status from WhatsappWeb's Socket + * + * @param status the new status message + */ + @Override + void onNewStatus(ChatMessageInfo status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNewsletters.java b/src/main/java/it/auties/whatsapp/listener/OnNewsletters.java new file mode 100644 index 000000000..b16332303 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNewsletters.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.newsletter.Newsletter; + +import java.util.Collection; + +public interface OnNewsletters extends Listener { + /** + * Called when the socket receives all the newsletters from WhatsappWeb's Socket + * + * @param newsletters the newsletters + */ + @Override + void onNewsletters(Collection newsletters); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNodeReceived.java b/src/main/java/it/auties/whatsapp/listener/OnNodeReceived.java new file mode 100644 index 000000000..d952b0c80 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNodeReceived.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.node.Node; + +public interface OnNodeReceived extends Listener { + /** + * Called when the socket receives a node from Whatsapp + * + * @param incoming the non-null node that was just received + */ + @Override + void onNodeReceived(Node incoming); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnNodeSent.java b/src/main/java/it/auties/whatsapp/listener/OnNodeSent.java new file mode 100644 index 000000000..3c06930be --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnNodeSent.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.node.Node; + +public interface OnNodeSent extends Listener { + /** + * Called when the socket sends a node to Whatsapp + * + * @param outgoing the non-null node that was just sent + */ + @Override + void onNodeSent(Node outgoing); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnPrivacySettingChanged.java b/src/main/java/it/auties/whatsapp/listener/OnPrivacySettingChanged.java new file mode 100644 index 000000000..c1bceedd1 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnPrivacySettingChanged.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.privacy.PrivacySettingEntry; + +public interface OnPrivacySettingChanged extends Listener { + /** + * Called when a privacy setting is modified + * + * @param oldPrivacyEntry the old entry + * @param newPrivacyEntry the new entry + */ + @Override + void onPrivacySettingChanged(PrivacySettingEntry oldPrivacyEntry, PrivacySettingEntry newPrivacyEntry); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnProfilePictureChanged.java b/src/main/java/it/auties/whatsapp/listener/OnProfilePictureChanged.java new file mode 100644 index 000000000..340fd767d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnProfilePictureChanged.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import java.net.URI; + +public interface OnProfilePictureChanged extends Listener { + /** + * Called when the companion's picture changes + * + * @param oldPicture the non-null old picture + * @param newPicture the non-null new picture + */ + @Override + void onProfilePictureChanged(URI oldPicture, URI newPicture); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnRegistrationCode.java b/src/main/java/it/auties/whatsapp/listener/OnRegistrationCode.java new file mode 100644 index 000000000..afab0c322 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnRegistrationCode.java @@ -0,0 +1,12 @@ +package it.auties.whatsapp.listener; + +public interface OnRegistrationCode extends Listener { + /** + * Called when an OTP is requested from a new device + * Only works on the mobile API + * + * @param code the registration code + */ + @Override + void onRegistrationCode(long code); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnSetting.java b/src/main/java/it/auties/whatsapp/listener/OnSetting.java new file mode 100644 index 000000000..66c831edf --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnSetting.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.setting.Setting; + +public interface OnSetting extends Listener { + /** + * Called when the socket receives a setting change from Whatsapp. + * + * @param setting the setting that was toggled + */ + @Override + void onSetting(Setting setting); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnSocketEvent.java b/src/main/java/it/auties/whatsapp/listener/OnSocketEvent.java new file mode 100644 index 000000000..73949c705 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnSocketEvent.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.SocketEvent; + +public interface OnSocketEvent extends Listener { + /** + * Called when an event regarding the underlying is fired + * + * @param event the event + */ + @Override + void onSocketEvent(SocketEvent event); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnStatus.java b/src/main/java/it/auties/whatsapp/listener/OnStatus.java new file mode 100644 index 000000000..c18feabb2 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnStatus.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.info.ChatMessageInfo; + +import java.util.Collection; + +public interface OnStatus extends Listener { + /** + * Called when the socket receives all the status updated from WhatsappWeb's Socket. + * + * @param status the status + */ + void onStatus(Collection status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnUserAboutChanged.java b/src/main/java/it/auties/whatsapp/listener/OnUserAboutChanged.java new file mode 100644 index 000000000..509302bfc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnUserAboutChanged.java @@ -0,0 +1,11 @@ +package it.auties.whatsapp.listener; + +public interface OnUserAboutChanged extends Listener { + /** + * Called when the companion's status changes + * + * @param oldAbout the non-null old about + * @param newAbout the non-null new about + */ + void onAboutChange(String oldAbout, String newAbout); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnUserLocaleChanged.java b/src/main/java/it/auties/whatsapp/listener/OnUserLocaleChanged.java new file mode 100644 index 000000000..8918af21b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnUserLocaleChanged.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.model.mobile.CountryLocale; + +public interface OnUserLocaleChanged extends Listener { + /** + * Called when the companion's locale changes + * + * @param oldLocale the non-null old locale + * @param newLocale the non-null new picture + */ + @Override + void onLocaleChanged(CountryLocale oldLocale, CountryLocale newLocale); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnUserNameChanged.java b/src/main/java/it/auties/whatsapp/listener/OnUserNameChanged.java new file mode 100644 index 000000000..72a1127bc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnUserNameChanged.java @@ -0,0 +1,12 @@ +package it.auties.whatsapp.listener; + +public interface OnUserNameChanged extends Listener { + /** + * Called when the companion's name changes + * + * @param oldName the non-null old name + * @param newName the non-null new name + */ + @Override + void onNameChanged(String oldName, String newName); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappAboutChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappAboutChanged.java new file mode 100644 index 000000000..8b3e136fc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappAboutChanged.java @@ -0,0 +1,14 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappAboutChanged extends Listener { + /** + * Called when the companion's status changes + * + * @param whatsapp an instance to the calling api + * @param oldAbout the non-null old about + * @param newAbout the non-null new about + */ + void onAboutChange(Whatsapp whatsapp, String oldAbout, String newAbout); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappAction.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappAction.java new file mode 100644 index 000000000..eddeaf840 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappAction.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.action.Action; +import it.auties.whatsapp.model.info.MessageIndexInfo; + +public interface OnWhatsappAction extends Listener { + /** + * Called when the socket receives a sync from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param action the sync that was executed + * @param messageIndexInfo the data about this action + */ + @Override + void onAction(Whatsapp whatsapp, Action action, MessageIndexInfo messageIndexInfo); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappCall.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappCall.java new file mode 100644 index 000000000..99c97df61 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappCall.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.call.Call; + +public interface OnWhatsappCall extends Listener { + /** + * Called when a phone call arrives + * + * @param whatsapp an instance to the calling api + * @param call the non-null phone call + */ + @Override + void onCall(Whatsapp whatsapp, Call call); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappChatMessagesSync.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappChatMessagesSync.java new file mode 100644 index 000000000..faaaf6224 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappChatMessagesSync.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.chat.Chat; + +public interface OnWhatsappChatMessagesSync extends Listener { + /** + * Called when the socket receives the recent message for a chat + * + * @param whatsapp an instance to the calling api + * @param chat the chat + * @param last whether the messages in this chat are complete or there are more coming + */ + @Override + void onChatMessagesSync(Whatsapp whatsapp, Chat chat, boolean last); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappChats.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappChats.java new file mode 100644 index 000000000..670afdc8c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappChats.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.chat.Chat; + +import java.util.Collection; + +public interface OnWhatsappChats extends Listener { + /** + * Called when the socket receives all the chats from WhatsappWeb's Socket. When this event is + * fired, it is guaranteed that all metadata excluding messages will be present. If you also need + * the messages to be loaded, please refer to {@link Listener#onChatMessagesSync(Chat, boolean)}. + * Particularly old chats may come later through + * {@link Listener#onChatMessagesSync(Chat, boolean)} + * + * @param whatsapp an instance to the calling api + * @param chats the chats + */ + @Override + void onChats(Whatsapp whatsapp, Collection chats); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactBlocked.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactBlocked.java new file mode 100644 index 000000000..767822c06 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactBlocked.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.contact.Contact; + +public interface OnWhatsappContactBlocked extends Listener { + /** + * Called when a contact is blocked or unblocked + * + * @param whatsapp an instance to the calling api + * @param contact the non-null contact + */ + @Override + void onContactBlocked(Whatsapp whatsapp, Contact contact); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPictureChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPictureChanged.java new file mode 100644 index 000000000..39d18f847 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPictureChanged.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.contact.Contact; + +public interface OnWhatsappContactPictureChanged extends Listener { + /** + * Called when a contact's profile picture changes + * + * @param whatsapp an instance to the calling api + * @param contact the contact whose pic changed + */ + @Override + void onProfilePictureChanged(Whatsapp whatsapp, Contact contact); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java new file mode 100644 index 000000000..26f69ec7b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContactPresence.java @@ -0,0 +1,19 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.jid.Jid; + +public interface OnWhatsappContactPresence extends Listener { + /** + * Called when the socket receives an update regarding the presence of a contact + * + * @param whatsapp an instance to the calling api + * @param chat the chat 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, Jid jid, ContactStatus status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappContacts.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContacts.java new file mode 100644 index 000000000..a20b6507e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappContacts.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.listener; + + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.contact.Contact; + +import java.util.Collection; + +public interface OnWhatsappContacts extends Listener { + /** + * Called when the socket receives all the contacts from WhatsappWeb's WebSocket + * + * @param whatsapp an instance to the calling api + * @param contacts the contacts + */ + @Override + void onContacts(Whatsapp whatsapp, Collection contacts); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappDisconnected.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappDisconnected.java new file mode 100644 index 000000000..f17ad7cdd --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappDisconnected.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.DisconnectReason; +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappDisconnected extends Listener { + /** + * Called when the socket successfully disconnects from WhatsappWeb's WebSocket + * + * @param whatsapp an instance to the calling api + * @param reason the errorReason why the session was disconnected + */ + @Override + void onDisconnected(Whatsapp whatsapp, DisconnectReason reason); +} + diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappFeatures.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappFeatures.java new file mode 100644 index 000000000..735bfcaf7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappFeatures.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +import java.util.List; + +public interface OnWhatsappFeatures extends Listener { + /** + * Called when the socket receives new features from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param features the non-null features that were sent + */ + @Override + void onFeatures(Whatsapp whatsapp, List features); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappGroupPictureChange.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappGroupPictureChange.java new file mode 100644 index 000000000..0c7b7c945 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappGroupPictureChange.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.chat.Chat; + +public interface OnWhatsappGroupPictureChange extends Listener { + /** + * Called when a group's picture changes + * + * @param whatsapp an instance to the calling api + * @param group the group whose pic changed + */ + @Override + void onGroupPictureChanged(Whatsapp whatsapp, Chat group); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappHistorySyncProgress.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappHistorySyncProgress.java new file mode 100644 index 000000000..17740b4bf --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappHistorySyncProgress.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappHistorySyncProgress extends Listener { + /** + * Called when the socket receives the sync percentage for the full or recent chunk of messages. + * This method is only called when the QR is first scanned and history is being synced. + * + * @param whatsapp an instance to the calling api + * @param percentage the percentage synced up to now + * @param recent whether the sync is about the recent messages or older messages + */ + @Override + void onHistorySyncProgress(Whatsapp whatsapp, int percentage, boolean recent); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java new file mode 100644 index 000000000..6e11dcfc6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLinkedDevices.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.jid.Jid; + +import java.util.Collection; + +public interface OnWhatsappLinkedDevices extends Listener { + /** + * Called when the list of companion devices is updated + * + * @param whatsapp an instance to the calling api + * @param devices the non-null devices + */ + @Override + void onLinkedDevices(Whatsapp whatsapp, Collection devices); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappLocaleChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLocaleChanged.java new file mode 100644 index 000000000..95dac22a6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLocaleChanged.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.mobile.CountryLocale; + +public interface OnWhatsappLocaleChanged extends Listener { + /** + * Called when the companion's locale changes + * + * @param whatsapp an instance to the calling api + * @param oldLocale the non-null old locale + * @param newLocale the non-null new picture + */ + @Override + void onLocaleChanged(Whatsapp whatsapp, CountryLocale oldLocale, CountryLocale newLocale); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappLoggedIn.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLoggedIn.java new file mode 100644 index 000000000..9500c0d1e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappLoggedIn.java @@ -0,0 +1,19 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +import java.util.Collection; + +public interface OnWhatsappLoggedIn extends Listener { + /** + * Called when the socket successfully establishes a connection and logs in into an account. When + * this event is called, any data, including chats and contact, is not guaranteed to be already in + * memory. Instead, {@link OnChats#onChats(Whatsapp, Collection)} ()} and + * {@link OnContacts#onContacts(Whatsapp, Collection)} ()} should be used. + * + * @param whatsapp an instance to the calling api + */ + @Override + void onLoggedIn(Whatsapp whatsapp); +} + diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappMediaStatus.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMediaStatus.java new file mode 100644 index 000000000..208f1192f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMediaStatus.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.ChatMessageInfo; + +import java.util.Collection; + +public interface OnWhatsappMediaStatus extends Listener { + /** + * Called when the socket receives all the status updated from WhatsappWeb's Socket. + * + * @param whatsapp an instance to the calling api + * @param status the status + */ + void onStatus(Whatsapp whatsapp, Collection status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageDeleted.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageDeleted.java new file mode 100644 index 000000000..cae83c614 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageDeleted.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnWhatsappMessageDeleted extends Listener { + /** + * Called when a message is deleted + * + * @param whatsapp an instance to the calling api + * @param info the message that was deleted + * @param everyone whether this message was deleted by you only for yourself or whether the + * message was permanently removed + */ + @Override + void onMessageDeleted(Whatsapp whatsapp, MessageInfo info, boolean everyone); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageReply.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageReply.java new file mode 100644 index 000000000..b213b16cb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageReply.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.ChatMessageInfo; +import it.auties.whatsapp.model.info.QuotedMessageInfo; + +public interface OnWhatsappMessageReply extends Listener { + /** + * Called when a message answers a previous message + * + * @param whatsapp an instance to the calling api + * @param info the answer message + * @param quoted the quoted message + */ + @Override + void onMessageReply(Whatsapp whatsapp, ChatMessageInfo info, QuotedMessageInfo quoted); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageStatus.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageStatus.java new file mode 100644 index 000000000..3f53759e8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMessageStatus.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnWhatsappMessageStatus extends Listener { + /** + * Called when the status of a message changes + * + * @param whatsapp an instance to the calling api + * @param info the message whose status changed + */ + @Override + void onMessageStatus(Whatsapp whatsapp, MessageInfo info); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappMetadata.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMetadata.java new file mode 100644 index 000000000..5d29a342e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappMetadata.java @@ -0,0 +1,19 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +import java.util.Map; + +public interface OnWhatsappMetadata extends Listener { + /** + * Called when an updated list of properties is received. This method is called both when a + * connection is established with WhatsappWeb and when new props are available. In the latter case + * though, this object should be considered as partial and is guaranteed to contain only updated + * entries. + * + * @param whatsapp an instance to the calling api + * @param metadata the updated list of properties + */ + @Override + void onMetadata(Whatsapp whatsapp, Map metadata); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNameChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNameChanged.java new file mode 100644 index 000000000..aebb7060a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNameChanged.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappNameChanged extends Listener { + /** + * Called when the companion's name changes + * + * @param whatsapp an instance to the calling api + * @param oldName the non-null old name + * @param newName the non-null new name + */ + @Override + void onNameChanged(Whatsapp whatsapp, String oldName, String newName); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMessage.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMessage.java new file mode 100644 index 000000000..7ef360cc2 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewMessage.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.MessageInfo; + +public interface OnWhatsappNewMessage extends Listener { + /** + * Called when a new message is received in a chat + * + * @param whatsapp an instance to the calling api + * @param info the message that was sent + */ + @Override + void onNewMessage(Whatsapp whatsapp, MessageInfo info); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewStatus.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewStatus.java new file mode 100644 index 000000000..a4136044f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewStatus.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.info.ChatMessageInfo; + +public interface OnWhatsappNewStatus extends Listener { + /** + * Called when the socket receives a new status from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param status the new status message + */ + @Override + void onNewStatus(Whatsapp whatsapp, ChatMessageInfo status); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewsletters.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewsletters.java new file mode 100644 index 000000000..e53e1bb60 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNewsletters.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.newsletter.Newsletter; + +import java.util.Collection; + +public interface OnWhatsappNewsletters extends Listener { + /** + * Called when the socket receives all the newsletters from WhatsappWeb's Socket + * + * @param whatsapp an instance to the calling api + * @param newsletters the newsletters + */ + @Override + void onNewsletters(Whatsapp whatsapp, Collection newsletters); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeReceived.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeReceived.java new file mode 100644 index 000000000..b38a1f186 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeReceived.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.node.Node; + +public interface OnWhatsappNodeReceived extends Listener { + /** + * Called when the socket receives a node from Whatsapp + * + * @param whatsapp an instance to the calling api + * @param incoming the non-null node that was just received + */ + @Override + void onNodeReceived(Whatsapp whatsapp, Node incoming); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeSent.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeSent.java new file mode 100644 index 000000000..ca9470919 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappNodeSent.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.node.Node; + +public interface OnWhatsappNodeSent extends Listener { + /** + * Called when the socket sends a node to Whatsapp + * + * @param whatsapp an instance to the calling api + * @param outgoing the non-null node that was just sent + */ + @Override + void onNodeSent(Whatsapp whatsapp, Node outgoing); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappPrivacySettingChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappPrivacySettingChanged.java new file mode 100644 index 000000000..d8ccdffce --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappPrivacySettingChanged.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.privacy.PrivacySettingEntry; + +public interface OnWhatsappPrivacySettingChanged extends Listener { + /** + * Called when a privacy setting is modified + * + * @param whatsapp an instance to the calling api + * @param oldPrivacyEntry the old entry + * @param newPrivacyEntry the new entry + */ + @Override + void onPrivacySettingChanged(Whatsapp whatsapp, PrivacySettingEntry oldPrivacyEntry, PrivacySettingEntry newPrivacyEntry); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappProfilePictureChanged.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappProfilePictureChanged.java new file mode 100644 index 000000000..1c36ab149 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappProfilePictureChanged.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +import java.net.URI; + +public interface OnWhatsappProfilePictureChanged extends Listener { + /** + * Called when the companion's picture changes + * + * @param whatsapp an instance to the calling api + * @param oldPicture the non-null old picture + * @param newPicture the non-null new picture + */ + @Override + void onProfilePictureChanged(Whatsapp whatsapp, URI oldPicture, URI newPicture); +} diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappRegistrationCode.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappRegistrationCode.java new file mode 100644 index 000000000..972929921 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappRegistrationCode.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappRegistrationCode extends Listener { + /** + * Called when an OTP is requested from a new device + * Only works on the mobile API + * + * @param whatsapp an instance to the calling api + * @param code the registration code + */ + @Override + void onRegistrationCode(Whatsapp whatsapp, long code); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappSetting.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappSetting.java new file mode 100644 index 000000000..8263c1f0d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappSetting.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.setting.Setting; + +public interface OnWhatsappSetting extends Listener { + /** + * Called when the socket receives a setting change from Whatsapp. + * + * @param whatsapp an instance to the calling api + * @param setting the setting that was toggled + */ + @Override + void onSetting(Whatsapp whatsapp, Setting setting); +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/listener/OnWhatsappSocketEvent.java b/src/main/java/it/auties/whatsapp/listener/OnWhatsappSocketEvent.java new file mode 100644 index 000000000..2ae6323c8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/OnWhatsappSocketEvent.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.SocketEvent; +import it.auties.whatsapp.api.Whatsapp; + +public interface OnWhatsappSocketEvent extends Listener { + /** + * Called when an event regarding the underlying is fired + * + * @param whatsapp an instance to the calling api + * @param event the event + */ + @Override + void onSocketEvent(Whatsapp whatsapp, SocketEvent event); +} diff --git a/src/main/java/it/auties/whatsapp/listener/RegisterListener.java b/src/main/java/it/auties/whatsapp/listener/RegisterListener.java new file mode 100644 index 000000000..560e5763b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/listener/RegisterListener.java @@ -0,0 +1,20 @@ +package it.auties.whatsapp.listener; + +import it.auties.whatsapp.api.Whatsapp; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to specify that a {@link Listener} should be dected automatically by + * {@link Whatsapp}. For this annotation to be recognized, the target class should implement + * {@link Listener} and provide a no argument constructor. If any of those conditions aren't met, a + * {@link RuntimeException} will be thrown. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface RegisterListener { + +} diff --git a/src/main/java/it/auties/whatsapp/model/action/Action.java b/src/main/java/it/auties/whatsapp/model/action/Action.java new file mode 100644 index 000000000..87612c6ca --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/Action.java @@ -0,0 +1,30 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model interface that represents an action + */ +public sealed interface Action extends ProtobufMessage permits AgentAction, AndroidUnsupportedActions, ArchiveChatAction, ChatAssignmentAction, ChatAssignmentOpenedStatusAction, ClearChatAction, ContactAction, DeleteChatAction, DeleteMessageForMeAction, LabelAssociationAction, LabelEditAction, MarkChatAsReadAction, MuteAction, NuxAction, PinAction, PrimaryVersionAction, QuickReplyAction, RecentEmojiWeightsAction, RemoveRecentStickerAction, StarAction, StickerAction, SubscriptionAction, TimeFormatAction, UserStatusMuteAction { + /** + * The name of this action + * + * @return a non-null string + */ + String indexName(); + + /** + * The version of this action + * + * @return a non-null int + */ + int actionVersion(); + + /** + * The type of this action + * + * @return a non-null type + */ + PatchType actionType(); +} diff --git a/src/main/java/it/auties/whatsapp/model/action/AgentAction.java b/src/main/java/it/auties/whatsapp/model/action/AgentAction.java new file mode 100644 index 000000000..da7ccb37d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/AgentAction.java @@ -0,0 +1,52 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents an agent + */ +@ProtobufMessageName("SyncActionValue.AgentAction") +public record AgentAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Optional name, + @ProtobufProperty(index = 2, type = ProtobufType.INT32) + int deviceId, + @ProtobufProperty(index = 3, type = ProtobufType.BOOL) + boolean deleted +) implements Action { + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "deviceAgent"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/AndroidUnsupportedActions.java b/src/main/java/it/auties/whatsapp/model/action/AndroidUnsupportedActions.java new file mode 100644 index 000000000..0084f7a3b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/AndroidUnsupportedActions.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents unsupported actions for android + */ +@ProtobufMessageName("SyncActionValue.AndroidUnsupportedActions") +public record AndroidUnsupportedActions( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean allowed +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "android_unsupported_actions"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 4; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/ArchiveChatAction.java b/src/main/java/it/auties/whatsapp/model/action/ArchiveChatAction.java new file mode 100644 index 000000000..e71a899a9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/ArchiveChatAction.java @@ -0,0 +1,50 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.ActionMessageRangeSync; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents an archived chat + */ +@ProtobufMessageName("SyncActionValue.ArchiveChatAction") +public record ArchiveChatAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean archived, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional messageRange +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "archive"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 3; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentAction.java b/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentAction.java new file mode 100644 index 000000000..086c43647 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentAction.java @@ -0,0 +1,48 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents the assignment of a chat + */ +@ProtobufMessageName("SyncActionValue.ChatAssignmentAction") +public record ChatAssignmentAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Optional deviceAgentId +) implements Action { + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "agentChatAssignment"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentOpenedStatusAction.java b/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentOpenedStatusAction.java new file mode 100644 index 000000000..602d5365b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/ChatAssignmentOpenedStatusAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents the assignment of a chat as opened + */ +@ProtobufMessageName("SyncActionValue.ChatAssignmentOpenedStatusAction") +public record ChatAssignmentOpenedStatusAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean chatOpened +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "agentChatAssignmentOpenedStatus"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/ClearChatAction.java b/src/main/java/it/auties/whatsapp/model/action/ClearChatAction.java new file mode 100644 index 000000000..cb099d23a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/ClearChatAction.java @@ -0,0 +1,48 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.ActionMessageRangeSync; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents a cleared chat + */ +@ProtobufMessageName("SyncActionValue.ClearChatAction") +public record ClearChatAction( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional messageRange +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "clearChat"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 6; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/ContactAction.java b/src/main/java/it/auties/whatsapp/model/action/ContactAction.java new file mode 100644 index 000000000..ee98adcc3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/ContactAction.java @@ -0,0 +1,60 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents a new contact push name + */ +@ProtobufMessageName("SyncActionValue.ContactAction") +public record ContactAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Optional fullName, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Optional firstName, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + Optional lidJid +) implements Action { + /** + * Returns the name of this contact + * + * @return an optional + */ + public Optional name() { + return fullName.or(this::firstName); + } + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "contact"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 2; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/action/DeleteChatAction.java b/src/main/java/it/auties/whatsapp/model/action/DeleteChatAction.java new file mode 100644 index 000000000..b41fa6920 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/DeleteChatAction.java @@ -0,0 +1,48 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.ActionMessageRangeSync; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents a deleted chat + */ +@ProtobufMessageName("SyncActionValue.DeleteChatAction") +public record DeleteChatAction( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional messageRange +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "deleteChat"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 6; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/DeleteMessageForMeAction.java b/src/main/java/it/auties/whatsapp/model/action/DeleteMessageForMeAction.java new file mode 100644 index 000000000..deead7313 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/DeleteMessageForMeAction.java @@ -0,0 +1,60 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * A model clas that represents a message deleted for this client + */ +@ProtobufMessageName("SyncActionValue.DeleteMessageForMeAction") +public record DeleteMessageForMeAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean deleteMedia, + @ProtobufProperty(index = 2, type = ProtobufType.INT64) + long messageTimestampSeconds +) implements Action { + /** + * Returns when the deleted message was sent + * + * @return an optional + */ + public Optional messageTimestamp() { + return Clock.parseSeconds(messageTimestampSeconds); + } + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "deleteMessageForMe"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 3; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/LabelAssociationAction.java b/src/main/java/it/auties/whatsapp/model/action/LabelAssociationAction.java new file mode 100644 index 000000000..743e343dc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/LabelAssociationAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents a label association + */ +@ProtobufMessageName("SyncActionValue.LabelAssociationAction") +public record LabelAssociationAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean labeled +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "label_message"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 3; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/LabelEditAction.java b/src/main/java/it/auties/whatsapp/model/action/LabelEditAction.java new file mode 100644 index 000000000..4fdefab46 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/LabelEditAction.java @@ -0,0 +1,51 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents an edit to a label + */ +@ProtobufMessageName("SyncActionValue.LabelEditAction") +public record LabelEditAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String name, + @ProtobufProperty(index = 2, type = ProtobufType.INT32) + int color, + @ProtobufProperty(index = 3, type = ProtobufType.INT32) + int id, + @ProtobufProperty(index = 4, type = ProtobufType.BOOL) + boolean deleted +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "label_edit"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 3; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/MarkChatAsReadAction.java b/src/main/java/it/auties/whatsapp/model/action/MarkChatAsReadAction.java new file mode 100644 index 000000000..ff13d80cc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/MarkChatAsReadAction.java @@ -0,0 +1,50 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.ActionMessageRangeSync; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Optional; + +/** + * A model clas that represents a new read status for a chat + */ +@ProtobufMessageName("SyncActionValue.MarkChatAsReadAction") +public record MarkChatAsReadAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean read, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional messageRange +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "markChatAsRead"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 3; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/MuteAction.java b/src/main/java/it/auties/whatsapp/model/action/MuteAction.java new file mode 100644 index 000000000..506365628 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/MuteAction.java @@ -0,0 +1,63 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * A model clas that represents a new mute status for a chat + */ +@ProtobufMessageName("SyncActionValue.MuteAction") +public record MuteAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean muted, + @ProtobufProperty(index = 2, type = ProtobufType.INT64) + OptionalLong muteEndTimestampSeconds, + @ProtobufProperty(index = 3, type = ProtobufType.BOOL) + boolean autoMuted +) implements Action { + /** + * Returns when the mute ends + * + * @return an optional + */ + public Optional muteEndTimestamp() { + return Clock.parseSeconds(muteEndTimestampSeconds.orElse(0)); + } + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "mute"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 2; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/action/NuxAction.java b/src/main/java/it/auties/whatsapp/model/action/NuxAction.java new file mode 100644 index 000000000..d3297d57e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/NuxAction.java @@ -0,0 +1,46 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * Unknown + */ +@ProtobufMessageName("SyncActionValue.NuxAction") +public record NuxAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean acknowledged +) implements Action { + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "nux"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/PinAction.java b/src/main/java/it/auties/whatsapp/model/action/PinAction.java new file mode 100644 index 000000000..7c8193565 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/PinAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents a new pin status for a chat + */ +@ProtobufMessageName("SyncActionValue.PinAction") +public record PinAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean pinned +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "pin_v1"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 5; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/PrimaryVersionAction.java b/src/main/java/it/auties/whatsapp/model/action/PrimaryVersionAction.java new file mode 100644 index 000000000..bb7e1689c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/PrimaryVersionAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model class that contains the main Whatsapp version being used + */ +@ProtobufMessageName("SyncActionValue.PrimaryVersionAction") +public record PrimaryVersionAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String version +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "primary_version"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/QuickReplyAction.java b/src/main/java/it/auties/whatsapp/model/action/QuickReplyAction.java new file mode 100644 index 000000000..c69d53310 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/QuickReplyAction.java @@ -0,0 +1,55 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.List; + +/** + * A model clas that represents the addition or deletion of a quick reply + */ +@ProtobufMessageName("SyncActionValue.QuickReplyAction") +public record QuickReplyAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String shortcut, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String message, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + List keywords, + @ProtobufProperty(index = 4, type = ProtobufType.INT32) + int count, + @ProtobufProperty(index = 5, type = ProtobufType.BOOL) + boolean deleted +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "quick_reply"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 2; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/RecentEmojiWeightsAction.java b/src/main/java/it/auties/whatsapp/model/action/RecentEmojiWeightsAction.java new file mode 100644 index 000000000..918ff52f1 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/RecentEmojiWeightsAction.java @@ -0,0 +1,48 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.model.sync.RecentEmojiWeight; + +import java.util.List; + +/** + * A model clas that represents a change in the weight of recent emojis + */ +@ProtobufMessageName("SyncActionValue.RecentEmojiWeightsAction") +public record RecentEmojiWeightsAction( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + List weights +) implements Action { + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public String indexName() { + throw new UnsupportedOperationException("Cannot send action"); + } + + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public int actionVersion() { + throw new UnsupportedOperationException("Cannot send action"); + } + + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public PatchType actionType() { + throw new UnsupportedOperationException("Cannot send action"); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/RemoveRecentStickerAction.java b/src/main/java/it/auties/whatsapp/model/action/RemoveRecentStickerAction.java new file mode 100644 index 000000000..8dd204ef8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/RemoveRecentStickerAction.java @@ -0,0 +1,58 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * A model class that represents the deletion of a sticker from the recent list + */ +@ProtobufMessageName("SyncActionValue.RemoveRecentStickerAction") +public record RemoveRecentStickerAction( + @ProtobufProperty(index = 1, type = ProtobufType.INT64) + long lastStickerSentTimestampSeconds +) implements Action { + /** + * Returns when the sticker was last sent + * + * @return an optional + */ + public Optional lastStickerSentTimestamp() { + return Clock.parseSeconds(lastStickerSentTimestampSeconds); + } + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "removeRecentSticker"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/StarAction.java b/src/main/java/it/auties/whatsapp/model/action/StarAction.java new file mode 100644 index 000000000..99a857991 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/StarAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents a new star status for a message + */ +@ProtobufMessageName("SyncActionValue.StarAction") +public record StarAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean starred +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "star"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 2; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/StickerAction.java b/src/main/java/it/auties/whatsapp/model/action/StickerAction.java new file mode 100644 index 000000000..fe029beca --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/StickerAction.java @@ -0,0 +1,64 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + + +/** + * A model clas that represents a sticker + */ +@ProtobufMessageName("SyncActionValue.StickerAction") +public record StickerAction( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String url, + @ProtobufProperty(index = 2, type = ProtobufType.BYTES) + byte[] fileEncSha256, + @ProtobufProperty(index = 3, type = ProtobufType.BYTES) + byte[] mediaKey, + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + String mimetype, + @ProtobufProperty(index = 5, type = ProtobufType.UINT32) + int height, + @ProtobufProperty(index = 6, type = ProtobufType.UINT32) + int width, + @ProtobufProperty(index = 7, type = ProtobufType.STRING) + String directPath, + @ProtobufProperty(index = 8, type = ProtobufType.UINT64) + long fileLength, + @ProtobufProperty(index = 9, type = ProtobufType.BOOL) + boolean favorite, + @ProtobufProperty(index = 10, type = ProtobufType.UINT32) + Integer deviceIdHint +) implements Action { + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public String indexName() { + throw new UnsupportedOperationException("Cannot send action"); + } + + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public int actionVersion() { + throw new UnsupportedOperationException("Cannot send action"); + } + + /** + * Always throws an exception as this action cannot be serialized + * + * @return an exception + */ + @Override + public PatchType actionType() { + throw new UnsupportedOperationException("Cannot send action"); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/SubscriptionAction.java b/src/main/java/it/auties/whatsapp/model/action/SubscriptionAction.java new file mode 100644 index 000000000..5b05375c9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/SubscriptionAction.java @@ -0,0 +1,62 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * A model clas that represents a subscription + */ +@ProtobufMessageName("SyncActionValue.SubscriptionAction") +public record SubscriptionAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean deactivated, + @ProtobufProperty(index = 2, type = ProtobufType.BOOL) + boolean autoRenewing, + @ProtobufProperty(index = 3, type = ProtobufType.INT64) + long expirationDateSeconds +) implements Action { + /** + * Returns when the subscription ends + * + * @return an optional + */ + public Optional expirationDate() { + return Clock.parseSeconds(expirationDateSeconds); + } + + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "subscription"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 1; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/TimeFormatAction.java b/src/main/java/it/auties/whatsapp/model/action/TimeFormatAction.java new file mode 100644 index 000000000..7d0def9c7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/TimeFormatAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents the time format used by the companion + */ +@ProtobufMessageName("SyncActionValue.TimeFormatAction") +public record TimeFormatAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean twentyFourHourFormatEnabled +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "time_format"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/action/UserStatusMuteAction.java b/src/main/java/it/auties/whatsapp/model/action/UserStatusMuteAction.java new file mode 100644 index 000000000..8e0a47271 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/action/UserStatusMuteAction.java @@ -0,0 +1,45 @@ +package it.auties.whatsapp.model.action; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.PatchType; + +/** + * A model clas that represents whether a user was muted + */ +@ProtobufMessageName("SyncActionValue.UserStatusMuteAction") +public record UserStatusMuteAction( + @ProtobufProperty(index = 1, type = ProtobufType.BOOL) + boolean muted +) implements Action { + /** + * The name of this action + * + * @return a non-null string + */ + @Override + public String indexName() { + return "userStatusMute"; + } + + /** + * The version of this action + * + * @return a non-null string + */ + @Override + public int actionVersion() { + return 7; + } + + /** + * The type of this action + * + * @return a non-null string + */ + @Override + public PatchType actionType() { + return null; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessAccountPayload.java b/src/main/java/it/auties/whatsapp/model/business/BusinessAccountPayload.java new file mode 100644 index 000000000..bd3e59411 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessAccountPayload.java @@ -0,0 +1,19 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that holds a payload about a business account. + */ +@ProtobufMessageName("BizAccountPayload") +public record BusinessAccountPayload( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + BusinessVerifiedNameCertificate certificate, + @ProtobufProperty(index = 2, type = ProtobufType.BYTES) + byte[] info +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessCatalogEntry.java b/src/main/java/it/auties/whatsapp/model/business/BusinessCatalogEntry.java new file mode 100644 index 000000000..332fc83c9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessCatalogEntry.java @@ -0,0 +1,71 @@ +package it.auties.whatsapp.model.business; + +import it.auties.whatsapp.model.node.Node; + +import java.net.URI; +import java.util.NoSuchElementException; + +/** + * A record class that represents a business catalog entry. + * + * @param id the unique identifier of the catalog entry + * @param encryptedImage the encrypted URL of the original image of the catalog entry + * @param reviewStatus the review status of the catalog entry + * @param availability the availability status of the catalog entry + * @param name the name of the catalog entry + * @param sellerId the unique identifier of the seller of the catalog entry + * @param uri the URI of the catalog entry + * @param description the description of the catalog entry + * @param price the price of the catalog entry + * @param currency the currency of the price of the catalog entry + * @param hidden whether the catalog entry is hidden or not + */ +public record BusinessCatalogEntry(String id, URI encryptedImage, BusinessReviewStatus reviewStatus, + BusinessItemAvailability availability, String name, String sellerId, + URI uri, String description, long price, String currency, + boolean hidden) { + /** + * A factory method that creates a BusinessCatalogEntry object from a given Node. + * + * @param node the node to get the data from + * @return a BusinessCatalogEntry object + * @throws NoSuchElementException if some required data is missing + */ + public static BusinessCatalogEntry of(Node node) { + var id = node.attributes().getRequiredString("id"); + var hidden = node.attributes().getBoolean("is_hidden"); + var name = node.findNode("name") + .flatMap(Node::contentAsString) + .orElseThrow(() -> new NoSuchElementException("Missing name for catalog entry")); + var encryptedImage = node.findNode("media") + .flatMap(entry -> entry.findNode("original_image_url")) + .flatMap(Node::contentAsString) + .map(URI::create) + .orElseThrow(() -> new NoSuchElementException("Missing image for catalog entry")); + var statusInfo = node.findNode("status_info") + .flatMap(entry -> entry.findNode("status")) + .flatMap(Node::contentAsString) + .map(BusinessReviewStatus::of) + .orElse(BusinessReviewStatus.NO_REVIEW); + var availability = node.findNode("availability") + .flatMap(Node::contentAsString) + .map(BusinessItemAvailability::of) + .orElse(BusinessItemAvailability.UNKNOWN); + var sellerId = node.findNode("retailer_id") + .flatMap(Node::contentAsString) + .orElseThrow(() -> new NoSuchElementException("Missing seller id for catalog entry")); + var uri = node.findNode("url") + .flatMap(Node::contentAsString) + .map(URI::create) + .orElseThrow(() -> new NoSuchElementException("Missing uri for catalog entry")); + var description = node.findNode("description").flatMap(Node::contentAsString).orElse(""); + var price = node.findNode("price") + .flatMap(Node::contentAsString) + .map(Long::parseUnsignedLong) + .orElseThrow(() -> new NoSuchElementException("Missing price for catalog entry")); + var currency = node.findNode("currency") + .flatMap(Node::contentAsString) + .orElseThrow(() -> new NoSuchElementException("Missing currency for catalog entry")); + return new BusinessCatalogEntry(id, encryptedImage, statusInfo, availability, name, sellerId, uri, description, price, currency, hidden); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessCategory.java b/src/main/java/it/auties/whatsapp/model/business/BusinessCategory.java new file mode 100644 index 000000000..7b46ae062 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessCategory.java @@ -0,0 +1,34 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.node.Node; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +/** + * A model class that represents a business category + * + * @param id the non-null id + * @param name the non-null display name + */ +public record BusinessCategory( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String id, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String name +) implements ProtobufMessage { + /** + * Constructs a category from a node + * + * @param node a non-null node + * @return a non-null category + */ + public static BusinessCategory of(Node node) { + var id = node.attributes().getRequiredString("id"); + var name = URLDecoder.decode(node.contentAsString().orElseThrow(), StandardCharsets.UTF_8); + return new BusinessCategory(id, name); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessCollectionEntry.java b/src/main/java/it/auties/whatsapp/model/business/BusinessCollectionEntry.java new file mode 100644 index 000000000..ed32e08e7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessCollectionEntry.java @@ -0,0 +1,35 @@ +package it.auties.whatsapp.model.business; + +import it.auties.whatsapp.model.node.Node; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Record class representing a business collection entry. + * + * @param id the id of the business collection + * @param name the name of the business collection + * @param products the list of products in the business collection + */ +public record BusinessCollectionEntry(String id, String name, + List products) { + /** + * Creates a {@code BusinessCollectionEntry} object from a {@code Node} object. + * + * @param node the node representing the business collection entry + * @return the created {@code BusinessCollectionEntry} object + * @throws NoSuchElementException if the id or name of the business collection is missing from the + * node + */ + public static BusinessCollectionEntry of(Node node) { + var id = node.findNode("id") + .flatMap(Node::contentAsString) + .orElseThrow(() -> new NoSuchElementException("Missing id from business collections")); + var name = node.findNode("name") + .flatMap(Node::contentAsString) + .orElseThrow(() -> new NoSuchElementException("Missing name from business collections")); + var products = node.findNodes("product").stream().map(BusinessCatalogEntry::of).toList(); + return new BusinessCollectionEntry(id, name, products); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessHours.java b/src/main/java/it/auties/whatsapp/model/business/BusinessHours.java new file mode 100644 index 000000000..b584f0828 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessHours.java @@ -0,0 +1,16 @@ +package it.auties.whatsapp.model.business; + + +import java.util.List; + +/** + * A business hours representation that contains the business' time zone and a list of business hour + * entries. + * + * @param timeZone The time zone of the business. + * @param entries A list of business hours entries that contains information about the hours of + * operation for each day of the week. + */ +public record BusinessHours(String timeZone, List entries) { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessHoursEntry.java b/src/main/java/it/auties/whatsapp/model/business/BusinessHoursEntry.java new file mode 100644 index 000000000..883869713 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessHoursEntry.java @@ -0,0 +1,37 @@ +package it.auties.whatsapp.model.business; + +import it.auties.whatsapp.model.node.Node; + +/** + * A business hours entry that represents the hours of operation for a single day of the week. + * + * @param day The day of the week that this entry represents. + * @param mode The mode of operation for this day. + * @param openTime The time in seconds since midnight that the business opens. + * @param closeTime The time in seconds since midnight that the business closes. + */ +public record BusinessHoursEntry(String day, String mode, long openTime, long closeTime) { + /** + * Creates a {@link BusinessHoursEntry} from a {@link Node}. + * + * @param node The node to extract the business hours entry information from. + * @return A {@link BusinessHoursEntry} extracted from the provided node. + */ + public static BusinessHoursEntry of(Node node) { + return new BusinessHoursEntry( + node.attributes().getString("day_of_week"), + node.attributes().getString("mode"), + node.attributes().getLong("open_time"), + node.attributes().getLong("close_time") + ); + } + + /** + * Returns whether the business is always open. + * + * @return whether the business is always open + */ + public boolean isAlwaysOpen() { + return openTime == 0 && closeTime == 0; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessItemAvailability.java b/src/main/java/it/auties/whatsapp/model/business/BusinessItemAvailability.java new file mode 100644 index 000000000..129e4e07b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessItemAvailability.java @@ -0,0 +1,35 @@ +package it.auties.whatsapp.model.business; + +import java.util.Arrays; +import java.util.Locale; + +/** + * An enumeration of possible Availabilities. + */ +public enum BusinessItemAvailability { + /** + * Indicates an unknown availability. + */ + UNKNOWN, + /** + * Indicates that the item is in stock. + */ + IN_STOCK, + /** + * Indicates that the item is out of stock. + */ + OUT_OF_STOCK; + + /** + * Returns an Availability based on the given name. + * + * @param name the name of the Availability + * @return an Availability + */ + public static BusinessItemAvailability of(String name) { + return Arrays.stream(values()) + .filter(entry -> entry.name().toLowerCase(Locale.ROOT).replaceAll("_", " ").equals(name)) + .findFirst() + .orElse(UNKNOWN); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessLocalizedName.java b/src/main/java/it/auties/whatsapp/model/business/BusinessLocalizedName.java new file mode 100644 index 000000000..3d4b068c5 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessLocalizedName.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a time a localizable name + */ +public record BusinessLocalizedName( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String lg, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String lc, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + String name +) implements ProtobufMessage { +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessPrivacyStatus.java b/src/main/java/it/auties/whatsapp/model/business/BusinessPrivacyStatus.java new file mode 100644 index 000000000..5aa07be3a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessPrivacyStatus.java @@ -0,0 +1,36 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +/** + * The constants of this enumerated type describe the various types of business privacy + */ +public enum BusinessPrivacyStatus implements ProtobufEnum { + /** + * End-to-end encryption + */ + E2EE(0), + /** + * Bsp encryption + */ + BSP(1), + /** + * Facebook encryption + */ + FACEBOOK(2), + /** + * Facebook and bsp encryption + */ + BSP_AND_FB(3); + + final int index; + + BusinessPrivacyStatus(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java b/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java new file mode 100644 index 000000000..e9e2150b7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessProfile.java @@ -0,0 +1,75 @@ +package it.auties.whatsapp.model.business; + +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.node.Node; + +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * This model class represents the metadata of a business profile + */ +public record BusinessProfile( + Jid jid, + Optional description, + Optional address, + Optional email, + Optional hours, + boolean cartEnabled, + List websites, + List categories +) { + /** + * Constructs a new profile from a node + * + * @param node a non-null node + * @return a non-null profile + */ + public static BusinessProfile of(Node node) { + var jid = node.attributes() + .getRequiredJid("jid"); + var address = node.findNode("address") + .flatMap(Node::contentAsString); + var description = node.findNode("description") + .flatMap(Node::contentAsString); + var websites = node.findNodes("website") + .stream() + .map(Node::contentAsString) + .flatMap(Optional::stream) + .map(URI::create) + .toList(); + var email = node.findNode("email") + .flatMap(Node::contentAsString); + var categories = node.findNodes("categories") + .stream() + .map(entry -> entry.findNode("category")) + .flatMap(Optional::stream) + .map(BusinessCategory::of) + .toList(); + var commerceExperience = node.findNode("profile_options"); + var cartEnabled = commerceExperience.flatMap(entry -> entry.findNode("cart_enabled")) + .flatMap(Node::contentAsBoolean) + .orElse(commerceExperience.isEmpty()); + var hours = createHours(node); + return new BusinessProfile(jid, description, address, email, hours, cartEnabled, websites, categories); + } + + private static Optional createHours(Node node) { + var timezone = node.findNode("business_hours") + .map(Node::attributes) + .map(attributes -> attributes.getNullableString("timezone")); + if (timezone.isEmpty()) { + return Optional.empty(); + } + + var entries = node.findNode("business_hours") + .stream() + .map(entry -> entry.findNodes("business_hours_config")) + .flatMap(Collection::stream) + .map(BusinessHoursEntry::of) + .toList(); + return Optional.of(new BusinessHours(timezone.get(), entries)); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessReviewStatus.java b/src/main/java/it/auties/whatsapp/model/business/BusinessReviewStatus.java new file mode 100644 index 000000000..bedd75874 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessReviewStatus.java @@ -0,0 +1,39 @@ +package it.auties.whatsapp.model.business; + +import java.util.Locale; + +/** + * An enumeration of possible ReviewStatuses. + */ +public enum BusinessReviewStatus { + /** + * Indicates that no review has been performed. + */ + NO_REVIEW, + /** + * Indicates that the review is pending. + */ + PENDING, + /** + * Indicates that the review was rejected. + */ + REJECTED, + /** + * Indicates that the review was approved. + */ + APPROVED, + /** + * Indicates that the review is outdated. + */ + OUTDATED; + + /** + * Returns a ReviewStatus based on the given name. + * + * @param name the name of the ReviewStatus + * @return a ReviewStatus + */ + public static BusinessReviewStatus of(String name) { + return valueOf(name.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameCertificate.java b/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameCertificate.java new file mode 100644 index 000000000..8343e251e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameCertificate.java @@ -0,0 +1,23 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a business certificate + */ +@ProtobufMessageName("VerifiedNameCertificate") +public record BusinessVerifiedNameCertificate( + @ProtobufProperty(index = 1, type = ProtobufType.BYTES) + byte[] encodedDetails, + @ProtobufProperty(index = 2, type = ProtobufType.BYTES) + byte[] signature, + @ProtobufProperty(index = 3, type = ProtobufType.BYTES) + byte[] serverSignature +) implements ProtobufMessage { + public BusinessVerifiedNameDetails details() { + return BusinessVerifiedNameDetailsSpec.decode(encodedDetails); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameDetails.java b/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameDetails.java new file mode 100644 index 000000000..5720447c4 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/business/BusinessVerifiedNameDetails.java @@ -0,0 +1,38 @@ +package it.auties.whatsapp.model.business; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; + + +/** + * A model class that represents a verified name + */ +@ProtobufMessageName("VerifiedNameCertificate.Details") +public record BusinessVerifiedNameDetails( + @ProtobufProperty(index = 1, type = ProtobufType.UINT64) + long serial, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String issuer, + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + String name, + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + List localizedNames, + @ProtobufProperty(index = 10, type = ProtobufType.UINT64) + long issueTimeSeconds +) implements ProtobufMessage { + /** + * Returns this object's timestampSeconds + * + * @return an optional + */ + public Optional issueTime() { + return Clock.parseSeconds(issueTimeSeconds); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/base/Button.java b/src/main/java/it/auties/whatsapp/model/button/base/Button.java new file mode 100644 index 000000000..f0eb257e7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/base/Button.java @@ -0,0 +1,66 @@ +package it.auties.whatsapp.model.button.base; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.base.ButtonBody.Type; +import it.auties.whatsapp.model.info.NativeFlowInfo; +import it.auties.whatsapp.util.BytesHelper; + +import java.util.HexFormat; +import java.util.Optional; + +/** + * A model class that represents a button + */ +@ProtobufMessageName("Message.ButtonsMessage.Button") +public record Button( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String id, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional bodyText, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Optional bodyNativeFlow, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Type bodyType +) implements ProtobufMessage { + /** + * Constructs a new button + * + * @param body the body of this button + * @return a non-null button + */ + public static Button of(ButtonBody body) { + var id = HexFormat.of().formatHex(BytesHelper.random(6)); + return Button.of(id, body); + } + + /** + * Constructs a new button + * + * @param id the non-null id of the button + * @param body the body of this button + * @return a non-null button + */ + public static Button of(String id, ButtonBody body) { + var builder = new ButtonBuilder() + .id(id); + switch (body) { + case ButtonText buttonText -> builder.bodyText(buttonText).bodyType(Type.TEXT); + case NativeFlowInfo flowInfo -> builder.bodyNativeFlow(flowInfo).bodyType(Type.NATIVE_FLOW); + case null -> builder.bodyType(Type.UNKNOWN); + } + return builder.build(); + } + + /** + * Returns the body of this button + * + * @return an optional + */ + public Optional body() { + return bodyText.isPresent() ? bodyText : bodyNativeFlow; + } + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/base/ButtonActionLink.java b/src/main/java/it/auties/whatsapp/model/button/base/ButtonActionLink.java new file mode 100644 index 000000000..3f2e0d727 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/base/ButtonActionLink.java @@ -0,0 +1,19 @@ +package it.auties.whatsapp.model.button.base; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * An action link for a button + */ +@ProtobufMessageName("ActionLink") +public record ButtonActionLink( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String url, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String buttonTitle +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/base/ButtonBody.java b/src/main/java/it/auties/whatsapp/model/button/base/ButtonBody.java new file mode 100644 index 000000000..3262ab52f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/base/ButtonBody.java @@ -0,0 +1,34 @@ +package it.auties.whatsapp.model.button.base; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.info.NativeFlowInfo; + +/** + * A model that represents the body of a button + */ +public sealed interface ButtonBody extends ProtobufMessage permits ButtonText, NativeFlowInfo { + /** + * Returns the type of this body + * + * @return a non-null type + */ + Type bodyType(); + + enum Type implements ProtobufEnum { + UNKNOWN(0), + TEXT(1), + NATIVE_FLOW(2); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/base/ButtonText.java b/src/main/java/it/auties/whatsapp/model/button/base/ButtonText.java new file mode 100644 index 000000000..3d5347fad --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/base/ButtonText.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.model.button.base; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents the text of a button + */ +public record ButtonText( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String content +) implements ButtonBody { + @Override + public Type bodyType() { + return Type.TEXT; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveBody.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveBody.java new file mode 100644 index 000000000..254e87607 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveBody.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents the body of a product + */ +@ProtobufMessageName("Message.InteractiveMessage.Body") +public record InteractiveBody( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String content +) implements ProtobufMessage { + public static Optional ofNullable(String content) { + return Optional.ofNullable(content) + .map(InteractiveBody::new); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveButton.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveButton.java new file mode 100644 index 000000000..8504b59d8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveButton.java @@ -0,0 +1,23 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents a native flow button + */ +@ProtobufMessageName("Message.InteractiveMessage.NativeFlowMessage.NativeFlowButton") +public record InteractiveButton( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String name, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Optional parameters +) implements ProtobufMessage { + public InteractiveButton(String name) { + this(name, Optional.empty()); + } +} 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 new file mode 100644 index 000000000..87ebfdc25 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveCollection.java @@ -0,0 +1,27 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.button.InteractiveMessageContent; + +/** + * A model class that represents a business collection + */ +@ProtobufMessageName("Message.InteractiveMessage.CollectionMessage") +public record InteractiveCollection( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid business, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String id, + @ProtobufProperty(index = 3, type = ProtobufType.INT32) + int version +) implements InteractiveMessageContent { + + + @Override + public Type contentType() { + return Type.COLLECTION; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveFooter.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveFooter.java new file mode 100644 index 000000000..862d56e27 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveFooter.java @@ -0,0 +1,23 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents the footer of a product + */ +@ProtobufMessageName("Message.InteractiveMessage.Footer") +public record InteractiveFooter( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String content +) implements ProtobufMessage { + + public static Optional ofNullable(String content) { + return Optional.ofNullable(content) + .map(InteractiveFooter::new); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeader.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeader.java new file mode 100644 index 000000000..37d853791 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeader.java @@ -0,0 +1,84 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufBuilder; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +import java.util.Optional; + + +/** + * A model class that represents the header of a product + */ +@ProtobufMessageName("Message.InteractiveMessage.Header") +public record InteractiveHeader( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String title, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Optional subtitle, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional attachmentDocument, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Optional attachmentImage, + @ProtobufProperty(index = 5, type = ProtobufType.BOOL) + boolean mediaAttachment, + @ProtobufProperty(index = 6, type = ProtobufType.BYTES) + Optional attachmentThumbnail, + @ProtobufProperty(index = 7, type = ProtobufType.OBJECT) + Optional attachmentVideo +) implements ProtobufMessage { + @ProtobufBuilder(className = "InteractiveHeaderSimpleBuilder") + static InteractiveHeader simpleBuilder(String title, String subtitle, InteractiveHeaderAttachment attachment) { + var builder = new InteractiveHeaderBuilder() + .title(title) + .subtitle(subtitle); + switch (attachment) { + case DocumentMessage documentMessage -> builder.attachmentDocument(documentMessage); + case ImageMessage imageMessage -> builder.attachmentImage(imageMessage); + case InteractiveHeaderThumbnail productHeaderThumbnail -> + builder.attachmentThumbnail(productHeaderThumbnail); + case VideoOrGifMessage videoMessage -> builder.attachmentVideo(videoMessage); + case null -> { + } + } + builder.mediaAttachment(attachment != null); + return builder.build(); + } + + /** + * Returns the type of attachment of this message + * + * @return a non-null attachment type + */ + public InteractiveHeaderAttachment.Type attachmentType() { + return attachment() + .map(InteractiveHeaderAttachment::interactiveHeaderType) + .orElse(InteractiveHeaderAttachment.Type.NONE); + } + + /** + * Returns the attachment of this message if present + * + * @return a non-null attachment type + */ + public Optional attachment() { + if (attachmentDocument.isPresent()) { + return attachmentDocument; + } + + if (attachmentImage.isPresent()) { + return attachmentImage; + } + + if (attachmentThumbnail.isPresent()) { + return attachmentThumbnail; + } + + return attachmentVideo; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderAttachment.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderAttachment.java new file mode 100644 index 000000000..703db8df5 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderAttachment.java @@ -0,0 +1,51 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +/** + * A sealed class that describes the various types of headers + */ +public sealed interface InteractiveHeaderAttachment permits DocumentMessage, ImageMessage, InteractiveHeaderThumbnail, VideoOrGifMessage { + Type interactiveHeaderType(); + + /** + * The constants of this enumerated type describe the various types of attachment that a product + * header can have + */ + enum Type implements ProtobufEnum { + /** + * No attachment + */ + NONE(0), + /** + * Document message + */ + DOCUMENT(3), + /** + * Image attachment + */ + IMAGE(4), + /** + * Jpeg attachment + */ + THUMBNAIL(6), + /** + * Video attachment + */ + VIDEO(7); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderThumbnail.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderThumbnail.java new file mode 100644 index 000000000..9064d1695 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveHeaderThumbnail.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufConverter; + +/** + * A model that represents the jpeg thumbnail of a {@link InteractiveHeader} + * + * @param thumbnail the non-null jpeg thumbnail + */ +public record InteractiveHeaderThumbnail(byte[] thumbnail) implements InteractiveHeaderAttachment { + @ProtobufConverter + public static InteractiveHeaderThumbnail of(byte[] thumbnail) { + return new InteractiveHeaderThumbnail(thumbnail); + } + + @ProtobufConverter + @Override + public byte[] thumbnail() { + return thumbnail; + } + + @Override + public Type interactiveHeaderType() { + return Type.THUMBNAIL; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveLocationAnnotation.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveLocationAnnotation.java new file mode 100644 index 000000000..935a9d1d1 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveLocationAnnotation.java @@ -0,0 +1,55 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.List; + +/** + * A model class that describes an interactive annotation linked to a message + */ +@ProtobufMessageName("InteractiveAnnotation") +public record InteractiveLocationAnnotation( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + List polygonVertices, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + InterativeLocation location +) implements ProtobufMessage { + /** + * Returns the type of sync + * + * @return a non-null Action + */ + public Action type() { + return location != null ? Action.LOCATION : Action.UNKNOWN; + } + + /** + * The constants of this enumerated type describe the various types of sync that an interactive + * annotation can provide + */ + public enum Action implements ProtobufEnum { + /** + * Unknown + */ + UNKNOWN(0), + /** + * Location + */ + LOCATION(2); + + final int index; + + Action(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveNativeFlow.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveNativeFlow.java new file mode 100644 index 000000000..4ff9296a6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveNativeFlow.java @@ -0,0 +1,28 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.message.button.InteractiveMessageContent; + +import java.util.List; + + +/** + * A model class that represents a native flow + * Here> is an explanation on how to use this kind of message + */ +@ProtobufMessageName("Message.InteractiveMessage.NativeFlowMessage") +public record InteractiveNativeFlow( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + List buttons, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String parameters, + @ProtobufProperty(index = 3, type = ProtobufType.INT32) + int version +) implements InteractiveMessageContent { + @Override + public Type contentType() { + return Type.NATIVE_FLOW; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractivePoint.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractivePoint.java new file mode 100644 index 000000000..accd9f006 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractivePoint.java @@ -0,0 +1,25 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * This model class describes a Point in space + */ +@ProtobufMessageName("Point") +public record InteractivePoint( + @ProtobufProperty(index = 1, type = ProtobufType.INT32) + @Deprecated + int xDeprecated, + @ProtobufProperty(index = 2, type = ProtobufType.INT32) + @Deprecated + int yDeprecated, + @ProtobufProperty(index = 3, type = ProtobufType.DOUBLE) + double x, + @ProtobufProperty(index = 4, type = ProtobufType.DOUBLE) + double y +) implements ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveResponseBody.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveResponseBody.java new file mode 100644 index 000000000..b1f81d65f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveResponseBody.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents the body of a product + */ +@ProtobufMessageName("Message.InteractiveResponseMessage.Body") +public record InteractiveResponseBody( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String content +) implements ProtobufMessage { + public static Optional ofNullable(String content) { + return Optional.ofNullable(content) + .map(InteractiveResponseBody::new); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveShop.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveShop.java new file mode 100644 index 000000000..bac46fb0f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InteractiveShop.java @@ -0,0 +1,61 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.message.button.InteractiveMessageContent; + + +/** + * A model class that represents a shop + */ +@ProtobufMessageName("Message.InteractiveMessage.ShopMessage") +public record InteractiveShop( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String id, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + SurfaceType surfaceType, + @ProtobufProperty(index = 3, type = ProtobufType.INT32) + int version +) implements InteractiveMessageContent { + @Override + public Type contentType() { + return Type.SHOP; + } + + /** + * The constants of this enumerated type describe the various types of surfaces that a + * {@link InteractiveShop} can have + */ + @ProtobufMessageName("Message.InteractiveMessage.ShopMessage.Surface") + public enum SurfaceType implements ProtobufEnum { + /** + * Unknown + */ + UNKNOWN_SURFACE(0), + /** + * Facebook + */ + FACEBOOK(1), + /** + * Instagram + */ + INSTAGRAM(2), + /** + * Whatsapp + */ + WHATSAPP(3); + + final int index; + + SurfaceType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/interactive/InterativeLocation.java b/src/main/java/it/auties/whatsapp/model/button/interactive/InterativeLocation.java new file mode 100644 index 000000000..f9516393a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/interactive/InterativeLocation.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.interactive; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * This model class describes a Location + */ +@ProtobufMessageName("Location") +public record InterativeLocation( + @ProtobufProperty(index = 1, type = ProtobufType.DOUBLE) + double latitude, + @ProtobufProperty(index = 2, type = ProtobufType.DOUBLE) + double longitude, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + String name +) implements ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/button/misc/ButtonOpaqueData.java b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonOpaqueData.java new file mode 100644 index 000000000..5194eee08 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonOpaqueData.java @@ -0,0 +1,71 @@ +package it.auties.whatsapp.model.button.misc; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.poll.PollOption; +import it.auties.whatsapp.model.poll.PollUpdateEncryptedMetadata; + +import java.util.List; +import java.util.Optional; + + +/** + * A model class that represents data about a button + */ +@ProtobufMessageName("MsgOpaqueData") +public record ButtonOpaqueData( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Optional body, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + Optional caption, + @ProtobufProperty(index = 5, type = ProtobufType.DOUBLE) + double longitude, + @ProtobufProperty(index = 7, type = ProtobufType.DOUBLE) + double latitude, + @ProtobufProperty(index = 8, type = ProtobufType.INT32) + int paymentAmount1000, + @ProtobufProperty(index = 9, type = ProtobufType.STRING) + Optional paymentNote, + @ProtobufProperty(index = 10, type = ProtobufType.STRING) + Optional canonicalUrl, + @ProtobufProperty(index = 11, type = ProtobufType.STRING) + Optional matchedText, + @ProtobufProperty(index = 12, type = ProtobufType.STRING) + Optional title, + @ProtobufProperty(index = 13, type = ProtobufType.STRING) + Optional description, + @ProtobufProperty(index = 6, type = ProtobufType.BOOL) + boolean isLive, + @ProtobufProperty(index = 14, type = ProtobufType.BYTES) + Optional futureProofBuffer, + @ProtobufProperty(index = 15, type = ProtobufType.STRING) + Optional clientUrl, + @ProtobufProperty(index = 16, type = ProtobufType.STRING) + Optional loc, + @ProtobufProperty(index = 17, type = ProtobufType.STRING) + Optional pollName, + @ProtobufProperty(index = 18, type = ProtobufType.OBJECT) + List pollOptions, + @ProtobufProperty(index = 20, type = ProtobufType.UINT32) + int pollSelectableOptionsCount, + @ProtobufProperty(index = 21, type = ProtobufType.BYTES) + Optional messageSecret, + @ProtobufProperty(index = 51, type = ProtobufType.STRING) + Optional originalSelfAuthor, + @ProtobufProperty(index = 22, type = ProtobufType.INT64) + long senderTimestampMs, + @ProtobufProperty(index = 23, type = ProtobufType.STRING) + Optional pollUpdateParentKey, + @ProtobufProperty(index = 24, type = ProtobufType.OBJECT) + Optional encPollVote, + @ProtobufProperty(index = 25, type = ProtobufType.STRING) + Optional encReactionTargetMessageKey, + @ProtobufProperty(index = 26, type = ProtobufType.BYTES) + Optional encReactionEncPayload, + @ProtobufProperty(index = 27, type = ProtobufType.BYTES) + Optional encReactionEncIv +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRow.java b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRow.java new file mode 100644 index 000000000..66263db96 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRow.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.model.button.misc; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.BytesHelper; + +import java.util.HexFormat; + +/** + * A model class that represents a row of buttons + */ +@ProtobufMessageName("Message.ListMessage.Row") +public record ButtonRow( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String title, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String description, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + String id +) implements ProtobufMessage { + public static ButtonRow of(String title, String description) { + return new ButtonRow(title, description, HexFormat.of().formatHex(BytesHelper.random(5))); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRowOpaqueData.java b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRowOpaqueData.java new file mode 100644 index 000000000..70ea6c85d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonRowOpaqueData.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.misc; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents data about a row + */ +@ProtobufMessageName("MsgRowOpaqueData") +public record ButtonRowOpaqueData( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional currentMessage, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional quotedMessage +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/misc/ButtonSection.java b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonSection.java new file mode 100644 index 000000000..f783d710d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/misc/ButtonSection.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.misc; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.List; + +/** + * A model class that represents a section of buttons + */ +@ProtobufMessageName("Message.ListMessage.Section") +public record ButtonSection( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String title, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + List rows +) implements ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/button/misc/SingleSelectReplyButton.java b/src/main/java/it/auties/whatsapp/model/button/misc/SingleSelectReplyButton.java new file mode 100644 index 000000000..2d705957f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/misc/SingleSelectReplyButton.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.model.button.misc; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents the selection of a row + */ +@ProtobufMessageName("Message.ListResponseMessage.SingleSelectReply") +public record SingleSelectReplyButton( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String rowId +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/TemplateFormatter.java b/src/main/java/it/auties/whatsapp/model/button/template/TemplateFormatter.java new file mode 100644 index 000000000..7e7eb646e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/TemplateFormatter.java @@ -0,0 +1,54 @@ +package it.auties.whatsapp.model.button.template; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplate; +import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplate; +import it.auties.whatsapp.model.message.button.InteractiveMessage; +import it.auties.whatsapp.model.message.button.TemplateMessage; + +/** + * A formatter used to structure a button message + */ +public sealed interface TemplateFormatter extends ProtobufMessage permits HighlyStructuredFourRowTemplate, HydratedFourRowTemplate, InteractiveMessage { + /** + * Returns the type of this formatter + * + * @return a non-null type + */ + Type templateType(); + + /** + * The constant of this enumerated type define the various of types of visual formats for a + * {@link TemplateMessage} + */ + enum Type implements ProtobufEnum { + /** + * No format + */ + NONE(0), + /** + * Four row template + */ + FOUR_ROW(1), + /** + * Hydrated four row template + */ + HYDRATED_FOUR_ROW(2), + /** + * Interactive message + */ + INTERACTIVE(3); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return this.index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredCurrency.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredCurrency.java new file mode 100644 index 000000000..58b5972d3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredCurrency.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a currency + */ +@ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMCurrency") +public record HighlyStructuredCurrency( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String currencyCode, + @ProtobufProperty(index = 2, type = ProtobufType.INT64) + long amount1000 +) implements HighlyStructuredLocalizableParameterValue { + @Override + public HighlyStructuredLocalizableParameterValue.Type parameterType() { + return HighlyStructuredLocalizableParameterValue.Type.CURRENCY; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTime.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTime.java new file mode 100644 index 000000000..1e36c9cd0 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTime.java @@ -0,0 +1,58 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents a time + */ +@ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime") +public record HighlyStructuredDateTime( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional dateComponent, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional dateUnixEpoch +) implements HighlyStructuredLocalizableParameterValue { + /** + * Constructs a new date time using a component + * + * @param dateComponent the non-null component + * @return a non-null date time + */ + public static HighlyStructuredDateTime of(HighlyStructuredDateTimeValue dateComponent) { + if (dateComponent instanceof HighlyStructuredDateTimeComponent highlyStructuredDateTimeComponent) { + return new HighlyStructuredDateTime(Optional.of(highlyStructuredDateTimeComponent), Optional.empty()); + } else if (dateComponent instanceof HighlyStructuredDateTimeUnixEpoch highlyStructuredDateTimeUnixEpoch) { + return new HighlyStructuredDateTime(Optional.empty(), Optional.of(highlyStructuredDateTimeUnixEpoch)); + } else { + return new HighlyStructuredDateTime(Optional.empty(), Optional.empty()); + } + } + + /** + * Returns the type of date of this component + * + * @return a non-null date type + */ + public HighlyStructuredDateTimeValue.Type dateType() { + return date().map(HighlyStructuredDateTimeValue::dateType) + .orElse(HighlyStructuredDateTimeValue.Type.NONE); + } + + /** + * Returns the date of this component + * + * @return a non-null date type + */ + public Optional date() { + return dateComponent.isPresent() ? dateComponent : dateUnixEpoch; + } + + @Override + public HighlyStructuredLocalizableParameterValue.Type parameterType() { + return HighlyStructuredLocalizableParameterValue.Type.DATE_TIME; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeComponent.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeComponent.java new file mode 100644 index 000000000..2647325dc --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeComponent.java @@ -0,0 +1,103 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a time component + */ +@ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent") +public record HighlyStructuredDateTimeComponent( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + DayOfWeek dayOfWeek, + @ProtobufProperty(index = 2, type = ProtobufType.UINT32) + int year, + @ProtobufProperty(index = 3, type = ProtobufType.UINT32) + int month, + @ProtobufProperty(index = 4, type = ProtobufType.UINT32) + int dayOfMonth, + @ProtobufProperty(index = 5, type = ProtobufType.UINT32) + int hour, + @ProtobufProperty(index = 6, type = ProtobufType.UINT32) + int minute, + @ProtobufProperty(index = 7, type = ProtobufType.OBJECT) + CalendarType calendar +) implements HighlyStructuredDateTimeValue { + @Override + public HighlyStructuredDateTimeValue.Type dateType() { + return HighlyStructuredDateTimeValue.Type.COMPONENT; + } + + /** + * The constants of this enumerated type describe the supported calendar types + */ + @ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent.CalendarType") + public enum CalendarType implements ProtobufEnum { + /** + * Gregorian calendar + */ + GREGORIAN(1), + /** + * Solar calendar + */ + SOLAR_HIJRI(2); + + final int index; + + CalendarType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + /** + * The constants of this enumerated type describe the days of the week + */ + @ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent.DayOfWeekType") + public enum DayOfWeek implements ProtobufEnum { + /** + * Monday + */ + MONDAY(1), + /** + * Tuesday + */ + TUESDAY(2), + /** + * Wednesday + */ + WEDNESDAY(3), + /** + * Thursday + */ + THURSDAY(4), + /** + * Friday + */ + FRIDAY(5), + /** + * Saturday + */ + SATURDAY(6), + /** + * Sunday + */ + SUNDAY(7); + + final int index; + + DayOfWeek(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeUnixEpoch.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeUnixEpoch.java new file mode 100644 index 000000000..dd22ee7cb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeUnixEpoch.java @@ -0,0 +1,33 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * A model class that represents a time as a unix epoch + */ +@ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeUnixEpoch") +public record HighlyStructuredDateTimeUnixEpoch( + @ProtobufProperty(index = 1, type = ProtobufType.INT64) + long timestampSeconds +) implements HighlyStructuredDateTimeValue { + + /** + * Returns the timestampSeconds as a zoned date time + * + * @return an optional + */ + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } + + @Override + public HighlyStructuredDateTimeValue.Type dateType() { + return HighlyStructuredDateTimeValue.Type.UNIX_EPOCH; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeValue.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeValue.java new file mode 100644 index 000000000..948ccc59d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredDateTimeValue.java @@ -0,0 +1,47 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; + +/** + * A model class that represents the value of a localizable parameter + */ +public sealed interface HighlyStructuredDateTimeValue extends ProtobufMessage permits HighlyStructuredDateTimeComponent, HighlyStructuredDateTimeUnixEpoch { + /** + * Returns the type of date + * + * @return a non-null type + */ + Type dateType(); + + + /** + * The constants of this enumerated type describe the various type of date types that a date time can wrap + */ + enum Type implements ProtobufEnum { + /** + * No date + */ + NONE(0), + /** + * Component date + */ + COMPONENT(1), + /** + * Unix epoch date + */ + UNIX_EPOCH(2); + + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameter.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameter.java new file mode 100644 index 000000000..52429099f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameter.java @@ -0,0 +1,61 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents a time a localizable parameter + */ +@ProtobufMessageName("Message.HighlyStructuredMessage.HSMLocalizableParameter") +public record HighlyStructuredLocalizableParameter( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String defaultValue, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional parameterCurrency, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional parameterDateTime +) implements ProtobufMessage { + /** + * Constructs a new localizable parameter with a default value and a parameter + * + * @param defaultValue the default value + * @param parameter the parameter + * @return a non-null localizable parameter + */ + public static HighlyStructuredLocalizableParameter of(String defaultValue, HighlyStructuredLocalizableParameterValue parameter) { + var builder = new HighlyStructuredLocalizableParameterBuilder() + .defaultValue(defaultValue); + switch (parameter) { + case HighlyStructuredCurrency highlyStructuredCurrency -> + builder.parameterCurrency(highlyStructuredCurrency); + case HighlyStructuredDateTime businessDateTime -> builder.parameterDateTime(businessDateTime); + case null -> { + } + } + return builder.build(); + } + + /** + * Returns the type of parameter that this message wraps + * + * @return a non-null parameter type + */ + public HighlyStructuredLocalizableParameterValue.Type parameterType() { + return parameter() + .map(HighlyStructuredLocalizableParameterValue::parameterType) + .orElse(HighlyStructuredLocalizableParameterValue.Type.NONE); + } + + /** + * Returns the parameter that this message wraps + * + * @return a non-null optional + */ + public Optional parameter() { + return parameterCurrency.isPresent() ? parameterCurrency : parameterDateTime; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameterValue.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameterValue.java new file mode 100644 index 000000000..f6ce2d48f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredLocalizableParameterValue.java @@ -0,0 +1,42 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; + +/** + * A model class that represents the value of a localizable parameter + */ +public sealed interface HighlyStructuredLocalizableParameterValue extends ProtobufMessage permits HighlyStructuredCurrency, HighlyStructuredDateTime { + /** + * Returns the type of parameter + * + * @return a non-null type + */ + Type parameterType(); + + enum Type implements ProtobufEnum { + /** + * No parameter + */ + NONE(0), + /** + * Currency parameter + */ + CURRENCY(2), + /** + * Date time parameter + */ + DATE_TIME(3); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredMessage.java b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredMessage.java new file mode 100644 index 000000000..8084d7aaa --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/highlyStructured/HighlyStructuredMessage.java @@ -0,0 +1,48 @@ +package it.auties.whatsapp.model.button.template.highlyStructured; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplateTitle; +import it.auties.whatsapp.model.message.button.TemplateMessage; +import it.auties.whatsapp.model.message.model.ButtonMessage; +import it.auties.whatsapp.model.message.model.MessageType; + +import java.util.List; +import java.util.Optional; + +/** + * A model class that represents a message that contains a highly structured message inside. Not + * really clear how this could be used, contributions are welcomed. + */ +@ProtobufMessageName("Message.HighlyStructuredMessage") +public record HighlyStructuredMessage( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String namespace, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String elementName, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + List params, + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + Optional fallbackLg, + @ProtobufProperty(index = 5, type = ProtobufType.STRING) + Optional fallbackLc, + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + List localizableParameters, + @ProtobufProperty(index = 7, type = ProtobufType.STRING) + Optional deterministicLg, + @ProtobufProperty(index = 8, type = ProtobufType.STRING) + Optional deterministicLc, + @ProtobufProperty(index = 9, type = ProtobufType.OBJECT) + TemplateMessage templateMessage +) implements ButtonMessage, HighlyStructuredFourRowTemplateTitle { + @Override + public MessageType type() { + return MessageType.HIGHLY_STRUCTURED; + } + + @Override + public Type titleType() { + return Type.HIGHLY_STRUCTURED; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButton.java new file mode 100644 index 000000000..44a2e8f38 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButton.java @@ -0,0 +1,58 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; + +/** + * A model that represents all types of hydrated buttons + */ +public sealed interface HighlyStructuredButton extends ProtobufMessage permits HighlyStructuredCallButton, HighlyStructuredQuickReplyButton, HighlyStructuredURLButton { + /** + * Returns the text of this button + * + * @return a non-null structure if the protobuf isn't corrupted + */ + HighlyStructuredMessage text(); + + /** + * Returns the type of this button + * + * @return a non-null type + */ + Type buttonType(); + + /** + * The constants of this enumerated type describe the various types of buttons that a template can + * wrap + */ + enum Type implements ProtobufEnum { + /** + * No button + */ + NONE(0), + /** + * Quick reply button + */ + QUICK_REPLY(1), + /** + * Url button + */ + URL(2), + /** + * Call button + */ + CALL(3); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButtonTemplate.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButtonTemplate.java new file mode 100644 index 000000000..a7e335ff6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredButtonTemplate.java @@ -0,0 +1,83 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents a template for a button + */ +@ProtobufMessageName("HydratedTemplateButton") +public record HighlyStructuredButtonTemplate( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional highlyStructuredQuickReplyButton, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional highlyStructuredUrlButton, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional highlyStructuredCallButton, + @ProtobufProperty(index = 4, type = ProtobufType.UINT32) + int index +) implements ProtobufMessage { + /** + * Constructs a new template + * + * @param highlyStructuredButton the button + * @return a non-null button template + */ + public static HighlyStructuredButtonTemplate of(HighlyStructuredButton highlyStructuredButton) { + return of(-1, highlyStructuredButton); + } + + /** + * Constructs a new template + * + * @param index the index + * @param highlyStructuredButton the button + * @return a non-null button template + */ + public static HighlyStructuredButtonTemplate of(int index, HighlyStructuredButton highlyStructuredButton) { + var builder = new HighlyStructuredButtonTemplateBuilder() + .index(index); + switch (highlyStructuredButton) { + case HighlyStructuredQuickReplyButton highlyStructuredQuickReplyButton -> + builder.highlyStructuredQuickReplyButton(highlyStructuredQuickReplyButton); + case HighlyStructuredURLButton highlyStructuredURLButton -> + builder.highlyStructuredUrlButton(highlyStructuredURLButton); + case HighlyStructuredCallButton highlyStructuredCallButton -> + builder.highlyStructuredCallButton(highlyStructuredCallButton); + case null -> { + } + } + return builder.build(); + } + + /** + * Returns this button + * + * @return a non-null optional + */ + public Optional button() { + if (highlyStructuredQuickReplyButton.isPresent()) { + return highlyStructuredQuickReplyButton; + } + + if (highlyStructuredUrlButton.isPresent()) { + return highlyStructuredUrlButton; + } + + return highlyStructuredCallButton; + } + + /** + * Returns the type of button that this message wraps + * + * @return a non-null button type + */ + public HighlyStructuredButton.Type buttonType() { + return button().map(HighlyStructuredButton::buttonType) + .orElse(HighlyStructuredButton.Type.NONE); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredCallButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredCallButton.java new file mode 100644 index 000000000..f3a1c2d82 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredCallButton.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; + +/** + * A model class that represents a button that can start a phone call + */ +@ProtobufMessageName("TemplateButton.CallButton") +public record HighlyStructuredCallButton( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + HighlyStructuredMessage text, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + HighlyStructuredMessage phoneNumber +) implements HighlyStructuredButton { + @Override + public Type buttonType() { + return Type.CALL; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplate.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplate.java new file mode 100644 index 000000000..1518b0a5b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplate.java @@ -0,0 +1,104 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufBuilder; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.TemplateFormatter; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.LocationMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +/** + * A model class that represents a four row template + */ +@ProtobufMessageName("Message.TemplateMessage.FourRowTemplate") +public record HighlyStructuredFourRowTemplate( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional titleDocument, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Optional titleHighlyStructured, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional titleImage, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Optional titleVideo, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + Optional titleLocation, + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + HighlyStructuredMessage content, + @ProtobufProperty(index = 7, type = ProtobufType.OBJECT) + Optional footer, + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + List buttons +) implements TemplateFormatter { + @ProtobufBuilder(className = "HighlyStructuredFourRowTemplateSimpleBuilder") + static HighlyStructuredFourRowTemplate simpleBuilder(HighlyStructuredFourRowTemplateTitle title, HighlyStructuredMessage content, HighlyStructuredMessage footer, List buttons) { + var builder = new HighlyStructuredFourRowTemplateBuilder() + .buttons(getIndexedButtons(buttons)) + .footer(footer); + switch (title) { + case DocumentMessage documentMessage -> builder.titleDocument(documentMessage); + case HighlyStructuredMessage highlyStructuredMessage -> + builder.titleHighlyStructured(highlyStructuredMessage); + case ImageMessage imageMessage -> builder.titleImage(imageMessage); + case VideoOrGifMessage videoMessage -> builder.titleVideo(videoMessage); + case LocationMessage locationMessage -> builder.titleLocation(locationMessage); + case null -> { + } + } + return builder.build(); + } + + private static List getIndexedButtons(List buttons) { + return IntStream.range(0, buttons.size()).mapToObj(index -> { + var button = buttons.get(index); + return new HighlyStructuredButtonTemplate(button.highlyStructuredQuickReplyButton(), button.highlyStructuredUrlButton(), button.highlyStructuredCallButton(), index + 1); + }).toList(); + } + + /** + * Returns the type of title that this template wraps + * + * @return a non-null title type + */ + public HighlyStructuredFourRowTemplateTitle.Type titleType() { + return title().map(HighlyStructuredFourRowTemplateTitle::titleType) + .orElse(HighlyStructuredFourRowTemplateTitle.Type.NONE); + } + + /** + * Returns the title of this template + * + * @return an optional + */ + public Optional title() { + if (titleDocument.isPresent()) { + return titleDocument; + } + + if (titleHighlyStructured.isPresent()) { + return titleHighlyStructured; + } + + if (titleImage.isPresent()) { + return titleImage; + } + + if (titleVideo.isPresent()) { + return titleVideo; + } + + return titleLocation; + } + + @Override + public Type templateType() { + return TemplateFormatter.Type.FOUR_ROW; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplateTitle.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplateTitle.java new file mode 100644 index 000000000..877f0a8d9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredFourRowTemplateTitle.java @@ -0,0 +1,63 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.LocationMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +/** + * A model that represents the title of a {@link HighlyStructuredFourRowTemplate} + */ +public sealed interface HighlyStructuredFourRowTemplateTitle extends ProtobufMessage permits DocumentMessage, HighlyStructuredMessage, ImageMessage, VideoOrGifMessage, LocationMessage { + /** + * Return the type of this title + * + * @return a non-null type + */ + Type titleType(); + + /** + * The constants of this enumerated type describe the various types of title that a template can + * have + */ + enum Type implements ProtobufEnum { + /** + * No title + */ + NONE(0), + /** + * Document title + */ + DOCUMENT(1), + /** + * Highly structured message title + */ + HIGHLY_STRUCTURED(2), + /** + * Image title + */ + IMAGE(3), + /** + * Video title + */ + VIDEO(4), + /** + * Location title + */ + LOCATION(5); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredQuickReplyButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredQuickReplyButton.java new file mode 100644 index 000000000..16087a075 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredQuickReplyButton.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; + +/** + * A model class that represents a quick reply button + */ +@ProtobufMessageName("TemplateButton.QuickReplyButton") +public record HighlyStructuredQuickReplyButton( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + HighlyStructuredMessage text, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String id +) implements HighlyStructuredButton { + public Type buttonType() { + return Type.QUICK_REPLY; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredURLButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredURLButton.java new file mode 100644 index 000000000..12e8f2f50 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hsm/HighlyStructuredURLButton.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.button.template.hsm; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.highlyStructured.HighlyStructuredMessage; + +/** + * A model class that represents an url button + */ +@ProtobufMessageName("TemplateButton.URLButton") +public record HighlyStructuredURLButton( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + HighlyStructuredMessage text, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + HighlyStructuredMessage url +) implements HighlyStructuredButton { + @Override + public Type buttonType() { + return Type.URL; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedButton.java new file mode 100644 index 000000000..d93381639 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedButton.java @@ -0,0 +1,57 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; + +/** + * A model that represents all types of hydrated buttons + */ +public sealed interface HydratedButton extends ProtobufMessage permits HydratedCallButton, HydratedQuickReplyButton, HydratedURLButton { + /** + * Returns the text of this button + * + * @return a non-null string if the protobuf isn't corrupted + */ + String text(); + + /** + * Returns the type of this button + * + * @return a non-null type + */ + Type buttonType(); + + /** + * The constants of this enumerated type describe the various types of buttons that a template can + * wrap + */ + enum Type implements ProtobufEnum { + /** + * No button + */ + NONE(0), + /** + * Quick reply button + */ + QUICK_REPLY(1), + /** + * Url button + */ + URL(2), + /** + * Call button + */ + CALL(3); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedCallButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedCallButton.java new file mode 100644 index 000000000..53d3002df --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedCallButton.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a hydrated button that can start a phone call + */ +@ProtobufMessageName("HydratedTemplateButton.HydratedCallButton") +public record HydratedCallButton( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String text, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String phoneNumber +) implements HydratedButton { + @Override + public Type buttonType() { + return Type.CALL; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplate.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplate.java new file mode 100644 index 000000000..a7fe75c25 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplate.java @@ -0,0 +1,107 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufBuilder; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.template.TemplateFormatter; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.LocationMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +/** + * A model class that represents a hydrated four row template + */ +@ProtobufMessageName("Message.TemplateMessage.HydratedFourRowTemplate") +public record HydratedFourRowTemplate( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Optional titleDocument, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Optional titleText, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional titleImage, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Optional titleVideo, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + Optional titleLocation, + @ProtobufProperty(index = 6, type = ProtobufType.STRING) + String body, + @ProtobufProperty(index = 7, type = ProtobufType.STRING) + Optional footer, + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + List hydratedButtons, + @ProtobufProperty(index = 9, type = ProtobufType.STRING) + String templateId +) implements TemplateFormatter { + @ProtobufBuilder(className = "HydratedFourRowTemplateSimpleBuilder") + static HydratedFourRowTemplate customBuilder(HydratedFourRowTemplateTitle title, String body, String footer, List buttons, String templateId) { + var builder = new HydratedFourRowTemplateBuilder() + .templateId(templateId) + .body(body) + .hydratedButtons(getIndexedButtons(buttons)) + .footer(footer); + switch (title) { + case DocumentMessage documentMessage -> builder.titleDocument(documentMessage); + case HydratedFourRowTemplateTextTitle hydratedFourRowTemplateTextTitle -> + builder.titleText(hydratedFourRowTemplateTextTitle); + case ImageMessage imageMessage -> builder.titleImage(imageMessage); + case VideoOrGifMessage videoMessage -> builder.titleVideo(videoMessage); + case LocationMessage locationMessage -> builder.titleLocation(locationMessage); + case null -> { + } + } + return builder.build(); + } + + private static List getIndexedButtons(List buttons) { + return IntStream.range(0, buttons.size()).mapToObj(index -> { + var button = buttons.get(index); + return new HydratedTemplateButton(button.quickReplyButton(), button.urlButton(), button.callButton(), index + 1); + }).toList(); + } + + /** + * Returns the type of title that this template wraps + * + * @return a non-null title type + */ + public HydratedFourRowTemplateTitle.Type titleType() { + return title().map(HydratedFourRowTemplateTitle::hydratedTitleType) + .orElse(HydratedFourRowTemplateTitle.Type.NONE); + } + + /** + * Returns the title of this template + * + * @return an optional + */ + public Optional title() { + if (titleDocument.isPresent()) { + return titleDocument; + } + + if (titleText.isPresent()) { + return titleText; + } + + if (titleImage.isPresent()) { + return titleImage; + } + + if (titleVideo.isPresent()) { + return titleVideo; + } + + return titleLocation; + } + + @Override + public Type templateType() { + return TemplateFormatter.Type.HYDRATED_FOUR_ROW; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTextTitle.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTextTitle.java new file mode 100644 index 000000000..fb3a18474 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTextTitle.java @@ -0,0 +1,25 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufConverter; + +/** + * A model class that represents a hydrated four row template + */ +public record HydratedFourRowTemplateTextTitle( + String text +) implements HydratedFourRowTemplateTitle { + @ProtobufConverter + public static HydratedFourRowTemplateTextTitle of(String text) { + return new HydratedFourRowTemplateTextTitle(text); + } + + @ProtobufConverter + public String text() { + return text; + } + + @Override + public Type hydratedTitleType() { + return Type.TEXT; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTitle.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTitle.java new file mode 100644 index 000000000..81b7e012a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedFourRowTemplateTitle.java @@ -0,0 +1,61 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.LocationMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +/** + * A model that represents the title of a {@link HydratedFourRowTemplate} + */ +public sealed interface HydratedFourRowTemplateTitle permits DocumentMessage, HydratedFourRowTemplateTextTitle, ImageMessage, VideoOrGifMessage, LocationMessage { + /** + * Return the type of this title + * + * @return a non-null type + */ + Type hydratedTitleType(); + + /** + * The constants of this enumerated type describe the various types of title that a template can + * wrap + */ + enum Type implements ProtobufEnum { + /** + * No title + */ + NONE(0), + /** + * Document title + */ + DOCUMENT(1), + /** + * Text title + */ + TEXT(2), + /** + * Image title + */ + IMAGE(3), + /** + * Video title + */ + VIDEO(4), + /** + * Location title + */ + LOCATION(5); + + final int index; + + Type(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedQuickReplyButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedQuickReplyButton.java new file mode 100644 index 000000000..55e2b2ecb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedQuickReplyButton.java @@ -0,0 +1,35 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.BytesHelper; + +import java.util.HexFormat; + +/** + * A model class that represents a hydrated quick reply button + */ +@ProtobufMessageName("HydratedTemplateButton.HydratedQuickReplyButton") +public record HydratedQuickReplyButton( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String text, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String id +) implements HydratedButton { + /** + * Constructs a new HydratedQuickReplyButton from a text with a random id + * + * @param text the non-null text + * @return a non-null HydratedQuickReplyButton + */ + public static HydratedQuickReplyButton of(String text) { + var id = HexFormat.of().formatHex(BytesHelper.random(6)); + return new HydratedQuickReplyButton(text, id); + } + + @Override + public Type buttonType() { + return Type.QUICK_REPLY; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedTemplateButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedTemplateButton.java new file mode 100644 index 000000000..48cedc517 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedTemplateButton.java @@ -0,0 +1,85 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + +/** + * A model class that represents a hydrated template for a button + */ +@ProtobufMessageName("HydratedTemplateButton") +public record HydratedTemplateButton( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + HydratedQuickReplyButton quickReplyButton, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + HydratedURLButton urlButton, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + HydratedCallButton callButton, + @ProtobufProperty(index = 4, type = ProtobufType.UINT32) + int index +) implements ProtobufMessage { + /** + * Constructs a new template button + * + * @param button the non-null button + * @return a non-null button template + */ + public static HydratedTemplateButton of(HydratedButton button) { + return of(-1, button); + } + + /** + * Constructs a new template button + * + * @param index the index + * @param button the non-null button + * @return a non-null button template + */ + public static HydratedTemplateButton of(int index, HydratedButton button) { + var builder = new HydratedTemplateButtonBuilder() + .index(index); + switch (button) { + case HydratedQuickReplyButton hydratedQuickReplyButton -> + builder.quickReplyButton(hydratedQuickReplyButton); + case HydratedURLButton hydratedURLButton -> builder.urlButton(hydratedURLButton); + case HydratedCallButton hydratedCallButton -> builder.callButton(hydratedCallButton); + case null -> { + } + } + return builder.build(); + } + + /** + * Returns this button + * + * @return a non-null optional + */ + public Optional button() { + if (quickReplyButton != null) { + return Optional.of(quickReplyButton); + } + + if (urlButton != null) { + return Optional.of(urlButton); + } + + if (callButton != null) { + return Optional.of(callButton); + } + + return Optional.empty(); + } + + /** + * Returns the type of button that this message wraps + * + * @return a non-null button type + */ + public HydratedButton.Type buttonType() { + return button().map(HydratedButton::buttonType) + .orElse(HydratedButton.Type.NONE); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedURLButton.java b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedURLButton.java new file mode 100644 index 000000000..5365f21fe --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/button/template/hydrated/HydratedURLButton.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.button.template.hydrated; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents a hydrated url button + */ +@ProtobufMessageName("HydratedTemplateButton.HydratedURLButton") +public record HydratedURLButton( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String text, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String url +) implements HydratedButton { + @Override + public Type buttonType() { + return Type.URL; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/call/Call.java b/src/main/java/it/auties/whatsapp/model/call/Call.java new file mode 100644 index 000000000..e592e4566 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/call/Call.java @@ -0,0 +1,25 @@ +package it.auties.whatsapp.model.call; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; + +public record Call( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid chat, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Jid caller, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + String id, + @ProtobufProperty(index = 4, type = ProtobufType.UINT64) + long timestampSeconds, + @ProtobufProperty(index = 5, type = ProtobufType.BOOL) + boolean video, + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + CallStatus status, + @ProtobufProperty(index = 7, type = ProtobufType.BOOL) + boolean offline +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/call/CallStatus.java b/src/main/java/it/auties/whatsapp/model/call/CallStatus.java new file mode 100644 index 000000000..fd4cc1553 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/call/CallStatus.java @@ -0,0 +1,21 @@ +package it.auties.whatsapp.model.call; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +public enum CallStatus implements ProtobufEnum { + RINGING(0), + ACCEPTED(1), + REJECTED(2), + TIMED_OUT(3); + + final int index; + + CallStatus(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/Chat.java b/src/main/java/it/auties/whatsapp/model/chat/Chat.java new file mode 100644 index 000000000..62f8fe314 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/Chat.java @@ -0,0 +1,1137 @@ +package it.auties.whatsapp.model.chat; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.contact.ContactStatus; +import it.auties.whatsapp.model.info.ChatMessageInfo; +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.ConcurrentLinkedHashedDequeue; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A model class that represents a Chat. A chat can be of two types: a conversation with a contact + * or a group. This class is only a model, this means that changing its values will have no real + * effect on WhatsappWeb's servers + */ +@ProtobufMessageName("Conversation") +public final class Chat implements ProtobufMessage, JidProvider { + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + final Jid jid; + + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + final ConcurrentLinkedHashedDequeue historySyncMessages; + + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + final Jid newJid; + + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + final Jid oldJid; + + @ProtobufProperty(index = 6, type = ProtobufType.UINT32) + int unreadMessagesCount; + + @ProtobufProperty(index = 7, type = ProtobufType.BOOL) + boolean readOnly; + + @ProtobufProperty(index = 8, type = ProtobufType.BOOL) + boolean endOfHistoryTransfer; + + @ProtobufProperty(index = 9, type = ProtobufType.UINT32) + ChatEphemeralTimer ephemeralMessageDuration; + + @ProtobufProperty(index = 10, type = ProtobufType.INT64) + long ephemeralMessagesToggleTimeSeconds; + + @ProtobufProperty(index = 11, type = ProtobufType.OBJECT) + EndOfHistoryTransferType endOfHistoryTransferType; + + @ProtobufProperty(index = 12, type = ProtobufType.UINT64) + long timestampSeconds; + + @ProtobufProperty(index = 13, type = ProtobufType.STRING) + String name; + + @ProtobufProperty(index = 15, type = ProtobufType.BOOL) + boolean notSpam; + + @ProtobufProperty(index = 16, type = ProtobufType.BOOL) + boolean archived; + @ProtobufProperty(index = 17, type = ProtobufType.OBJECT) + ChatDisappear disappearInitiator; + + @ProtobufProperty(index = 19, type = ProtobufType.BOOL) + boolean markedAsUnread; + + @ProtobufProperty(index = 20, type = ProtobufType.OBJECT) + final List participants; + + @ProtobufProperty(index = 21, type = ProtobufType.BYTES) + byte[] token; + + @ProtobufProperty(index = 22, type = ProtobufType.UINT64) + long tokenTimestampSeconds; + + @ProtobufProperty(index = 23, type = ProtobufType.BYTES) + byte[] identityKey; + + @ProtobufProperty(index = 24, type = ProtobufType.UINT32) + int pinnedTimestampSeconds; + + @ProtobufProperty(index = 25, type = ProtobufType.UINT64) + ChatMute mute; + + @ProtobufProperty(index = 26, type = ProtobufType.OBJECT) + ChatWallpaper wallpaper; + + @ProtobufProperty(index = 27, type = ProtobufType.OBJECT) + MediaVisibility mediaVisibility; + + @ProtobufProperty(index = 28, type = ProtobufType.UINT64) + long tokenSenderTimestampSeconds; + + @ProtobufProperty(index = 29, type = ProtobufType.BOOL) + boolean suspended; + + @ProtobufProperty(index = 30, type = ProtobufType.BOOL) + boolean terminated; + + @ProtobufProperty(index = 31, type = ProtobufType.UINT64) + long foundationTimestampSeconds; + + @ProtobufProperty(index = 32, type = ProtobufType.STRING) + Jid founder; + @ProtobufProperty(index = 33, type = ProtobufType.STRING) + String description; + + @ProtobufProperty(index = 34, type = ProtobufType.BOOL) + boolean support; + + @ProtobufProperty(index = 35, type = ProtobufType.BOOL) + boolean parentGroup; + + @ProtobufProperty(index = 36, type = ProtobufType.BOOL) + boolean defaultSubGroup; + + @ProtobufProperty(index = 37, type = ProtobufType.STRING) + final Jid parentGroupJid; + + @ProtobufProperty(index = 38, type = ProtobufType.STRING) + String displayName; + + @ProtobufProperty(index = 39, type = ProtobufType.STRING) + Jid phoneJid; + + @ProtobufProperty(index = 40, type = ProtobufType.BOOL) + boolean shareOwnPhoneNumber; + + @ProtobufProperty(index = 41, type = ProtobufType.BOOL) + boolean pnhDuplicateLidThread; + + @ProtobufProperty(index = 42, type = ProtobufType.STRING) + Jid lidJid; + + @ProtobufProperty(index = 999, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final ConcurrentHashMap presences; + + @ProtobufProperty(index = 1000, type = ProtobufType.STRING) + final Set participantsPreKeys; + + @ProtobufProperty(index = 1001, type = ProtobufType.OBJECT) + final Set pastParticipants; + + private boolean update; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Chat(Jid jid, ConcurrentLinkedHashedDequeue historySyncMessages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean readOnly, boolean endOfHistoryTransfer, ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, List participants, byte[] token, long tokenTimestampSeconds, byte[] identityKey, int pinnedTimestampSeconds, ChatMute mute, ChatWallpaper wallpaper, 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, ConcurrentHashMap presences, Set participantsPreKeys, Set pastParticipants) { + this.jid = jid; + this.historySyncMessages = historySyncMessages; + this.newJid = newJid; + this.oldJid = oldJid; + this.unreadMessagesCount = unreadMessagesCount; + this.readOnly = readOnly; + this.endOfHistoryTransfer = endOfHistoryTransfer; + this.ephemeralMessageDuration = ephemeralMessageDuration; + this.ephemeralMessagesToggleTimeSeconds = ephemeralMessagesToggleTimeSeconds; + this.endOfHistoryTransferType = endOfHistoryTransferType; + this.timestampSeconds = timestampSeconds; + this.name = name; + this.notSpam = notSpam; + this.archived = archived; + this.disappearInitiator = disappearInitiator; + this.markedAsUnread = markedAsUnread; + this.participants = participants; + this.token = token; + this.tokenTimestampSeconds = tokenTimestampSeconds; + this.identityKey = identityKey; + this.pinnedTimestampSeconds = pinnedTimestampSeconds; + this.mute = mute; + this.wallpaper = wallpaper; + this.mediaVisibility = mediaVisibility; + this.tokenSenderTimestampSeconds = tokenSenderTimestampSeconds; + this.suspended = suspended; + this.terminated = terminated; + this.foundationTimestampSeconds = foundationTimestampSeconds; + this.founder = founder; + this.description = description; + this.support = support; + this.parentGroup = parentGroup; + this.defaultSubGroup = defaultSubGroup; + this.parentGroupJid = parentGroupJid; + this.displayName = displayName; + this.phoneJid = phoneJid; + this.shareOwnPhoneNumber = shareOwnPhoneNumber; + this.pnhDuplicateLidThread = pnhDuplicateLidThread; + this.lidJid = lidJid; + this.presences = presences; + this.participantsPreKeys = participantsPreKeys; + this.pastParticipants = pastParticipants; + } + + /** + * Returns the name of this chat + * + * @return a non-null string + */ + public String name() { + if (name != null) { + return name; + } + + if (displayName != null) { + return displayName; + } + + return jid.user(); + } + + /** + * Returns a boolean to represent whether this chat is a group or not + * + * @return true if this chat is a group + */ + public boolean isGroup() { + return jid.type() == JidType.GROUP; + } + + /** + * Returns a boolean to represent whether this chat is pinned or not + * + * @return true if this chat is pinned + */ + public boolean isPinned() { + return pinnedTimestampSeconds != 0; + } + + /** + * Returns a boolean to represent whether ephemeral messages are enabled for this chat + * + * @return true if ephemeral messages are enabled for this chat + */ + public boolean isEphemeral() { + return ephemeralMessageDuration != ChatEphemeralTimer.OFF && ephemeralMessagesToggleTimeSeconds != 0; + } + + /** + * Returns all the unread messages in this chat + * + * @return a non-null collection + */ + public Collection unreadMessages() { + if (!hasUnreadMessages()) { + return List.of(); + } + + return historySyncMessages.stream() + .limit(unreadMessagesCount()) + .map(HistorySyncMessage::messageInfo) + .toList(); + } + + /** + * Returns a boolean to represent whether this chat has unread messages + * + * @return true if this chat has unread messages + */ + public boolean hasUnreadMessages() { + return unreadMessagesCount > 0; + } + + /** + * Returns an optional value containing the seconds this chat was pinned + * + * @return an optional + */ + public Optional pinnedTimestamp() { + return Clock.parseSeconds(pinnedTimestampSeconds); + } + + /** + * Returns the timestampSeconds for the creation of this chat in seconds since + * {@link Instant#EPOCH} + * + * @return an optional + */ + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } + + /** + * Returns an optional value containing the seconds in seconds since + * {@link Instant#EPOCH} when ephemeral messages were turned on + * + * @return an optional + */ + public Optional ephemeralMessagesToggleTime() { + return Clock.parseSeconds(ephemeralMessagesToggleTimeSeconds); + } + + /** + * Returns an optional value containing the latest message in chronological terms for this chat + * + * @return an optional + */ + public Optional newestMessage() { + return Optional.ofNullable(historySyncMessages.peekLast()) + .map(HistorySyncMessage::messageInfo); + } + + /** + * Returns an optional value containing the first message in chronological terms for this chat + * + * @return an optional + */ + public Optional oldestMessage() { + return Optional.ofNullable(historySyncMessages.peekFirst()) + .map(HistorySyncMessage::messageInfo); + } + + /** + * Returns an optional value containing the latest message in chronological terms for this chat + * with type that isn't server + * + * @return an optional + */ + public Optional newestStandardMessage() { + return findMessageBy(this::isStandardMessage, true); + } + + /** + * Returns an optional value containing the first message in chronological terms for this chat + * with type that isn't server + * + * @return an optional + */ + public Optional oldestStandardMessage() { + return findMessageBy(this::isStandardMessage, false); + } + + private boolean isStandardMessage(ChatMessageInfo info) { + return !info.message().hasCategory(MessageCategory.SERVER) && info.stubType().isEmpty(); + } + + /** + * Returns an optional value containing the latest message in chronological terms for this chat + * sent from you + * + * @return an optional + */ + public Optional newestMessageFromMe() { + return findMessageBy(this::isMessageFromMe, true); + } + + /** + * Returns an optional value containing the first message in chronological terms for this chat + * sent from you + * + * @return an optional + */ + public Optional oldestMessageFromMe() { + return findMessageBy(this::isMessageFromMe, false); + } + + private boolean isMessageFromMe(ChatMessageInfo info) { + return !info.message().hasCategory(MessageCategory.SERVER) && info.stubType().isEmpty() && info.fromMe(); + } + + /** + * Returns an optional value containing the latest message in chronological terms for this chat + * with type server + * + * @return an optional + */ + public Optional newestServerMessage() { + return findMessageBy(this::isServerMessage, true); + } + + /** + * Returns an optional value containing the first message in chronological terms for this chat + * with type server + * + * @return an optional + */ + public Optional oldestServerMessage() { + return findMessageBy(this::isServerMessage, false); + } + + private boolean isServerMessage(ChatMessageInfo info) { + return info.message().hasCategory(MessageCategory.SERVER) || info.stubType().isPresent(); + } + + private Optional findMessageBy(Function filter, boolean newest) { + var descendingIterator = newest ? historySyncMessages.descendingIterator() : historySyncMessages.iterator(); + while (descendingIterator.hasNext()) { + var info = descendingIterator.next().messageInfo(); + if (filter.apply(info)) { + return Optional.of(info); + } + } + + return Optional.empty(); + } + + + /** + * Returns all the starred messages in this chat + * + * @return a non-null list of messages + */ + public Collection starredMessages() { + return historySyncMessages.stream() + .map(HistorySyncMessage::messageInfo) + .filter(ChatMessageInfo::starred) + .toList(); + } + + /** + * Returns the timestampSeconds for the creation of this chat's token + * + * @return an optional + */ + public Optional tokenTimestamp() { + return Clock.parseSeconds(tokenTimestampSeconds); + } + + /** + * Returns the timestampSeconds for the token sender creation of this chat + * + * @return an optional + */ + public Optional tokenSenderTimestamp() { + return Clock.parseSeconds(tokenSenderTimestampSeconds); + } + + /** + * Returns the timestampSeconds for the creation of this chat if it's a group + * + * @return an optional + */ + public Optional foundationTimestamp() { + return Clock.parseSeconds(foundationTimestampSeconds); + } + + /** + * Adds a new unspecified amount of messages to this chat and sorts them accordingly + * + * @param newMessages the non-null messages to add + */ + public void addMessages(Collection newMessages) { + historySyncMessages.addAll(newMessages); + this.update = true; + } + + /** + * Adds a new unspecified amount of messages to this chat and sorts them accordingly + * + * @param oldMessages the non-null messages to add + */ + public void addOldMessages(Collection oldMessages) { + oldMessages.forEach(historySyncMessages::addFirst); + this.update = true; + } + + /** + * Adds a message to the chat in the most recent slot available + * + * @param info the message to add to the chat + * @return whether the message was added + */ + public boolean addNewMessage(ChatMessageInfo info) { + var sync = new HistorySyncMessage(info, historySyncMessages.size()); + if (historySyncMessages.contains(sync)) { + return false; + } + historySyncMessages.add(sync); + this.update = true; + updateChatTimestamp(info); + return true; + } + + /** + * Adds a message to the chat in the oldest slot available + * + * @param info the message to add to the chat + * @return whether the message was added + */ + public boolean addOldMessage(HistorySyncMessage info) { + historySyncMessages.addFirst(info); + this.update = true; + return true; + } + + /** + * Remove a message from the chat + * + * @param info the message to remove + * @return whether the message was removed + */ + public boolean removeMessage(ChatMessageInfo info) { + var result = historySyncMessages.removeIf(entry -> Objects.equals(entry.messageInfo().id(), info.id())); + if (result) { + this.update = true; + } + + refreshChatTimestamp(); + return result; + } + + /** + * Remove a message from the chat + * + * @param predicate the predicate that determines if a message should be removed + * @return whether the message was removed + */ + public boolean removeMessage(Predicate predicate) { + var result = historySyncMessages.removeIf(entry -> predicate.test(entry.messageInfo())); + refreshChatTimestamp(); + return result; + } + + private void refreshChatTimestamp() { + var message = newestMessage(); + if (message.isEmpty()) { + return; + } + + updateChatTimestamp(message.get()); + } + + private void updateChatTimestamp(ChatMessageInfo info) { + if(info.timestampSeconds().isEmpty()) { + return; + } + + var newTimestamp = info.timestampSeconds() + .getAsLong(); + var oldTimeStamp = newestMessage() + .map(value -> value.timestampSeconds().orElse(0L)) + .orElse(0L); + if (oldTimeStamp > newTimestamp) { + return; + } + + this.timestampSeconds = newTimestamp; + this.update = true; + } + + /** + * Removes all messages from the chat + */ + public void removeMessages() { + historySyncMessages.clear(); + this.update = true; + } + + /** + * Returns an immutable list of messages wrapped in history syncs + * This is useful for the proto + * + * @return a non-null collection + */ + public Collection messages() { + return Collections.unmodifiableCollection(historySyncMessages); + } + + /** + * Adds a collection of participants to this chat + * + * @param participants the participants to add + */ + public void addParticipants(Collection participants) { + participants.forEach(this::addParticipant); + this.update = true; + } + + /** + * Adds a participant to this chat + * + * @param jid the non-null jid of the participant + * @param role the role of the participant + * @return whether the participant was added + */ + public boolean addParticipant(Jid jid, GroupRole role) { + var result = addParticipant(new GroupParticipant(jid, role)); + if (result) { + this.update = true; + } + + return result; + } + + /** + * Adds a participant to this chat + * + * @param participant the non-null participant + * @return whether the participant was added + */ + public boolean addParticipant(GroupParticipant participant) { + var result = participants.add(participant); + this.update = true; + return true; + } + + /** + * Removes a participant from this chat + * + * @param jid the non-null jid of the participant + * @return whether the participant was removed + */ + public boolean removeParticipant(Jid jid) { + var result = participants.removeIf(entry -> Objects.equals(entry.jid(), jid)); + if (result) { + this.update = true; + } + + return result; + } + + /** + * Finds a participant by jid + * 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(Jid jid) { + return participants.stream() + .filter(entry -> Objects.equals(entry.jid(), jid)) + .findFirst(); + } + + /** + * Adds a past participant + * + * @param participant the non-null jid of the past participant + * @return whether the participant was added + */ + public boolean addPastParticipant(GroupPastParticipant participant) { + var result = pastParticipants.add(participant); + if (result) { + this.update = true; + } + + return result; + } + + /** + * Adds a collection of past participants + * + * @param pastParticipants the non-null list of past participants + * @return whether the participant were added + */ + public boolean addPastParticipants(List pastParticipants) { + var result = true; + for (var pastParticipant : pastParticipants) { + result &= this.pastParticipants.add(pastParticipant); + } + + if (result) { + this.update = true; + } + + return result; + } + + /** + * Removes a past participant + * + * @param jid the non-null jid of the past participant + * @return whether the participant was removed + */ + public boolean removePastParticipant(Jid jid) { + var result = pastParticipants.removeIf(entry -> Objects.equals(entry.jid(), jid)); + if (result) { + this.update = true; + } + + return result; + } + + /** + * Finds a past participant by jid + * + * @param jid the non-null jid of the past participant + * @return the past participant, if present + */ + public Optional findPastParticipant(Jid jid) { + return pastParticipants.stream() + .filter(entry -> Objects.equals(entry.jid(), jid)) + .findFirst(); + } + + public Set participantsPreKeys() { + return Collections.unmodifiableSet(participantsPreKeys); + } + + public void addParticipantsPreKeys(Collection jids) { + participantsPreKeys.addAll(jids); + this.update = true; + } + + public void clearParticipantsPreKeys() { + participantsPreKeys.clear(); + this.update = true; + } + + /** + * Checks if this chat is equal to another chat + * + * @param other the chat + * @return a boolean + */ + public boolean equals(Object other) { + return other instanceof Chat that + && Objects.equals(this.jid(), that.jid()); + } + + /** + * Returns this object as a jid + * + * @return a non-null jid + */ + @Override + public Jid toJid() { + return jid(); + } + + /** + * Returns the hash code for this chat + * + * @return an int + */ + @Override + public int hashCode() { + return Objects.hash(jid()); + } + + public Jid jid() { + return jid; + } + + public Collection historySyncMessages() { + return historySyncMessages; + } + + public Optional newJid() { + return Optional.ofNullable(newJid); + } + + public Optional oldJid() { + return Optional.ofNullable(oldJid); + } + + public int unreadMessagesCount() { + return unreadMessagesCount; + } + + public boolean readOnly() { + return readOnly; + } + + public boolean endOfHistoryTransfer() { + return endOfHistoryTransfer; + } + + public ChatEphemeralTimer ephemeralMessageDuration() { + return ephemeralMessageDuration; + } + + public long ephemeralMessagesToggleTimeSeconds() { + return ephemeralMessagesToggleTimeSeconds; + } + + public Optional endOfHistoryTransferType() { + return Optional.ofNullable(endOfHistoryTransferType); + } + + public long timestampSeconds() { + return timestampSeconds; + } + + public boolean notSpam() { + return notSpam; + } + + public boolean archived() { + return archived; + } + + public Optional disappearInitiator() { + return Optional.ofNullable(disappearInitiator); + } + + public boolean markedAsUnread() { + return markedAsUnread; + } + + public List participants() { + return Collections.unmodifiableList(participants); + } + + public Optional token() { + return Optional.ofNullable(token); + } + + public long tokenTimestampSeconds() { + return tokenTimestampSeconds; + } + + public Optional identityKey() { + return Optional.ofNullable(identityKey); + } + + public int pinnedTimestampSeconds() { + return pinnedTimestampSeconds; + } + + public ChatMute mute() { + return mute; + } + + public Optional wallpaper() { + return Optional.ofNullable(wallpaper); + } + + public MediaVisibility mediaVisibility() { + return mediaVisibility; + } + + public long tokenSenderTimestampSeconds() { + return tokenSenderTimestampSeconds; + } + + public boolean suspended() { + return suspended; + } + + public boolean terminated() { + return terminated; + } + + public long foundationTimestampSeconds() { + return foundationTimestampSeconds; + } + + public Optional founder() { + return Optional.ofNullable(founder); + } + + public Optional description() { + return Optional.ofNullable(description); + } + + public boolean support() { + return support; + } + + public boolean parentGroup() { + return parentGroup; + } + + public boolean defaultSubGroup() { + return defaultSubGroup; + } + + public Optional parentGroupJid() { + return Optional.ofNullable(parentGroupJid); + } + + public Optional displayName() { + return Optional.ofNullable(displayName); + } + + public Optional phoneJid() { + return Optional.ofNullable(phoneJid); + } + + public boolean pnhDuplicateLidThread() { + return pnhDuplicateLidThread; + } + + public Optional lidJid() { + return Optional.ofNullable(lidJid); + } + + public ConcurrentHashMap presences() { + return presences; + } + + public Set pastParticipants() { + return pastParticipants; + } + + public boolean hasName() { + return name != null; + } + + public boolean shareOwnPhoneNumber() { + return shareOwnPhoneNumber; + } + + public Chat setUnreadMessagesCount(int unreadMessagesCount) { + this.unreadMessagesCount = unreadMessagesCount; + this.update = true; + return this; + } + + public Chat setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + this.update = true; + return this; + } + + public Chat setEndOfHistoryTransfer(boolean endOfHistoryTransfer) { + this.endOfHistoryTransfer = endOfHistoryTransfer; + this.update = true; + return this; + } + + public Chat setEphemeralMessageDuration(ChatEphemeralTimer ephemeralMessageDuration) { + this.ephemeralMessageDuration = ephemeralMessageDuration; + this.update = true; + return this; + } + + public Chat setEphemeralMessagesToggleTimeSeconds(long ephemeralMessagesToggleTimeSeconds) { + this.ephemeralMessagesToggleTimeSeconds = ephemeralMessagesToggleTimeSeconds; + this.update = true; + return this; + } + + public Chat setEndOfHistoryTransferType(EndOfHistoryTransferType endOfHistoryTransferType) { + this.endOfHistoryTransferType = endOfHistoryTransferType; + this.update = true; + return this; + } + + public Chat setTimestampSeconds(long timestampSeconds) { + this.timestampSeconds = timestampSeconds; + this.update = true; + return this; + } + + public Chat setName(String name) { + this.name = name; + this.update = true; + return this; + } + + public Chat setNotSpam(boolean notSpam) { + this.notSpam = notSpam; + this.update = true; + return this; + } + + public Chat setArchived(boolean archived) { + this.archived = archived; + this.update = true; + return this; + } + + public Chat setDisappearInitiator(ChatDisappear disappearInitiator) { + this.disappearInitiator = disappearInitiator; + this.update = true; + return this; + } + + public Chat setMarkedAsUnread(boolean markedAsUnread) { + this.markedAsUnread = markedAsUnread; + this.update = true; + return this; + } + + public Chat setToken(byte[] token) { + this.token = token; + this.update = true; + return this; + } + + public Chat setTokenTimestampSeconds(long tokenTimestampSeconds) { + this.tokenTimestampSeconds = tokenTimestampSeconds; + this.update = true; + return this; + } + + public Chat setIdentityKey(byte[] identityKey) { + this.identityKey = identityKey; + this.update = true; + return this; + } + + public Chat setPinnedTimestampSeconds(int pinnedTimestampSeconds) { + this.pinnedTimestampSeconds = pinnedTimestampSeconds; + this.update = true; + return this; + } + + public Chat setMute(ChatMute mute) { + this.mute = mute; + this.update = true; + return this; + } + + public Chat setWallpaper(ChatWallpaper wallpaper) { + this.wallpaper = wallpaper; + this.update = true; + return this; + } + + public Chat setMediaVisibility(MediaVisibility mediaVisibility) { + this.mediaVisibility = mediaVisibility; + this.update = true; + return this; + } + + public Chat setTokenSenderTimestampSeconds(long tokenSenderTimestampSeconds) { + this.tokenSenderTimestampSeconds = tokenSenderTimestampSeconds; + this.update = true; + return this; + } + + public Chat setSuspended(boolean suspended) { + this.suspended = suspended; + this.update = true; + return this; + } + + public Chat setTerminated(boolean terminated) { + this.terminated = terminated; + this.update = true; + return this; + } + + public Chat setFoundationTimestampSeconds(long foundationTimestampSeconds) { + this.foundationTimestampSeconds = foundationTimestampSeconds; + this.update = true; + return this; + } + + public Chat setFounder(Jid founder) { + this.founder = founder; + this.update = true; + return this; + } + + public Chat setDescription(String description) { + this.description = description; + this.update = true; + return this; + } + + public Chat setSupport(boolean support) { + this.support = support; + this.update = true; + return this; + } + + public Chat setParentGroup(boolean parentGroup) { + this.parentGroup = parentGroup; + this.update = true; + return this; + } + + public Chat setDefaultSubGroup(boolean defaultSubGroup) { + this.defaultSubGroup = defaultSubGroup; + this.update = true; + return this; + } + + public Chat setDisplayName(String displayName) { + this.displayName = displayName; + this.update = true; + return this; + } + + public Chat setPhoneJid(Jid phoneJid) { + this.phoneJid = phoneJid; + this.update = true; + return this; + } + + public Chat setShareOwnPhoneNumber(boolean shareOwnPhoneNumber) { + this.shareOwnPhoneNumber = shareOwnPhoneNumber; + this.update = true; + return this; + } + + public Chat setPnhDuplicateLidThread(boolean pnhDuplicateLidThread) { + this.pnhDuplicateLidThread = pnhDuplicateLidThread; + this.update = true; + return this; + } + + public Chat setLidJid(Jid lidJid) { + this.lidJid = lidJid; + this.update = true; + return this; + } + + public boolean hasUpdate() { + return update; + } + + /** + * The constants of this enumerated type describe the various types of transfers that can regard a + * chat history sync + */ + @ProtobufMessageName("Conversation.EndOfHistoryTransferType") + public enum EndOfHistoryTransferType implements ProtobufEnum { + /** + * Complete, but more messages remain on the phone + */ + COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY(0), + + /** + * Complete and no more messages remain on the phone + */ + COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY(1); + + final int index; + + EndOfHistoryTransferType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatDisappear.java b/src/main/java/it/auties/whatsapp/model/chat/ChatDisappear.java new file mode 100644 index 000000000..8e475b5cb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatDisappear.java @@ -0,0 +1,54 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Objects; + +/** + * A model that represents a chat disappear mode + */ +@ProtobufMessageName("DisappearingMode") +public record ChatDisappear( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Initiator initiator +) implements ProtobufMessage { + /** + * The constants of this enumerated type describe the various actors that can initialize + * disappearing messages in a chat + */ + @ProtobufMessageName("DisappearingMode.Initiator") + public enum Initiator implements ProtobufEnum { + /** + * Changed in chat + */ + CHANGED_IN_CHAT(0), + /** + * Initiated by me + */ + INITIATED_BY_ME(1), + /** + * Initiated by other + */ + INITIATED_BY_OTHER(2); + + final int index; + + Initiator(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(initiator.index()); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatEphemeralTimer.java b/src/main/java/it/auties/whatsapp/model/chat/ChatEphemeralTimer.java new file mode 100644 index 000000000..5df95501b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatEphemeralTimer.java @@ -0,0 +1,63 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +import java.time.Duration; +import java.util.Arrays; + +/** + * Enum representing the ChatEphemeralTimer period. Each constant is associated with a specific + * duration period. + */ +public enum ChatEphemeralTimer implements ProtobufEnum { + /** + * ChatEphemeralTimer with duration of 0 days. + */ + OFF(0, Duration.ofDays(0)), + + /** + * ChatEphemeralTimer with duration of 1 day. + */ + ONE_DAY(1, Duration.ofDays(1)), + + /** + * ChatEphemeralTimer with duration of 7 days. + */ + ONE_WEEK(2, Duration.ofDays(7)), + + /** + * ChatEphemeralTimer with duration of 90 days. + */ + THREE_MONTHS(3, Duration.ofDays(90)); + + private final Duration period; + final int index; + + ChatEphemeralTimer(@ProtobufEnumIndex int index, Duration period) { + this.index = index; + this.period = period; + } + + public int index() { + return index; + } + + public Duration period() { + return period; + } + + @ProtobufConverter + public static ChatEphemeralTimer of(int value) { + return Arrays.stream(values()) + .filter(entry -> entry.period().toSeconds() == value || entry.period().toDays() == value) + .findFirst() + .orElse(OFF); + } + + @ProtobufConverter + public int periodSeconds() { + return (int) period.toSeconds(); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatMute.java b/src/main/java/it/auties/whatsapp/model/chat/ChatMute.java new file mode 100644 index 000000000..9ac07c457 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatMute.java @@ -0,0 +1,156 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.whatsapp.util.Clock; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * An immutable model class that represents a mute + * + * @param endTimeStamp the end date of the mute associated with this object stored as second since + * {@link Instant#EPOCH} + */ +public record ChatMute(long endTimeStamp) { + /** + * Not muted flag + */ + private static final long NOT_MUTED_FLAG = 0; + + /** + * Muted flag + */ + private static final long MUTED_INDEFINITELY_FLAG = -1; + + /** + * Not muted constant + */ + private static final ChatMute NOT_MUTED = new ChatMute(NOT_MUTED_FLAG); + + /** + * Muted constant + */ + private static final ChatMute MUTED_INDEFINITELY = new ChatMute(MUTED_INDEFINITELY_FLAG); + + /** + * Constructs a new not muted ChatMute + * + * @return a non-null mute + */ + public static ChatMute notMuted() { + return NOT_MUTED; + } + + /** + * Constructs a new muted ChatMute + * + * @return a non-null mute + */ + public static ChatMute muted() { + return MUTED_INDEFINITELY; + } + + /** + * Constructs a new mute that lasts eight hours + * + * @return a non-null mute + */ + public static ChatMute mutedForEightHours() { + return muted(ZonedDateTime.now().plusHours(8).toEpochSecond()); + } + + /** + * Do not use this method, reserved for protobuf + */ + @ProtobufConverter + public static ChatMute ofProtobuf(long object) { + return muted(object); + } + + /** + * Constructs a new mute for a duration in endTimeStamp + * + * @param seconds can be null and is considered as not muted + * @return a non-null mute + */ + public static ChatMute muted(Long seconds) { + if (seconds == null || seconds == NOT_MUTED_FLAG) { + return NOT_MUTED; + } + if (seconds == MUTED_INDEFINITELY_FLAG) { + return MUTED_INDEFINITELY; + } + return new ChatMute(seconds); + } + + /** + * Constructs a new mute that lasts one week + * + * @return a non-null mute + */ + public static ChatMute mutedForOneWeek() { + return muted(ZonedDateTime.now().plusWeeks(1).toEpochSecond()); + } + + /** + * Returns whether the chat associated with this object is muted or not. + * + * @return true if the chat associated with this object is muted + */ + public boolean isMuted() { + return type() != Type.NOT_MUTED; + } + + /** + * Returns a non-null enum that describes the type of mute for this object + * + * @return a non-null enum that describes the type of mute for this object + */ + public Type type() { + if (endTimeStamp == MUTED_INDEFINITELY_FLAG) { + return Type.MUTED_INDEFINITELY; + } + if (endTimeStamp == NOT_MUTED_FLAG) { + return Type.NOT_MUTED; + } + return Type.MUTED_FOR_TIMEFRAME; + } + + /** + * Returns the date when this mute expires if the chat is muted and not indefinitely + * + * @return a non-empty optional date if {@link ChatMute#endTimeStamp} > 0 + */ + public Optional end() { + return Clock.parseSeconds(endTimeStamp); + } + + @ProtobufConverter + public long endTimeStamp() { + return endTimeStamp; + } + + /** + * The constants of this enumerated type describe the various types of mute a {@link ChatMute} can + * describe + */ + public enum Type { + /** + * This constant describes a {@link ChatMute} that holds a seconds greater than 0 Simply put, + * {@link ChatMute#endTimeStamp()} > 0 + */ + MUTED_FOR_TIMEFRAME, + /** + * This constant describes a {@link ChatMute} that holds a seconds equal to -1 Simply put, + * {@link ChatMute#endTimeStamp()} == -1 + */ + MUTED_INDEFINITELY, + /** + * This constant describes a {@link ChatMute} that holds a seconds equal to 0 Simply put, + * {@link ChatMute#endTimeStamp()} == 0 + */ + NOT_MUTED + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatSettingPolicy.java b/src/main/java/it/auties/whatsapp/model/chat/ChatSettingPolicy.java new file mode 100644 index 000000000..64f8de492 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatSettingPolicy.java @@ -0,0 +1,25 @@ +package it.auties.whatsapp.model.chat; + +/** + * The constants of this enumerated type describe the various policies that can be enforced for a {@link GroupSetting} or {@link CommunitySetting} in a {@link Chat} + */ +public enum ChatSettingPolicy { + /** + * Allows both admins and users + */ + ANYONE, + /** + * Allows only admins + */ + ADMINS; + + /** + * Returns a GroupPolicy based on a boolean value obtained from Whatsapp + * + * @param input the boolean value obtained from Whatsapp + * @return a non-null GroupPolicy + */ + public static ChatSettingPolicy of(boolean input) { + return input ? ADMINS : ANYONE; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/ChatWallpaper.java b/src/main/java/it/auties/whatsapp/model/chat/ChatWallpaper.java new file mode 100644 index 000000000..c5eeb7d7c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/ChatWallpaper.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +/** + * A model class that represents the wallpaper of a chat. + */ +@ProtobufMessageName("WallpaperSettings") +public record ChatWallpaper( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String filename, + @ProtobufProperty(index = 2, type = ProtobufType.UINT32) + int opacity +) implements ProtobufMessage { +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/CommunitySetting.java b/src/main/java/it/auties/whatsapp/model/chat/CommunitySetting.java new file mode 100644 index 000000000..fcff65e1e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/CommunitySetting.java @@ -0,0 +1,11 @@ +package it.auties.whatsapp.model.chat; + +/** + * The constants of this enumerated type describe the various settings that can be toggled for a community + */ +public enum CommunitySetting { + /** + * Who can add and remove groups from a community + */ + MODIFY_GROUPS +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupAction.java b/src/main/java/it/auties/whatsapp/model/chat/GroupAction.java new file mode 100644 index 000000000..3b3806fad --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupAction.java @@ -0,0 +1,37 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.whatsapp.api.Whatsapp; +import it.auties.whatsapp.model.contact.Contact; + +/** + * The constants of this enumerated type describe the various actions that can be executed on a + * {@link Contact} in a {@link Chat}. Said chat should be a group: {@link Chat#isGroup()}. Said + * actions can be executed using various methods in {@link Whatsapp}. + */ +public enum GroupAction { + /** + * Adds a contact to a group + */ + ADD, + /** + * Removes a contact from a group + */ + REMOVE, + /** + * Promotes a contact to admin in a group + */ + PROMOTE, + /** + * Demotes a contact to user in a group + */ + DEMOTE; + + /** + * Returns the name of this enumerated constant + * + * @return a lowercase non-null String + */ + public String data() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java b/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java new file mode 100644 index 000000000..e69335259 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupMetadata.java @@ -0,0 +1,29 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.whatsapp.model.jid.Jid; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * This model class represents the metadata of a group + */ +public record GroupMetadata( + Jid jid, + String subject, + Optional subjectAuthor, + Optional subjectTimestamp, + Optional foundationTimestamp, + Optional founder, + Optional description, + Optional descriptionId, + Map policies, + List participants, + Optional ephemeralExpiration, + boolean isCommunity, + boolean isOpenCommunity +) { + +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java b/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java new file mode 100644 index 000000000..8e3f1dbe4 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupParticipant.java @@ -0,0 +1,53 @@ +package it.auties.whatsapp.model.chat; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; + +import java.util.Objects; + +/** + * A model class that represents a participant of a group. + */ +@ProtobufMessageName("GroupParticipant") +public final class GroupParticipant implements ProtobufMessage { + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + private final Jid jid; + + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + private GroupRole role; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public GroupParticipant(Jid jid, GroupRole role) { + this.jid = jid; + this.role = Objects.requireNonNullElse(role, GroupRole.USER); + } + + public Jid jid() { + return jid; + } + + public GroupRole role() { + return role; + } + + public void setRole(GroupRole role) { + this.role = role; + } + + @Override + public String toString() { + return "GroupParticipant{" + + "jid=" + jid + + ", role=" + role + + '}'; + } + + @Override + public int hashCode() { + return Objects.hash(jid, role.index()); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java new file mode 100644 index 000000000..c168b0819 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipant.java @@ -0,0 +1,62 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + + +/** + * Class representing a past participant in a chat + */ +@ProtobufMessageName("PastParticipant") +public record GroupPastParticipant( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid jid, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + Reason reason, + @ProtobufProperty(index = 3, type = ProtobufType.UINT64) + long timestampSeconds +) implements ProtobufMessage { + + /** + * Returns when the past participant left the chat + * + * @return an optional + */ + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } + + /** + * Enum representing the errorReason for a past participant leaving the chat. + */ + @ProtobufMessageName("PastParticipant.LeaveReason") + public enum Reason implements ProtobufEnum { + /** + * The past participant left the chat voluntarily. + */ + LEFT(0), + /** + * The past participant was removed from the chat. + */ + REMOVED(1); + + final int index; + + Reason(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java new file mode 100644 index 000000000..b60753758 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupPastParticipants.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; + +import java.util.List; + +/** + * Class representing a list of past participants in a chat group + */ +@ProtobufMessageName("PastParticipants") +public record GroupPastParticipants( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid groupJid, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + List pastParticipants +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupRole.java b/src/main/java/it/auties/whatsapp/model/chat/GroupRole.java new file mode 100644 index 000000000..f532acb82 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupRole.java @@ -0,0 +1,53 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.whatsapp.api.Whatsapp; + +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * The constants of this enumerated type describe the various roles that a {@link GroupParticipant} + * can have in a group. Said roles can be changed using various methods in {@link Whatsapp}. + */ +@ProtobufMessageName("GroupParticipant.Rank") +public enum GroupRole implements ProtobufEnum { + /** + * A participant of the group with no special powers + */ + USER(0, null), + /** + * A participant of the group with administration powers + */ + ADMIN(1, "admin"), + /** + * The founder of the group, also known as super admin + */ + FOUNDER(2, "superadmin"); + + final int index; + private final String data; + + GroupRole(@ProtobufEnumIndex int index, String data) { + this.index = index; + this.data = data; + } + + public static GroupRole of(String input) { + return Arrays.stream(values()) + .filter(entry -> Objects.equals(entry.data(), input)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Cannot find GroupRole for %s".formatted(input))); + } + + public int index() { + return index; + } + + public String data() { + return data; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/chat/GroupSetting.java b/src/main/java/it/auties/whatsapp/model/chat/GroupSetting.java new file mode 100644 index 000000000..9c0fc9d9c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/chat/GroupSetting.java @@ -0,0 +1,29 @@ +package it.auties.whatsapp.model.chat; + +import it.auties.whatsapp.api.Whatsapp; + +/** + * The constants of this enumerated type describe the various settings that can be toggled for a + * group. Said settings can be changed using various methods in {@link Whatsapp}. + */ +public enum GroupSetting { + /** + * Who can edit the metadata of a group + */ + EDIT_GROUP_INFO, + + /** + * Who can send messages in a group + */ + SEND_MESSAGES, + + /** + * Who can add new members + */ + ADD_PARTICIPANTS, + + /** + * Who can accept new members + */ + APPROVE_PARTICIPANTS +} diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java new file mode 100644 index 000000000..7bdc1b70c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionDevice.java @@ -0,0 +1,152 @@ +package it.auties.whatsapp.model.companion; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.signal.auth.UserAgent.PlatformType; +import it.auties.whatsapp.model.signal.auth.Version; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +/** + * A model for a mobile companion + * + * @param model the non-null model of the device + * @param manufacturer the non-null manufacturer of the device + * @param platform the non-null os of the device + * @param appVersion the version of the app, or empty + * @param osVersion the non-null os version of the device + */ +public record CompanionDevice( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String model, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String manufacturer, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + PlatformType platform, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Optional appVersion, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + Version osVersion +) implements ProtobufMessage { + private static final List IPHONES = List.of( + "iPhone_11", + "iPhone_11_Pro", + "iPhone_11_Pro_Max", + "iPhone_12", + "iPhone_12_Pro", + "iPhone_12_Pro_Max", + "iPhone_13", + "iPhone_13_Pro", + "iPhone_13_Pro_Max", + "iPhone_14", + "iPhone_14_Plus", + "iPhone_14_Pro", + "iPhone_14_Pro_Max", + "iPhone_15", + "iPhone_15_Plus", + "iPhone_15_Pro", + "iPhone_15_Pro_Max" + ); + + public static CompanionDevice web() { + return web(null); + } + + public static CompanionDevice web(Version appVersion) { + return new CompanionDevice( + "Chrome", + "Google", + PlatformType.WEB, + Optional.ofNullable(appVersion), + Version.of("1.0") + ); + } + + public static CompanionDevice ios(boolean business) { + return ios(null, business); + } + + public static CompanionDevice ios(Version appVersion, boolean business) { + return new CompanionDevice( + IPHONES.get(ThreadLocalRandom.current().nextInt(IPHONES.size())), + "Apple", + business ? PlatformType.IOS_BUSINESS : PlatformType.IOS, + Optional.ofNullable(appVersion), + Version.of("17.2.1") + ); + } + + public static CompanionDevice android(boolean business) { + return android(null, business); + } + + + public static CompanionDevice android(Version appVersion, boolean business) { + return new CompanionDevice( + "P60", + "HUAWEI", + business ? PlatformType.ANDROID_BUSINESS : PlatformType.ANDROID, + Optional.ofNullable(appVersion), + Version.of("10.11.0") + ); + } + + public String toUserAgent(Version appVersion) { + return "WhatsApp/%s %s/%s Device/%s".formatted( + appVersion, + platformName(), + osVersion.toString(), + deviceName() + ); + } + + public CompanionDevice toPersonal() { + if(!platform.isBusiness()) { + return this; + } + + return new CompanionDevice( + model, + manufacturer, + platform.toPersonal(), + appVersion, + osVersion + ); + } + + public CompanionDevice toBusiness() { + if(platform.isBusiness()) { + return this; + } + + return new CompanionDevice( + model, + manufacturer, + platform.toBusiness(), + appVersion, + osVersion + ); + } + + private String deviceName() { + return switch (platform()) { + case ANDROID, ANDROID_BUSINESS -> manufacturer + " " + model; + case IOS, IOS_BUSINESS -> model; + case KAIOS -> manufacturer + "+" + model; + default -> throw new IllegalStateException("Unsupported mobile os"); + }; + } + + private String platformName() { + return switch (platform()) { + case ANDROID -> "Android"; + case ANDROID_BUSINESS -> "SMBA"; + case IOS -> "iOS"; + case IOS_BUSINESS -> "SMB iOS"; + default -> throw new IllegalStateException("Unsupported mobile os"); + }; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionHashState.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionHashState.java new file mode 100644 index 000000000..edd440f4b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionHashState.java @@ -0,0 +1,128 @@ +package it.auties.whatsapp.model.companion; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.node.Attributes; +import it.auties.whatsapp.model.node.Node; +import it.auties.whatsapp.model.sync.PatchType; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static it.auties.whatsapp.model.node.Node.of; + +public final class CompanionHashState implements ProtobufMessage { + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + private PatchType type; + + @ProtobufProperty(index = 2, type = ProtobufType.INT64) + private long version; + + @ProtobufProperty(index = 3, type = ProtobufType.BYTES) + private byte[] hash; + + @ProtobufProperty(index = 4, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.BYTES) + private Map indexValueMap; + + public CompanionHashState(PatchType type) { + this(type, 0); + } + + public CompanionHashState(PatchType type, long version) { + this.type = type; + this.version = version; + this.hash = new byte[128]; + this.indexValueMap = new HashMap<>(); + } + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public CompanionHashState(PatchType type, long version, byte[] hash, Map indexValueMap) { + this.type = type; + this.version = version; + this.hash = hash; + this.indexValueMap = indexValueMap; + } + + public Node toNode() { + var attributes = Attributes.of() + .put("name", type) + .put("version", version) + .put("return_snapshot", version == 0) + .toMap(); + return of("collection", attributes); + } + + public CompanionHashState copy() { + return new CompanionHashState(type, version, Arrays.copyOf(hash, hash.length), new HashMap<>(indexValueMap)); + } + + private boolean checkIndexEquality(CompanionHashState that) { + if (indexValueMap.size() != that.indexValueMap().size()) { + return false; + } + return indexValueMap().entrySet() + .stream() + .allMatch(entry -> checkIndexEntryEquality(that, entry.getKey(), entry.getValue())); + } + + private static boolean checkIndexEntryEquality(CompanionHashState that, String thisKey, byte[] thisValue) { + var thatValue = that.indexValueMap().get(thisKey); + return thatValue != null && Arrays.equals(thatValue, thisValue); + } + + public PatchType type() { + return this.type; + } + + public long version() { + return this.version; + } + + public byte[] hash() { + return this.hash; + } + + public Map indexValueMap() { + return this.indexValueMap; + } + + public CompanionHashState setType(PatchType name) { + this.type = name; + return this; + } + + public CompanionHashState setVersion(long version) { + this.version = version; + return this; + } + + public CompanionHashState setHash(byte[] hash) { + this.hash = hash; + return this; + } + + public CompanionHashState setIndexValueMap(Map indexValueMap) { + this.indexValueMap = indexValueMap; + return this; + } + + + @Override + public boolean equals(Object o) { + return o instanceof CompanionHashState that + && this.version == that.version() + && this.type == that.type() + && Arrays.equals(this.hash, that.hash()) && checkIndexEquality(that); + } + + @Override + public int hashCode() { + var result = Objects.hash(type, version, indexValueMap); + result = 31 * result + Arrays.hashCode(hash); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionLinkResult.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionLinkResult.java new file mode 100644 index 000000000..96c79bf2a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionLinkResult.java @@ -0,0 +1,22 @@ +package it.auties.whatsapp.model.companion; + +/** + * The constants of this enumeration describe the various types of recommendedChannels that can be yielded by a new device's registration through the mobile api + */ +public enum CompanionLinkResult { + /** + * The device was successfully linked + */ + SUCCESS, + + /** + * The limit of devices, as of now four, has already been reached + */ + MAX_DEVICES_ERROR, + + /** + * The device couldn't be linked because of an unknown error + * This usually means that the qr code is no longer valid + */ + RETRY_ERROR +} diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionPatch.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionPatch.java new file mode 100644 index 000000000..24fd3cb78 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionPatch.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.model.companion; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; + +public record CompanionPatch( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid companion, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + CompanionHashState state +) implements ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionProperty.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionProperty.java new file mode 100644 index 000000000..3c26adf49 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionProperty.java @@ -0,0 +1,13 @@ +package it.auties.whatsapp.model.companion; + +/** + * A model that represents an immutable property associated with the linked device + * + * @param name the name of the property + * @param code an id that represents the property + * @param value the value associated with this property + * @param defaultValue the default value for this property + */ +public record CompanionProperty(String name, double code, Object value, Object defaultValue) { + +} diff --git a/src/main/java/it/auties/whatsapp/model/companion/CompanionSyncKey.java b/src/main/java/it/auties/whatsapp/model/companion/CompanionSyncKey.java new file mode 100644 index 000000000..e9c74f38b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/companion/CompanionSyncKey.java @@ -0,0 +1,18 @@ +package it.auties.whatsapp.model.companion; + +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.sync.AppStateSyncKey; + +import java.util.LinkedList; + +public record CompanionSyncKey( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Jid companion, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + LinkedList keys +) implements ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/contact/Contact.java b/src/main/java/it/auties/whatsapp/model/contact/Contact.java new file mode 100644 index 000000000..b0ba77f1b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/contact/Contact.java @@ -0,0 +1,181 @@ +package it.auties.whatsapp.model.contact; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +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 it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * 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 JidProvider, ProtobufMessage { + /** + * The non-null unique jid used to identify this contact + */ + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + private final Jid jid; + + /** + * The nullable name specified by this contact when he created a Whatsapp account. Theoretically, + * it should not be possible for this field to be null as it's required when registering for + * Whatsapp. Though it looks that it can be removed later, so it's nullable. + */ + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + private String chosenName; + + /** + * The nullable name associated with this contact on the phone connected with Whatsapp + */ + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + private String fullName; + + /** + * The nullable short name associated with this contact on the phone connected with Whatsapp If a + * name is available, theoretically, also a short name should be + */ + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + private String shortName; + + /** + * The nullable last known presence of this contact. This field is associated only with the + * presence of this contact in the corresponding conversation. If, for example, this contact is + * 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(JidProvider)}. + */ + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + private ContactStatus lastKnownPresence; + + /** + * The nullable last seconds this contact was seen available. Any contact can decide to hide this + * information in their privacy settings. + */ + @ProtobufProperty(index = 6, type = ProtobufType.UINT64) + private Long lastSeenSeconds; + + /** + * Whether this contact is blocked + */ + @ProtobufProperty(index = 7, type = ProtobufType.BOOL) + private boolean blocked; + + public Contact(Jid jid) { + this.jid = jid; + this.lastKnownPresence = ContactStatus.UNAVAILABLE; + } + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public Contact(Jid jid, String chosenName, String fullName, String shortName, ContactStatus lastKnownPresence, Long lastSeenSeconds, boolean blocked) { + this.jid = jid; + this.chosenName = chosenName; + this.fullName = fullName; + this.shortName = shortName; + this.lastKnownPresence = lastKnownPresence; + this.lastSeenSeconds = lastSeenSeconds; + this.blocked = blocked; + } + + public Jid jid() { + return this.jid; + } + + public String name() { + if (shortName != null) { + return shortName; + } + + if (fullName != null) { + return fullName; + } + + if (chosenName != null) { + return chosenName; + } + + return jid().user(); + } + + public OptionalLong lastSeenSeconds() { + return Clock.parseTimestamp(lastSeenSeconds); + } + + public Optional lastSeen() { + return Clock.parseSeconds(lastSeenSeconds); + } + + public Optional chosenName() { + return Optional.ofNullable(this.chosenName); + } + + public Optional fullName() { + return Optional.ofNullable(this.fullName); + } + + public Optional shortName() { + return Optional.ofNullable(this.shortName); + } + + public ContactStatus lastKnownPresence() { + return this.lastKnownPresence; + } + + public boolean blocked() { + return this.blocked; + } + + public Contact setChosenName(String chosenName) { + this.chosenName = chosenName; + return this; + } + + public Contact setFullName(String fullName) { + this.fullName = fullName; + return this; + } + + public Contact setShortName(String shortName) { + this.shortName = shortName; + return this; + } + + public Contact setLastKnownPresence(ContactStatus lastKnownPresence) { + this.lastKnownPresence = lastKnownPresence; + return this; + } + + public Contact setLastSeen(ZonedDateTime lastSeen) { + this.lastSeenSeconds = lastSeen.toEpochSecond(); + return this; + } + + public Contact setBlocked(boolean blocked) { + this.blocked = blocked; + return this; + } + + @Override + public int hashCode() { + return Objects.hashCode(this.jid()); + } + + public boolean equals(Object other) { + return other instanceof Contact that && Objects.equals(this.jid(), that.jid()); + } + + @Override + public 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 new file mode 100644 index 000000000..50581bfde --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/contact/ContactCard.java @@ -0,0 +1,161 @@ +package it.auties.whatsapp.model.contact; + +import ezvcard.Ezvcard; +import ezvcard.VCard; +import ezvcard.VCardVersion; +import ezvcard.property.SimpleProperty; +import ezvcard.property.Telephone; +import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.whatsapp.model.jid.Jid; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static it.auties.whatsapp.util.Specification.Whatsapp.*; + + +/** + * A model class to represent and build the vcard of a contact + */ +public sealed interface ContactCard { + @ProtobufConverter + static ContactCard ofNullable(String vcard) { + return vcard == null ? null : of(vcard); + } + + /** + * Parses a vcard + * If the vCard dependency wasn't included, or a parsing error occurs, a raw representation is returned + * + * @param vcard the non-null vcard to parse + * @return a non-null vcard + */ + static ContactCard of(String vcard) { + try { + var parsed = Ezvcard.parse(vcard).first(); + var version = Objects.requireNonNullElse(parsed.getVersion().getVersion(), VCardVersion.V3_0.getVersion()); + var name = Optional.ofNullable(parsed.getFormattedName().getValue()); + var phoneNumbers = parsed.getTelephoneNumbers() + .stream() + .filter(ContactCard::isValidPhoneNumber) + .collect(Collectors.toUnmodifiableMap(ContactCard::getPhoneType, ContactCard::getPhoneValue, ContactCard::joinPhoneNumbers)); + var businessName = Optional.ofNullable(parsed.getExtendedProperty(BUSINESS_NAME_VCARD_PROPERTY)) + .map(SimpleProperty::getValue); + return new Parsed(version, name, phoneNumbers, businessName); + } catch (Throwable ignored) { + return new Raw(vcard); + } + } + + /** + * Creates a new vcard + * + * @param name the nullable name of the contact + * @param phoneNumber the non-null phone number of the contact + * @return a vcard + */ + static ContactCard of(String name, Jid phoneNumber) { + return of(name, phoneNumber, null); + } + + /** + * Creates a new vcard + * + * @param name the nullable name of the contact + * @param phoneNumber the non-null phone number of the contact + * @param businessName the nullable business name of the contact + * @return a vcard + */ + static ContactCard of(String name, Jid phoneNumber, String businessName) { + return new Parsed( + VCardVersion.V3_0.getVersion(), + Optional.ofNullable(name), + new HashMap<>(Map.of(DEFAULT_NUMBER_VCARD_TYPE, List.of(Objects.requireNonNull(phoneNumber)))), + Optional.ofNullable(businessName) + ); + } + + private static boolean isValidPhoneNumber(Telephone entry) { + return getPhoneType(entry) != null && entry.getParameter(PHONE_NUMBER_VCARD_PROPERTY) != null; + } + + private static String getPhoneType(Telephone entry) { + return entry.getParameters().getType(); + } + + private static List getPhoneValue(Telephone entry) { + return List.of(Jid.of(entry.getParameter(PHONE_NUMBER_VCARD_PROPERTY))); + } + + private static List joinPhoneNumbers(List first, List second) { + return Stream.of(first, second).flatMap(Collection::stream).toList(); + } + + @ProtobufConverter + String toVcard(); + + /** + * A parsed representation of the vcard + */ + record Parsed( + String version, + Optional name, + Map> phoneNumbers, + Optional businessName + ) implements ContactCard { + public List defaultPhoneNumbers() { + return Objects.requireNonNullElseGet(phoneNumbers.get(DEFAULT_NUMBER_VCARD_TYPE), List::of); + } + + private void addPhoneNumber(VCard vcard, String type, Jid contact) { + var telephone = new Telephone(contact.toPhoneNumber()); + telephone.getParameters().setType(type); + telephone.getParameters().put(PHONE_NUMBER_VCARD_PROPERTY, contact.user()); + vcard.addTelephoneNumber(telephone); + } + + public void addPhoneNumber(Jid contact) { + addPhoneNumber(DEFAULT_NUMBER_VCARD_TYPE, contact); + } + + public void addPhoneNumber(String category, Jid contact) { + var oldValue = phoneNumbers.get(category); + if (oldValue == null) { + phoneNumbers.put(category, List.of(contact)); + return; + } + + var values = new ArrayList<>(oldValue); + values.add(contact); + phoneNumbers.put(category, Collections.unmodifiableList(values)); + } + + /** + * Converts this object in a valid vcard + * + * @return a non-null String + */ + @Override + @ProtobufConverter + public String toVcard() { + var vcard = new VCard(); + vcard.setVersion(VCardVersion.valueOfByStr(version())); + vcard.setFormattedName(name.orElse(null)); + phoneNumbers().forEach((type, contacts) -> contacts.forEach(contact -> addPhoneNumber(vcard, type, contact))); + businessName.ifPresent(value -> vcard.addExtendedProperty(BUSINESS_NAME_VCARD_PROPERTY, value)); + return Ezvcard.write(vcard).go(); + } + } + + /** + * A raw representation of the vcard + */ + record Raw(String toVcard) implements ContactCard { + @Override + @ProtobufConverter + public String toVcard() { + return toVcard; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/contact/ContactStatus.java b/src/main/java/it/auties/whatsapp/model/contact/ContactStatus.java new file mode 100644 index 000000000..caf4c3944 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/contact/ContactStatus.java @@ -0,0 +1,52 @@ +package it.auties.whatsapp.model.contact; + + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.model.ProtobufEnum; + +import java.util.Arrays; +import java.util.Optional; + +/** + * The constants of this enumerated type describe the various status that a {@link Contact} can be + * in + */ +public enum ContactStatus implements ProtobufEnum { + /** + * When the contact is online + */ + AVAILABLE(0), + /** + * When the contact is offline + */ + UNAVAILABLE(1), + /** + * When the contact is writing a text message + */ + COMPOSING(2), + /** + * When the contact is recording an audio message + */ + RECORDING(3); + + final int index; + + ContactStatus(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + + public static Optional of(String name) { + return Arrays.stream(values()) + .filter(entry -> entry.name().equalsIgnoreCase(name)) + .findFirst(); + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/info/AdReplyInfo.java b/src/main/java/it/auties/whatsapp/model/info/AdReplyInfo.java new file mode 100644 index 000000000..6a312950b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/AdReplyInfo.java @@ -0,0 +1,57 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + + +/** + * A model class that holds the information related to an companion reply. + */ +@ProtobufMessageName("ContextInfo.AdReplyInfo") +public record AdReplyInfo( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String advertiserName, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + MediaType mediaType, + @ProtobufProperty(index = 16, type = ProtobufType.BYTES) + Optional thumbnail, + @ProtobufProperty(index = 17, type = ProtobufType.STRING) + Optional caption +) implements Info, ProtobufMessage { + + /** + * The constants of this enumerated type describe the various types of companion that a + * {@link AdReplyInfo} can link to + */ + @ProtobufMessageName("ContextInfo.AdReplyInfo.MediaType") + public enum MediaType implements ProtobufEnum { + /** + * Unknown type + */ + NONE(0), + /** + * Image type + */ + IMAGE(1), + /** + * Video type + */ + VIDEO(2); + + final int index; + + MediaType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/BusinessAccountLinkInfo.java b/src/main/java/it/auties/whatsapp/model/info/BusinessAccountLinkInfo.java new file mode 100644 index 000000000..ce0a5710e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/BusinessAccountLinkInfo.java @@ -0,0 +1,86 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * A model class that holds a payload about a business link info. + */ +@ProtobufMessageName("BizAccountLinkInfo") +public record BusinessAccountLinkInfo( + @ProtobufProperty(index = 1, type = ProtobufType.UINT64) + long businessId, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String phoneNumber, + @ProtobufProperty(index = 3, type = ProtobufType.UINT64) + long issueTimeSeconds, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + HostStorageType hostStorage, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + AccountType accountType +) implements ProtobufMessage { + /** + * Returns this object's timestampSeconds + * + * @return an optional + */ + public Optional issueTime() { + return Clock.parseSeconds(issueTimeSeconds); + } + + /** + * The constants of this enumerated type describe the various types of business accounts + */ + @ProtobufMessageName("BizAccountLinkInfo.AccountType") + public enum AccountType implements ProtobufEnum { + /** + * Enterprise + */ + ENTERPRISE(0), + /** + * Page + */ + PAGE(1); + + final int index; + + AccountType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + @ProtobufMessageName("BizAccountLinkInfo.HostStorageType") + public enum HostStorageType implements ProtobufEnum { + /** + * Hosted on a private server ("On-Premise") + */ + ON_PREMISE(0), + + /** + * Hosted by facebook + */ + FACEBOOK(1); + + final int index; + + HostStorageType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/BusinessIdentityInfo.java b/src/main/java/it/auties/whatsapp/model/info/BusinessIdentityInfo.java new file mode 100644 index 000000000..8684624c6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/BusinessIdentityInfo.java @@ -0,0 +1,126 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificate; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + + +/** + * A model class that holds the information related to the identity of a business account. + */ +@ProtobufMessageName("BizIdentityInfo") +public record BusinessIdentityInfo( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + VerifiedLevel level, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + BusinessVerifiedNameCertificate certificate, + @ProtobufProperty(index = 3, type = ProtobufType.BOOL) + boolean signed, + @ProtobufProperty(index = 4, type = ProtobufType.BOOL) + boolean revoked, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + HostStorageType hostStorage, + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + ActorsType actualActors, + @ProtobufProperty(index = 7, type = ProtobufType.UINT64) + long privacyModeTimestampSeconds, + @ProtobufProperty(index = 8, type = ProtobufType.UINT64) + long featureControls +) implements Info, ProtobufMessage { + /** + * Returns the privacy mode timestampSeconds + * + * @return an optional + */ + public Optional privacyModeTimestamp() { + return Clock.parseSeconds(privacyModeTimestampSeconds); + } + + /** + * The constants of this enumerated type describe the various types of actors of a business account + */ + @ProtobufMessageName("BizIdentityInfo.ActualActorsType") + public enum ActorsType implements ProtobufEnum { + /** + * Self + */ + SELF(0), + /** + * Bsp + */ + BSP(1); + + final int index; + + ActorsType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + /** + * The constants of this enumerated type describe the various types of verification that a business + * account can have + */ + @ProtobufMessageName("BizIdentityInfo.VerifiedLevelValue") + public enum VerifiedLevel implements ProtobufEnum { + /** + * Unknown + */ + UNKNOWN(0), + + /** + * Low + */ + LOW(1), + + /** + * High + */ + HIGH(2); + + final int index; + + VerifiedLevel(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + @ProtobufMessageName("BizIdentityInfo.HostStorageType") + public enum HostStorageType implements ProtobufEnum { + /** + * Hosted on a private server ("On-Premise") + */ + ON_PREMISE(0), + + /** + * Hosted by facebook + */ + FACEBOOK(1); + + final int index; + + HostStorageType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java new file mode 100644 index 000000000..c73ee00a9 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/ChatMessageInfo.java @@ -0,0 +1,736 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +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.jid.Jid; +import it.auties.whatsapp.model.media.MediaData; +import it.auties.whatsapp.model.message.model.*; +import it.auties.whatsapp.model.message.standard.LiveLocationMessage; +import it.auties.whatsapp.model.message.standard.ReactionMessage; +import it.auties.whatsapp.model.poll.PollAdditionalMetadata; +import it.auties.whatsapp.model.poll.PollUpdate; +import it.auties.whatsapp.model.sync.PhotoChange; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.*; + +import static java.util.Objects.requireNonNullElseGet; + +/** + * A model class that holds the information related to a {@link Message}. + */ +@ProtobufMessageName("WebMessageInfo") +public final class ChatMessageInfo implements MessageInfo, MessageStatusInfo, ProtobufMessage { + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + private final ChatMessageKey key; + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + private MessageContainer message; + @ProtobufProperty(index = 3, type = ProtobufType.UINT64) + private final long timestampSeconds; + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + private MessageStatus status; + @ProtobufProperty(index = 5, type = ProtobufType.STRING) + private final Jid senderJid; + @ProtobufProperty(index = 6, type = ProtobufType.UINT64) + private final long messageC2STimestamp; + @ProtobufProperty(index = 16, type = ProtobufType.BOOL) + private boolean ignore; + @ProtobufProperty(index = 17, type = ProtobufType.BOOL) + private boolean starred; + @ProtobufProperty(index = 18, type = ProtobufType.BOOL) + private final boolean broadcast; + @ProtobufProperty(index = 19, type = ProtobufType.STRING) + private final String pushName; + @ProtobufProperty(index = 20, type = ProtobufType.BYTES) + private final byte[] mediaCiphertextSha256; + @ProtobufProperty(index = 21, type = ProtobufType.BOOL) + private final boolean multicast; + @ProtobufProperty(index = 22, type = ProtobufType.BOOL) + private final boolean urlText; + @ProtobufProperty(index = 23, type = ProtobufType.BOOL) + private final boolean urlNumber; + @ProtobufProperty(index = 24, type = ProtobufType.OBJECT) + private final StubType stubType; + @ProtobufProperty(index = 25, type = ProtobufType.BOOL) + private final boolean clearMedia; + @ProtobufProperty(index = 26, type = ProtobufType.STRING) + private final List stubParameters; + @ProtobufProperty(index = 27, type = ProtobufType.UINT32) + private final int duration; + @ProtobufProperty(index = 28, type = ProtobufType.STRING) + private final List labels; + @ProtobufProperty(index = 29, type = ProtobufType.OBJECT) + private final PaymentInfo paymentInfo; + @ProtobufProperty(index = 30, type = ProtobufType.OBJECT) + private final LiveLocationMessage finalLiveLocation; + @ProtobufProperty(index = 31, type = ProtobufType.OBJECT) + private final PaymentInfo quotedPaymentInfo; + @ProtobufProperty(index = 32, type = ProtobufType.UINT64) + private final long ephemeralStartTimestamp; + @ProtobufProperty(index = 33, type = ProtobufType.UINT32) + private final int ephemeralDuration; + @ProtobufProperty(index = 34, type = ProtobufType.BOOL) + private final boolean enableEphemeral; + @ProtobufProperty(index = 35, type = ProtobufType.BOOL) + private final boolean ephemeralOutOfSync; + @ProtobufProperty(index = 36, type = ProtobufType.OBJECT) + private final BusinessPrivacyStatus businessPrivacyStatus; + @ProtobufProperty(index = 37, type = ProtobufType.STRING) + private final String businessVerifiedName; + @ProtobufProperty(index = 38, type = ProtobufType.OBJECT) + private final MediaData mediaData; + @ProtobufProperty(index = 39, type = ProtobufType.OBJECT) + private final PhotoChange photoChange; + @ProtobufProperty(index = 40, type = ProtobufType.OBJECT) + private final MessageReceipt receipt; + @ProtobufProperty(index = 41, type = ProtobufType.OBJECT) + private final List reactions; + @ProtobufProperty(index = 42, type = ProtobufType.OBJECT) + private final MediaData quotedStickerData; + @ProtobufProperty(index = 43, type = ProtobufType.BYTES) + private final byte[] futureProofData; + @ProtobufProperty(index = 44, type = ProtobufType.OBJECT) + private final PublicServiceAnnouncementStatus psaStatus; + @ProtobufProperty(index = 45, type = ProtobufType.OBJECT) + private final List pollUpdates; + @ProtobufProperty(index = 46, type = ProtobufType.OBJECT) + private PollAdditionalMetadata pollAdditionalMetadata; + @ProtobufProperty(index = 47, type = ProtobufType.STRING) + private final String agentId; + @ProtobufProperty(index = 48, type = ProtobufType.BOOL) + private final boolean statusAlreadyViewed; + @ProtobufProperty(index = 49, type = ProtobufType.BYTES) + private byte[] messageSecret; + @ProtobufProperty(index = 50, type = ProtobufType.OBJECT) + private final KeepInChat keepInChat; + @ProtobufProperty(index = 51, type = ProtobufType.STRING) + private final Jid originalSender; + @ProtobufProperty(index = 52, type = ProtobufType.UINT64) + private long revokeTimestampSeconds; + @JsonBackReference + private Chat chat; + + private Contact sender; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ChatMessageInfo(ChatMessageKey key, MessageContainer message, long timestampSeconds, 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, 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, Chat chat, Contact sender) { + this.key = key; + this.message = message; + this.timestampSeconds = timestampSeconds; + this.status = status; + this.senderJid = senderJid; + this.messageC2STimestamp = messageC2STimestamp; + this.ignore = ignore; + this.starred = starred; + this.broadcast = broadcast; + this.pushName = pushName; + this.mediaCiphertextSha256 = mediaCiphertextSha256; + this.multicast = multicast; + this.urlText = urlText; + this.urlNumber = urlNumber; + this.stubType = stubType; + this.clearMedia = clearMedia; + this.stubParameters = stubParameters; + this.duration = duration; + this.labels = labels; + this.paymentInfo = paymentInfo; + this.finalLiveLocation = finalLiveLocation; + this.quotedPaymentInfo = quotedPaymentInfo; + this.ephemeralStartTimestamp = ephemeralStartTimestamp; + this.ephemeralDuration = ephemeralDuration; + this.enableEphemeral = enableEphemeral; + this.ephemeralOutOfSync = ephemeralOutOfSync; + this.businessPrivacyStatus = businessPrivacyStatus; + this.businessVerifiedName = businessVerifiedName; + this.mediaData = mediaData; + this.photoChange = photoChange; + this.receipt = receipt; + this.reactions = reactions; + this.quotedStickerData = quotedStickerData; + this.futureProofData = futureProofData; + this.psaStatus = psaStatus; + this.pollUpdates = pollUpdates; + this.pollAdditionalMetadata = pollAdditionalMetadata; + this.agentId = agentId; + this.statusAlreadyViewed = statusAlreadyViewed; + this.messageSecret = messageSecret; + this.keepInChat = keepInChat; + this.originalSender = originalSender; + this.revokeTimestampSeconds = revokeTimestampSeconds; + this.chat = chat; + this.sender = sender; + } + + + public ChatMessageInfo(ChatMessageKey key, MessageContainer message, long timestampSeconds, 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, 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; + this.status = status; + this.senderJid = senderJid; + this.messageC2STimestamp = messageC2STimestamp; + this.ignore = ignore; + this.starred = starred; + this.broadcast = broadcast; + this.pushName = pushName; + this.mediaCiphertextSha256 = mediaCiphertextSha256; + this.multicast = multicast; + this.urlText = urlText; + this.urlNumber = urlNumber; + this.stubType = stubType; + this.clearMedia = clearMedia; + this.stubParameters = stubParameters; + this.duration = duration; + this.labels = labels; + this.paymentInfo = paymentInfo; + this.finalLiveLocation = finalLiveLocation; + this.quotedPaymentInfo = quotedPaymentInfo; + this.ephemeralStartTimestamp = ephemeralStartTimestamp; + this.ephemeralDuration = ephemeralDuration; + this.enableEphemeral = enableEphemeral; + this.ephemeralOutOfSync = ephemeralOutOfSync; + this.businessPrivacyStatus = businessPrivacyStatus; + this.businessVerifiedName = businessVerifiedName; + this.mediaData = mediaData; + this.photoChange = photoChange; + this.receipt = Objects.requireNonNullElseGet(receipt, MessageReceipt::new); + this.reactions = reactions; + this.quotedStickerData = quotedStickerData; + this.futureProofData = futureProofData; + this.psaStatus = psaStatus; + this.pollUpdates = pollUpdates; + this.pollAdditionalMetadata = pollAdditionalMetadata; + this.agentId = agentId; + this.statusAlreadyViewed = statusAlreadyViewed; + this.messageSecret = messageSecret; + this.keepInChat = keepInChat; + this.originalSender = originalSender; + this.revokeTimestampSeconds = revokeTimestampSeconds; + } + + /** + * Determines whether the message was sent by you or by someone else + * + * @return a boolean + */ + public boolean fromMe() { + return key.fromMe(); + } + + /** + * Returns the name of the chat where this message is or its pretty jid + * + * @return a non-null String + */ + public String chatName() { + if (chat != null) { + return chat.name(); + } + + return chatJid().user(); + } + + /** + * Returns the jid of the contact or group that sent the message. + * + * @return a non-null ContactJid + */ + public Jid chatJid() { + return key.chatJid(); + } + + /** + * Returns the name of the person that sent this message or its pretty jid + * + * @return a non-null String + */ + public String senderName() { + return sender().map(Contact::name).orElseGet(senderJid()::user); + } + + /** + * Returns the message quoted by this message + * + * @return a non-empty optional {@link ChatMessageInfo} if this message quotes a message in memory + */ + public Optional quotedMessage() { + return Optional.of(message) + .flatMap(MessageContainer::contentWithContext) + .flatMap(ContextualMessage::contextInfo) + .flatMap(QuotedMessageInfo::of); + } + + /** + * Returns the timestampSeconds for this message + * + * @return an optional + */ + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } + + /** + * Returns the timestampSeconds for this message + * + * @return an optional + */ + public Optional revokeTimestamp() { + return Clock.parseSeconds(revokeTimestampSeconds); + } + + @Override + public int hashCode() { + return Objects.hashCode(id()); + } + + public boolean equals(Object object) { + return object instanceof ChatMessageInfo that + && Objects.equals(this.id(), that.id()) + && Objects.equals(this.stubType, that.stubType); + } + + /** + * Returns the id of the message + * + * @return a non-null String + */ + public String id() { + return key.id(); + } + + /** + * Returns the jid of the sender + * + * @return a non-null ContactJid + */ + public Jid senderJid() { + return requireNonNullElseGet(senderJid, () -> key.senderJid().orElseGet(key::chatJid)); + } + + @Override + public Jid parentJid() { + return chatJid(); + } + + public ChatMessageKey key() { + return key; + } + + @Override + public MessageContainer message() { + return message; + } + + public ChatMessageInfo setMessage(MessageContainer message) { + this.message = message; + return this; + } + + public OptionalLong timestampSeconds() { + return Clock.parseTimestamp(timestampSeconds); + } + + @Override + public MessageStatus status() { + return status; + } + + public long messageC2STimestamp() { + return messageC2STimestamp; + } + + public boolean ignore() { + return ignore; + } + + public ChatMessageInfo setIgnore(boolean ignore) { + this.ignore = ignore; + return this; + } + + public boolean starred() { + return starred; + } + + public boolean broadcast() { + return broadcast; + } + + public Optional pushName() { + return Optional.ofNullable(pushName); + } + + public Optional mediaCiphertextSha256() { + return Optional.ofNullable(mediaCiphertextSha256); + } + + public boolean multicast() { + return multicast; + } + + public boolean urlText() { + return urlText; + } + + public boolean urlNumber() { + return urlNumber; + } + + public Optional stubType() { + return Optional.ofNullable(stubType); + } + + public boolean clearMedia() { + return clearMedia; + } + + public List stubParameters() { + return stubParameters; + } + + public int duration() { + return duration; + } + + public List labels() { + return labels; + } + + public Optional paymentInfo() { + return Optional.ofNullable(paymentInfo); + } + + public Optional finalLiveLocation() { + return Optional.ofNullable(finalLiveLocation); + } + + public Optional quotedPaymentInfo() { + return Optional.ofNullable(quotedPaymentInfo); + } + + public long ephemeralStartTimestamp() { + return ephemeralStartTimestamp; + } + + public int ephemeralDuration() { + return ephemeralDuration; + } + + public boolean enableEphemeral() { + return enableEphemeral; + } + + public boolean ephemeralOutOfSync() { + return ephemeralOutOfSync; + } + + public Optional businessPrivacyStatus() { + return Optional.ofNullable(businessPrivacyStatus); + } + + public Optional businessVerifiedName() { + return Optional.ofNullable(businessVerifiedName); + } + + public Optional mediaData() { + return Optional.ofNullable(mediaData); + } + + public Optional photoChange() { + return Optional.ofNullable(photoChange); + } + + public MessageReceipt receipt() { + return receipt; + } + + public List reactions() { + return reactions; + } + + public Optional quotedStickerData() { + return Optional.ofNullable(quotedStickerData); + } + + public byte[] futureProofData() { + return futureProofData; + } + + public Optional psaStatus() { + return Optional.ofNullable(psaStatus); + } + + public List pollUpdates() { + return pollUpdates; + } + + public Optional pollAdditionalMetadata() { + return Optional.ofNullable(pollAdditionalMetadata); + } + + public ChatMessageInfo setPollAdditionalMetadata(PollAdditionalMetadata pollAdditionalMetadata) { + this.pollAdditionalMetadata = pollAdditionalMetadata; + return this; + } + + public Optional agentId() { + return Optional.ofNullable(agentId); + } + + public boolean statusAlreadyViewed() { + return statusAlreadyViewed; + } + + public Optional messageSecret() { + return Optional.ofNullable(messageSecret); + } + + public ChatMessageInfo setMessageSecret(byte[] messageSecret) { + this.messageSecret = messageSecret; + return this; + } + + public Optional keepInChat() { + return Optional.ofNullable(keepInChat); + } + + public Optional originalSender() { + return Optional.ofNullable(originalSender); + } + + public long revokeTimestampSeconds() { + return revokeTimestampSeconds; + } + + public Optional chat() { + return Optional.ofNullable(chat); + } + + public ChatMessageInfo setChat(Chat chat) { + this.chat = chat; + return this; + } + + public Optional sender() { + return Optional.ofNullable(sender); + } + + public ChatMessageInfo setSender(Contact sender) { + this.sender = sender; + return this; + } + + @Override + public ChatMessageInfo setStatus(MessageStatus status) { + this.status = status; + return this; + } + + public ChatMessageInfo setStarred(boolean starred) { + this.starred = starred; + return this; + } + + public ChatMessageInfo setRevokeTimestampSeconds(long revokeTimestampSeconds) { + this.revokeTimestampSeconds = revokeTimestampSeconds; + return this; + } + + /** + * The constants of this enumerated type describe the various types of server message that a {@link ChatMessageInfo} can describe + */ + public enum StubType implements ProtobufEnum { + UNKNOWN(0, List.of("unknown")), + REVOKE(1, List.of("revoked")), + CIPHERTEXT(2, List.of("ciphertext")), + FUTUREPROOF(3, List.of("phone")), + NON_VERIFIED_TRANSITION(4, List.of("non_verified_transition")), + UNVERIFIED_TRANSITION(5, List.of("unverified_transition")), + VERIFIED_TRANSITION(6, List.of("verified_transition")), + VERIFIED_LOW_UNKNOWN(7, List.of("verified_low_unknown")), + VERIFIED_HIGH(8, List.of("verified_high")), + VERIFIED_INITIAL_UNKNOWN(9, List.of("verified_initial_unknown")), + VERIFIED_INITIAL_LOW(10, List.of("verified_initial_low")), + VERIFIED_INITIAL_HIGH(11, List.of("verified_initial_high")), + VERIFIED_TRANSITION_ANY_TO_NONE(12, List.of("verified_transition_any_to_none")), + VERIFIED_TRANSITION_ANY_TO_HIGH(13, List.of("verified_transition_any_to_high")), + VERIFIED_TRANSITION_HIGH_TO_LOW(14, List.of("verified_transition_high_to_low")), + VERIFIED_TRANSITION_HIGH_TO_UNKNOWN(15, List.of("verified_transition_high_to_unknown")), + VERIFIED_TRANSITION_UNKNOWN_TO_LOW(16, List.of("verified_transition_unknown_to_low")), + VERIFIED_TRANSITION_LOW_TO_UNKNOWN(17, List.of("verified_transition_low_to_unknown")), + VERIFIED_TRANSITION_NONE_TO_LOW(18, List.of("verified_transition_none_to_low")), + VERIFIED_TRANSITION_NONE_TO_UNKNOWN(19, List.of("verified_transition_none_to_unknown")), + GROUP_CREATE(20, List.of("create")), + GROUP_CHANGE_SUBJECT(21, List.of("subject")), + GROUP_CHANGE_ICON(22, List.of("picture")), + GROUP_CHANGE_INVITE_LINK(23, List.of("revoke_invite")), + GROUP_CHANGE_DESCRIPTION(24, List.of("description")), + GROUP_CHANGE_RESTRICT(25, List.of("restrict", "locked", "unlocked")), + GROUP_CHANGE_ANNOUNCE(26, List.of("announce", "announcement", "not_announcement")), + GROUP_PARTICIPANT_ADD(27, List.of("add")), + GROUP_PARTICIPANT_REMOVE(28, List.of("remove")), + GROUP_PARTICIPANT_PROMOTE(29, List.of("promote")), + GROUP_PARTICIPANT_DEMOTE(30, List.of("demote")), + GROUP_PARTICIPANT_INVITE(31, List.of("invite")), + GROUP_PARTICIPANT_LEAVE(32, List.of("leave")), + GROUP_PARTICIPANT_CHANGE_NUMBER(33, List.of("modify")), + BROADCAST_CREATE(34, List.of("create")), + BROADCAST_ADD(35, List.of("add")), + BROADCAST_REMOVE(36, List.of("remove")), + GENERIC_NOTIFICATION(37, List.of("notification")), + E2E_IDENTITY_CHANGED(38, List.of("identity")), + E2E_ENCRYPTED(39, List.of("encrypt")), + CALL_MISSED_VOICE(40, List.of("miss")), + CALL_MISSED_VIDEO(41, List.of("miss_video")), + INDIVIDUAL_CHANGE_NUMBER(42, List.of("change_number")), + GROUP_DELETE(43, List.of("delete")), + GROUP_ANNOUNCE_MODE_MESSAGE_BOUNCE(44, List.of("announce_msg_bounce")), + CALL_MISSED_GROUP_VOICE(45, List.of("miss_group")), + CALL_MISSED_GROUP_VIDEO(46, List.of("miss_group_video")), + PAYMENT_CIPHERTEXT(47, List.of("ciphertext")), + PAYMENT_FUTUREPROOF(48, List.of("futureproof")), + PAYMENT_TRANSACTION_STATUS_UPDATE_FAILED(49, List.of("payment_transaction_status_update_failed")), + PAYMENT_TRANSACTION_STATUS_UPDATE_REFUNDED(50, List.of("payment_transaction_status_update_refunded")), + PAYMENT_TRANSACTION_STATUS_UPDATE_REFUND_FAILED(51, List.of("payment_transaction_status_update_refund_failed")), + PAYMENT_TRANSACTION_STATUS_RECEIVER_PENDING_SETUP(52, List.of("payment_transaction_status_receiver_pending_setup")), + PAYMENT_TRANSACTION_STATUS_RECEIVER_SUCCESS_AFTER_HICCUP(53, List.of("payment_transaction_status_receiver_success_after_hiccup")), + PAYMENT_ACTION_ACCOUNT_SETUP_REMINDER(54, List.of("payment_action_account_setup_reminder")), + PAYMENT_ACTION_SEND_PAYMENT_REMINDER(55, List.of("payment_action_send_payment_reminder")), + PAYMENT_ACTION_SEND_PAYMENT_INVITATION(56, List.of("payment_action_send_payment_invitation")), + PAYMENT_ACTION_REQUEST_DECLINED(57, List.of("payment_action_request_declined")), + PAYMENT_ACTION_REQUEST_EXPIRED(58, List.of("payment_action_request_expired")), + PAYMENT_ACTION_REQUEST_CANCELLED(59, List.of("payment_transaction_request_cancelled")), + BIZ_VERIFIED_TRANSITION_TOP_TO_BOTTOM(60, List.of("biz_verified_transition_top_to_bottom")), + BIZ_VERIFIED_TRANSITION_BOTTOM_TO_TOP(61, List.of("biz_verified_transition_bottom_to_top")), + BIZ_INTRO_TOP(62, List.of("biz_intro_top")), + BIZ_INTRO_BOTTOM(63, List.of("biz_intro_bottom")), + BIZ_NAME_CHANGE(64, List.of("biz_name_change")), + BIZ_MOVE_TO_CONSUMER_APP(65, List.of("biz_move_to_consumer_app")), + BIZ_TWO_TIER_MIGRATION_TOP(66, List.of("biz_two_tier_migration_top")), + BIZ_TWO_TIER_MIGRATION_BOTTOM(67, List.of("biz_two_tier_migration_bottom")), + OVERSIZED(68, List.of("oversized")), + GROUP_CHANGE_NO_FREQUENTLY_FORWARDED(69, List.of("frequently_forwarded_ok", "no_frequently_forwarded")), + GROUP_V4_ADD_INVITE_SENT(70, List.of("v4_add_invite_sent")), + GROUP_PARTICIPANT_ADD_REQUEST_JOIN(71, List.of("v4_add_invite_join")), + CHANGE_EPHEMERAL_SETTING(72, List.of("ephemeral", "not_ephemeral")), + E2E_DEVICE_CHANGED(73, List.of("device")), + VIEWED_ONCE(74, List.of()), + E2E_ENCRYPTED_NOW(75, List.of("encrypt_now")), + BLUE_MSG_BSP_FB_TO_BSP_PREMISE(76, List.of("blue_msg_bsp_fb_to_bsp_premise")), + BLUE_MSG_BSP_FB_TO_SELF_FB(77, List.of("blue_msg_bsp_fb_to_self_fb")), + BLUE_MSG_BSP_FB_TO_SELF_PREMISE(78, List.of("blue_msg_bsp_fb_to_self_premise")), + BLUE_MSG_BSP_FB_UNVERIFIED(79, List.of("blue_msg_bsp_fb_unverified")), + BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(80, List.of("blue_msg_bsp_fb_unverified_to_self_premise_verified")), + BLUE_MSG_BSP_FB_VERIFIED(81, List.of("blue_msg_bsp_fb_verified")), + BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(82, List.of("blue_msg_bsp_fb_verified_to_self_premise_unverified")), + BLUE_MSG_BSP_PREMISE_TO_SELF_PREMISE(83, List.of("blue_msg_bsp_premise_to_self_premise")), + BLUE_MSG_BSP_PREMISE_UNVERIFIED(84, List.of("blue_msg_bsp_premise_unverified")), + BLUE_MSG_BSP_PREMISE_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(85, List.of("blue_msg_bsp_premise_unverified_to_self_premise_verified")), + BLUE_MSG_BSP_PREMISE_VERIFIED(86, List.of("blue_msg_bsp_premise_verified")), + BLUE_MSG_BSP_PREMISE_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(87, List.of("blue_msg_bsp_premise_verified_to_self_premise_unverified")), + BLUE_MSG_CONSUMER_TO_BSP_FB_UNVERIFIED(88, List.of("blue_msg_consumer_to_bsp_fb_unverified")), + BLUE_MSG_CONSUMER_TO_BSP_PREMISE_UNVERIFIED(89, List.of("blue_msg_consumer_to_bsp_premise_unverified")), + BLUE_MSG_CONSUMER_TO_SELF_FB_UNVERIFIED(90, List.of("blue_msg_consumer_to_self_fb_unverified")), + BLUE_MSG_CONSUMER_TO_SELF_PREMISE_UNVERIFIED(91, List.of("blue_msg_consumer_to_self_premise_unverified")), + BLUE_MSG_SELF_FB_TO_BSP_PREMISE(92, List.of("blue_msg_self_fb_to_bsp_premise")), + BLUE_MSG_SELF_FB_TO_SELF_PREMISE(93, List.of("blue_msg_self_fb_to_self_premise")), + BLUE_MSG_SELF_FB_UNVERIFIED(94, List.of("blue_msg_self_fb_unverified")), + BLUE_MSG_SELF_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(95, List.of("blue_msg_self_fb_unverified_to_self_premise_verified")), + BLUE_MSG_SELF_FB_VERIFIED(96, List.of("blue_msg_self_fb_verified")), + BLUE_MSG_SELF_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(97, List.of("blue_msg_self_fb_verified_to_self_premise_unverified")), + BLUE_MSG_SELF_PREMISE_TO_BSP_PREMISE(98, List.of("blue_msg_self_premise_to_bsp_premise")), + BLUE_MSG_SELF_PREMISE_UNVERIFIED(99, List.of("blue_msg_self_premise_unverified")), + BLUE_MSG_SELF_PREMISE_VERIFIED(100, List.of("blue_msg_self_premise_verified")), + BLUE_MSG_TO_BSP_FB(101, List.of("blue_msg_to_bsp_fb")), + BLUE_MSG_TO_CONSUMER(102, List.of("blue_msg_to_consumer")), + BLUE_MSG_TO_SELF_FB(103, List.of("blue_msg_to_self_fb")), + BLUE_MSG_UNVERIFIED_TO_BSP_FB_VERIFIED(104, List.of("blue_msg_unverified_to_bsp_fb_verified")), + BLUE_MSG_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(105, List.of("blue_msg_unverified_to_bsp_premise_verified")), + BLUE_MSG_UNVERIFIED_TO_SELF_FB_VERIFIED(106, List.of("blue_msg_unverified_to_self_fb_verified")), + BLUE_MSG_UNVERIFIED_TO_VERIFIED(107, List.of("blue_msg_unverified_to_verified")), + BLUE_MSG_VERIFIED_TO_BSP_FB_UNVERIFIED(108, List.of("blue_msg_verified_to_bsp_fb_unverified")), + BLUE_MSG_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(109, List.of("blue_msg_verified_to_bsp_premise_unverified")), + BLUE_MSG_VERIFIED_TO_SELF_FB_UNVERIFIED(110, List.of("blue_msg_verified_to_self_fb_unverified")), + BLUE_MSG_VERIFIED_TO_UNVERIFIED(111, List.of("blue_msg_verified_to_unverified")), + BLUE_MSG_BSP_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(112, List.of("blue_msg_bsp_fb_unverified_to_bsp_premise_verified")), + BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_FB_VERIFIED(113, List.of("blue_msg_bsp_fb_unverified_to_self_fb_verified")), + BLUE_MSG_BSP_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(114, List.of("blue_msg_bsp_fb_verified_to_bsp_premise_unverified")), + BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_FB_UNVERIFIED(115, List.of("blue_msg_bsp_fb_verified_to_self_fb_unverified")), + BLUE_MSG_SELF_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(116, List.of("blue_msg_self_fb_unverified_to_bsp_premise_verified")), + BLUE_MSG_SELF_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(117, List.of("blue_msg_self_fb_verified_to_bsp_premise_unverified")), + E2E_IDENTITY_UNAVAILABLE(118, List.of("e2e_identity_unavailable")), + GROUP_CREATING(119, List.of()), + GROUP_CREATE_FAILED(120, List.of()), + GROUP_BOUNCED(121, List.of()), + BLOCK_CONTACT(122, List.of("block_contact")), + EPHEMERAL_SETTING_NOT_APPLIED(123, List.of()), + SYNC_FAILED(124, List.of()), + SYNCING(125, List.of()), + BIZ_PRIVACY_MODE_INIT_FB(126, List.of("biz_privacy_mode_init_fb")), + BIZ_PRIVACY_MODE_INIT_BSP(127, List.of("biz_privacy_mode_init_bsp")), + BIZ_PRIVACY_MODE_TO_FB(128, List.of("biz_privacy_mode_to_fb")), + BIZ_PRIVACY_MODE_TO_BSP(129, List.of("biz_privacy_mode_to_bsp")), + DISAPPEARING_MODE(130, List.of("disappearing_mode")), + E2E_DEVICE_FETCH_FAILED(131, List.of()), + ADMIN_REVOKE(132, List.of("admin")), + GROUP_INVITE_LINK_GROWTH_LOCKED(133, List.of("growth_locked", "growth_unlocked")), + COMMUNITY_LINK_PARENT_GROUP(134, List.of("parent_group_link")), + COMMUNITY_LINK_SIBLING_GROUP(135, List.of("sibling_group_link")), + COMMUNITY_LINK_SUB_GROUP(136, List.of("sub_group_link", "link")), + COMMUNITY_UNLINK_PARENT_GROUP(137, List.of("parent_group_unlink")), + COMMUNITY_UNLINK_SIBLING_GROUP(138, List.of("sibling_group_unlink")), + COMMUNITY_UNLINK_SUB_GROUP(139, List.of("sub_group_unlink", "unlink")), + GROUP_PARTICIPANT_ACCEPT(140, List.of()), + GROUP_PARTICIPANT_LINKED_GROUP_JOIN(141, List.of("linked_group_join")), + COMMUNITY_CREATE(142, List.of("community_create")), + EPHEMERAL_KEEP_IN_CHAT(143, List.of("ephemeral_keep_in_chat")), + GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST(144, List.of("membership_approval_request")), + GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE(145, List.of("membership_approval_mode")), + INTEGRITY_UNLINK_PARENT_GROUP(146, List.of("integrity_parent_group_unlink")), + COMMUNITY_PARTICIPANT_PROMOTE(147, List.of("linked_group_promote")), + COMMUNITY_PARTICIPANT_DEMOTE(148, List.of("linked_group_demote")), + COMMUNITY_PARENT_GROUP_DELETED(149, List.of("delete_parent_group")), + COMMUNITY_LINK_PARENT_GROUP_MEMBERSHIP_APPROVAL(150, List.of("parent_group_link_membership_approval")), + GROUP_PARTICIPANT_JOINED_GROUP_AND_PARENT_GROUP(151, List.of("auto_add")), + MASKED_THREAD_CREATED(152, List.of("masked_thread_created")), + MASKED_THREAD_UNMASKED(153, List.of()), + BIZ_CHAT_ASSIGNMENT(154, List.of("chat_assignment")), + CHAT_PSA(155, List.of("e2e_notification")), + CHAT_POLL_CREATION_MESSAGE(156, List.of()), + CAG_MASKED_THREAD_CREATED(157, List.of("cag_masked_thread_created")), + COMMUNITY_PARENT_GROUP_SUBJECT_CHANGED(158, List.of("subject")), + CAG_INVITE_AUTO_ADD(159, List.of("invite_auto_add")), + BIZ_CHAT_ASSIGNMENT_UNASSIGN(160, List.of("chat_assignment_unassign")), + CAG_INVITE_AUTO_JOINED(161, List.of("invite_auto_add")); + + final int index; + private final List symbols; + + StubType(@ProtobufEnumIndex int index, List symbols) { + this.index = index; + this.symbols = symbols; + } + + public int index() { + return index; + } + + public List symbols() { + return symbols; + } + + public static Optional of(String symbol) { + return Arrays.stream(values()).filter(entry -> entry.symbols().contains(symbol)).findFirst(); + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java b/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java new file mode 100644 index 000000000..09f194377 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/ContextInfo.java @@ -0,0 +1,404 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.base.ButtonActionLink; +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.chat.ChatDisappear; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.ChatMessageKey; +import it.auties.whatsapp.model.message.model.MessageContainer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * A model class that holds the information related to a {@link it.auties.whatsapp.model.message.model.ContextualMessage}. + */ +@ProtobufMessageName("ContextInfo") +public final class ContextInfo implements Info, ProtobufMessage { + /** + * The jid of the message that this ContextualMessage quotes + */ + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + private final String quotedMessageId; + + /** + * The jid of the contact that sent the message that this ContextualMessage quotes + */ + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + private final Jid quotedMessageSenderJid; + + /** + * The message container that this ContextualMessage quotes + */ + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + private final MessageContainer quotedMessage; + + /** + * The jid of the contact that sent the message that this ContextualMessage quotes + */ + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + private final Jid quotedMessageChatJid; + + /** + * A list of the contacts' jids mentioned in this ContextualMessage + */ + @ProtobufProperty(index = 15, type = ProtobufType.STRING) + private final List mentions; + + /** + * Conversation source + */ + @ProtobufProperty(index = 18, type = ProtobufType.STRING) + private final String conversionSource; + + /** + * Conversation data + */ + @ProtobufProperty(index = 19, type = ProtobufType.BYTES) + private final byte[] conversionData; + + /** + * Conversation delay in endTimeStamp + */ + @ProtobufProperty(index = 20, type = ProtobufType.UINT32) + private final int conversionDelaySeconds; + + /** + * Forwarding score + */ + @ProtobufProperty(index = 21, type = ProtobufType.UINT32) + private final int forwardingScore; + + /** + * Whether this ContextualMessage is forwarded + */ + @ProtobufProperty(index = 22, type = ProtobufType.BOOL) + private final boolean forwarded; + + /** + * The ad that this ContextualMessage quotes + */ + @ProtobufProperty(index = 23, type = ProtobufType.OBJECT) + private final AdReplyInfo quotedAd; + + /** + * Placeholder key + */ + @ProtobufProperty(index = 24, type = ProtobufType.OBJECT) + private final ChatMessageKey placeholderKey; + + /** + * The expiration in seconds for this ContextualMessage. Only valid if the chat where this message + * was sent is ephemeral. + */ + @ProtobufProperty(index = 25, type = ProtobufType.UINT32) + private int ephemeralExpiration; + + /** + * The timestampSeconds, that is the seconds in seconds since {@link java.time.Instant#EPOCH}, of the + * last modification to the ephemeral settings for the chat where this ContextualMessage was + * sent. + */ + @ProtobufProperty(index = 26, type = ProtobufType.INT64) + private long ephemeralSettingTimestamp; + + /** + * Ephemeral shared secret + */ + @ProtobufProperty(index = 27, type = ProtobufType.BYTES) + private final byte[] ephemeralSharedSecret; + + /** + * External ad reply + */ + @ProtobufProperty(index = 28, type = ProtobufType.OBJECT) + private final ExternalAdReplyInfo externalAdReply; + + /** + * Entry point conversion source + */ + @ProtobufProperty(index = 29, type = ProtobufType.STRING) + private final String entryPointConversionSource; + + /** + * Entry point conversion app + */ + @ProtobufProperty(index = 30, type = ProtobufType.STRING) + private final String entryPointConversionApp; + + /** + * Entry point conversion delay in endTimeStamp + */ + @ProtobufProperty(index = 31, type = ProtobufType.UINT32) + private final int entryPointConversionDelaySeconds; + + /** + * Disappearing mode + */ + @ProtobufProperty(index = 32, type = ProtobufType.OBJECT) + private final ChatDisappear disappearingMode; + + /** + * Action link + */ + @ProtobufProperty(index = 33, type = ProtobufType.OBJECT) + private final ButtonActionLink actionLink; + + /** + * Group subject + */ + @ProtobufProperty(index = 34, type = ProtobufType.STRING) + private final String groupSubject; + + /** + * Parent group + */ + @ProtobufProperty(index = 35, type = ProtobufType.STRING) + private final Jid parentGroup; + + /** + * Trust banner type + */ + @ProtobufProperty(index = 37, type = ProtobufType.STRING) + private final String trustBannerType; + + /** + * Trust banner action + */ + @ProtobufProperty(index = 38, type = ProtobufType.UINT32) + private final int trustBannerAction; + + /** + * The contact that sent the message that this ContextualMessage quotes + */ + private Contact quotedMessageSender; + + /** + * The contact that sent the message that this ContextualMessage quotes + */ + @JsonBackReference + private Chat quotedMessageChat; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ContextInfo(String quotedMessageId, Jid quotedMessageSenderJid, MessageContainer quotedMessage, Jid quotedMessageChatJid, List mentions, String conversionSource, byte[] conversionData, int conversionDelaySeconds, int forwardingScore, boolean forwarded, AdReplyInfo quotedAd, ChatMessageKey placeholderKey, int ephemeralExpiration, long ephemeralSettingTimestamp, byte[] ephemeralSharedSecret, ExternalAdReplyInfo externalAdReply, String entryPointConversionSource, String entryPointConversionApp, int entryPointConversionDelaySeconds, ChatDisappear disappearingMode, ButtonActionLink actionLink, String groupSubject, Jid parentGroup, String trustBannerType, int trustBannerAction) { + this.quotedMessageId = quotedMessageId; + this.quotedMessageSenderJid = quotedMessageSenderJid; + this.quotedMessage = quotedMessage; + this.quotedMessageChatJid = quotedMessageChatJid; + this.mentions = mentions; + this.conversionSource = conversionSource; + this.conversionData = conversionData; + this.conversionDelaySeconds = conversionDelaySeconds; + this.forwardingScore = forwardingScore; + this.forwarded = forwarded; + this.quotedAd = quotedAd; + this.placeholderKey = placeholderKey; + this.ephemeralExpiration = ephemeralExpiration; + this.ephemeralSettingTimestamp = ephemeralSettingTimestamp; + this.ephemeralSharedSecret = ephemeralSharedSecret; + this.externalAdReply = externalAdReply; + this.entryPointConversionSource = entryPointConversionSource; + this.entryPointConversionApp = entryPointConversionApp; + this.entryPointConversionDelaySeconds = entryPointConversionDelaySeconds; + this.disappearingMode = disappearingMode; + this.actionLink = actionLink; + this.groupSubject = groupSubject; + this.parentGroup = parentGroup; + this.trustBannerType = trustBannerType; + this.trustBannerAction = trustBannerAction; + } + + public static ContextInfo of(MessageInfo quotedMessage) { + return new ContextInfoBuilder() + .quotedMessageId(quotedMessage.id()) + .quotedMessage(quotedMessage.message()) + .quotedMessageChatJid(quotedMessage.parentJid()) + .quotedMessageSenderJid(quotedMessage.senderJid()) + .mentions(new ArrayList<>()) + .build(); + } + + public static ContextInfo empty() { + return new ContextInfoBuilder() + .mentions(new ArrayList<>()) + .build(); + } + + /** + * Returns the sender of the quoted message + * + * @return an optional + */ + public Optional quotedMessageSender() { + return Optional.ofNullable(quotedMessageSender); + } + + public ContextInfo setQuotedMessageSender(Contact quotedMessageSender) { + this.quotedMessageSender = quotedMessageSender; + return this; + } + + /** + * Returns the chat jid of the quoted message + * + * @return an optional + */ + public Optional quotedMessageChatJid() { + return Optional.ofNullable(quotedMessageChatJid).or(this::quotedMessageSenderJid); + } + + /** + * Returns the jid of the sender of the quoted message + * + * @return an optional + */ + public Optional quotedMessageSenderJid() { + return Optional.ofNullable(quotedMessageSenderJid); + } + + /** + * Returns whether this context info has information about a quoted message + * + * @return a boolean + */ + public boolean hasQuotedMessage() { + return quotedMessageId().isPresent() + && quotedMessage().isPresent() + && quotedMessageChat().isPresent(); + } + + /** + * Returns the id of the quoted message + * + * @return an optional + */ + public Optional quotedMessageId() { + return Optional.ofNullable(quotedMessageId); + } + + /** + * Returns the quoted message + * + * @return an optional + */ + public Optional quotedMessage() { + return Optional.ofNullable(quotedMessage); + } + + /** + * Returns the chat of the quoted message + * + * @return an optional + */ + public Optional quotedMessageChat() { + return Optional.ofNullable(quotedMessageChat); + } + + + public ContextInfo setQuotedMessageChat(Chat quotedMessageChat) { + this.quotedMessageChat = quotedMessageChat; + return this; + } + + public List mentions() { + return mentions; + } + + public Optional conversionSource() { + return Optional.ofNullable(conversionSource); + } + + public Optional conversionData() { + return Optional.ofNullable(conversionData); + } + + public int conversionDelaySeconds() { + return conversionDelaySeconds; + } + + public int forwardingScore() { + return forwardingScore; + } + + public boolean forwarded() { + return forwarded; + } + + public Optional quotedAd() { + return Optional.ofNullable(quotedAd); + } + + public Optional placeholderKey() { + return Optional.ofNullable(placeholderKey); + } + + public int ephemeralExpiration() { + return ephemeralExpiration; + } + + public ContextInfo setEphemeralExpiration(int ephemeralExpiration) { + this.ephemeralExpiration = ephemeralExpiration; + return this; + } + + public long ephemeralSettingTimestamp() { + return ephemeralSettingTimestamp; + } + + public ContextInfo setEphemeralSettingTimestamp(long ephemeralSettingTimestamp) { + this.ephemeralSettingTimestamp = ephemeralSettingTimestamp; + return this; + } + + public Optional ephemeralSharedSecret() { + return Optional.ofNullable(ephemeralSharedSecret); + } + + public Optional externalAdReply() { + return Optional.ofNullable(externalAdReply); + } + + public Optional entryPointConversionSource() { + return Optional.ofNullable(entryPointConversionSource); + } + + public Optional entryPointConversionApp() { + return Optional.ofNullable(entryPointConversionApp); + } + + public int entryPointConversionDelaySeconds() { + return entryPointConversionDelaySeconds; + } + + public Optional disappearingMode() { + return Optional.ofNullable(disappearingMode); + } + + public Optional actionLink() { + return Optional.ofNullable(actionLink); + } + + public Optional groupSubject() { + return Optional.ofNullable(groupSubject); + } + + public Optional parentGroup() { + return Optional.ofNullable(parentGroup); + } + + public Optional trustBannerType() { + return Optional.ofNullable(trustBannerType); + } + + public int trustBannerAction() { + return trustBannerAction; + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java b/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java new file mode 100644 index 000000000..da626a369 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/DeviceContextInfo.java @@ -0,0 +1,53 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.sync.DeviceListMetadata; + +import java.util.Optional; + +@ProtobufMessageName("MessageContextInfo") +public final class DeviceContextInfo implements Info, ProtobufMessage { + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + private final DeviceListMetadata deviceListMetadata; + + @ProtobufProperty(index = 2, type = ProtobufType.INT32) + private final int deviceListMetadataVersion; + + @ProtobufProperty(index = 3, type = ProtobufType.BYTES) + private byte[] messageSecret; + + @ProtobufProperty(index = 4, type = ProtobufType.BYTES) + private final byte[] paddingBytes; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public DeviceContextInfo(DeviceListMetadata deviceListMetadata, int deviceListMetadataVersion, byte[] messageSecret, byte[] paddingBytes) { + this.deviceListMetadata = deviceListMetadata; + this.deviceListMetadataVersion = deviceListMetadataVersion; + this.messageSecret = messageSecret; + this.paddingBytes = paddingBytes; + } + + public Optional deviceListMetadata() { + return Optional.ofNullable(deviceListMetadata); + } + + public int deviceListMetadataVersion() { + return deviceListMetadataVersion; + } + + public Optional messageSecret() { + return Optional.ofNullable(messageSecret); + } + + public void setMessageSecret(byte[] messageSecret) { + this.messageSecret = messageSecret; + } + + public Optional paddingBytes() { + return Optional.ofNullable(paddingBytes); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/ExternalAdReplyInfo.java b/src/main/java/it/auties/whatsapp/model/info/ExternalAdReplyInfo.java new file mode 100644 index 000000000..3fa195758 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/ExternalAdReplyInfo.java @@ -0,0 +1,73 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + +import java.util.Optional; + + +/** + * A model class that holds the information related to an advertisement. + */ +@ProtobufMessageName("ContextInfo.ExternalAdReplyInfo") +public record ExternalAdReplyInfo( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + Optional title, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + Optional body, + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + Optional mediaType, + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + Optional thumbnailUrl, + @ProtobufProperty(index = 5, type = ProtobufType.STRING) + Optional mediaUrl, + @ProtobufProperty(index = 6, type = ProtobufType.BYTES) + Optional thumbnail, + @ProtobufProperty(index = 7, type = ProtobufType.STRING) + Optional sourceType, + @ProtobufProperty(index = 8, type = ProtobufType.STRING) + Optional sourceId, + @ProtobufProperty(index = 9, type = ProtobufType.STRING) + Optional sourceUrl, + @ProtobufProperty(index = 10, type = ProtobufType.BOOL) + boolean containsAutoReply, + @ProtobufProperty(index = 11, type = ProtobufType.BOOL) + boolean renderLargerThumbnail, + @ProtobufProperty(index = 12, type = ProtobufType.BOOL) + boolean showAdAttribution, + @ProtobufProperty(index = 13, type = ProtobufType.STRING) + Optional ctwaClid +) implements Info, ProtobufMessage { + /** + * The constants of this enumerated type describe the various types of media that an ad can wrap + */ + @ProtobufMessageName("ChatRowOpaqueData.DraftMessage.CtwaContextData.ContextInfoExternalAdReplyInfoMediaType") + public enum MediaType implements ProtobufEnum { + /** + * No media + */ + NONE(0), + /** + * Image + */ + IMAGE(1), + /** + * Video + */ + VIDEO(2); + + final int index; + + MediaType(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/Info.java b/src/main/java/it/auties/whatsapp/model/info/Info.java new file mode 100644 index 000000000..dfce64e87 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/Info.java @@ -0,0 +1,5 @@ +package it.auties.whatsapp.model.info; + +public sealed interface Info permits AdReplyInfo, BusinessIdentityInfo, ContextInfo, DeviceContextInfo, ExternalAdReplyInfo, MessageIndexInfo, MessageInfo, MessageStatusInfo, NativeFlowInfo, NotificationMessageInfo, PaymentInfo, ProductListInfo, WebNotificationsInfo { + +} diff --git a/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java b/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java new file mode 100644 index 000000000..ed4214cb7 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/MessageIndexInfo.java @@ -0,0 +1,53 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.core.type.TypeReference; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.util.Json; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +/** + * An index that contains data about a setting change or an action + * + * @param type the type of the change + * @param chatJid the chat where the change happened + * @param messageId the nullable id of the message regarding the chane + * @param fromMe whether the change regards yourself + */ +public record MessageIndexInfo(String type, Optional chatJid, Optional messageId, + boolean fromMe) implements Info { + /** + * Constructs a new message index info + * + * @param type the type of the change + * @param chatJid the chat where the change happened + * @param messageId the nullable id of the message regarding the chane + * @param fromMe whether the change regards yourself + * @return a non-null message index info + */ + public static MessageIndexInfo of(String type, Jid chatJid, String messageId, boolean fromMe) { + return new MessageIndexInfo(type, Optional.ofNullable(chatJid), Optional.ofNullable(messageId), fromMe); + } + + /** + * Constructs a new index info from a json string + * + * @param json the non-null json string + * @return a non-null index info + */ + public static MessageIndexInfo ofJson(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(Jid::of); + var messageId = getProperty(array, 2); + var fromMe = getProperty(array, 3).map(Boolean::parseBoolean).orElse(false); + return new MessageIndexInfo(type, chatJid, messageId, fromMe); + } + + private static Optional getProperty(List list, int index) { + return list.size() > index ? Optional.ofNullable(list.get(index)) : Optional.empty(); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java new file mode 100644 index 000000000..0cee310c8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/MessageInfo.java @@ -0,0 +1,23 @@ +package it.auties.whatsapp.model.info; + +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.MessageContainer; +import it.auties.whatsapp.util.Json; + +import java.util.OptionalLong; + +public sealed interface MessageInfo extends Info permits ChatMessageInfo, NewsletterMessageInfo, MessageStatusInfo, QuotedMessageInfo { + Jid parentJid(); + + Jid senderJid(); + + String id(); + + MessageContainer message(); + + OptionalLong timestampSeconds(); + + default String toJson() { + return Json.writeValueAsString(this, true); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/MessageStatusInfo.java b/src/main/java/it/auties/whatsapp/model/info/MessageStatusInfo.java new file mode 100644 index 000000000..2d975cfc3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/MessageStatusInfo.java @@ -0,0 +1,9 @@ +package it.auties.whatsapp.model.info; + +import it.auties.whatsapp.model.message.model.MessageStatus; + +public sealed interface MessageStatusInfo extends Info, MessageInfo permits ChatMessageInfo, NewsletterMessageInfo { + MessageStatus status(); + + T setStatus(MessageStatus status); +} diff --git a/src/main/java/it/auties/whatsapp/model/info/NativeFlowInfo.java b/src/main/java/it/auties/whatsapp/model/info/NativeFlowInfo.java new file mode 100644 index 000000000..2671512dd --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/NativeFlowInfo.java @@ -0,0 +1,23 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.base.ButtonBody; + +/** + * A model class that holds the information related to a native flow. + */ +@ProtobufMessageName("Message.ButtonsMessage.Button.NativeFlowInfo") +public record NativeFlowInfo( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String name, + @ProtobufProperty(index = 2, type = ProtobufType.STRING) + String parameters +) implements Info, ButtonBody, ProtobufMessage { + @Override + public Type bodyType() { + return Type.NATIVE_FLOW; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java new file mode 100644 index 000000000..54980624e --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/NewsletterMessageInfo.java @@ -0,0 +1,155 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.MessageContainer; +import it.auties.whatsapp.model.message.model.MessageStatus; +import it.auties.whatsapp.model.newsletter.Newsletter; +import it.auties.whatsapp.model.newsletter.NewsletterReaction; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.*; + +public final class NewsletterMessageInfo implements MessageInfo, MessageStatusInfo, ProtobufMessage { + @JsonBackReference + private Newsletter newsletter; + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + private final String id; + @ProtobufProperty(index = 2, type = ProtobufType.INT32) + private final int serverId; + @ProtobufProperty(index = 3, type = ProtobufType.UINT64) + private final Long timestampSeconds; + @ProtobufProperty(index = 4, type = ProtobufType.UINT64) + private final Long views; + @ProtobufProperty(index = 5, type = ProtobufType.MAP, keyType = ProtobufType.STRING, valueType = ProtobufType.OBJECT) + final Map reactions; + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + private final MessageContainer message; + @ProtobufProperty(index = 7, type = ProtobufType.OBJECT) + private MessageStatus status; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public NewsletterMessageInfo(String id, int serverId, Long timestampSeconds, Long views, Map reactions, MessageContainer message, MessageStatus status) { + this.id = id; + this.serverId = serverId; + this.timestampSeconds = timestampSeconds; + this.views = views; + this.reactions = reactions; + this.message = message; + this.status = status; + } + + public NewsletterMessageInfo setNewsletter(Newsletter newsletter) { + this.newsletter = newsletter; + return this; + } + + public Jid newsletterJid() { + return newsletter.jid(); + } + + @Override + public Jid parentJid() { + return newsletterJid(); + } + + @Override + public Jid senderJid() { + return newsletterJid(); + } + + public Newsletter newsletter() { + return newsletter; + } + + public String id() { + return id; + } + + public int serverId() { + return serverId; + } + + @Override + public OptionalLong timestampSeconds() { + return timestampSeconds == null ? OptionalLong.empty() : OptionalLong.of(timestampSeconds); + } + + public OptionalLong views() { + return views == null ? OptionalLong.empty() : OptionalLong.of(views); + } + + public MessageContainer message() { + return message; + } + + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } + + @Override + public MessageStatus status() { + return status; + } + + @Override + public NewsletterMessageInfo setStatus(MessageStatus status) { + this.status = status; + return this; + } + + public Collection reactions() { + return Collections.unmodifiableCollection(reactions.values()); + } + + public Optional findReaction(String value) { + return Optional.ofNullable(reactions.get(value)); + } + + public Optional addReaction(NewsletterReaction reaction) { + return Optional.ofNullable(reactions.put(reaction.content(), reaction)); + } + + public Optional removeReaction(String code) { + return Optional.ofNullable(reactions.remove(code)); + } + + public void incrementReaction(String code, boolean fromMe) { + findReaction(code).ifPresentOrElse(reaction -> { + reaction.setCount(reaction.count() + 1); + reaction.setFromMe(fromMe); + }, () -> { + var reaction = new NewsletterReaction(code, 1, fromMe); + addReaction(reaction); + }); + } + + public void decrementReaction(String code) { + findReaction(code).ifPresent(reaction -> { + if(reaction.count() <= 1) { + removeReaction(reaction.content()); + return; + } + + reaction.setCount(reaction.count() - 1); + reaction.setFromMe(false); + }); + } + + + @Override + public boolean equals(Object obj) { + return obj instanceof NewsletterMessageInfo that && Objects.equals(this.id(), that.id()); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/src/main/java/it/auties/whatsapp/model/info/NotificationMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/NotificationMessageInfo.java new file mode 100644 index 000000000..fd6fbaccd --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/NotificationMessageInfo.java @@ -0,0 +1,33 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.message.model.ChatMessageKey; +import it.auties.whatsapp.model.message.model.MessageContainer; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + +@ProtobufMessageName("NotificationMessageInfo") +public record NotificationMessageInfo( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + ChatMessageKey key, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + MessageContainer message, + @ProtobufProperty(index = 3, type = ProtobufType.UINT64) + long messageTimestampSeconds, + @ProtobufProperty(index = 4, type = ProtobufType.STRING) + Optional participant +) implements Info, ProtobufMessage { + /** + * Returns when the message was sent + * + * @return an optional + */ + public Optional messageTimestamp() { + return Clock.parseSeconds(messageTimestampSeconds); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java b/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java new file mode 100644 index 000000000..c1dd58495 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/PaymentInfo.java @@ -0,0 +1,206 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufEnum; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.ChatMessageKey; +import it.auties.whatsapp.model.payment.PaymentMoney; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.Optional; + + +/** + * A model class that holds the information related to a payment. + */ +@ProtobufMessageName("PaymentInfo") +public record PaymentInfo( + @Deprecated + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + Currency currencyDeprecated, + @ProtobufProperty(index = 2, type = ProtobufType.UINT64) + long amount1000, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + Jid receiverJid, + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + Status status, + @ProtobufProperty(index = 5, type = ProtobufType.UINT64) + long transactionTimestampSeconds, + @ProtobufProperty(index = 6, type = ProtobufType.OBJECT) + ChatMessageKey requestMessageKey, + @ProtobufProperty(index = 7, type = ProtobufType.UINT64) + long expiryTimestampSeconds, + @ProtobufProperty(index = 8, type = ProtobufType.BOOL) + boolean futureProofed, + @ProtobufProperty(index = 9, type = ProtobufType.STRING) + String currency, + @ProtobufProperty(index = 10, type = ProtobufType.OBJECT) + TransactionStatus transactionStatus, + @ProtobufProperty(index = 11, type = ProtobufType.BOOL) + boolean useNoviFormat, + @ProtobufProperty(index = 12, type = ProtobufType.OBJECT) + PaymentMoney primaryAmount, + @ProtobufProperty(index = 13, type = ProtobufType.OBJECT) + PaymentMoney exchangeAmount +) implements Info, ProtobufMessage { + /** + * Returns when the transaction happened + * + * @return an optional + */ + public Optional transactionTimestamp() { + return Clock.parseSeconds(transactionTimestampSeconds); + } + + /** + * Returns when the transaction expires + * + * @return an optional + */ + public Optional expiryTimestamp() { + return Clock.parseSeconds(expiryTimestampSeconds); + } + + /** + * The constants of this enumerated type describe the status of a payment described by a + * {@link PaymentInfo} + */ + @ProtobufMessageName("PaymentInfo.Status") + public enum Status implements ProtobufEnum { + /** + * Unknown status + */ + UNKNOWN_STATUS(0), + /** + * Processing + */ + PROCESSING(1), + /** + * Sent + */ + SENT(2), + /** + * Need to accept + */ + NEED_TO_ACCEPT(3), + /** + * Complete + */ + COMPLETE(4), + /** + * Could not complete + */ + COULD_NOT_COMPLETE(5), + /** + * Refunded + */ + REFUNDED(6), + /** + * Expired + */ + EXPIRED(7), + /** + * Rejected + */ + REJECTED(8), + /** + * Cancelled + */ + CANCELLED(9), + /** + * Waiting for payer + */ + WAITING_FOR_PAYER(10), + /** + * Waiting + */ + WAITING(11); + + final int index; + + Status(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + /** + * The constants of this enumerated type describe the currencies supported for a transaction + * described by a {@link PaymentInfo} + */ + @ProtobufMessageName("PaymentInfo.Currency") + public enum Currency implements ProtobufEnum { + /** + * Unknown currency + */ + UNKNOWN_CURRENCY(0), + /** + * Indian rupees + */ + INR(1); + + final int index; + + Currency(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } + + @ProtobufMessageName("PaymentInfo.TxnStatus") + public enum TransactionStatus implements ProtobufEnum { + UNKNOWN(0), + PENDING_SETUP(1), + PENDING_RECEIVER_SETUP(2), + INIT(3), + SUCCESS(4), + COMPLETED(5), + FAILED(6), + FAILED_RISK(7), + FAILED_PROCESSING(8), + FAILED_RECEIVER_PROCESSING(9), + FAILED_DA(10), + FAILED_DA_FINAL(11), + REFUNDED_TXN(12), + REFUND_FAILED(13), + REFUND_FAILED_PROCESSING(14), + REFUND_FAILED_DA(15), + EXPIRED_TXN(16), + AUTH_CANCELED(17), + AUTH_CANCEL_FAILED_PROCESSING(18), + AUTH_CANCEL_FAILED(19), + COLLECT_INIT(20), + COLLECT_SUCCESS(21), + COLLECT_FAILED(22), + COLLECT_FAILED_RISK(23), + COLLECT_REJECTED(24), + COLLECT_EXPIRED(25), + COLLECT_CANCELED(26), + COLLECT_CANCELLING(27), + IN_REVIEW(28), + REVERSAL_SUCCESS(29), + REVERSAL_PENDING(30), + REFUND_PENDING(31); + + final int index; + + TransactionStatus(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java b/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java new file mode 100644 index 000000000..a2aff998d --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/ProductListInfo.java @@ -0,0 +1,26 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.product.ProductListHeaderImage; +import it.auties.whatsapp.model.product.ProductSection; + +import java.util.List; + +/** + * A model class that holds the information related to a list of products. + */ +@ProtobufMessageName("Message.ListMessage.ProductListInfo") +public record ProductListInfo( + @ProtobufProperty(index = 1, type = ProtobufType.OBJECT) + List productSections, + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + ProductListHeaderImage headerImage, + @ProtobufProperty(index = 3, type = ProtobufType.STRING) + Jid seller +) implements Info, ProtobufMessage { + +} diff --git a/src/main/java/it/auties/whatsapp/model/info/QuotedMessageInfo.java b/src/main/java/it/auties/whatsapp/model/info/QuotedMessageInfo.java new file mode 100644 index 000000000..75698cd0f --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/QuotedMessageInfo.java @@ -0,0 +1,102 @@ +package it.auties.whatsapp.model.info; + +import com.fasterxml.jackson.annotation.JsonCreator; +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.jid.Jid; +import it.auties.whatsapp.model.message.model.MessageContainer; + +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalLong; + +/** + * An immutable model class that represents a quoted message + */ +public final class QuotedMessageInfo implements MessageInfo { + /** + * The id of the message + */ + private final String id; + + /** + * The chat of the message + */ + private final Chat chat; + + /** + * The sender of the message + */ + private final Contact sender; + + /** + * The message + */ + private final MessageContainer message; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public QuotedMessageInfo(String id, Chat chat, Contact sender, MessageContainer message) { + this.id = id; + this.chat = chat; + this.sender = sender; + this.message = message; + } + + /** + * Constructs a quoted message from a context info + * + * @param contextInfo the non-null context info + * @return an optional quoted message + */ + public static Optional of(ContextInfo contextInfo) { + if (!contextInfo.hasQuotedMessage()) { + return Optional.empty(); + } + var id = contextInfo.quotedMessageId().orElseThrow(); + var chat = contextInfo.quotedMessageChat().orElseThrow(); + var sender = contextInfo.quotedMessageSender().orElse(null); + var message = contextInfo.quotedMessage().orElseThrow(); + return Optional.of(new QuotedMessageInfo(id, chat, sender, message)); + } + + @Override + public Jid parentJid() { + return chat.jid(); + } + + /** + * Returns the sender's jid + * + * @return a jid + */ + @Override + public Jid senderJid() { + return Objects.requireNonNullElseGet(sender.jid(), this::parentJid); + } + + /** + * Returns the sender of this message + * + * @return an optional + */ + public Optional sender() { + return Optional.ofNullable(sender); + } + + public String id() { + return id; + } + + public Optional chat() { + return Optional.of(chat); + } + + public MessageContainer message() { + return message; + } + + @Override + public OptionalLong timestampSeconds() { + return OptionalLong.empty(); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/info/WebNotificationsInfo.java b/src/main/java/it/auties/whatsapp/model/info/WebNotificationsInfo.java new file mode 100644 index 000000000..8201b6ef4 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/info/WebNotificationsInfo.java @@ -0,0 +1,32 @@ +package it.auties.whatsapp.model.info; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.util.Clock; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; + +@ProtobufMessageName("WebNotificationsInfo") +public record WebNotificationsInfo( + @ProtobufProperty(index = 2, type = ProtobufType.UINT64) + long timestampSeconds, + @ProtobufProperty(index = 3, type = ProtobufType.UINT32) + int unreadChats, + @ProtobufProperty(index = 4, type = ProtobufType.UINT32) + int notifyMessageCount, + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + List notifyMessages +) implements Info, ProtobufMessage { + /** + * Returns when the notification was sent + * + * @return an optional + */ + public Optional timestamp() { + return Clock.parseSeconds(timestampSeconds); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/jid/Jid.java b/src/main/java/it/auties/whatsapp/model/jid/Jid.java new file mode 100644 index 000000000..f3d0f41de --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/jid/Jid.java @@ -0,0 +1,277 @@ +package it.auties.whatsapp.model.jid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import it.auties.protobuf.annotation.ProtobufConverter; +import it.auties.whatsapp.model.signal.session.SessionAddress; + +import java.util.Objects; + +/** + * 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 Jid(String user, JidServer server, Integer device, Integer agent) implements JidProvider { + /** + * Default constructor + */ + public Jid(String user, JidServer server, Integer device, Integer agent) { + this.user = user != null && user.startsWith("+") ? user.substring(1) : user; + this.server = server; + this.device = device; + this.agent = agent; + } + + /** + * Constructs a new ContactId that represents a server + * + * @param server the non-null custom server + * @return a non-null contact jid + */ + public static Jid ofServer(JidServer server) { + return of(null, server); + } + + @ProtobufConverter // Reserved for protobuf + public static Jid ofProtobuf(String input) { + return input == null ? null : Jid.of(input); + } + + /** + * Constructs a new ContactId for a user from a jid and a custom server + * + * @param jid the nullable jid of the user + * @param server the non-null custom server + * @return a non-null contact jid + */ + public static Jid of(String jid, JidServer server) { + var complexUser = withoutServer(jid); + if (complexUser == null) { + return new Jid(null, server, null, null); + } + + if (complexUser.contains(":")) { + var simpleUser = complexUser.split(":", 2); + var user = simpleUser[0]; + var device = Integer.parseUnsignedInt(simpleUser[1]); + if (user.contains("_")) { + var simpleUserAgent = user.split("_", 2); + var agent = tryParseAgent(simpleUserAgent[1]); + return new Jid(simpleUserAgent[0], server, device, agent); + } + return new Jid(user, server, device, null); + } + + if (!complexUser.contains("_")) { + return new Jid(complexUser, server, null, null); + } + + var simpleUserAgent = complexUser.split("_", 2); + var agent = tryParseAgent(simpleUserAgent[1]); + return new Jid(simpleUserAgent[0], server, null, agent); + } + + /** + * Parses a nullable jid to the Whatsapp Jid Format + * + * @param jid the nullable jid to parse + * @return null if {@code jid == null}, otherwise a non-null string + */ + public static String withoutServer(String jid) { + if (jid == null) { + return null; + } + for (var server : JidServer.values()) { + jid = jid.replace("@%s".formatted(server), ""); + } + return jid; + } + + private static Integer tryParseAgent(String string) { + try { + return Integer.parseUnsignedInt(string); + } catch (NumberFormatException exception) { + return null; + } + } + + /** + * Constructs a new ContactId for a device + * + * @param jid the nullable jid of the user + * @param device the device jid + * @return a non-null contact jid + */ + public static Jid ofDevice(String jid, int device) { + return new Jid(withoutServer(jid), JidServer.WHATSAPP, device, null); + } + + /** + * Constructs a new ContactId for a user from a jid + * + * @param jid the non-null jid of the user + * @return a non-null contact jid + */ + @JsonCreator + public static Jid of(String jid) { + return of(jid, JidServer.of(jid)); + } + + /** + * Constructs a new ContactId for a user from a jid + * + * @param jid the non-null jid of the user + * @return a non-null contact jid + */ + public static Jid of(long jid) { + return of(String.valueOf(jid), JidServer.WHATSAPP); + } + + /** + * Returns the type of this jid + * + * @return a non null type + */ + 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 NEWSLETTER -> JidType.NEWSLETTER; + case USER -> switch (user()) { + case "server" -> JidType.SERVER; + case "0" -> JidType.ANNOUNCEMENT; + case "16508638904" -> JidType.IAS; + case "16505361212" -> JidType.OFFICIAL_BUSINESS_ACCOUNT; + default -> JidType.UNKNOWN; + }; + }; + } + + /** + * Returns whether this jid is associated with a companion device + * + * @return true if this jid is a companion + */ + public boolean isCompanion() { + return device() != 0; + } + + /** + * Returns whether this jid ends with the provided server + * + * @param server the server to check against + * @return a boolean + */ + public boolean hasServer(JidServer server) { + return server() == server; + } + + /** + * Returns whether this jid is a server jid + * + * @param server the server to check against + * @return a boolean + */ + public boolean isServerJid(JidServer server) { + return user() == null && server() == server; + } + + /** + * Returns a new jid using with a different server + * + * @param server the new server + * @return a non-null jid + */ + public Jid withServer(JidServer server) { + return new Jid(user(), server, device, agent); + } + + /** + * Converts this jid to a user jid + * + * @return a non-null jid + */ + public Jid withoutDevice() { + return of(user(), server()); + } + + /** + * Converts this jid to a non-formatted phone number + * + * @return a non-null String + */ + public String toPhoneNumber() { + return "+%s".formatted(user); + } + + /** + * Converts this jid to a String + * + * @return a non-null String + */ + @JsonValue + @ProtobufConverter + @Override + public String toString() { + var user = Objects.requireNonNullElse(user(), ""); + var agent = hasAgent() ? "_%s".formatted(agent()) : ""; + var device = hasDevice() ? ":%s".formatted(device()) : ""; + var leading = "%s%s%s".formatted(user, agent, device); + return leading.isEmpty() ? server().toString() : "%s@%s".formatted(leading, server()); + } + + /** + * Converts this jid to a signal address + * + * @return a non-null {@link SessionAddress} + */ + public SessionAddress toSignalAddress() { + return new SessionAddress(user(), device()); + } + + /** + * Returns this object as a jid + * + * @return a non-null jid + */ + @Override + public Jid toJid() { + return this; + } + + @Override + public Integer device() { + return Objects.requireNonNullElse(device, 0); + } + + /** + * Returns whether this jid specifies a device + * + * @return a boolean + */ + public boolean hasDevice() { + return device != null; + } + + @Override + public Integer agent() { + return Objects.requireNonNullElse(agent, 0); + } + + /** + * Returns whether this jid specifies an agent + * + * @return a boolean + */ + public boolean hasAgent() { + return agent != null; + } + + @Override + public int hashCode() { + return Objects.hashCode(toString()); + } +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/jid/JidProvider.java b/src/main/java/it/auties/whatsapp/model/jid/JidProvider.java new file mode 100644 index 000000000..b89471449 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/jid/JidProvider.java @@ -0,0 +1,17 @@ +package it.auties.whatsapp.model.jid; + +import it.auties.whatsapp.model.chat.Chat; +import it.auties.whatsapp.model.contact.Contact; +import it.auties.whatsapp.model.newsletter.Newsletter; + +/** + * Utility interface to make providing a jid easier + */ +public sealed interface JidProvider permits Chat, Newsletter, Contact, Jid { + /** + * Returns this object as a jid + * + * @return a non-null jid + */ + Jid toJid(); +} diff --git a/src/main/java/it/auties/whatsapp/model/jid/JidServer.java b/src/main/java/it/auties/whatsapp/model/jid/JidServer.java new file mode 100644 index 000000000..7a3998f3a --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/jid/JidServer.java @@ -0,0 +1,69 @@ +package it.auties.whatsapp.model.jid; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +/** + * The constants of this enumerated type describe the various servers that a jid might be linked + * to + */ +public enum JidServer { + /** + * User + */ + USER("c.us"), + /** + * Groups and communities + */ + GROUP("g.us"), + /** + * Broadcast group + */ + BROADCAST("broadcast"), + /** + * Group call + */ + GROUP_CALL("call"), + /** + * Whatsapp + */ + WHATSAPP("s.whatsapp.net"), + /** + * Lid + */ + LID("lid"), + /** + * Newsletter + */ + NEWSLETTER("newsletter"); + + private final String address; + + JidServer(String address) { + this.address = address; + } + + @JsonCreator + public static JidServer of(String address) { + return Arrays.stream(values()) + .filter(entry -> address != null && address.endsWith(entry.address())) + .findFirst() + .orElse(WHATSAPP); + } + + public String address() { + return address; + } + + public Jid toJid() { + return Jid.ofServer(this); + } + + @Override + @JsonValue + public String toString() { + return address(); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/jid/JidType.java b/src/main/java/it/auties/whatsapp/model/jid/JidType.java new file mode 100644 index 000000000..020cfbffb --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/jid/JidType.java @@ -0,0 +1,63 @@ +package it.auties.whatsapp.model.jid; + +/** + * The constants of this enumerated type describe the various types of jids currently supported + */ +public enum JidType { + /** + * Represents a device connected using the multi device beta + */ + COMPANION, + /** + * Regular Whatsapp contact Jid + */ + USER, + /** + * Official survey account + */ + OFFICIAL_SURVEY_ACCOUNT, + /** + * Lid + */ + LID, + /** + * Broadcast list + */ + BROADCAST, + /** + * Official business account + */ + OFFICIAL_BUSINESS_ACCOUNT, + /** + * Group Chat Jid + */ + GROUP, + /** + * Group Call Jid + */ + GROUP_CALL, + /** + * Server Jid: Used to send nodes to Whatsapp usually + */ + SERVER, + /** + * Announcements Chat Jid: Read only chat, usually used by Whatsapp for log updates + */ + ANNOUNCEMENT, + /** + * IAS Chat jid + */ + IAS, + /** + * Image Status Jid of a contact + */ + STATUS, + /** + * Unknown Jid type + */ + UNKNOWN, + /** + * Channel + */ + NEWSLETTER +} diff --git a/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java b/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java new file mode 100644 index 000000000..b91596cc6 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/AttachmentType.java @@ -0,0 +1,55 @@ +package it.auties.whatsapp.model.media; + +import java.util.Optional; + +/** + * The constants of this enumerated type describe the various types of attachments supported by Whatsapp + */ +public enum AttachmentType { + NONE(null, null, false), + AUDIO("mms/audio", "WhatsApp Audio Keys", false), + DOCUMENT("mms/document", "WhatsApp Document Keys", false), + GIF("mms/gif", "WhatsApp Video Keys", false), + IMAGE("mms/image", "WhatsApp Image Keys", false), + PROFILE_PICTURE("pps/photo", null, false), + PRODUCT("mms/image", "WhatsApp Image Keys", false), + VOICE("mms/ptt", "WhatsApp Audio Keys", false), + STICKER("mms/sticker", "WhatsApp Image Keys", false), + THUMBNAIL_DOCUMENT("mms/thumbnail-document", "WhatsApp Document Thumbnail Keys", false), + THUMBNAIL_LINK("mms/thumbnail-link", "WhatsApp Link Thumbnail Keys", false), + VIDEO("mms/video", "WhatsApp Video Keys", false), + APP_STATE("mms/md-app-state", "WhatsApp App State Keys", true), + HISTORY_SYNC("mms/md-msg-hist", "WhatsApp History Keys", true), + PRODUCT_CATALOG_IMAGE("product/image", null, false), + BUSINESS_COVER_PHOTO("pps/biz-cover-photo", null, false), + NEWSLETTER_AUDIO("newsletter/newsletter-audio", null, false), + NEWSLETTER_IMAGE("newsletter/newsletter-image", null, false), + NEWSLETTER_DOCUMENT("newsletter/newsletter-document", null, false), + NEWSLETTER_GIF("newsletter/newsletter-gif", null, false), + NEWSLETTER_VOICE("newsletter/newsletter-ptt", null, false), + NEWSLETTER_STICKER("newsletter/newsletter-sticker", null, false), + NEWSLETTER_THUMBNAIL_LINK("newsletter/newsletter-thumbnail-link", null, false), + NEWSLETTER_VIDEO("newsletter/newsletter-video", null, false); + + private final String path; + private final String keyName; + private final boolean inflatable; + + AttachmentType(String path, String keyName, boolean inflatable) { + this.path = path; + this.keyName = keyName; + this.inflatable = inflatable; + } + + public Optional path() { + return Optional.ofNullable(path); + } + + public Optional keyName() { + return Optional.ofNullable(keyName); + } + + public boolean inflatable() { + return this.inflatable; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaConnection.java b/src/main/java/it/auties/whatsapp/model/media/MediaConnection.java new file mode 100644 index 000000000..9f9b3e05b --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaConnection.java @@ -0,0 +1,7 @@ +package it.auties.whatsapp.model.media; + + +import java.util.List; + +public record MediaConnection(String auth, int ttl, int maxBuckets, long timestamp, List hosts) { +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaData.java b/src/main/java/it/auties/whatsapp/model/media/MediaData.java new file mode 100644 index 000000000..b8aa3e616 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaData.java @@ -0,0 +1,15 @@ +package it.auties.whatsapp.model.media; + +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.protobuf.model.ProtobufType; + + +@ProtobufMessageName("MediaData") +public record MediaData( + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + String localPath +) implements ProtobufMessage { + +} \ No newline at end of file diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaDimensions.java b/src/main/java/it/auties/whatsapp/model/media/MediaDimensions.java new file mode 100644 index 000000000..c712c2f2c --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaDimensions.java @@ -0,0 +1,11 @@ +package it.auties.whatsapp.model.media; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MediaDimensions(@JsonProperty("width") int width, @JsonProperty("height") int height) { + private static final MediaDimensions DEFAULT = new MediaDimensions(128, 128); + + public static MediaDimensions defaultDimensions() { + return DEFAULT; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaFile.java b/src/main/java/it/auties/whatsapp/model/media/MediaFile.java new file mode 100644 index 000000000..5c3e3a053 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaFile.java @@ -0,0 +1,6 @@ +package it.auties.whatsapp.model.media; + +public record MediaFile(byte[] encryptedFile, byte[] fileSha256, byte[] fileEncSha256, byte[] mediaKey, long fileLength, + String directPath, String url, String handle, Long timestamp) { + +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java b/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java new file mode 100644 index 000000000..d63943942 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaKeys.java @@ -0,0 +1,28 @@ +package it.auties.whatsapp.model.media; + +import it.auties.whatsapp.crypto.Hkdf; +import it.auties.whatsapp.util.BytesHelper; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static it.auties.whatsapp.util.Specification.Signal.IV_LENGTH; +import static it.auties.whatsapp.util.Specification.Signal.KEY_LENGTH; + +public record MediaKeys(byte[] mediaKey, byte[] iv, byte[] cipherKey, byte[] macKey, byte[] ref) { + private static final int EXPANDED_SIZE = 112; + + public static MediaKeys random(String type) { + return of(BytesHelper.random(32), type); + } + + public static MediaKeys of(byte[] key, String type) { + var keyName = type.getBytes(StandardCharsets.UTF_8); + var expanded = Hkdf.extractAndExpand(key, keyName, EXPANDED_SIZE); + var iv = Arrays.copyOfRange(expanded, 0, IV_LENGTH); + var cipherKey = Arrays.copyOfRange(expanded, IV_LENGTH, IV_LENGTH + KEY_LENGTH); + var macKey = Arrays.copyOfRange(expanded, IV_LENGTH + KEY_LENGTH, IV_LENGTH + KEY_LENGTH * 2); + var ref = Arrays.copyOfRange(expanded, IV_LENGTH + KEY_LENGTH * 2, expanded.length); + return new MediaKeys(key, iv, cipherKey, macKey, ref); + } +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java b/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java new file mode 100644 index 000000000..fb6730cf3 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaUpload.java @@ -0,0 +1,8 @@ +package it.auties.whatsapp.model.media; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MediaUpload(@JsonProperty("direct_path") String directPath, @JsonProperty("url") String url, + @JsonProperty("handle") String handle) { + +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MediaVisibility.java b/src/main/java/it/auties/whatsapp/model/media/MediaVisibility.java new file mode 100644 index 000000000..1f81152c8 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MediaVisibility.java @@ -0,0 +1,35 @@ +package it.auties.whatsapp.model.media; + +import it.auties.protobuf.annotation.ProtobufEnumIndex; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.model.ProtobufEnum; + +/** + * The constants of this enumerated type describe the various types of media visibility that can be + * set for a chat + */ +@ProtobufMessageName("MediaVisibility") +public enum MediaVisibility implements ProtobufEnum { + /** + * Default + */ + DEFAULT(0), + /** + * Off + */ + OFF(1), + /** + * On + */ + ON(2); + + final int index; + + MediaVisibility(@ProtobufEnumIndex int index) { + this.index = index; + } + + public int index() { + return index; + } +} diff --git a/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java b/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java new file mode 100644 index 000000000..c123b1903 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/media/MutableAttachmentProvider.java @@ -0,0 +1,114 @@ +package it.auties.whatsapp.model.media; + +import it.auties.protobuf.model.ProtobufMessage; +import it.auties.whatsapp.model.message.model.MediaMessage; +import it.auties.whatsapp.model.sync.ExternalBlobReference; +import it.auties.whatsapp.model.sync.HistorySyncNotification; + +import java.util.Optional; +import java.util.OptionalLong; + +/** + * A sealed interface that represents a class that can provide data about a media + */ +public sealed interface MutableAttachmentProvider> extends ProtobufMessage + permits MediaMessage, ExternalBlobReference, HistorySyncNotification { + /** + * Returns the url to the media + * + * @return a nullable String + */ + Optional mediaUrl(); + + /** + * Sets the media url of this provider + * + * @return the same provider + */ + T setMediaUrl(String mediaUrl); + + /** + * Returns the direct path to the media + * + * @return a nullable String + */ + Optional mediaDirectPath(); + + /** + * Sets the direct path of this provider + * + * @return the same provider + */ + T setMediaDirectPath(String mediaDirectPath); + + /** + * Returns the key of this media + * + * @return a non-null array of bytes + */ + Optional mediaKey(); + + /** + * Sets the media key of this provider + * + * @return the same provider + */ + T setMediaKey(byte[] bytes); + + /** + * Sets the timestamp of the media key + * + * @return the same provider + */ + T setMediaKeyTimestamp(Long timestamp); + + /** + * Returns the sha256 of this media + * + * @return a non-null array of bytes + */ + Optional mediaSha256(); + + /** + * Sets the sha256 of the media in this provider + * + * @return the same provider + */ + T setMediaSha256(byte[] bytes); + + /** + * Returns the sha256 of this encrypted media + * + * @return a non-null array of bytes + */ + Optional mediaEncryptedSha256(); + + /** + * Sets the sha256 of the encrypted media in this provider + * + * @return the same provider + */ + T setMediaEncryptedSha256(byte[] bytes); + + /** + * Returns the size of this media + * + * @return a long + */ + OptionalLong mediaSize(); + + /** + * Sets the size of this media + * + * @return a long + */ + T setMediaSize(long mediaSize); + + + /** + * Returns the type of this attachment + * + * @return a non-null attachment + */ + AttachmentType attachmentType(); +} diff --git a/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java b/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java new file mode 100644 index 000000000..15a64a093 --- /dev/null +++ b/src/main/java/it/auties/whatsapp/model/message/button/ButtonsMessage.java @@ -0,0 +1,181 @@ +package it.auties.whatsapp.model.message.button; + +import it.auties.protobuf.annotation.ProtobufBuilder; +import it.auties.protobuf.annotation.ProtobufMessageName; +import it.auties.protobuf.annotation.ProtobufProperty; +import it.auties.protobuf.model.ProtobufType; +import it.auties.whatsapp.model.button.base.Button; +import it.auties.whatsapp.model.info.ContextInfo; +import it.auties.whatsapp.model.message.button.ButtonsMessageHeader.Type; +import it.auties.whatsapp.model.message.model.ButtonMessage; +import it.auties.whatsapp.model.message.model.ContextualMessage; +import it.auties.whatsapp.model.message.model.MessageType; +import it.auties.whatsapp.model.message.standard.DocumentMessage; +import it.auties.whatsapp.model.message.standard.ImageMessage; +import it.auties.whatsapp.model.message.standard.LocationMessage; +import it.auties.whatsapp.model.message.standard.VideoOrGifMessage; + +import java.util.List; +import java.util.Optional; + +/** + * A model class that represents a message that contains buttons inside + */ +@ProtobufMessageName("Message.ButtonsMessage") +public final class ButtonsMessage implements ButtonMessage, ContextualMessage { + @ProtobufProperty(index = 1, type = ProtobufType.STRING) + private final ButtonsMessageHeaderText headerText; + @ProtobufProperty(index = 2, type = ProtobufType.OBJECT) + private final DocumentMessage headerDocument; + @ProtobufProperty(index = 3, type = ProtobufType.OBJECT) + private final ImageMessage headerImage; + @ProtobufProperty(index = 4, type = ProtobufType.OBJECT) + private final VideoOrGifMessage headerVideo; + @ProtobufProperty(index = 5, type = ProtobufType.OBJECT) + private final LocationMessage headerLocation; + @ProtobufProperty(index = 6, type = ProtobufType.STRING) + private final String body; + @ProtobufProperty(index = 7, type = ProtobufType.STRING) + private final String footer; + @ProtobufProperty(index = 8, type = ProtobufType.OBJECT) + private ContextInfo contextInfo; + @ProtobufProperty(index = 9, type = ProtobufType.OBJECT) + private final List