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..065b63688
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..9e7977a8a
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..1a736ea19
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1108 @@
+# Cobalt
+
+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
+
+ - Dependency
+ ```xml
+
+ com.github.auties00
+ cobalt
+ 0.0.4
+
+ ```
+
+ - Annotation processor (required for @RegisterListener)
+ ```xml
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ com.github.auties00
+ cobalt
+ 0.0.4
+
+
+
+
+ ```
+
+#### Gradle
+
+- Groovy DSL
+ - Dependency
+ ```groovy
+ implementation 'com.github.auties00:cobalt:0.0.4'
+ ```
+
+ - Annotation processor (required for @RegisterListener)
+ ```groovy
+ annotationProcessor 'com.github.auties00:cobalt:0.0.4'
+ ```
+
+- Kotlin DSL
+ - Dependency
+ ```groovy
+ implementation("com.github.auties00:cobalt:0.0.4")
+ ```
+
+ - Annotation processor (required for @RegisterListener)
+ ```groovy
+ annotationProcessor("com.github.auties00:cobalt:0.0.4")
+ ```
+
+### 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 [CobaltAnalyzer](https://github.com/Auties00/CobaltAnalyzer), 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)
+ ```
+- 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:_** @RegisterListener will only work if you register the annotation processor provided by Cobalt
+
+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/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..f7a12a29d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,371 @@
+
+
+ 4.0.0
+ com.github.auties00
+ cobalt
+ 0.0.4
+ ${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/
+
+
+
+
+
+ AsposeJavaAPI
+ Aspose Java API
+ https://releases.aspose.com/java/repo/
+
+
+ jitpack.io
+ Jitpack
+ https://jitpack.io
+
+
+
+
+
+ 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
+ 3.0.0-M9
+ 3.0.1
+ 3.11.0
+ 3.2.1
+ 3.5.0
+ 1.6.13
+ 3.5.1
+ 3.0.4
+ 5.10.0-M1
+ 2.15.2
+ 1.2
+ 5.1.4
+ 2.3
+ 0.12.1
+ 2.2
+ 8.13.13
+ 2.15.0
+ master-SNAPSHOT
+ 1.27
+ 22.11
+ 2023.09.10
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${maven.compiler.plugin.version}
+
+
+ process-test-annotations
+ generate-test-resources
+
+ testCompile
+
+
+
+
+ ${groupId}
+ ${artifactId}
+ ${version}
+
+
+
+ it.auties.whatsapp.listener.processor.RegisterListenerProcessor
+
+
+
+
+
+
+ ${java.version}
+ ${java.version}
+
+
+ com.github.auties00
+ protobuf-serialization-plugin
+ ${protoc.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}
+
+
+
+
+ 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}
+
+
+
+
+ com.googlecode.libphonenumber
+ libphonenumber
+ ${libphonenumber.version}
+
+
+
+
+ com.github.Auties00
+ apk-parser
+ ${apk.parser.version}
+
+
+
+
+ com.googlecode.plist
+ dd-plist
+ ${plist.version}
+
+
+
+
+ com.github.auties00
+ link-preview
+ ${linkpreview.version}
+
+
+ com.aspose
+ aspose-words
+ ${aspose.words.version}
+ jdk17
+
+
+ com.github.kokorin.jaffree
+ jaffree
+ ${ffmpeg.version}
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.32
+
+
+ com.googlecode.ez-vcard
+ ez-vcard
+ ${vcard.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit.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/gcm.proto b/proto/gcm.proto
new file mode 100644
index 000000000..daad7babe
--- /dev/null
+++ b/proto/gcm.proto
@@ -0,0 +1,542 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Logging information for Android "checkin" events (automatic, periodic
+// requests made by Android devices to the server).
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+option go_package = ".;checkin_proto";
+package checkin_proto;
+
+// Build characteristics unique to the Chrome browser, and Chrome OS
+message ChromeBuildProto {
+ enum Platform {
+ PLATFORM_WIN = 1;
+ PLATFORM_MAC = 2;
+ PLATFORM_LINUX = 3;
+ PLATFORM_CROS = 4;
+ PLATFORM_IOS = 5;
+ // Just a placeholder. Likely don't need it due to the presence of the
+ // Android GCM on phone/tablet devices.
+ PLATFORM_ANDROID = 6;
+ }
+
+ enum Channel {
+ CHANNEL_STABLE = 1;
+ CHANNEL_BETA = 2;
+ CHANNEL_DEV = 3;
+ CHANNEL_CANARY = 4;
+ CHANNEL_UNKNOWN = 5; // for tip of tree or custom builds
+ }
+
+ // The platform of the device.
+ optional Platform platform = 1;
+
+ // The Chrome instance's version.
+ optional string chrome_version = 2;
+
+ // The Channel (build type) of Chrome.
+ optional Channel channel = 3;
+}
+
+// Information sent by the device in a "checkin" request.
+message AndroidCheckinProto {
+ // Miliseconds since the Unix epoch of the device's last successful checkin.
+ optional int64 last_checkin_msec = 2;
+
+ // The current MCC+MNC of the mobile device's current cell.
+ optional string cell_operator = 6;
+
+ // The MCC+MNC of the SIM card (different from operator if the
+ // device is roaming, for instance).
+ optional string sim_operator = 7;
+
+ // The device's current roaming state (reported starting in eclair builds).
+ // Currently one of "{,not}mobile-{,not}roaming", if it is present at all.
+ optional string roaming = 8;
+
+ // For devices supporting multiple user profiles (which may be
+ // supported starting in jellybean), the ordinal number of the
+ // profile that is checking in. This is 0 for the primary profile
+ // (which can't be changed without wiping the device), and 1,2,3,...
+ // for additional profiles (which can be added and deleted freely).
+ optional int32 user_number = 9;
+
+ // Class of device. Indicates the type of build proto
+ // (IosBuildProto/ChromeBuildProto/AndroidBuildProto)
+ // That is included in this proto
+ optional DeviceType type = 12 [default = DEVICE_ANDROID_OS];
+
+ // For devices running MCS on Chrome, build-specific characteristics
+ // of the browser. There are no hardware aspects (except for ChromeOS).
+ // This will only be populated for Chrome builds/ChromeOS devices
+ optional checkin_proto.ChromeBuildProto chrome_build = 13;
+
+ // Note: Some of the Android specific optional fields were skipped to limit
+ // the protobuf definition.
+ // Next 14
+}
+
+// enum values correspond to the type of device.
+// Used in the AndroidCheckinProto and Device proto.
+enum DeviceType {
+ // Android Device
+ DEVICE_ANDROID_OS = 1;
+
+ // Apple IOS device
+ DEVICE_IOS_OS = 2;
+
+ // Chrome browser - Not Chrome OS. No hardware records.
+ DEVICE_CHROME_BROWSER = 3;
+
+ // Chrome OS
+ DEVICE_CHROME_OS = 4;
+}
+
+// A concrete name/value pair sent to the device's Gservices database.
+message GservicesSetting {
+ required bytes name = 1;
+ required bytes value = 2;
+}
+
+// Devices send this every few hours to tell us how they're doing.
+message AndroidCheckinRequest {
+ // IMEI (used by GSM phones) is sent and stored as 15 decimal
+ // digits; the 15th is a check digit.
+ optional string imei = 1; // IMEI, reported but not logged.
+
+ // MEID (used by CDMA phones) is sent and stored as 14 hexadecimal
+ // digits (no check digit).
+ optional string meid = 10; // MEID, reported but not logged.
+
+ // MAC address (used by non-phone devices). 12 hexadecimal digits;
+ // no separators (eg "0016E6513AC2", not "00:16:E6:51:3A:C2").
+ repeated string mac_addr = 9; // MAC address, reported but not logged.
+
+ // An array parallel to mac_addr, describing the type of interface.
+ // Currently accepted values: "wifi", "ethernet", "bluetooth". If
+ // not present, "wifi" is assumed.
+ repeated string mac_addr_type = 19;
+
+ // Serial number (a manufacturer-defined unique hardware
+ // identifier). Alphanumeric, case-insensitive.
+ optional string serial_number = 16;
+
+ // Older CDMA networks use an ESN (8 hex digits) instead of an MEID.
+ optional string esn = 17; // ESN, reported but not logged
+
+ optional int64 id = 2; // Android device ID, not logged
+ optional int64 logging_id = 7; // Pseudonymous logging ID for Sawmill
+ optional string digest = 3; // Digest of device provisioning, not logged.
+ optional string locale = 6; // Current locale in standard (xx_XX) format
+ required AndroidCheckinProto checkin = 4;
+
+ // DEPRECATED, see AndroidCheckinProto.requested_group
+ optional string desired_build = 5;
+
+ // Blob of data from the Market app to be passed to Market API server
+ optional string market_checkin = 8;
+
+ // SID cookies of any google accounts stored on the phone. Not logged.
+ repeated string account_cookie = 11;
+
+ // Time zone. Not currently logged.
+ optional string time_zone = 12;
+
+ // Security token used to validate the checkin request.
+ // Required for android IDs issued to Froyo+ devices, not for legacy IDs.
+ optional fixed64 security_token = 13;
+
+ // Version of checkin protocol.
+ //
+ // There are currently two versions:
+ //
+ // - version field missing: android IDs are assigned based on
+ // hardware identifiers. unsecured in the sense that you can
+ // "unregister" someone's phone by sending a registration request
+ // with their IMEI/MEID/MAC.
+ //
+ // - version=2: android IDs are assigned randomly. The device is
+ // sent a security token that must be included in all future
+ // checkins for that android id.
+ //
+ // - version=3: same as version 2, but the 'fragment' field is
+ // provided, and the device understands incremental updates to the
+ // gservices table (ie, only returning the keys whose values have
+ // changed.)
+ //
+ // (version=1 was skipped to avoid confusion with the "missing"
+ // version field that is effectively version 1.)
+ optional int32 version = 14;
+
+ // OTA certs accepted by device (base-64 SHA-1 of cert files). Not
+ // logged.
+ repeated string ota_cert = 15;
+
+ // Honeycomb and newer devices send configuration data with their checkin.
+ // optional DeviceConfigurationProto device_configuration = 18;
+
+ // A single CheckinTask on the device may lead to multiple checkin
+ // requests if there is too much log data to upload in a single
+ // request. For version 3 and up, this field will be filled in with
+ // the number of the request, starting with 0.
+ optional int32 fragment = 20;
+
+ // For devices supporting multiple users, the name of the current
+ // profile (they all check in independently, just as if they were
+ // multiple physical devices). This may not be set, even if the
+ // device is using multiuser. (checkin.user_number should be set to
+ // the ordinal of the user.)
+ optional string user_name = 21;
+
+ // For devices supporting multiple user profiles, the serial number
+ // for the user checking in. Not logged. May not be set, even if
+ // the device supportes multiuser. checkin.user_number is the
+ // ordinal of the user (0, 1, 2, ...), which may be reused if users
+ // are deleted and re-created. user_serial_number is never reused
+ // (unless the device is wiped).
+ optional int32 user_serial_number = 22;
+
+ // NEXT TAG: 23
+}
+
+// The response to the device.
+message AndroidCheckinResponse {
+ required bool stats_ok = 1; // Whether statistics were recorded properly.
+ optional int64 time_msec = 3; // Time of day from server (Java epoch).
+ // repeated AndroidIntentProto intent = 2;
+
+ // Provisioning is sent if the request included an obsolete digest.
+ //
+ // For version <= 2, 'digest' contains the digest that should be
+ // sent back to the server on the next checkin, and 'setting'
+ // contains the entire gservices table (which replaces the entire
+ // current table on the device).
+ //
+ // for version >= 3, 'digest' will be absent. If 'settings_diff'
+ // is false, then 'setting' contains the entire table, as in version
+ // 2. If 'settings_diff' is true, then 'delete_setting' contains
+ // the keys to delete, and 'setting' contains only keys to be added
+ // or for which the value has changed. All other keys in the
+ // current table should be left untouched. If 'settings_diff' is
+ // absent, don't touch the existing gservices table.
+ //
+ optional string digest = 4;
+ optional bool settings_diff = 9;
+ repeated string delete_setting = 10;
+ repeated GservicesSetting setting = 5;
+
+ optional bool market_ok = 6; // If Market got the market_checkin data OK.
+
+ optional fixed64 android_id = 7; // From the request, or newly assigned
+ optional fixed64 security_token = 8; // The associated security token
+
+ optional string version_info = 11;
+ // NEXT TAG: 12
+}
+
+message HeartbeatPing {
+ optional int32 stream_id = 1;
+ optional int32 last_stream_id_received = 2;
+ optional int64 status = 3;
+}
+
+/**
+ TAG: 1
+ */
+message HeartbeatAck {
+ optional int32 stream_id = 1;
+ optional int32 last_stream_id_received = 2;
+ optional int64 status = 3;
+}
+
+message ErrorInfo {
+ required int32 code = 1;
+ optional string message = 2;
+ optional string type = 3;
+ optional Extension extension = 4;
+}
+
+// MobileSettings class.
+// "u:f", "u:b", "u:s" - multi user devices reporting foreground, background
+// and stopped users.
+// hbping: heatbeat ping interval
+// rmq2v: include explicit stream IDs
+
+message Setting {
+ required string name = 1;
+ required string value = 2;
+}
+
+message HeartbeatStat {
+ required string ip = 1;
+ required bool timeout = 2;
+ required int32 interval_ms = 3;
+}
+
+message HeartbeatConfig {
+ optional bool upload_stat = 1;
+ optional string ip = 2;
+ optional int32 interval_ms = 3;
+}
+
+// ClientEvents are used to inform the server of failed and successful
+// connections.
+message ClientEvent {
+ enum Type {
+ UNKNOWN = 0;
+ // Count of discarded events if the buffer filled up and was trimmed.
+ DISCARDED_EVENTS = 1;
+ // Failed connection event: the connection failed to be established or we
+ // had a login error.
+ FAILED_CONNECTION = 2;
+ // Successful connection event: information about the last successful
+ // connection, including the time at which it was established.
+ SUCCESSFUL_CONNECTION = 3;
+ }
+
+ // Common fields [1-99]
+ optional Type type = 1;
+
+ // Fields for DISCARDED_EVENTS messages [100-199]
+ optional uint32 number_discarded_events = 100;
+
+ // Fields for FAILED_CONNECTION and SUCCESSFUL_CONNECTION messages [200-299]
+ // Network type is a value in net::NetworkChangeNotifier::ConnectionType.
+ optional int32 network_type = 200;
+ // Reserved for network_port.
+ reserved 201;
+ optional uint64 time_connection_started_ms = 202;
+ optional uint64 time_connection_ended_ms = 203;
+ // Error code should be a net::Error value.
+ optional int32 error_code = 204;
+
+ // Fields for SUCCESSFUL_CONNECTION messages [300-399]
+ optional uint64 time_connection_established_ms = 300;
+}
+
+/**
+ TAG: 2
+ */
+message LoginRequest {
+ enum AuthService {
+ ANDROID_ID = 2;
+ }
+ required string id = 1; // Must be present ( proto required ), may be empty
+ // string.
+ // mcs.android.com.
+ required string domain = 2;
+ // Decimal android ID
+ required string user = 3;
+
+ required string resource = 4;
+
+ // Secret
+ required string auth_token = 5;
+
+ // Format is: android-HEX_DEVICE_ID
+ // The user is the decimal value.
+ optional string device_id = 6;
+
+ // RMQ1 - no longer used
+ optional int64 last_rmq_id = 7;
+
+ repeated Setting setting = 8;
+ //optional int32 compress = 9;
+ repeated string received_persistent_id = 10;
+
+ // Replaced by "rmq2v" setting
+ // optional bool include_stream_ids = 11;
+
+ optional bool adaptive_heartbeat = 12;
+ optional HeartbeatStat heartbeat_stat = 13;
+ // Must be true.
+ optional bool use_rmq2 = 14;
+ optional int64 account_id = 15;
+
+ // ANDROID_ID = 2
+ optional AuthService auth_service = 16;
+
+ optional int32 network_type = 17;
+ optional int64 status = 18;
+
+ // 19, 20, and 21 are not currently populated by Chrome.
+ reserved 19, 20, 21;
+
+ // Events recorded on the client after the last successful connection.
+ repeated ClientEvent client_event = 22;
+}
+
+/**
+ * TAG: 3
+ */
+message LoginResponse {
+ required string id = 1;
+ // Not used.
+ optional string jid = 2;
+ // Null if login was ok.
+ optional ErrorInfo error = 3;
+ repeated Setting setting = 4;
+ optional int32 stream_id = 5;
+ // Should be "1"
+ optional int32 last_stream_id_received = 6;
+ optional HeartbeatConfig heartbeat_config = 7;
+ // used by the client to synchronize with the server timestamp.
+ optional int64 server_timestamp = 8;
+}
+
+message StreamErrorStanza {
+ required string type = 1;
+ optional string text = 2;
+}
+
+/**
+ * TAG: 4
+ */
+message Close {
+}
+
+message Extension {
+ // 12: SelectiveAck
+ // 13: StreamAck
+ required int32 id = 1;
+ required bytes data = 2;
+}
+
+/**
+ * TAG: 7
+ * IqRequest must contain a single extension. IqResponse may contain 0 or 1
+ * extensions.
+ */
+message IqStanza {
+ enum IqType {
+ GET = 0;
+ SET = 1;
+ RESULT = 2;
+ IQ_ERROR = 3;
+ }
+
+ optional int64 rmq_id = 1;
+ required IqType type = 2;
+ required string id = 3;
+ optional string from = 4;
+ optional string to = 5;
+ optional ErrorInfo error = 6;
+
+ // Only field used in the 38+ protocol (besides common last_stream_id_received, status, rmq_id)
+ optional Extension extension = 7;
+
+ optional string persistent_id = 8;
+ optional int32 stream_id = 9;
+ optional int32 last_stream_id_received = 10;
+ optional int64 account_id = 11;
+ optional int64 status = 12;
+}
+
+message AppData {
+ required string key = 1;
+ required string value = 2;
+}
+
+/**
+ * TAG: 8
+ */
+message DataMessageStanza {
+ // Not used.
+ // optional int64 rmq_id = 1;
+
+ // This is the message ID, set by client, DMP.9 (message_id)
+ optional string id = 2;
+
+ // Project ID of the sender, DMP.1
+ required string from = 3;
+
+ // Part of DMRequest - also the key in DataMessageProto.
+ optional string to = 4;
+
+ // Package name. DMP.2
+ required string category = 5;
+
+ // The collapsed key, DMP.3
+ optional string token = 6;
+
+ // User data + GOOGLE. prefixed special entries, DMP.4
+ repeated AppData app_data = 7;
+
+ // Not used.
+ optional bool from_trusted_server = 8;
+
+ // Part of the ACK protocol, returned in DataMessageResponse on server side.
+ // It's part of the key of DMP.
+ optional string persistent_id = 9;
+
+ // In-stream ack. Increments on each message sent - a bit redundant
+ // Not used in DMP/DMR.
+ optional int32 stream_id = 10;
+ optional int32 last_stream_id_received = 11;
+
+ // Not used.
+ // optional string permission = 12;
+
+ // Sent by the device shortly after registration.
+ optional string reg_id = 13;
+
+ // Not used.
+ // optional string pkg_signature = 14;
+ // Not used.
+ // optional string client_id = 15;
+
+ // serial number of the target user, DMP.8
+ // It is the 'serial number' according to user manager.
+ optional int64 device_user_id = 16;
+
+ // Time to live, in seconds.
+ optional int32 ttl = 17;
+ // Timestamp ( according to client ) when message was sent by app, in seconds
+ optional int64 sent = 18;
+
+ // How long has the message been queued before the flush, in seconds.
+ // This is needed to account for the time difference between server and
+ // client: server should adjust 'sent' based on its 'receive' time.
+ optional int32 queued = 19;
+
+ optional int64 status = 20;
+
+ // Optional field containing the binary payload of the message.
+ optional bytes raw_data = 21;
+
+ // Not used.
+ // The maximum delay of the message, in seconds.
+ // optional int32 max_delay = 22;
+
+ // Not used.
+ // How long the message was delayed before it was sent, in seconds.
+ // optional int32 actual_delay = 23;
+
+ // If set the server requests immediate ack. Used for important messages and
+ // for testing.
+ optional bool immediate_ack = 24;
+
+ // Not used.
+ // Enables message receipts from MCS/GCM back to CCS clients
+ // optional bool delivery_receipt_requested = 25;
+}
+
+/**
+ Included in IQ with ID 13, sent from client or server after 10 unconfirmed
+ messages.
+ */
+message StreamAck {
+ // No last_streamid_received required. This is included within an IqStanza,
+ // which includes the last_stream_id_received.
+}
+
+/**
+ Included in IQ sent after LoginResponse from server with ID 12.
+*/
+message SelectiveAck {
+ repeated string id = 1;
+}
\ No newline at end of file
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..5c12ca84c
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/ErrorHandler.java
@@ -0,0 +1,163 @@
+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.BiConsumer;
+
+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 whatsapp the caller api
+ * @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(Whatsapp whatsapp, Location location, Throwable throwable);
+
+ /**
+ * Default error handler. Prints the exception on the terminal.
+ *
+ * @return a non-null error handler
+ */
+ @SuppressWarnings("CallToPrintStackTrace")
+ static ErrorHandler toTerminal() {
+ return defaultErrorHandler((api, error) -> error.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((api, error) -> Exceptions.save(error));
+ }
+
+ /**
+ * 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((api, error) -> Exceptions.save(directory, error));
+ }
+
+ /**
+ * 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(BiConsumer printer) {
+ return (whatsapp, location, throwable) -> {
+ var logger = System.getLogger("ErrorHandler");
+ logger.log(ERROR, "Socket failure at %s".formatted(location));
+ if (printer != null) {
+ printer.accept(whatsapp, throwable);
+ }
+
+ if (location == CRYPTOGRAPHY && whatsapp.store().clientType() == 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, "Restore");
+ 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 response 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..5ceb23902
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/MobileOptionsBuilder.java
@@ -0,0 +1,136 @@
+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 java.util.Optional;
+
+@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)
+ .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, 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);
+ }
+}
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..f5a9813b6
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/MobileRegistrationBuilder.java
@@ -0,0 +1,201 @@
+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.net.URI;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+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;
+ RegisteredResult result;
+ AsyncVerificationCodeSupplier verificationCodeSupplier;
+
+ MobileRegistrationBuilder(Store store, Keys keys, ErrorHandler errorHandler) {
+ this.store = store;
+ this.keys = keys;
+ this.errorHandler = errorHandler;
+ }
+
+ public final static class Unregistered extends MobileRegistrationBuilder {
+ private UnverifiedResult unregisteredResult;
+ private VerificationCodeMethod verificationCodeMethod;
+
+ Unregistered(Store store, Keys keys, ErrorHandler errorHandler) {
+ super(store, keys, errorHandler);
+ 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 proxy(URI proxy) {
+ store.setProxy(proxy);
+ 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)
+ .build();
+ return this.result = new RegisteredResult(api, Optional.ofNullable(response));
+ });
+ }
+
+ var api = Whatsapp.customBuilder()
+ .store(store)
+ .keys(keys)
+ .errorHandler(errorHandler)
+ .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, verificationCodeSupplier);
+ return this.unregisteredResult = new UnverifiedResult(unverified, Optional.ofNullable(response));
+ });
+ }
+
+ var unverified = new Unverified(store, keys, errorHandler, 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, AsyncVerificationCodeSupplier verificationCodeSupplier) {
+ super(store, keys, errorHandler);
+ 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;
+ }
+
+ public Unverified proxy(URI proxy) {
+ store.setProxy(proxy);
+ 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)
+ .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..4f10e1cb3
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/OptionsBuilder.java
@@ -0,0 +1,113 @@
+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 java.net.URI;
+
+@SuppressWarnings("unused")
+public sealed class OptionsBuilder> permits MobileOptionsBuilder, WebOptionsBuilder {
+ Store store;
+ Keys keys;
+ ErrorHandler errorHandler;
+
+ 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 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 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..7a7f05487
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/WebOptionsBuilder.java
@@ -0,0 +1,102 @@
+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);
+ }
+
+ /**
+ * Whether the library should send receipts automatically for messages
+ * By default disabled
+ * For the web api, if enabled, the companion won't receive notifications
+ *
+ * @return the same instance for chaining
+ */
+ public WebOptionsBuilder automaticMessageReceipts(boolean automaticMessageReceipts) {
+ store.setAutomaticMessageReceipts(automaticMessageReceipts);
+ return this;
+ }
+
+ /**
+ * 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)
+ .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)
+ .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)
+ .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..16becb886
--- /dev/null
+++ b/src/main/java/it/auties/whatsapp/api/Whatsapp.java
@@ -0,0 +1,3773 @@
+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.listener.processor.RegisterListenerProcessor;
+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.NewsletterAdminInviteMessageBuilder;
+import it.auties.whatsapp.model.message.standard.ReactionMessageBuilder;
+import it.auties.whatsapp.model.message.standard.TextMessage;
+import it.auties.whatsapp.model.newsletter.*;
+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.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+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;
+
+ /**
+ * 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) {
+ this.socketHandler = new SocketHandler(this, store, keys, errorHandler, webVerificationHandler);
+ addDisconnectionHandler(store);
+ registerListenersAutomatically(store);
+ }
+
+ private static void addDisconnectionHandler(Store store) {
+ store.addListener((OnDisconnected) (reason) -> {
+ if (reason != DisconnectReason.RECONNECTING) {
+ removeInstanceByUuid(store.uuid());
+ }
+ });
+ }
+
+ private void registerListenersAutomatically(Store store) {
+ if (!store.autodetectListeners()) {
+ return;
+ }
+
+ try {
+ var clazz = Class.forName(RegisterListenerProcessor.qualifiedClassName());
+ var method = clazz.getMethod(RegisterListenerProcessor.methodName(), Whatsapp.class);
+ method.invoke(null, this);
+ }catch (ClassNotFoundException exception) {
+ // Ignored, this can happen if the compilation environment didn't register the processor
+ }catch (ReflectiveOperationException exception) {
+ throw new RuntimeException("Cannot register listeners automatically", exception);
+ }
+ }
+
+ /**
+ * 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 CompletableFuture connect() {
+ return socketHandler.connect()
+ .thenRunAsync(() -> instances.put(store().uuid(), this))
+ .thenApply(ignored -> this);
+ }
+
+ /**
+ * Waits for this session to be disconnected
+ */
+ public void awaitDisconnection() {
+ var future = new CompletableFuture();
+ addDisconnectedListener((reason) -> {
+ if(reason == DisconnectReason.DISCONNECTED || reason == DisconnectReason.LOGGED_OUT) {
+ future.complete(null);
+ }
+ });
+ future.join();
+ }
+
+ /**
+ * 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> sendReaction(MessageInfo message, String reaction) {
+ var key = new ChatMessageKeyBuilder()
+ .id(ChatMessageKey.randomId())
+ .chatJid(message.parentJid())
+ .senderJid(message.senderJid())
+ .fromMe(Objects.equals(message.senderJid().toSimpleJid(), jidOrThrowError().toSimpleJid()))
+ .id(message.id())
+ .build();
+ var reactionMessage = new ReactionMessageBuilder()
+ .key(key)
+ .content(reaction)
+ .timestampSeconds(Instant.now().toEpochMilli())
+ .build();
+ return sendChatMessage(message.parentJid(), MessageContainer.of(reactionMessage), false);
+ }
+
+ /**
+ * 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> 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 extends MessageInfo> 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) {
+ return sendChatMessage(recipient, message, message.content().type() == MessageType.TEXT);
+ }
+
+ public CompletableFuture sendChatMessage(JidProvider recipient, MessageContainer message, boolean compose) {
+ 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) {
+ if(store().clientType() == ClientType.MOBILE) {
+ throw new IllegalStateException("The mobile API cannot send messages or the account will be banned, if you need to send messages please contact me on Telegram @Auties00");
+ }
+
+ 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) {
+ if(store().clientType() == ClientType.MOBILE) {
+ throw new IllegalStateException("The mobile API cannot send messages or the account will be banned, if you need to send messages please contact me on Telegram @Auties00");
+ }
+
+ 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