Skip to content

Instantly share code, notes, and snippets.

@datfooldive
Created December 29, 2025 07:20
Show Gist options
  • Select an option

  • Save datfooldive/16e33cbadc27bf24612a5120e561a343 to your computer and use it in GitHub Desktop.

Select an option

Save datfooldive/16e33cbadc27bf24612a5120e561a343 to your computer and use it in GitHub Desktop.
This file has been truncated, but you can view the full file.
================================================
FILE: README.md
================================================
# Cobalt
> **_IMPORTANT:_** The library is currently being rewritten to prepare for a stable release. The code on the master branch is under active development.
Whatsapp4j has been renamed to Cobalt to comply with an official request coming from Whatsapp.
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)
I can also work on sponsored features and/or projects!
### Java version
This library requires at least [Java 21](https://openjdk.java.net/projects/jdk/21/).
GraalVM native compilation is supported!
### 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.
### Can this library get my device banned?
While there is no risk in using this library with your main account, keep in mind that Whatsapp has anti-spam measures for their web client.
If you add a participant from a brand-new number to a group, it will most likely get you banned.
If you compile the library yourself, don't run the CI on a brand-new number, or it will get banned for spamming too many requests(the CI has to test that all the library works).
In short, if you use this library without a malicious intent, you will never get banned.
### How to install
#### Maven
```xml
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>cobalt</artifactId>
<version>0.0.10</version>
</dependency>
```
#### Gradle
- Groovy DSL
```groovy
implementation 'com.github.auties00:cobalt:0.0.10'
```
- Kotlin DSL
```groovy
implementation("com.github.auties00:cobalt:0.0.10")
```
### Javadocs & Documentation
Javadocs for Cobalt are available [here](https://www.javadoc.io/doc/com.github.auties00/cobalt/0.0.10).
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)
Check the frida module to understand how I go about reversing features
### 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
<details>
<summary>Detailed Walkthrough</summary>
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)
```
- whatsappMessagePreviewHandler - Whether a media preview should be generated for text messages containing links
```java
.whatsappMessagePreviewHandler(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 sessionRecord
Finally select the registration status of your sessionRecord:
- Creates a new registered sessionRecord: this means that the QR code was already scanned / the OTP was already sent to Whatsapp
```java
.registered()
```
- Creates a new unregistered sessionRecord: 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 sessionRecord:
```java
.connect()
```
to connect to Whatsapp.
Remember to handle the result using, for example, `join` to await the connection's result.
Finally, if you want to pause the current thread until the connection is closed, use:
```java
.awaitDisconnection()
```
</details>
<details>
<summary>Web QR Pairing Example</summary>
```java
Whatsapp.webBuilder() // Use the Web api
.newConnection() // Create a new connection
.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
.awaitDisconnection(); // Wait
```
</details>
<details>
<summary>Web Pairing Code Example</summary>
```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
.newConnection() // Create a new connection
.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
.awaitDisconnection(); // Wait
```
</details>
<details>
<summary>Mobile Example</summary>
```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
.newConnection() // Create a new connection
.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 value asynchronously, if necessary
.join() // Await the result
.whatsapp() // Access the Whatsapp instance
.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
.awaitDisconnection(); // Wait
```
</details>
### How to close a connection
There are three ways to close a connection:
1. Disconnect
```java
api.disconnect();
```
> **_IMPORTANT:_** The sessionRecord remains valid for future uses
2. Reconnect
```java
api.reconnect();
```
> **_IMPORTANT:_** The sessionRecord remains valid for future uses
3. Log out
```java
api.logout();
```
> **_IMPORTANT:_** The sessionRecord 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 com.github.auties00.cobalt.listener.WhatsAppClientListener;
public class MyListener implements com.github.auties00.cobalt.listener.WhatsAppClientListener {
@Override
public void onLoggedIn() {
System.out.println("Hello :)");
}
}
```
Remember to register this listener:
```java
api.addListener(new MyListener());
```
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 sessionRecord at `$HOME/.whatsapp4j/[web|mobile]/<session_id>`.
The data is stored in protobuf files.
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 sessionRecord disconnects
When the sessionRecord 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. RECONNECTING
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
class ThrowOnLogOut implements WhatsappListener {
@Override
public void onDisconnected(DisconnectReason reason) {
if (reason != SocketEvent.LOGGED_OUT) {
return;
}
throw new RuntimeException("Hey, I was logged off :/");
}
}
```
4. BANNED
The client was banned by Whatsapp, usually happens when sending spam messages to people that aren't in your contact list
### 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 value 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 value 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 value 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);
```
### 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
- **Create a community:**
```java
var future = api.createCommunity("New Community Name", "Optional community description");
```
- **Query community metadata:**
```java
var future = api.queryCommunityMetadata(communityJid);
```
- **Deactivate a community:**
```java
var future = api.deactivateCommunity(communityJid);
```
- **Change community picture:**
```java
byte[] imageBytes = ...; // Or use URI
var future = api.changeCommunityPicture(communityJid, imageBytes);
var removeFuture = api.changeCommunityPicture(communityJid, (byte[]) null); // Remove picture
```
- **Change community subject (name):**
```java
var future = api.changeCommunitySubject(communityJid, "Updated Community Name");
```
- **Change community description:**
```java
var future = api.changeCommunityDescription(communityJid, "Updated description");
var removeFuture = api.changeCommunityDescription(communityJid, null); // Remove description
```
- **Change community setting:**
```java
// Who can add groups (MODIFY_GROUPS) or add participants (ADD_PARTICIPANTS)
var future = api.changeCommunitySetting(communityJid, CommunitySetting.MODIFY_GROUPS, ChatSettingPolicy.ADMINS);
```
- **Link groups to a community:**
```java
var future = api.addCommunityGroups(communityJid, groupJid1, groupJid2);
```
- **Unlink a group from a community:**
```java
var future = api.removeCommunityGroup(communityJid, groupJid);
```
- **Promote participants to admin in a community:**
```java
var future = api.promoteCommunityParticipants(communityJid, contactJid1, contactJid2);
```
- **Demote participants to member in a community:**
```java
var future = api.demoteCommunityParticipants(communityJid, contactJid1, contactJid2);
```
- **Add participants to a community:** (Adds to announcement group)
```java
var future = api.addCommunityParticipants(communityJid, contactJid1, contactJid2);
```
- **Remove participants from a community:**
```java
var future = api.removeCommunityParticipants(communityJid, contactJid1, contactJid2);
```
- **Leave a community:** (Leaves community and all linked groups)
```java
var future = api.leaveCommunity(communityJid);
```
### Newsletters / Channels
- **Query recommended newsletters:**
```java
var future = api.queryRecommendedNewsletters("US", 50); // Country code and limit
```
- **Query newsletter messages:**
```java
var future = api.queryNewsletterMessages(newsletterJid, 100); // Query last 100 messages
```
- **Subscribe to newsletter reactions:**
```java
var future = api.subscribeToNewsletterReactions(newsletterJid);
```
- **Create a newsletter:**
```java
byte[] pictureBytes = ...; // Optional picture
var future = api.createNewsletter("My Newsletter", "Description", pictureBytes);
var simpleFuture = api.createNewsletter("Simple Newsletter"); // Name only
```
- **Change newsletter description:**
```java
var future = api.changeNewsletterDescription(newsletterJid, "New Description");
var removeFuture = api.changeNewsletterDescription(newsletterJid, null); // Remove description
```
- **Join a newsletter:**
```java
var future = api.joinNewsletter(newsletterJid);
```
- **Leave a newsletter:**
```java
var future = api.leaveNewsletter(newsletterJid);
```
- **Query newsletter subscribers count:**
```java
var future = api.queryNewsletterSubscribers(newsletterJid);
```
- **Invite newsletter admins:** (Sends an invite message to the user)
```java
var future = api.inviteNewsletterAdmins(newsletterJid, "Join as admin!", adminJid1, adminJid2);
```
- **Revoke newsletter admin invite:**
```java
var future = api.revokeNewsletterAdminInvite(newsletterJid, adminJid);
```
- **Accept newsletter admin invite:**
```java
var future = api.acceptNewsletterAdminInvite(newsletterJid);
```
- **Query newsletter metadata:**
```java
// Role required depends on what info you need (e.g., GUEST, SUBSCRIBER, ADMIN)
var future = api.queryNewsletter(newsletterJid, NewsletterViewerRole.SUBSCRIBER);
```
- **Send newsletter message:**
```java
var future = api.sendNewsletterMessage(newsletterJid, message);
```
- **Edit newsletter message:**
```java
var future = api.editMessage(oldMessage, newContent);
```
- **Delete newsletter message:**
```java
var future = api.deleteMessage(messageToDelete);
```
- **Download newsletter media:**
```java
var future = api.downloadMedia(mediaMessageInfo);
```
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.
================================================
FILE: LICENSE
================================================
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.
================================================
FILE: mvnw
================================================
#!/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 "$@"
================================================
FILE: mvnw.cmd
================================================
@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%
================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.auties00</groupId>
<artifactId>cobalt</artifactId>
<version>0.1.0</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Standalone fully-featured WhatsApp Web API for Java and Kotlin</description>
<url>https://github.com/Auties00/Cobalt</url>
<developers>
<developer>
<name>Alessandro Autiero</name>
<email>alautiero@gmail.com</email>
</developer>
</developers>
<licenses>
<license>
<name>MIT License</name>
<url>https://www.opensource.org/licenses/mit-license.php</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<url>https://github.com/Auties00/Cobalt/tree/master</url>
<connection>scm:git:https://github.com/Auties00/Cobalt.git</connection>
<developerConnection>scm:ssh:https://github.com/Auties00/Cobalt.git</developerConnection>
</scm>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
<repositories>
<repository>
<id>jitpack.io</id>
<name>Jitpack</name>
<url>https://jitpack.io</url>
</repository>
</repositories>
<profiles>
<profile>
<id>sign</id>
<activation>
<property>
<name>performRelease</name>
<value>true</value>
</property>
</activation>
<build>
<plugins>
<!-- Attach the sourcecode -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>${maven.source.plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Generate and attach javadocs from source -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven.javadoc.plugin.version}</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<release>${java.version}</release>
<failOnError>true</failOnError>
<show>private</show>
<debug>true</debug>
<verbose>true</verbose>
</configuration>
</plugin>
<!-- Sign the artifacts -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven.gpg.plugin.version}</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
<configuration>
<keyname>${gpg.keyname}</keyname>
<passphraseServerId>${gpg.passphrase}</passphraseServerId>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
</execution>
</executions>
</plugin>
<!-- Deploy the artifact -->
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>${maven.staging.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
<autoPublish>true</autoPublish>
<waitUntil>published</waitUntil>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>uber-jar</id>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>25</java.version>
<maven.surefire.plugin.version>3.0.0-M9</maven.surefire.plugin.version>
<maven.gpg.plugin.version>3.0.1</maven.gpg.plugin.version>
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
<maven.source.plugin.version>3.2.1</maven.source.plugin.version>
<maven.javadoc.plugin.version>3.5.0</maven.javadoc.plugin.version>
<maven.staging.plugin.version>0.8.0</maven.staging.plugin.version>
<zxing.version>3.5.1</zxing.version>
<json.version>2.0.57</json.version>
<protoc.version>3.4.5</protoc.version>
<junit.version>5.10.0-M1</junit.version>
<libsignal.version>0.0.3</libsignal.version>
<curve25519.version>3.0.1</curve25519.version>
<lazysodium.version>5.1.4</lazysodium.version>
<linkpreview.version>2.3</linkpreview.version>
<vcard.version>0.12.1</vcard.version>
<qr.terminal.version>2.2</qr.terminal.version>
<libphonenumber.version>9.0.4</libphonenumber.version>
<maven.versions.version>2.15.0</maven.versions.version>
<plist.version>1.27</plist.version>
<sl4j.version>2.0.13</sl4j.version>
<apk.parser.version>2.6.10</apk.parser.version>
<collections.version>1.0.0</collections.version>
</properties>
<build>
<plugins>
<!-- Compile the project-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>25</source>
<target>25</target>
<release>${java.version}</release>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.github.auties00</groupId>
<artifactId>protobuf-serialization-plugin</artifactId>
<version>${protoc.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
<!-- Test the library to be sure that everything works-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Generate and print QR code (Web API) -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${zxing.version}</version>
</dependency>
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>qr-terminal</artifactId>
<version>${qr.terminal.version}</version>
</dependency>
<!-- Cryptography (used by Whatsapp) -->
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>libsignal</artifactId>
<version>${libsignal.version}</version>
</dependency>
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>curve25519</artifactId>
<version>${curve25519.version}</version>
</dependency>
<!-- Protobuf serialization (used by Whatsapp) -->
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>protobuf-base</artifactId>
<version>${protoc.version}</version>
</dependency>
<!-- JSON serialization (used by Whatsapp) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${json.version}</version>
</dependency>
<!-- Parse phone numbers (Mobile API) -->
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
<version>${libphonenumber.version}</version>
</dependency>
<!-- Parse Android APK (Mobile API) -->
<dependency>
<groupId>net.dongliu</groupId>
<artifactId>apk-parser</artifactId>
<version>${apk.parser.version}</version>
</dependency>
<!-- Generate message previews -->
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>link-preview</artifactId>
<version>${linkpreview.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.ez-vcard</groupId>
<artifactId>ez-vcard</artifactId>
<version>${vcard.version}</version>
</dependency>
<!-- Store messages in memory -->
<dependency>
<groupId>com.github.auties00</groupId>
<artifactId>collections</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Testing framework -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
================================================
FILE: frida/README.md
================================================
# Research Module
This directory contains various research scripts I developed
Integrity scripts run an HTTP server on top of the Whatsapp app to bypass functions that need device integrity
None of this stuff is maintained, it's just public for people who love reverse engineering and want to see how I developed the library
================================================
FILE: frida/mobile/android/integrity/README.md
================================================
# Android middleware
### Requirements
1. Rooted android phone with Play Services
2. Magisk with Zygisk enabled and [PlayIntegrityFix module](https://github.com/chiteroman/PlayIntegrityFix) installed
3. [Frida server installed](https://frida.re/docs/android/)
4. Whatsapp and/or Whatsapp business installed on the phone **from the Play Store** (APKs don't work)
### How to run
1. Run `npm install` in the android directory
2. Open Whatsapp/Whatsapp Business and try to register a number (needed to load gpia components, won't work if you don't do it)
3. Run:
- `frida -U "WhatsApp" -l server_with_dependencies.js` (WhatsApp)
- `frida -U "WhatsApp Business" -l server_with_dependencies.js` (WhatsApp Business)
================================================
FILE: frida/mobile/android/integrity/package.json
================================================
{
"name": "android",
"version": "1.0.0",
"description": "Example showing how to use frida-net's server functionality",
"private": true,
"main": "demo.js",
"scripts": {
"prepare": "npm run build",
"build": "frida-compile server.js -o server_with_dependencies.js -c",
"watch": "frida-compile server.js -o server_with_dependencies.js -w"
},
"devDependencies": {
"@types/frida-gum": "^18.5.1",
"@types/node": "^18.19.3",
"frida-compile": "^16.4.1"
}
}
================================================
FILE: frida/mobile/android/integrity/server.js
================================================
import http from 'http';
import url from "url";
import {nextTick} from 'node:process';
let CountdownLatch = function (limit, onSuccess) {
this.limit = limit
this.count = 0
this.waitBlock = onSuccess
}
CountdownLatch.prototype.countDown = function () {
this.count = this.count + 1
if (this.limit <= this.count) {
this.waitBlock()
}
}
CountdownLatch.prototype.onSuccess = function (success) {
this.waitBlock = success
}
function semaphore(capacity) {
var semaphore = {
capacity: capacity || 1,
current: 0,
queue: [],
firstHere: false,
take: function () {
if (semaphore.firstHere === false) {
semaphore.current++;
semaphore.firstHere = true;
var isFirst = 1;
} else {
var isFirst = 0;
}
var item = {n: 1};
if (typeof arguments[0] == 'function') {
item.task = arguments[0];
} else {
item.n = arguments[0];
}
if (arguments.length >= 2) {
if (typeof arguments[1] == 'function') item.task = arguments[1];
else item.n = arguments[1];
}
var task = item.task;
item.task = function () {
task(semaphore.leave);
};
if (semaphore.current + item.n - isFirst > semaphore.capacity) {
if (isFirst === 1) {
semaphore.current--;
semaphore.firstHere = false;
}
return semaphore.queue.push(item);
}
semaphore.current += item.n - isFirst;
item.task(semaphore.leave);
if (isFirst === 1) semaphore.firstHere = false;
},
leave: function (n) {
n = n || 1;
semaphore.current -= n;
if (!semaphore.queue.length) {
if (semaphore.current < 0) {
throw new Error('leave called too many times.');
}
return;
}
var item = semaphore.queue[0];
if (item.n + semaphore.current > semaphore.capacity) {
return;
}
semaphore.queue.shift();
semaphore.current += item.n;
nextTick(item.task);
},
available: function (n) {
n = n || 1;
return (semaphore.current + n <= semaphore.capacity);
}
};
return semaphore;
}
let setupLatch = new CountdownLatch(2)
let integrityTokenProvider, integrityRequestType, integrityRequestBuilder
let integrityCounter = 0
let integritySemaphore = semaphore()
let certificateCounter = 0
let infoData
Java.perform(function () {
const Modifier = Java.use("java.lang.reflect.Modifier")
const KeyPairGenerator = Java.use('java.security.KeyPairGenerator')
const KeyStore = Java.use('java.security.KeyStore')
const KeyStorePrivateKeyEntry = Java.use("java.security.KeyStore$PrivateKeyEntry")
const Signature = Java.use('java.security.Signature')
const Base64 = Java.use('java.util.Base64')
const Date = Java.use('java.util.Date')
const ByteBuffer = Java.use('java.nio.ByteBuffer')
const ByteOrder = Java.use("java.nio.ByteOrder")
const ByteArrayOutputStream = Java.use('java.io.ByteArrayOutputStream')
const System = Java.use('java.lang.System')
const Arrays = Java.use('java.util.Arrays')
const File = Java.use('java.io.File')
const Files = Java.use('java.nio.file.Files')
const MessageDigest = Java.use('java.security.MessageDigest')
const ZipInputStream = Java.use("java.util.zip.ZipInputStream")
const ActivityThread = Java.use('android.app.ActivityThread')
const OnSuccessListenerType = Java.use("com.google.android.gms.tasks.OnSuccessListener")
const OnFailureListenerType = Java.use("com.google.android.gms.tasks.OnFailureListener")
const KeyGenParameterSpecBuilder = Java.use('android.security.keystore.KeyGenParameterSpec$Builder')
const KeyProperties = Java.use('android.security.keystore.KeyProperties')
const PackageManager = Java.use("android.content.pm.PackageManager")
const Math = Java.use("java.lang.Math")
const JavaString = Java.use("java.lang.String")
const StandardCharsets = Java.use("java.nio.charset.StandardCharsets")
const SecretKeyFactory = Java.use("javax.crypto.SecretKeyFactory")
const PBEKeySpec = Java.use("javax.crypto.spec.PBEKeySpec")
const Key = Java.use("java.security.Key")
const Path = Java.use("java.nio.file.Path")
const projectId = 293955441834
const appSignature = "3987d043d10aefaf5a8710b3671418fe57e0e19b653c9df82558feb5ffce5d44"
const secretKeySalt = Base64.getDecoder().decode("PkTwKSZqUfAUyR0rPQ8hYJ0wNsQQ3dW1+3SCnyTXIfEAxxS75FwkDf47wNv/c8pP3p0GXKR6OOQmhyERwx74fw1RYSU10I4r1gyBVDbRJ40pidjM41G1I1oN")
const personalPackageId = "com.whatsapp"
const personalServerPort = 1119
const businessServerPort = 1120
function findIntegrityManagerProvider() {
const loadedClasses = Java.enumerateLoadedClassesSync()
for (const className of loadedClasses) {
if (className.startsWith("com.google.android.play.core.integrity")) {
const targetClass = Java.use(className)
const methods = targetClass.class.getDeclaredMethods()
for (const method of methods) {
if (Modifier.isStatic(method.getModifiers())) {
const parameterTypes = method.getParameterTypes()
if (parameterTypes.length === 1 && parameterTypes[0].getName() === "android.content.Context") {
return targetClass[method.getName()]
}
}
}
}
}
throw new Error('Cannot find IntegrityManagerFactoryCreate method')
}
function getIntegrityManager(provider) {
const context = ActivityThread.currentApplication().getApplicationContext()
const integrityFactory = provider.overload("android.content.Context").call(context)
const methods = integrityFactory.class.getDeclaredMethods()
if (methods.length !== 1) {
throw new Error('Too many methods in integrity factory: ' + methods.length)
}
const method = methods[0]
return integrityFactory[method.getName()].call(integrityFactory)
}
function findPrepareIntegrityRequestMeta(integrityManager) {
const prepareIntegrityManagerMethods = integrityManager.class.getDeclaredMethods()
if (prepareIntegrityManagerMethods.length !== 1) {
throw new Error('Too many methods in integrity manager: ' + prepareIntegrityManagerMethods.length)
}
const prepareIntegrityManagerMethod = prepareIntegrityManagerMethods[0]
const prepareIntegrityManagerMethodParamTypes = prepareIntegrityManagerMethod.getParameterTypes()
if (prepareIntegrityManagerMethodParamTypes.length !== 1) {
throw new Error('Unexpected number of parameters: ' + prepareIntegrityManagerMethodParamTypes.length)
}
const prepareRequestType = prepareIntegrityManagerMethodParamTypes[0]
const prepareRequestTypeClass = Java.use(prepareRequestType.getName())
const prepareRequestTypeMethods = prepareRequestType.getDeclaredMethods()
for (const prepareRequestTypeMethod of prepareRequestTypeMethods) {
if (Modifier.isStatic(prepareRequestTypeMethod.getModifiers()) && prepareRequestTypeMethod.getParameterTypes().length === 0) {
return [prepareRequestTypeClass, prepareRequestTypeClass[prepareRequestTypeMethod.getName()]]
}
}
throw new Error('Cannot find prepare request builder method')
}
function createIntegrityTokenProvider(integrityManager, prepareIntegrityRequestType, prepareIntegrityRequestBuilder, onSuccess, onError) {
const integrityTokenPrepareRequestBuilder = prepareIntegrityRequestBuilder.overload().call(prepareIntegrityRequestType)
integrityTokenPrepareRequestBuilder.setCloudProjectNumber(projectId)
const integrityTokenPrepareRequest = integrityTokenPrepareRequestBuilder.build()
const integrityTokenPrepareResponse = integrityManager.prepareIntegrityToken(integrityTokenPrepareRequest)
const onTokenProviderCreatedListenerType = Java.registerClass({
name: 'IntegrityTokenProviderHandler', implements: [OnSuccessListenerType], methods: {
onSuccess: function (result) {
onSuccess(Java.cast(result, Java.use(result.$className)))
}
}
})
let onTokenProviderFailedListenerType = Java.registerClass({
name: 'IntegrityTokenProviderErrorHandler', implements: [OnFailureListenerType], methods: {
onFailure: function (result) {
let javaResult = Java.cast(result, Java.use(result.$className))
onError(javaResult.getMessage())
}
}
})
const onTokenProviderCreatedListener = onTokenProviderCreatedListenerType.$new()
const onTokenProviderFailureListener = onTokenProviderFailedListenerType.$new()
integrityTokenPrepareResponse["addOnSuccessListener"].overload('com.google.android.gms.tasks.OnSuccessListener').call(integrityTokenPrepareResponse, onTokenProviderCreatedListener)
integrityTokenPrepareResponse["addOnFailureListener"].overload('com.google.android.gms.tasks.OnFailureListener').call(integrityTokenPrepareResponse, onTokenProviderFailureListener)
}
function calculateIntegrityToken(integrityTokenProvider, integrityRequestType, integrityRequestBuilderMethod, authKey, onSuccess, onError) {
integrityCounter++
let integrityRequestBuilder = integrityRequestBuilderMethod.overload().call(integrityRequestType)
integrityRequestBuilder.setRequestHash(authKey)
let integrityRequest = integrityRequestBuilder.build()
let javaIntegrityTokenProvider = Java.cast(integrityTokenProvider, Java.use(integrityTokenProvider.$className))
let integrityTokenResponse = javaIntegrityTokenProvider.request(integrityRequest)
let onIntegrityTokenSuccessListenerType = Java.registerClass({
name: 'TokenSuccessHandler' + integrityCounter, implements: [OnSuccessListenerType], methods: {
onSuccess: function (result) {
let javaResult = Java.cast(result, Java.use(result.$className))
onSuccess(javaResult.token())
}
}
})
let onIntegrityTokenErrorListenerType = Java.registerClass({
name: 'TokenFailureHandler' + integrityCounter, implements: [OnFailureListenerType], methods: {
onFailure: function (result) {
let javaResult = Java.cast(result, Java.use(result.$className))
onError(javaResult.getMessage())
}
}
})
let onIntegrityTokenSuccessListener = onIntegrityTokenSuccessListenerType.$new()
let onIntegrityTokenErrorListener = onIntegrityTokenErrorListenerType.$new()
integrityTokenResponse["addOnSuccessListener"].overload('com.google.android.gms.tasks.OnSuccessListener').call(integrityTokenResponse, onIntegrityTokenSuccessListener)
integrityTokenResponse["addOnFailureListener"].overload('com.google.android.gms.tasks.OnFailureListener').call(integrityTokenResponse, onIntegrityTokenErrorListener)
}
function initIntegrityComponent() {
const integrityManagerProvider = findIntegrityManagerProvider()
const integrityManager = getIntegrityManager(integrityManagerProvider)
const [prepareIntegrityRequestType, prepareIntegrityRequestBuilder] = findPrepareIntegrityRequestMeta(integrityManager)
createIntegrityTokenProvider(integrityManager, prepareIntegrityRequestType, prepareIntegrityRequestBuilder, (result) => {
const integrityManagerMethods = result.class.getDeclaredMethods()
if (integrityManagerMethods.length !== 1) {
throw new Error('Too many methods in integrity manager: ' + integrityManagerMethods.length)
}
const integrityManagerMethod = integrityManagerMethods[0]
const integrityManagerMethodParamTypes = integrityManagerMethod.getParameterTypes()
if (integrityManagerMethodParamTypes.length !== 1) {
throw new Error('Unexpected number of parameters: ' + integrityManagerMethodParamTypes.length)
}
const requestType = integrityManagerMethodParamTypes[0]
const requestTypeClass = Java.use(requestType.getName())
const requestTypeMethods = requestType.getDeclaredMethods()
for (const requestTypeMethod of requestTypeMethods) {
if (Modifier.isStatic(requestTypeMethod.getModifiers()) && requestTypeMethod.getParameterTypes().length === 0) {
integrityTokenProvider = result
integrityRequestType = requestTypeClass
integrityRequestBuilder = requestTypeClass[requestTypeMethod.getName()]
console.log("[*] Initialized integrity component")
setupLatch.countDown()
return
}
}
throw new Error('Cannot find request builder method')
}, (error) => {
throw new Error('Cannot prepare integrity manager: ', error.toString(), "\n", error.stack.toString())
})
}
function sha256(file, length) {
let inputStream = Files.newInputStream(file, Java.array("java.nio.file.OpenOption", new Array(0)))
let data = Java.array("byte", new Array(4096).fill(0))
let digest = MessageDigest.getInstance("SHA-256")
let total = 0
let read
while ((read = inputStream.read(data)) !== -1 && (length === undefined || total < length)) {
digest.update(data, 0, length === undefined ? read : Math.min(read, length - total));
total += read
}
inputStream.close();
return digest.digest();
}
function sha1(data) {
let digest = MessageDigest.getInstance("SHA-1")
digest.update(data, 0, data.length)
return digest.digest();
}
function md5(inputStream) {
let data = Java.array("byte", new Array(4096).fill(0))
let digest = MessageDigest.getInstance("MD5")
let read
while ((read = inputStream.read(data, 0, data.length)) !== -1) {
digest.update(data, 0, read);
}
return digest.digest();
}
function getApk(context) {
let packageName = context.getPackageName()
let rawPath = context.getPackageManager().getApplicationInfo(packageName, 0).sourceDir.value
let file = File.$new(rawPath)
return Java.cast(file.toPath(), Path)
}
function readZipEntry(zipInputStream) {
let output = ByteArrayOutputStream.$new()
let data = Java.array("byte", new Array(4096).fill(0))
let read
while ((read = zipInputStream.read(data, 0, data.length)) !== -1) {
output.write(data, 0, read)
}
output.close()
return output.toByteArray()
}
function getDataInApk(apkPath) {
let zipInputStream = ZipInputStream.$new(Files.newInputStream(apkPath, Java.array("java.nio.file.OpenOption", new Array(0))))
let zipEntry = undefined
let classesMd5 = undefined
let aboutLogo = undefined
do {
zipEntry = zipInputStream.getNextEntry()
if (zipEntry != null) {
if (zipEntry.getName().includes("classes.dex")) {
classesMd5 = md5(zipInputStream)
} else if (zipEntry.getName().includes("about_logo.png")) {
aboutLogo = readZipEntry(zipInputStream)
}
}
} while (zipEntry !== undefined && (classesMd5 === undefined || aboutLogo === undefined))
zipInputStream.close()
return [classesMd5, aboutLogo]
}
function intInfoComponent() {
let context = ActivityThread.currentApplication().getApplicationContext()
let packageName = context.getPackageName()
let packageInfo = context.getPackageManager().getPackageInfo(packageName, 0)
let packageVersion = packageInfo.versionName.value
let apkPath = getApk(context)
let apkSha256 = sha256(apkPath)
let apkShatr = sha256(apkPath, 10 * 1024 * 1024)
let [classesMd5, aboutLogo] = getDataInApk(apkPath)
if (classesMd5 === undefined || aboutLogo === undefined) {
throw new Error("Incomplete apk data")
}
let packageNameBytes = JavaString.$new(packageName).getBytes(StandardCharsets.UTF_8.value)
let password = Java.array("byte", new Array(packageNameBytes.length + aboutLogo.length).fill(0))
System.arraycopy(packageNameBytes, 0, password, 0, packageNameBytes.length)
System.arraycopy(aboutLogo, 0, password, packageNameBytes.length, aboutLogo.length)
let passwordChars = Java.array("char", new Array(password.length).fill(''))
for (let i = 0; i < passwordChars.length; i++) {
passwordChars[i] = String.fromCharCode(password[i] & 0xFF);
}
let factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1And8BIT")
let key = PBEKeySpec.$new(passwordChars, secretKeySalt, 128, 512)
let secretKey = Java.cast(factory.generateSecret(key), Key).getEncoded()
let signatures = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES.value).signatures.value
if (signatures.length !== 1) {
throw new Error("Unexpected number of signatures: ", signatures.length)
}
let signature = signatures[0].toByteArray()
infoData = {
"packageName": packageName,
"version": packageVersion,
"apkPath": apkPath.toString(),
"apkSha256": Base64.getEncoder().encodeToString(apkSha256),
"apkShatr": Base64.getEncoder().encodeToString(apkShatr),
"apkSize": Files.size(apkPath),
"classesMd5": Base64.getEncoder().encodeToString(classesMd5),
"secretKey": Base64.getEncoder().encodeToString(secretKey),
"signature": Base64.getEncoder().encodeToString(signature),
"signatureSha1": Base64.getEncoder().encodeToString(sha1(signature))
}
console.log("[*] Initialized info component")
setupLatch.countDown()
}
function onIntegrity(req, res) {
integritySemaphore.take(() => {
let authKey = req.authKey
try {
let nonce = Base64.getEncoder().withoutPadding().encodeToString(Base64.getUrlDecoder().decode(authKey))
calculateIntegrityToken(integrityTokenProvider, integrityRequestType, integrityRequestBuilder, nonce, (token) => {
res.end(JSON.stringify({
"token": token
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
}, (error) => {
res.end(JSON.stringify({
"error": error.toString() + "\n" + error.stack
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
})
} catch (error) {
res.end(JSON.stringify({
"error": error.toString() + "\n" + error.stack
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
}
})
}
function onCert(req, res) {
let authKey = Base64.getDecoder().decode(req.authKey)
let enc = Base64.getDecoder().decode(req.enc)
try {
certificateCounter++
let alias = "ws_cert_" + certificateCounter
let ks = KeyStore.getInstance('AndroidKeyStore')
ks.load(null)
ks.deleteEntry(alias)
let expireTime = Date.$new()
expireTime.setTime(System.currentTimeMillis().valueOf() + 80 * 365 * 24 * 60 * 60 * 1000)
let attestationChallenge = ByteBuffer.allocate(authKey.length + 9)
attestationChallenge.order(ByteOrder.BIG_ENDIAN.value)
attestationChallenge.putLong(System.currentTimeMillis().valueOf() / 1000 - 15)
attestationChallenge.put(0x1F)
attestationChallenge.put(authKey)
let attestationChallengeBytes = Java.array("byte", new Array(attestationChallenge.remaining()).fill(0));
attestationChallenge.get(attestationChallengeBytes);
let keyPairGenerator = KeyPairGenerator.getInstance('EC', 'AndroidKeyStore')
let keySpec = KeyGenParameterSpecBuilder.$new(alias, KeyProperties.PURPOSE_SIGN.value)
.setDigests(Java.array('java.lang.String', [KeyProperties.DIGEST_SHA256.value, KeyProperties.DIGEST_SHA512.value]))
.setUserAuthenticationRequired(false)
.setCertificateNotAfter(expireTime)
.setAttestationChallenge(attestationChallengeBytes)
.build()
keyPairGenerator.initialize(keySpec)
keyPairGenerator.generateKeyPair()
let certs = ks.getCertificateChain(alias)
let ba = ByteArrayOutputStream.$new()
for (let i = certs.length - 1; i >= 1; i--) {
let encoded = certs[i].getEncoded()
ba.write(encoded, 0, encoded.length)
}
let c0Hex = toHexString(certs[0].getEncoded())
let pubHex = toHexString(authKey)
let timeBytes = ByteBuffer.allocate(8)
.putLong(System.currentTimeMillis())
.array()
let time = toHexString(timeBytes).substring(4)
let pubIndex = c0Hex.indexOf(pubHex)
let timeIndex = pubIndex + 64 + 20
let signIndex = timeIndex + time.length + 80
let tailIndex = signIndex + appSignature.length
let newC0Hex = c0Hex.substring(0, timeIndex)
+ time
+ c0Hex.substring(timeIndex + time.length, signIndex)
+ appSignature
+ c0Hex.substring(tailIndex)
let newC0HexBytes = hexStringToByteArray(newC0Hex)
ba.write(newC0HexBytes, 0, newC0HexBytes.length)
let s = Signature.getInstance('SHA256withECDSA')
let entry = Java.cast(ks.getEntry(alias, null), KeyStorePrivateKeyEntry)
let privateKey = entry.getPrivateKey()
s.initSign(privateKey)
s.update(enc)
ks.deleteEntry(alias)
let encSign = Base64.getUrlEncoder().withoutPadding().encodeToString(s.sign())
let encCert = Base64.getEncoder().encodeToString(ba.toByteArray())
ba.close()
res.end(JSON.stringify({
"signature": encSign,
"certificate": encCert
}))
} catch (error) {
res.end(JSON.stringify({
"error": error.toString() + "\n" + error.stack
}))
}
}
function hexStringToByteArray(s) {
const result = []
for (let i = 0; i < s.length; i += 2) {
result.push(parseInt(s.substring(i, i + 2), 16))
}
return Java.array('byte', result)
}
function toHexString(byteArray) {
let result = ''
for (let i = 0; i < byteArray.length; i++) {
result += ('0' + (byteArray[i] & 0xFF).toString(16)).slice(-2)
}
return result
}
function onInfo(res) {
try {
res.end(JSON.stringify(infoData))
} catch (error) {
res.end(JSON.stringify({
"error": error.toString() + "\n" + error.stack
}))
}
}
console.log("[*] Initializing server components...")
setupLatch.onSuccess(() => {
console.log("[*] All server components are ready")
const serverPort = infoData["packageName"] === personalPackageId ? personalServerPort : businessServerPort
const server = http.createServer((req, res) => {
let parsedRequest = url.parse(req.url, true)
switch (parsedRequest.pathname) {
case "/integrity":
res.writeHead(200, {"Content-Type": "application/json"});
onIntegrity(parsedRequest.query, res)
break;
case "/cert":
res.writeHead(200, {"Content-Type": "application/json"});
onCert(parsedRequest.query, res)
break;
case "/info":
res.writeHead(200, {"Content-Type": "application/json"});
onInfo(res)
break;
default:
res.writeHead(404, {"Content-Type": "application/json"});
res.end(JSON.stringify({"error": "Unknown method"}))
break;
}
})
server.listen(serverPort, () => {
console.log("[*] Server ready on port", serverPort)
})
})
initIntegrityComponent()
intInfoComponent()
})
================================================
FILE: frida/mobile/android/nodes/index.js
================================================
[Empty file]
================================================
FILE: frida/mobile/ios/registration,js
================================================
// Print registration public key
Interceptor.attach(ObjC.classes.WAECAgreement["+ calculateAgreementFromPublicKey:privateKey:"].implementation, {
onEnter(args) {
console.log("[*] Registration Public key:", arrayBufferToHex(ObjC.Object(args[2])["- key"]().bytes().readByteArray(32)))
}
})
function arrayBufferToHex(arrayBuffer) {
const byteArray = new Uint8Array(arrayBuffer);
let hexString = '';
for (let i = 0; i < byteArray.length; i++) {
const hex = byteArray[i].toString(16);
hexString += (hex.length === 1 ? '0' : '') + hex;
}
return hexString;
}
================================================
FILE: frida/mobile/ios/integrity/README.md
================================================
# iOS middleware
### Requirements
1. iPhone with Jailbreak
2. [Frida server installed](https://frida.re/docs/ios/)
3. Whatsapp and/or Whatsapp Business installed from the App Store
### How to run
1. Run `npm install` in the ios directory
2. Disable sleep on your iPhone
3. Run:
- `frida -U -l server_with_dependencies.js -f "net.whatsapp.WhatsApp"` (WhatsApp)
- `frida -U -l server_with_dependencies.js -f "net.whatsapp.WhatsAppSMB"` (WhatsApp Business)
================================================
FILE: frida/mobile/ios/integrity/package.json
================================================
{
"name": "ios",
"version": "1.0.0",
"description": "Example showing how to use frida-net's server functionality",
"private": true,
"main": "demo.js",
"scripts": {
"prepare": "npm run build",
"build": "frida-compile server.js -o server_with_dependencies.js -c",
"watch": "frida-compile server.js -o server_with_dependencies.js -w"
},
"devDependencies": {
"@types/frida-gum": "^18.5.1",
"@types/node": "^18.19.3",
"frida-compile": "^16.4.1"
}
}
================================================
FILE: frida/mobile/ios/integrity/server.js
================================================
import http from 'http';
import url from "url";
import {nextTick} from 'node:process';
setTimeout(() => {
console.log("[*] Script loaded")
function semaphore(capacity) {
var semaphore = {
capacity: capacity || 1,
current: 0,
queue: [],
firstHere: false,
take: function () {
if (semaphore.firstHere === false) {
semaphore.current++;
semaphore.firstHere = true;
var isFirst = 1;
} else {
var isFirst = 0;
}
var item = {n: 1};
if (typeof arguments[0] == 'function') {
item.task = arguments[0];
} else {
item.n = arguments[0];
}
if (arguments.length >= 2) {
if (typeof arguments[1] == 'function') item.task = arguments[1];
else item.n = arguments[1];
}
var task = item.task;
item.task = function () {
task(semaphore.leave);
};
if (semaphore.current + item.n - isFirst > semaphore.capacity) {
if (isFirst === 1) {
semaphore.current--;
semaphore.firstHere = false;
}
return semaphore.queue.push(item);
}
semaphore.current += item.n - isFirst;
item.task(semaphore.leave);
if (isFirst === 1) semaphore.firstHere = false;
},
leave: function (n) {
n = n || 1;
semaphore.current -= n;
if (!semaphore.queue.length) {
if (semaphore.current < 0) {
throw new Error('leave called too many times.');
}
return;
}
var item = semaphore.queue[0];
if (item.n + semaphore.current > semaphore.capacity) {
return;
}
semaphore.queue.shift();
semaphore.current += item.n;
nextTick(item.task);
},
available: function (n) {
n = n || 1;
return (semaphore.current + n <= semaphore.capacity);
}
};
return semaphore;
}
let integritySemaphore = semaphore()
const personalBundleName = "net.whatsapp.WhatsApp"
const personalServerPort = 1119
const businessServerPort = 1120
const bundleName = ObjC.classes.NSBundle.mainBundle().infoDictionary().objectForKey_("CFBundleIdentifier").toString()
const NSData = ObjC.classes.NSData
const NSString = ObjC.classes.NSString
const DCAppAttestService = ObjC.classes.DCAppAttestService["+ sharedService"]()
function onIntegrity(req, res) {
integritySemaphore.take(() => {
try {
const authKey = NSData.alloc().initWithBase64EncodedString_options_(NSString.stringWithUTF8String_(Memory.allocUtf8String(req.authKey)), 0).SHA256Hash()
let keyHandler = new ObjC.Block({
retType: 'void',
argTypes: ['object', 'object'],
implementation(keyId, error) {
let keyIdData = keyId.toString()
if (error !== null) {
res.end(JSON.stringify({
"error": error.toString()
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
} else {
let attestationHandler = new ObjC.Block({
retType: 'void',
argTypes: ['object', 'object'],
implementation(attestation, error) {
if (error !== null) {
res.end(JSON.stringify({
"error": error.toString()
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
} else {
attestation = attestation.base64EncodedStringWithOptions_(0).toString()
keyId = NSString.stringWithUTF8String_(Memory.allocUtf8String(keyIdData))
let assertionHandler = new ObjC.Block({
retType: 'void',
argTypes: ['object', 'object'],
implementation(assertion, error) {
if (error !== null) {
res.end(JSON.stringify({
"error": error.toString()
}))
} else {
assertion = assertion.base64EncodedStringWithOptions_(0).toString()
res.end(JSON.stringify({
"attestation": attestation,
"assertion": assertion
}))
}
setTimeout(() => integritySemaphore.leave(1), 1000)
}
});
DCAppAttestService["- generateAssertion:clientDataHash:completionHandler:"](keyId, authKey, assertionHandler)
}
}
});
DCAppAttestService["- attestKey:clientDataHash:completionHandler:"](keyId, authKey, attestationHandler)
}
}
});
DCAppAttestService.generateKeyWithCompletionHandler_(keyHandler)
} catch (error) {
res.end(JSON.stringify({
"error": error.toString() + "\n" + error.stack.toString()
}))
setTimeout(() => integritySemaphore.leave(1), 1000)
}
})
}
console.log("[*] All server components are ready")
const server = http.createServer((req, res) => {
let parsedRequest = url.parse(req.url, true)
switch (parsedRequest.pathname) {
case "/integrity":
res.writeHead(200, {"Content-Type": "application/json"});
onIntegrity(parsedRequest.query, res)
break;
default:
res.writeHead(404, {"Content-Type": "application/json"});
res.end(JSON.stringify({"error": "Unknown method"}))
break;
}
})
const serverPort = bundleName === personalBundleName ? personalServerPort : businessServerPort
server.listen(serverPort, () => {
console.log("[*] Server ready on port", serverPort)
})
}, 1000)
================================================
FILE: frida/mobile/ios/nodes/index.js
================================================
Interceptor.attach(Module.getExportByName("SharedModules", "mbedtls_gcm_update"), {
onEnter(args) {
console.log("[*] Called mbedtls_gcm_update", Memory.readUtf8String(args[2], args[1].toInt32()))
}
})
================================================
FILE: frida/web/nodes/index.js
================================================
// https://github.com/Auties00/CobaltAnalyzer
================================================
FILE: proto/signal.proto
================================================
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;
}
================================================
FILE: proto/extractor/README.md
================================================
# 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)
================================================
FILE: proto/extractor/index.js
================================================
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
}
const sendRequest = async (url, headers) => {
let response = await fetch(url, {
headers: headers
})
if (!response.ok) {
throw new Error(`HTTP request to ${url} returned status code ${response.status}`);
}
return await response.text()
}
async function findAppModules() {
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
'Sec-Fetch-Dest': 'script',
'Sec-Fetch-Mode': 'no-cors',
'Sec-Fetch-Site': 'same-origin',
"Sec-Fetch-User": "?1",
'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 sendRequest(`${baseURL}/serviceworker.js`, headers)
const versions = [...serviceworker.matchAll(/assets-manifest-([\d\.]+).json/g)].map(r => r[1])
const version = versions[0]
let bootstrapQRURL = ''
if(version) {
const asset = await sendRequest(`${baseURL}/assets-manifest-${version}.json`, headers)
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 sendRequest(baseURL, headers)
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 sendRequest(bootstrapQRURL, headers)
// 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 com.github.auties00.whatsapp.model.unsupported;\n\n\n${sortedStr}`
const destinationPath = '../whatsapp.proto'
await fs.writeFile(destinationPath, decodedProtoStr)
console.log(`Extracted protobuf schema to "${destinationPath}"`)
})()
================================================
FILE: proto/extractor/package.json
================================================
{
"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"
}
}
================================================
FILE: src/main/java/module-info.java
================================================
module com.github.auties00.cobalt {
// Http client
requires java.net.http;
// Cryptography
requires com.github.auties00.libsignal;
requires com.github.auties00.curve25519;
// QR related dependencies
requires com.google.zxing;
requires com.google.zxing.javase;
requires it.auties.qr;
requires static java.desktop;
// Serialization (Protobuf, JSON)
requires it.auties.protobuf.base;
requires com.alibaba.fastjson2;
// Generate message previews
requires it.auties.linkpreview;
requires com.googlecode.ezvcard;
// Message store
requires com.github.auties00.collections;
// Mobile api
requires net.dongliu.apkparser;
requires com.google.i18n.phonenumbers.libphonenumber;
// Client
exports com.github.auties00.cobalt.client;
// Exception
exports com.github.auties00.cobalt.exception;
// Node
exports com.github.auties00.cobalt.node;
// Proto models - action
exports com.github.auties00.cobalt.model.action;
// Proto models - auth
exports com.github.auties00.cobalt.model.auth;
// Proto models - business
exports com.github.auties00.cobalt.model.business;
// Proto models - button
exports com.github.auties00.cobalt.model.button.interactive;
exports com.github.auties00.cobalt.model.button.base;
exports com.github.auties00.cobalt.model.button.template.hydrated;
exports com.github.auties00.cobalt.model.button.template.highlyStructured;
// Proto models - call
exports com.github.auties00.cobalt.model.call;
// Proto models - chat
exports com.github.auties00.cobalt.model.chat;
// Proto models - contact
exports com.github.auties00.cobalt.model.contact;
// Proto models - info
exports com.github.auties00.cobalt.model.info;
// Proto models - jid
exports com.github.auties00.cobalt.model.jid;
// Proto models - media
exports com.github.auties00.cobalt.model.media;
// Proto models - message
exports com.github.auties00.cobalt.model.message.button;
exports com.github.auties00.cobalt.model.message.server;
exports com.github.auties00.cobalt.model.message.model;
exports com.github.auties00.cobalt.model.message.payment;
exports com.github.auties00.cobalt.model.message.standard;
// Proto models - newsletters
exports com.github.auties00.cobalt.model.newsletter;
// Proto models - payment
exports com.github.auties00.cobalt.model.payment;
// Proto models - poll
exports com.github.auties00.cobalt.model.poll;
// Proto models - preferences
exports com.github.auties00.cobalt.model.preferences;
// Proto models - privacy
exports com.github.auties00.cobalt.model.privacy;
// Proto models - product
exports com.github.auties00.cobalt.model.product;
// Proto models - setting
exports com.github.auties00.cobalt.model.setting;
// Proto models - sync
exports com.github.auties00.cobalt.model.sync;
// Store
exports com.github.auties00.cobalt.store;
// Media
exports com.github.auties00.cobalt.media;
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientBuilder.java
================================================
package com.github.auties00.cobalt.client;
import com.github.auties00.cobalt.client.registration.WhatsAppMobileClientRegistration;
import com.github.auties00.cobalt.model.auth.Version;
import com.github.auties00.cobalt.model.business.BusinessCategory;
import com.github.auties00.cobalt.model.jid.JidDevice;
import com.github.auties00.cobalt.store.WhatsappStore;
import com.github.auties00.cobalt.store.WhatsappStoreBuilder;
import com.github.auties00.cobalt.store.WhatsappStoreSerializer;
import com.github.auties00.libsignal.key.SignalIdentityKeyPair;
import java.net.URI;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* A builder for WhatsApp API client instances with support for both web-based and mobile-based connections.
* <p>
* This class implements a fluent builder pattern with specialized inner classes that handle:
* <ul>
* <li>Web client connections (through QR codes or pairing codes)</li>
* <li>Mobile client connections (through phone numbers and verification)</li>
* <li>Custom client configurations for advanced use cases</li>
* </ul>
* <p>
* The builder provides a clean, type-safe API for creating and configuring WhatsApp client instances
* with appropriate connection, serialization, and authentication options.
*/
public sealed class WhatsAppClientBuilder {
private static final WhatsAppClientMessagePreviewHandler DEFAULT_MESSAGE_PREVIEW_HANDLER = WhatsAppClientMessagePreviewHandler.enabled(true);
private static final WhatsAppClientErrorHandler DEFAULT_ERROR_HANDLER = WhatsAppClientErrorHandler.toTerminal();
private static final WhatsAppClientVerificationHandler.Web DEFAULT_WEB_VERIFICATION_HANDLER = WhatsAppClientVerificationHandler.Web.QrCode.toTerminal();
static final WhatsAppClientBuilder INSTANCE = new WhatsAppClientBuilder();
private WhatsAppClientBuilder() {
}
/**
* Creates a web client with the default Protobuf serializer
*
* @return a non-null web client instance
*/
public Client.Web webClient() {
return new Client.Web(WhatsappStoreSerializer.toProtobuf());
}
/**
* Creates a web client with a custom serializer
*
* @param serializer the serializer to use for data persistence, must not be null
* @return a non-null web client instance
* @throws NullPointerException if serializer is null
*/
public Client.Web webClient(WhatsappStoreSerializer serializer) {
Objects.requireNonNull(serializer, "serializer must not be null");
return new Client.Web(serializer);
}
/**
* Creates a mobile client with the default Protobuf serializer
*
* @return a non-null mobile client instance
*/
public Client.Mobile mobileClient() {
return new Client.Mobile(WhatsappStoreSerializer.toProtobuf());
}
/**
* Creates a mobile client with a custom serializer
*
* @param serializer the serializer to use for data persistence, must not be null
* @return a non-null mobile client instance
* @throws NullPointerException if serializer is null
*/
public Client.Mobile mobileClient(WhatsappStoreSerializer serializer) {
Objects.requireNonNull(serializer, "serializer must not be null");
return new Client.Mobile(serializer);
}
/**
* Creates a custom client for advanced configuration
*
* @return a non-null custom client instance
*/
public Custom customClient() {
return new Custom();
}
public static abstract sealed class Client extends WhatsAppClientBuilder {
final WhatsappStoreSerializer serializer;
private Client(WhatsappStoreSerializer serializer) {
this.serializer = Objects.requireNonNull(serializer, "serializer must not be null");
}
/**
* Creates a new connection using a random UUID
*
* @return a non-null options selector
*/
public abstract Options createConnection();
/**
* Loads a connection from the six parts key representation
*
* @param sixParts the six parts keys to use to create the connection, must not be null
* @return a non-null options selector
* @throws NullPointerException if sixParts is null
*/
public abstract Options loadConnection(WhatsAppClientSixPartsKeys sixParts);
/**
* Loads the last serialized connection.
* If no connection is available, an empty {@link Optional} will be returned.
*
* @return an {@link Optional} containing the last serialized connection, empty otherwise
*/
public abstract Optional<Options> loadLastConnection();
/**
* Loads the last serialized connection.
* If no connection is available, a new one will be created.
*
* @return a non-null options selector
*/
public abstract Options loadLastOrCreateConnection();
/**
* Loads the connection whose id matches {@code uuid}.
* If {@code uuid} is null, or if no connection has an id that matches {@code uuid}, an empty {@link Optional} will be returned.
*
* @param uuid the id to use for the connection; can be null
* @return an {@link Optional} containing the connection whose id matches {@code uuid}, empty otherwise
*/
public abstract Optional<Options> loadConnection(UUID uuid);
/**
* Loads the connection whose id matches {@code uuid}.
* If {@code uuid} is null, or if no connection has an id that matches {@code uuid}, a new connection will be created.
*
* @param uuid the id to use for the connection; can be null
* @return a non-null options selector
*/
public abstract Options loadOrCreateConnection(UUID uuid);
/**
* Loads the connection whose phone number matches the given UUID.
* If the UUID is null, or if no connection matches the given UUID, a new connection will be created.
*
* @param phoneNumber the phone value to use to create the connection, can be null (will generate a random UUID)
* @return a non-null options selector
*/
public abstract Optional<Options> loadConnection(Long phoneNumber);
/**
* Loads the connection whose id matches {@code phoneNumber}.
* If {@code phoneNumber} is null, or if no connection matches {@code phoneNumber}, a new connection will be created.
*
* @param phoneNumber the id to use for the connection, can be null
* @return a non-null options selector
*/
public abstract Options loadOrCreateConnection(Long phoneNumber);
private static WhatsappStore newStore(UUID id, Long phoneNumber, WhatsAppClientType clientType, SignalIdentityKeyPair identityKeyPair, SignalIdentityKeyPair noiseKeyPair, boolean registered, byte[] identityId) {
var device = switch (clientType) {
case WEB -> JidDevice.web();
case MOBILE -> JidDevice.ios(false);
};
return new WhatsappStoreBuilder()
.uuid(Objects.requireNonNullElseGet(id, UUID::randomUUID))
.phoneNumber(phoneNumber)
.clientType(Objects.requireNonNull(clientType, "clientType must not be null"))
.device(device)
.identityId(identityId)
.identityKeyPair(identityKeyPair)
.noiseKeyPair(noiseKeyPair)
.registered(registered)
.build();
}
public static final class Web extends Client {
private Web(WhatsappStoreSerializer serializer) {
super(serializer);
}
/**
* Creates a new connection using a random UUID
*
* @return a non-null options selector
*/
@Override
public Options.Web createConnection() {
return loadOrCreateConnection(UUID.randomUUID());
}
/**
* 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
*/
@Override
public Options.Web loadLastOrCreateConnection() {
var uuids = serializer.listIds(WhatsAppClientType.WEB);
if(uuids.isEmpty()) {
return createConnection();
}else {
return loadOrCreateConnection(uuids.getLast());
}
}
@Override
public Optional<Options> loadConnection(UUID uuid) {
if (uuid == null) {
return Optional.empty();
}
return serializer.startDeserialize(WhatsAppClientType.WEB, uuid, null)
.map(Options.Web::new);
}
/**
* Creates a new connection using a unique identifier
* If a session with the given id already exists, it will be retrieved.
* Otherwise, a new one will be created.
*
* @param uuid the UUID to use to create the connection, can be null (will generate a random UUID)
* @return a non-null options selector
*/
@Override
public Options.Web loadOrCreateConnection(UUID uuid) {
var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID);
var store = serializer.startDeserialize(WhatsAppClientType.WEB, sessionUuid, null)
.orElseGet(() -> newStore(sessionUuid, null, WhatsAppClientType.WEB, null, null, false, null));
return new Options.Web(store);
}
@Override
public Optional<Options> loadConnection(Long phoneNumber) {
if (phoneNumber == null) {
return Optional.empty();
}
return serializer.startDeserialize(WhatsAppClientType.WEB, null, phoneNumber)
.map(Options.Web::new);
}
/**
* Creates a new connection using a phone value
* If a session with the given phone value already exists, it will be retrieved.
* Otherwise, a new one will be created.
*
* @param phoneNumber the phone value to use to create the connection, can be null (will generate a random UUID)
* @return a non-null options selector
*/
@Override
public Options.Web loadOrCreateConnection(Long phoneNumber) {
var store = serializer.startDeserialize(WhatsAppClientType.WEB, null, phoneNumber)
.orElseGet(() -> newStore(null, phoneNumber, WhatsAppClientType.WEB, null, null, false, null));
return new Options.Web(store);
}
/**
* Creates a new connection using a six parts key representation
*
* @param sixParts the six parts keys to use to create the connection, must not be null
* @return a non-null options selector
* @throws NullPointerException if sixParts is null
*/
@Override
public Options.Web loadConnection(WhatsAppClientSixPartsKeys sixParts) {
Objects.requireNonNull(sixParts, "sixParts must not be null");
var serialized = serializer.startDeserialize(WhatsAppClientType.WEB, null, sixParts.phoneNumber());
if(serialized.isPresent()) {
return new Options.Web(serialized.get());
}
var store = newStore(null, sixParts.phoneNumber(), WhatsAppClientType.WEB, sixParts.identityKeyPair(), sixParts.noiseKeyPair(), true, sixParts.identityId());
return new Options.Web(store);
}
@Override
public Optional<Options> loadLastConnection() {
var uuids = serializer.listIds(WhatsAppClientType.WEB);
if(uuids.isEmpty()) {
return Optional.empty();
}else {
return loadConnection(uuids.getLast());
}
}
}
public static final class Mobile extends Client {
private Mobile(WhatsappStoreSerializer serializer) {
super(serializer);
}
/**
* Creates a new connection using a random UUID
*
* @return a non-null options selector
*/
@Override
public Options.Mobile createConnection() {
return loadOrCreateConnection(UUID.randomUUID());
}
/**
* 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
*/
@Override
public Options.Mobile loadLastOrCreateConnection() {
var uuids = serializer.listIds(WhatsAppClientType.MOBILE);
if(uuids.isEmpty()) {
return createConnection();
}else {
return loadOrCreateConnection(uuids.getLast());
}
}
@Override
public Optional<Options> loadConnection(UUID uuid) {
if(uuid == null) {
return Optional.empty();
}
return serializer.startDeserialize(WhatsAppClientType.MOBILE, uuid, null)
.map(Options.Mobile::new);
}
/**
* Creates a new connection using a unique identifier
* If a session with the given id already exists, it will be retrieved.
* Otherwise, a new one will be created.
*
* @param uuid the UUID to use to create the connection, can be null (will generate a random UUID)
* @return a non-null options selector
*/
@Override
public Options.Mobile loadOrCreateConnection(UUID uuid) {
var sessionUuid = Objects.requireNonNullElseGet(uuid, UUID::randomUUID);
var store = serializer.startDeserialize(WhatsAppClientType.MOBILE, sessionUuid, null)
.orElseGet(() -> newStore(sessionUuid, null, WhatsAppClientType.MOBILE, null, null, false, null));
return new Options.Mobile(store);
}
@Override
public Optional<Options> loadConnection(Long phoneNumber) {
if(phoneNumber == null) {
return Optional.empty();
}
return serializer.startDeserialize(WhatsAppClientType.MOBILE, null, phoneNumber)
.map(Options.Mobile::new);
}
/**
* Creates a new connection using a phone value
* If a session with the given phone value already exists, it will be retrieved.
* Otherwise, a new one will be created.
*
* @param phoneNumber the phone value to use to create the connection, can be null (will generate a random UUID)
* @return a non-null options selector
*/
@Override
public Options.Mobile loadOrCreateConnection(Long phoneNumber) {
var store = serializer.startDeserialize(WhatsAppClientType.MOBILE, null, phoneNumber)
.orElseGet(() -> newStore(null, phoneNumber, WhatsAppClientType.MOBILE, null, null, false, null));
return new Options.Mobile(store);
}
/**
* Creates a new connection using a six parts key representation
*
* @param sixParts the six parts keys to use to create the connection, must not be null
* @return a non-null options selector
* @throws NullPointerException if sixParts is null
*/
@Override
public Options.Mobile loadConnection(WhatsAppClientSixPartsKeys sixParts) {
Objects.requireNonNull(sixParts, "sixParts must not be null");
var serialized = serializer.startDeserialize(WhatsAppClientType.MOBILE, null, sixParts.phoneNumber());
if(serialized.isPresent()) {
return new Options.Mobile(serialized.get());
}
var store = newStore(null, sixParts.phoneNumber(), WhatsAppClientType.MOBILE, sixParts.identityKeyPair(), sixParts.noiseKeyPair(), true, sixParts.identityId());
return new Options.Mobile(store);
}
@Override
public Optional<Options> loadLastConnection() {
var uuids = serializer.listIds(WhatsAppClientType.MOBILE);
if(uuids.isEmpty()) {
return Optional.empty();
}else {
return loadConnection(uuids.getLast());
}
}
}
}
public static sealed class Options extends WhatsAppClientBuilder {
final WhatsappStore store;
WhatsAppClientMessagePreviewHandler messagePreviewHandler;
WhatsAppClientErrorHandler errorHandler;
private Options(WhatsappStore store) {
this.store = Objects.requireNonNull(store, "store must not be null");
}
/**
* Sets a proxy for the connection
*
* @param proxy the proxy to use, can be null to use no proxy
* @return the same instance for chaining
*/
public Options proxy(URI proxy) {
store.setProxy(proxy);
return this;
}
/**
* Sets the companion device for the connection
*
* @param device the companion device, can be null
* @return the same instance for chaining
*/
public Options device(JidDevice device) {
store.setDevice(device);
return this;
}
/**
* Controls whether the library should send receipts automatically for messages
* By default disabled
* For the web API, if enabled, the companion won't receive notifications
*
* @param automaticMessageReceipts true to enable automatic message receipts, false otherwise
* @return the same instance for chaining
*/
public Options automaticMessageReceipts(boolean automaticMessageReceipts) {
store.setAutomaticMessageReceipts(automaticMessageReceipts);
return this;
}
/**
* Sets the client version for the connection
* This allows customization of the WhatsApp client version identifier
*
* @param clientVersion the client version to use, can be null to use the default
* @return the same instance for chaining
*/
public Options clientVersion(Version clientVersion) {
store.setClientVersion(clientVersion);
return this;
}
/**
* Sets a handler for message previews
*
* @param messagePreviewHandler the handler to use, can be null
* @return the same instance for chaining
*/
public Options messagePreviewHandler(WhatsAppClientMessagePreviewHandler messagePreviewHandler) {
this.messagePreviewHandler = messagePreviewHandler;
return this;
}
/**
* Sets an error handler for the connection
*
* @param errorHandler the error handler to use, can be null
* @return the same instance for chaining
*/
public Options errorHandler(WhatsAppClientErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
public static final class Web extends Options {
private Web(WhatsappStore store) {
super(store);
}
/**
* Sets a proxy for the connection
*
* @param proxy the proxy to use, can be null to use no proxy
* @return the same instance for chaining
*/
@Override
public Web proxy(URI proxy) {
return (Web) super.proxy(proxy);
}
/**
* Sets the companion device for the connection
*
* @param device the companion device, can be null
* @return the same instance for chaining
*/
@Override
public Web device(JidDevice device) {
return (Web) super.device(device);
}
/**
* Sets a handler for message previews
*
* @param messagePreviewHandler the handler to use, can be null
* @return the same instance for chaining
*/
@Override
public Web messagePreviewHandler(WhatsAppClientMessagePreviewHandler messagePreviewHandler) {
return (Web) super.messagePreviewHandler(messagePreviewHandler);
}
/**
* Sets an error handler for the connection
*
* @param errorHandler the error handler to use, can be null
* @return the same instance for chaining
*/
@Override
public Web errorHandler(WhatsAppClientErrorHandler errorHandler) {
return (Web) super.errorHandler(errorHandler);
}
/**
* Controls whether the library should send receipts automatically for messages
* By default disabled
* For the web API, if enabled, the companion won't receive notifications
*
* @param automaticMessageReceipts true to enable automatic message receipts, false otherwise
* @return the same instance for chaining
*/
@Override
public Web automaticMessageReceipts(boolean automaticMessageReceipts) {
return (Web) super.automaticMessageReceipts(automaticMessageReceipts);
}
/**
* Sets the client version for the connection
* This allows customization of the WhatsApp client version identifier
*
* @param clientVersion the client version to use, can be null to use the default
* @return the same instance for chaining
*/
@Override
public Web clientVersion(Version clientVersion) {
return (Web) super.clientVersion(clientVersion);
}
/**
* Sets the display name for the WhatsApp account
*
* @param name the name to set, can be null
* @return the same instance for chaining
*/
public Web name(String name) {
store.setName(name);
return this;
}
/**
* Sets how much chat history WhatsApp should send when the QR is first scanned
* By default, one year
*
* @param historyLength the history policy to use, must not be null
* @return the same instance for chaining
* @throws NullPointerException if historyLength is null
*/
public Web historySetting(WhatsAppWebClientHistory historyLength) {
Objects.requireNonNull(historyLength, "historyLength must not be null");
store.setWebHistoryPolicy(historyLength);
return this;
}
/**
* Creates a WhatsApp instance with a QR code handler
*
* @param qrHandler the handler to process QR codes, must not be null
* @return a non-null WhatsApp instance
* @throws NullPointerException if qrHandler is null
*/
public WhatsAppClient unregistered(WhatsAppClientVerificationHandler.Web.QrCode qrHandler) {
Objects.requireNonNull(qrHandler, "qrHandler must not be null");
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
return new WhatsAppClient(store, qrHandler, messagePreviewHandler, errorHandler);
}
/**
* Creates a WhatsApp instance with an OTP handler
*
* @param phoneNumber the phone value of the user, must be valid
* @param pairingCodeHandler the handler for the pairing code, must not be null
* @return a non-null WhatsApp instance
* @throws NullPointerException if pairingCodeHandler is null
*/
public WhatsAppClient unregistered(long phoneNumber, WhatsAppClientVerificationHandler.Web.PairingCode pairingCodeHandler) {
Objects.requireNonNull(pairingCodeHandler, "pairingCodeHandler must not be null");
store.setPhoneNumber(phoneNumber);
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
return new WhatsAppClient(store, pairingCodeHandler, messagePreviewHandler, errorHandler);
}
/**
* 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 containing the WhatsApp instance if registered, empty otherwise
*/
public Optional<WhatsAppClient> registered() {
if (!store.registered()) {
return Optional.empty();
}
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
var result = new WhatsAppClient(store, null, messagePreviewHandler, errorHandler);
return Optional.of(result);
}
}
public static final class Mobile extends Options {
private Mobile(WhatsappStore store) {
super(store);
}
/**
* Sets a proxy for the connection
*
* @param proxy the proxy to use, can be null to use no proxy
* @return the same instance for chaining
*/
@Override
public Mobile proxy(URI proxy) {
store.setProxy(proxy);
return this;
}
/**
* Sets the companion device for the connection
*
* @param device the companion device, can be null
* @return the same instance for chaining
*/
@Override
public Mobile device(JidDevice device) {
store.setDevice(device);
return this;
}
/**
* Sets an error handler for the connection
*
* @param errorHandler the error handler to use, can be null
* @return the same instance for chaining
*/
@Override
public Mobile errorHandler(WhatsAppClientErrorHandler errorHandler) {
super.errorHandler(errorHandler);
return this;
}
/**
* Controls whether the library should send receipts automatically for messages
* By default disabled
* For the web API, if enabled, the companion won't receive notifications
*
* @param automaticMessageReceipts true to enable automatic message receipts, false otherwise
* @return the same instance for chaining
*/
@Override
public Mobile automaticMessageReceipts(boolean automaticMessageReceipts) {
super.automaticMessageReceipts(automaticMessageReceipts);
return this;
}
/**
* Sets the client version for the connection
* This allows customization of the WhatsApp client version identifier
*
* @param clientVersion the client version to use, can be null to use the default
* @return the same instance for chaining
*/
@Override
public Mobile clientVersion(Version clientVersion) {
return (Mobile) super.clientVersion(clientVersion);
}
/**
* Sets the display name for the WhatsApp account
*
* @param name the name to set, can be null
* @return the same instance for chaining
*/
public Options name(String name) {
store.setName(name);
return this;
}
/**
* Sets the about/status message for the WhatsApp account
*
* @param about the about message to set, can be null
* @return the same instance for chaining
*/
public Options about(String about) {
store.setAbout(about);
return this;
}
/**
* Sets the business' address
*
* @param businessAddress the address to set, can be null
* @return the same instance for chaining
*/
public Mobile businessAddress(String businessAddress) {
store.setBusinessAddress(businessAddress);
return this;
}
/**
* Sets the business' address longitude
*
* @param businessLongitude the longitude to set, can be null
* @return the same instance for chaining
*/
public Mobile businessLongitude(Double businessLongitude) {
store.setBusinessLongitude(businessLongitude);
return this;
}
/**
* Sets the business' address latitude
*
* @param businessLatitude the latitude to set, can be null
* @return the same instance for chaining
*/
public Mobile businessLatitude(Double businessLatitude) {
store.setBusinessLatitude(businessLatitude);
return this;
}
/**
* Sets the business' description
*
* @param businessDescription the description to set, can be null
* @return the same instance for chaining
*/
public Mobile businessDescription(String businessDescription) {
store.setBusinessDescription(businessDescription);
return this;
}
/**
* Sets the business' website URL
*
* @param businessWebsite the website URL to set, can be null
* @return the same instance for chaining
*/
public Mobile businessWebsite(String businessWebsite) {
store.setBusinessWebsite(businessWebsite);
return this;
}
/**
* Sets the business' email address
*
* @param businessEmail the email address to set, can be null
* @return the same instance for chaining
*/
public Mobile businessEmail(String businessEmail) {
store.setBusinessEmail(businessEmail);
return this;
}
/**
* Sets the business' category
*
* @param businessCategory the category to set, can be null
* @return the same instance for chaining
*/
public Mobile businessCategory(BusinessCategory businessCategory) {
store.setBusinessCategory(businessCategory);
return this;
}
/**
* Creates a WhatsApp instance assuming the session is already registered
* This means that the verification code has already been sent to WhatsApp
*
* @return an optional containing the WhatsApp instance if registered, empty otherwise
*/
public Optional<WhatsAppClient> registered() {
if (!store.registered()) {
return Optional.empty();
}
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
var result = new WhatsAppClient(store, null, messagePreviewHandler, errorHandler);
return Optional.of(result);
}
/**
* Creates a WhatsApp instance for a session that needs registration
* This means that you may or may not have a verification code, but it hasn't been sent to WhatsApp yet
*
* @param phoneNumber the phone value to register, must be valid
* @param verification the verification handler to use, must not be null
* @return a non-null WhatsApp instance
* @throws NullPointerException if verification is null
* @throws IllegalArgumentException if the store already has a phone number set, and the phone number is different from the one being registered
*/
public WhatsAppClient register(long phoneNumber, WhatsAppClientVerificationHandler.Mobile verification) {
Objects.requireNonNull(verification, "verification must not be null");
var oldPhoneNumber = store.phoneNumber();
if(oldPhoneNumber.isPresent() && oldPhoneNumber.getAsLong() != phoneNumber) {
throw new IllegalArgumentException("The phone number(" + phoneNumber + ") must match the existing phone number(" + oldPhoneNumber.getAsLong() + ")");
}else {
store.setPhoneNumber(phoneNumber);
}
if (!store.registered()) {
try(var registration = WhatsAppMobileClientRegistration.of(store, verification)) {
registration.register();
}
}
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
return new WhatsAppClient(store, null, messagePreviewHandler, errorHandler);
}
}
}
public static final class Custom extends WhatsAppClientBuilder {
private WhatsappStore store;
private WhatsAppClientMessagePreviewHandler messagePreviewHandler;
private WhatsAppClientErrorHandler errorHandler;
private WhatsAppClientVerificationHandler.Web webVerificationHandler;
private Custom() {
}
/**
* Sets the store for the connection
*
* @param store the store to use, can be null
* @return the same instance for chaining
*/
public Custom store(WhatsappStore store) {
this.store = store;
return this;
}
/**
* Sets an error handler for the connection
*
* @param errorHandler the error handler to use, can be null
* @return the same instance for chaining
*/
public Custom errorHandler(WhatsAppClientErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
/**
* Sets the web verification handler for the connection
*
* @param webVerificationHandler the verification handler to use, can be null
* @return the same instance for chaining
*/
public Custom webVerificationSupport(WhatsAppClientVerificationHandler.Web webVerificationHandler) {
this.webVerificationHandler = webVerificationHandler;
return this;
}
/**
* Sets a message preview handler for the connection
*
* @param messagePreviewHandler the handler to use, can be null
* @return the same instance for chaining
*/
public Custom messagePreviewHandler(WhatsAppClientMessagePreviewHandler messagePreviewHandler) {
this.messagePreviewHandler = messagePreviewHandler;
return this;
}
/**
* Builds a WhatsApp instance with the configured parameters
*
* @return a non-null WhatsApp instance
* @throws NullPointerException if store or keys are null
* @throws IllegalArgumentException if there is a UUID mismatch between store and keys
*/
public WhatsAppClient build() {
var store = Objects.requireNonNull(this.store, "Expected a valid store");
var webVerificationHandler = switch (store.clientType()) {
case WEB -> Objects.requireNonNullElse(this.webVerificationHandler, DEFAULT_WEB_VERIFICATION_HANDLER);
case MOBILE -> null;
};
var messagePreviewHandler = Objects.requireNonNullElse(this.messagePreviewHandler, DEFAULT_MESSAGE_PREVIEW_HANDLER);
var errorHandler = Objects.requireNonNullElse(this.errorHandler, DEFAULT_ERROR_HANDLER);
return new WhatsAppClient(store, webVerificationHandler, messagePreviewHandler, errorHandler);
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientDisconnectReason.java
================================================
package com.github.auties00.cobalt.client;
/**
* Defines the various reasons for which a WhatsApp session can be terminated.
* This enumeration is used to indicate the cause of disconnection when a session ends,
* which helps with proper handling of reconnection logic and user notifications.
*/
public enum WhatsAppClientDisconnectReason {
/**
* Indicates a normal disconnection initiated by the user or system.
* This is the default reason when no specific cause is identified.
*/
DISCONNECTED,
/**
* Indicates that the session is being terminated for reconnection.
* This typically happens during network changes or when refreshing the connection.
* The application should attempt to establish a new connection when this reason is given.
*/
RECONNECTING,
/**
* Indicates that the user has explicitly logged out of the WhatsApp session.
* This requires the user to re-authenticate before establishing a new connection.
* All session credentials should be cleared when this reason is encountered.
*/
LOGGED_OUT,
/**
* Indicates that the account has been banned from using WhatsApp.
* This is a terminal state that requires user intervention with WhatsApp support.
* No automatic reconnection should be attempted when this reason is given.
*/
BANNED
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientErrorHandler.java
================================================
package com.github.auties00.cobalt.client;
import com.github.auties00.cobalt.exception.MalformedNodeException;
import com.github.auties00.cobalt.exception.SessionBadMacException;
import com.github.auties00.cobalt.exception.SessionConflictException;
import com.github.auties00.cobalt.exception.WebAppStateSyncFatalException;
import com.github.auties00.cobalt.model.jid.Jid;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.function.BiConsumer;
import static com.github.auties00.cobalt.client.WhatsAppClientErrorHandler.Location.*;
import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.Logger.Level.WARNING;
/**
* A handler interface for managing error scenarios that occur within the WhatsApp API.
* <p>
* This interface enables customizable error handling strategies for different types of failures
* that can occur during API operations, such as network issues, authentication errors,
* cryptographic failures, and stream processing problems.
* <p>
* The handler determines how the application should respond to these errors through the
* {@link Result} enum, which supports actions like discarding errors, disconnecting,
* reconnecting, or logging out completely.
* <p>
* Several predefined error handlers are provided through static factory methods that implement
* common error handling patterns, including logging to the terminal or saving to files.
*/
@SuppressWarnings("unused")
@FunctionalInterface
public interface WhatsAppClientErrorHandler {
/**
* Processes an error that occurred within the WhatsApp API.
* <p>
* When an error occurs in any component of the API, this method is called with details
* about where the error happened and the associated exception. The implementation should
* evaluate the error context and determine the appropriate response action.
*
* @param whatsapp the WhatsApp API instance where the error occurred
* @param location the specific component or operation where the error was detected
* @param throwable the exception containing error details, if available
* @return a {@link Result} value indicating how the API should respond to the error
*/
Result handleError(WhatsAppClient whatsapp, Location location, Throwable throwable);
/**
* Creates an error handler that logs errors to the terminal's standard error.
* <p>
* This handler prints full stack traces to the console, making it suitable for
* debugging and development environments.
*
* @return a new error handler that prints exceptions to the terminal
*/
@SuppressWarnings("CallToPrintStackTrace")
static WhatsAppClientErrorHandler toTerminal() {
return defaultErrorHandler((api, error) -> error.printStackTrace());
}
/**
* Creates an error handler that saves error information to files in the default location.
* <p>
* This handler saves detailed error information to files in the $HOME/.cobalt/errors directory,
* making it useful for production environments where logs need to be preserved.
*
* @return a new error handler that persists exceptions to files
*/
static WhatsAppClientErrorHandler toFile() {
return toFile(Path.of(System.getProperty("user.home"), ".cobalt", "errors"));
}
/**
* Creates an error handler that saves error information to files in a specified directory.
* <p>
* This handler works like {@link #toFile()} but allows specifying a custom directory
* where error logs will be saved.
*
* @param directory the directory where error files should be saved
* @return a new error handler that persists exceptions to the specified directory
*/
static WhatsAppClientErrorHandler toFile(Path directory) {
return defaultErrorHandler((api, throwable) -> Thread.startVirtualThread(() -> {
var stackTraceWriter = new StringWriter();
try(var stackTracePrinter = new PrintWriter(stackTraceWriter)) {
var path = directory.resolve(System.currentTimeMillis() + ".txt");
throwable.printStackTrace(stackTracePrinter);
Files.writeString(path, stackTraceWriter.toString(), StandardOpenOption.CREATE);
} catch (IOException exception) {
throw new UncheckedIOException("Cannot serialize exception", exception);
}
}));
}
private static WhatsAppClientErrorHandler defaultErrorHandler(BiConsumer<WhatsAppClient, Throwable> printer) {
return (whatsapp, location, throwable) -> {
var logger = System.getLogger("ErrorHandler");
var jid = whatsapp.store()
.jid()
.map(Jid::user)
.orElse("UNKNOWN");
if(location == RECONNECT) {
logger.log(WARNING, "[{0}] Cannot reconnect: retrying on next timeout", jid);
return Result.DISCARD;
}
var critical = isCriticalError(location, throwable);
logger.log(ERROR, "[{0}] Socket failure at {1}: {2} failure", jid, location, critical ? "Critical" : "Ignored");
if (printer != null) {
printer.accept(whatsapp, throwable);
}
return critical ? Result.DISCONNECT : Result.DISCARD;
};
}
private static boolean isCriticalError(Location location, Throwable throwable) {
return location == AUTH // Can't log in
|| (location == WEB_APP_STATE && throwable instanceof WebAppStateSyncFatalException) // Web app state sync failed
|| location == CRYPTOGRAPHY // Can't encrypt/decrypt a node
|| (location == STREAM && (throwable instanceof SessionConflictException || throwable instanceof SessionBadMacException || throwable instanceof MalformedNodeException)); // Something went wrong in the stream
}
/**
* Defines the possible locations within the WhatsApp API where errors can occur.
* <p>
* Each value represents a specific component or operation that may encounter errors,
* helping to categorize and handle errors appropriately based on their source.
*/
enum Location {
/**
* Indicates an error with an unspecified or unknown source
*/
UNKNOWN,
/**
* Indicates an error that occurred during the authentication process
*/
AUTH,
/**
* Indicates an error in cryptographic operations, such as encryption or decryption
*/
CRYPTOGRAPHY,
/**
* Indicates an error that occurred while establishing or renewing media connections
*/
MEDIA_CONNECTION,
/**
* Indicates an error in the underlying communication stream
*/
STREAM,
/**
* Indicates an error that occurred while retrieving web app state data
*/
WEB_APP_STATE,
/**
* Indicates an error in message serialization or deserialization
*/
MESSAGE,
/**
* Indicates an error that occurred during message history synchronization
*/
HISTORY_SYNC,
/**
* Indicates an error that occurred during connection re-establishment
*/
RECONNECT
}
/**
* Defines the possible response actions when handling errors.
* <p>
* These values determine how the API should proceed after encountering an error,
* ranging from ignoring the error to terminating the session completely.
*/
enum Result {
/**
* Indicates that the error should be ignored, allowing the session to continue
*/
DISCARD,
/**
* Indicates that the current session should be disconnected but preserved for future use
*/
DISCONNECT,
/**
* Indicates that the session should be disconnected and immediately reconnected
*/
RECONNECT,
/**
* Indicates that the current session should be completely terminated and deleted
*/
LOG_OUT
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientListener.java
================================================
package com.github.auties00.cobalt.client;
import com.github.auties00.cobalt.node.Node;
import com.github.auties00.cobalt.model.action.Action;
import com.github.auties00.cobalt.model.call.Call;
import com.github.auties00.cobalt.model.chat.Chat;
import com.github.auties00.cobalt.model.chat.ChatPastParticipant;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.info.ChatMessageInfo;
import com.github.auties00.cobalt.model.info.MessageIndexInfo;
import com.github.auties00.cobalt.model.info.MessageInfo;
import com.github.auties00.cobalt.model.info.QuotedMessageInfo;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.newsletter.Newsletter;
import com.github.auties00.cobalt.model.privacy.PrivacySettingEntry;
import com.github.auties00.cobalt.model.setting.Setting;
import java.util.Collection;
import java.util.List;
/**
* An event listener interface for WhatsApp communication events.
* <p>
* This interface provides callback methods for various events that occur during
* a WhatsApp session, such as message reception, connection state changes, and data updates.
* <p>
* Register a listener using {@link WhatsAppClient#addListener(WhatsAppClientListener)} to receive these events.
* All methods have empty default implementations, allowing you to override only the ones you need.
*
* @see WhatsAppClient#addListener(WhatsAppClientListener)
* @see WhatsAppClient#removeListener(WhatsAppClientListener)
*/
// TODO: Maybe it's better to have client and store have listeners so everything can be listened
public interface WhatsAppClientListener {
/**
* Called when a node is sent to the WhatsApp server.
*
* @param whatsapp an instance of the calling API
* @param outgoing the non-null node that was sent
*/
default void onNodeSent(WhatsAppClient whatsapp, Node outgoing) {
}
/**
* Called when a node is received from the WhatsApp server.
*
* @param whatsapp an instance of the calling API
* @param incoming the non-null node that was received
*/
default void onNodeReceived(WhatsAppClient whatsapp, Node incoming) {
}
/**
* Called when a successful connection and login to a WhatsApp account is established.
* <p>
* Note: When this event is fired, data such as chats and contacts may not yet be loaded
* into memory. For specific data types, use the corresponding event handlers:
* {@link #onChats(WhatsAppClient, Collection)}, {@link #onContacts(WhatsAppClient, Collection)}, etc.
*
* @param whatsapp an instance of the calling API
*/
default void onLoggedIn(WhatsAppClient whatsapp) {
}
/**
* Called when the connection to WhatsApp is terminated.
*
* @param whatsapp an instance of the calling API
* @param reason the reason for disconnection, indicating why the session was terminated
* @see WhatsAppClientDisconnectReason
*/
default void onDisconnected(WhatsAppClient whatsapp, WhatsAppClientDisconnectReason reason) {
}
/**
* Called when an action is received from WhatsApp Web.
* <p>
* This event is only triggered for web client connections.
*
* @param whatsapp an instance of the calling API
* @param action the action that was executed
* @param messageIndexInfo the data associated with this action
*/
default void onWebAppStateAction(WhatsAppClient whatsapp, Action action, MessageIndexInfo messageIndexInfo) {
}
/**
* Called when a setting is received from WhatsApp Web.
* <p>
* This event is only triggered for web client connections.
*
* @param whatsapp an instance of the calling API
* @param setting the setting that was toggled
*/
default void onWebAppStateSetting(WhatsAppClient whatsapp, Setting setting) {
}
/**
* Called when primary features are received from WhatsApp Web.
* <p>
* This event is only triggered for web client connections.
*
* @param whatsapp an instance of the calling API
* @param features the non-null collection of features that were sent
*/
default void onWebAppPrimaryFeatures(WhatsAppClient whatsapp, List<String> features) {
}
/**
* Called when all contacts are received from WhatsApp.
*
* @param whatsapp an instance of the calling API
* @param contacts the collection of contacts
*/
default void onContacts(WhatsAppClient whatsapp, Collection<Contact> contacts) {
}
/**
* Called when a contact's presence status is updated.
*
* @param whatsapp an instance of the calling API
* @param conversation the chat related to this presence update
* @param participant the contact whose presence status changed
*/
default void onContactPresence(WhatsAppClient whatsapp, Jid conversation, Jid participant) {
}
/**
* Called when all chats are received from WhatsApp.
* <p>
* When this event is fired, all chat metadata is available, excluding message content.
* For message content, refer to {@link #onWebHistorySyncMessages(WhatsAppClient, Chat, boolean)}.
* Note that particularly old chats may be loaded later through the history sync process.
*
* @param whatsapp an instance of the calling API
* @param chats the collection of chats
*/
default void onChats(WhatsAppClient whatsapp, Collection<Chat> chats) {
}
/**
* Called when all newsletters are received from WhatsApp.
*
* @param whatsapp an instance of the calling API
* @param newsletters the collection of newsletters
*/
default void onNewsletters(WhatsAppClient whatsapp, Collection<Newsletter> newsletters) {
}
/**
* Called when messages for a chat are received during history synchronization.
* <p>
* This event is only triggered during initial QR code scanning and history syncing.
* In subsequent connections, messages will already be loaded in the chats.
*
* @param whatsapp an instance of the calling API
* @param chat the chat being synchronized
* @param last true if these are the final messages for this chat, false if more are coming
*/
default void onWebHistorySyncMessages(WhatsAppClient whatsapp, Chat chat, boolean last) {
}
/**
* Called when past participants for a group are received during history synchronization.
*
* @param whatsapp an instance of the calling API
* @param chatJid the non-null group chat JID
* @param chatPastParticipants the non-null collection of past participants
*/
default void onWebHistorySyncPastParticipants(WhatsAppClient whatsapp, Jid chatJid, Collection<ChatPastParticipant> chatPastParticipants) {
}
/**
* Called with the progress of the history synchronization process.
* <p>
* This event is only triggered during initial QR code scanning and history syncing.
*
* @param whatsapp an instance of the calling API
* @param percentage the percentage of synchronization completed
* @param recent true if syncing recent messages, false if syncing older messages
*/
default void onWebHistorySyncProgress(WhatsAppClient whatsapp, int percentage, boolean recent) {
}
/**
* Called when a new message is received.
*
* @param whatsapp an instance of the calling API
* @param info the message that was received
*/
default void onNewMessage(WhatsAppClient whatsapp, MessageInfo info) {
}
/**
* Called when a message is deleted.
*
* @param whatsapp an instance of the calling API
* @param info the message that was deleted
* @param everyone true if the message was deleted for everyone, false if deleted only for the user
*/
default void onMessageDeleted(WhatsAppClient whatsapp, MessageInfo info, boolean everyone) {
}
/**
* Called when a message's status changes (e.g., sent, delivered, read).
*
* @param whatsapp an instance of the calling API
* @param info the message whose status changed
*/
default void onMessageStatus(WhatsAppClient whatsapp, MessageInfo info) {
}
/**
* Called when all status updates are received from WhatsApp.
*
* @param whatsapp an instance of the calling API
* @param status the collection of status updates
*/
default void onStatus(WhatsAppClient whatsapp, Collection<ChatMessageInfo> status) {
}
/**
* Called when a new status update is received.
*
* @param whatsapp an instance of the calling API
* @param status the new status message
*/
default void onNewStatus(WhatsAppClient whatsapp, ChatMessageInfo status) {
}
/**
* Called when a message is sent in reply to a previous message.
*
* @param whatsapp an instance of the calling API
* @param response the reply message
* @param quoted the message being replied to
*/
default void onMessageReply(WhatsAppClient whatsapp, MessageInfo response, QuotedMessageInfo quoted) {
}
/**
* Called when a contact's profile picture changes.
*
* @param whatsapp an instance of the calling API
* @param jid the contact whose profile picture changed
*/
default void onProfilePictureChanged(WhatsAppClient whatsapp, Jid jid) {
}
/**
* Called when the user's display name changes.
*
* @param whatsapp an instance of the calling API
* @param oldName the non-null previous name
* @param newName the non-null new name
*/
default void onNameChanged(WhatsAppClient whatsapp, String oldName, String newName) {
}
/**
* Called when the user's about/status text changes.
*
* @param whatsapp an instance of the calling API
* @param oldAbout the non-null previous about text
* @param newAbout the non-null new about text
*/
default void onAboutChanged(WhatsAppClient whatsapp, String oldAbout, String newAbout) {
}
/**
* Called when the user's locale settings change.
*
* @param whatsapp an instance of the calling API
* @param oldLocale the non-null previous locale
* @param newLocale the non-null new locale
*/
default void onLocaleChanged(WhatsAppClient whatsapp, String oldLocale, String newLocale) {
}
/**
* Called when a contact is blocked or unblocked.
*
* @param whatsapp an instance of the calling API
* @param contact the non-null contact that was blocked or unblocked
*/
default void onContactBlocked(WhatsAppClient whatsapp, Jid contact) {
}
/**
* Called when a new contact is added to the contact list.
*
* @param whatsapp an instance of the calling API
* @param contact the new contact
*/
default void onNewContact(WhatsAppClient whatsapp, Contact contact) {
}
/**
* Called when a privacy setting is changed.
*
* @param whatsapp an instance of the calling API
* @param newPrivacyEntry the new privacy setting
*/
default void onPrivacySettingChanged(WhatsAppClient whatsapp, PrivacySettingEntry newPrivacyEntry) {
}
/**
* Called when a registration code (OTP) is requested from a new device.
* <p>
* Note: This event is only triggered for the mobile API.
*
* @param whatsapp an instance of the calling API
* @param code the registration code
*/
default void onRegistrationCode(WhatsAppClient whatsapp, long code) {
}
/**
* Called when a phone call is received.
*
* @param whatsapp an instance of the calling API
* @param call the non-null phone call information
*/
default void onCall(WhatsAppClient whatsapp, Call call) {
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsappClientListenerConsumer.java
================================================
package com.github.auties00.cobalt.client;
/**
* A convenient interface to provide functional overloads for {@link WhatsAppClientListener}
*/
public sealed interface WhatsappClientListenerConsumer {
/**
* A functional listener that takes no parameters
*/
@FunctionalInterface
non-sealed interface Empty extends WhatsappClientListenerConsumer {
void accept();
}
/**
* A functional listener that takes one parameter
*/
@FunctionalInterface
non-sealed interface Unary<F> extends WhatsappClientListenerConsumer {
void accept(F value);
}
/**
* A functional listener that takes two parameters
*/
@FunctionalInterface
non-sealed interface Binary<F, S> extends WhatsappClientListenerConsumer {
void accept(F first, S second);
}
/**
* A functional listener that takes three parameters
*/
@FunctionalInterface
non-sealed interface Ternary<F, S, T> extends WhatsappClientListenerConsumer {
void accept(F first, S second, T third);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientMessagePreviewHandler.java
================================================
package com.github.auties00.cobalt.client;
import com.github.auties00.cobalt.model.message.model.Message;
import com.github.auties00.cobalt.model.message.standard.TextMessage;
import it.auties.linkpreview.LinkPreview;
import java.util.Objects;
/**
* A functional interface that handles link preview generation for WhatsApp messages.
* <p>
* This interface provides mechanisms to automatically detect URLs in messages and enrich them
* with preview metadata such as titles, descriptions, and thumbnails.
*
* <p>The handler processes {@link Message} instances by:
* <ul>
* <li>Scanning the message text for URLs</li>
* <li>Fetching preview metadata (title, description, images, videos)</li>
* <li>Optionally replacing detected text with canonical URLs</li>
* <li>Setting thumbnail images from the largest available media</li>
* <li>Configuring appropriate preview types (video or none)</li>
* </ul>
*
* @see Message
* @see TextMessage
* @see LinkPreview
*/
@FunctionalInterface
public interface WhatsAppClientMessagePreviewHandler {
/**
* Creates an enabled preview handler that processes messages to generate link previews.
* <p>
* This implementation:
* <ul>
* <li>Uses the LinkPreview library to detect and process URLs in message text</li>
* <li>Extracts metadata including title, description, and media thumbnails</li>
* <li>Sets the largest available image as the message thumbnail</li>
* <li>Configures video preview type for video content, or none for other content</li>
* <li>Handles network errors gracefully by ignoring failed thumbnail downloads</li>
* </ul>
*
* <p>Non-text messages are ignored and left unmodified.
*
* @param allowInference when {@code true}, allows the handler to replace detected URL text
* with canonical URLs if they differ. When {@code false}, preserves
* the original message text unchanged.
* @return a non-null preview handler that processes messages for link previews
*
* @apiNote The handler selects thumbnails based on resolution (width × height), choosing
* the largest available image or video thumbnail. Network failures during thumbnail
* download are silently ignored to prevent message processing interruption.
*/
static WhatsAppClientMessagePreviewHandler enabled(boolean allowInference) {
return message -> {
if(!(message instanceof TextMessage textMessage)) {
return;
}
var preview = LinkPreview.createPreview(textMessage.text());
preview.ifPresent(match -> {
var uri = match.result().toString();
if (allowInference && !Objects.equals(match.text(), uri)) {
textMessage.setText(textMessage.text().replace(match.text(), uri));
}
textMessage.setMatchedText(uri);
textMessage.setTitle(match.result().title());
textMessage.setDescription(match.result().siteDescription());
match.result()
.images()
.stream()
.reduce((first, second) -> first.width() * first.height() > second.width() * second.height() ? first : second)
.ifPresent(media -> {
try (var stream = media.uri().toURL().openStream()) {
textMessage.setThumbnail(stream.readAllBytes());
} catch (Throwable ignored) {
}
textMessage.setThumbnailWidth(media.width());
textMessage.setThumbnailHeight(media.height());
});
match.result()
.videos()
.stream()
.reduce((first, second) -> first.width() * first.height() > second.width() * second.height() ? first : second)
.ifPresentOrElse(
media -> {
textMessage.setCanonicalUrl(media.uri().toString());
textMessage.setThumbnailWidth(media.width());
textMessage.setThumbnailHeight(media.height());
textMessage.setPreviewType(TextMessage.PreviewType.VIDEO);
},
() -> {
textMessage.setCanonicalUrl(match.result().uri().toString());
textMessage.setPreviewType(TextMessage.PreviewType.NONE);
}
);
});
};
}
/**
* Creates a disabled preview handler that performs no processing on messages.
* <p>
* This is a no-op implementation that leaves messages unchanged, effectively
* disabling link preview generation. Use this when you want to handle link previews
* manually or disable the feature entirely.
*
* @return a non-null preview handler that performs no operations
*/
static WhatsAppClientMessagePreviewHandler disabled() {
return _ -> {};
}
/**
* Processes a message to add link preview attributes.
* <p>
* Implementations should examine the message for URLs and populate relevant
* preview fields such as title, description, thumbnail, and canonical URL.
* The method should handle different message types appropriately, typically
* focusing on text-based messages that support link previews.
*
* <p>The method should handle network failures gracefully and avoid throwing exceptions
* that could interrupt message processing.
*
* <p>Common processing steps include:
* <ul>
* <li>Checking if the message type supports link previews</li>
* <li>Detecting URLs in the message content</li>
* <li>Fetching metadata from detected URLs</li>
* <li>Setting preview fields on the message object</li>
* <li>Downloading and setting thumbnail images</li>
* <li>Configuring appropriate preview types</li>
* </ul>
*
* @param message the message to process for link previews, must not be null.
* The message object may be modified in-place with preview data
* if it supports link previews (e.g., {@link TextMessage}).
*/
void attribute(Message message);
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientSixPartsKeys.java
================================================
package com.github.auties00.cobalt.client;
import com.github.auties00.libsignal.key.SignalIdentityKeyPair;
import com.github.auties00.libsignal.key.SignalIdentityPrivateKey;
import com.github.auties00.libsignal.key.SignalIdentityPublicKey;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;
/**
* Represents the six-part authentication keys used for WhatsApp mobile client connections.
* <p>
* This class encapsulates the cryptographic credentials required to authenticate a WhatsApp mobile
* client session. The six parts consist of:
* <ol>
* <li>Phone number (with optional '+' prefix)</li>
* <li>Noise protocol public key (Base64 encoded)</li>
* <li>Noise protocol private key (Base64 encoded)</li>
* <li>Signal identity public key (Base64 encoded)</li>
* <li>Signal identity private key (Base64 encoded)</li>
* <li>Identity ID (Base64 encoded)</li>
* </ol>
* <p>
* These credentials are typically obtained from an existing WhatsApp mobile installation and can be
* used to restore a session or create a new connection using {@link WhatsAppClientBuilder}.
* <p>
* The six parts must be provided as a comma-separated string, with optional whitespace and newlines
* that will be automatically stripped during parsing.
* <p>
*
* @see WhatsAppClientBuilder
* @see SignalIdentityKeyPair
*/
public final class WhatsAppClientSixPartsKeys {
private final long phoneNumber;
private final SignalIdentityKeyPair noiseKeyPair;
private final SignalIdentityKeyPair identityKeyPair;
private final byte[] identityId;
/**
* Constructs a new WhatsappSixPartsKeys instance with the specified components.
*
* @param phoneNumber the phone number associated with the WhatsApp account
* @param noiseKeyPair the Noise protocol key pair used for secure channel establishment
* @param identityKeyPair the Signal identity key pair used for end-to-end encryption
* @param identityId the unique identity identifier for this account
*/
public WhatsAppClientSixPartsKeys(long phoneNumber, SignalIdentityKeyPair noiseKeyPair, SignalIdentityKeyPair identityKeyPair, byte[] identityId) {
this.phoneNumber = phoneNumber;
this.noiseKeyPair = noiseKeyPair;
this.identityKeyPair = identityKeyPair;
this.identityId = identityId;
}
/**
* Parses a six-parts authentication string and creates a WhatsappSixPartsKeys instance.
* <p>
* The input string must contain exactly six comma-separated parts in the following order:
* <ol>
* <li>Phone number (with optional '+' prefix)</li>
* <li>Noise public key (Base64 encoded)</li>
* <li>Noise private key (Base64 encoded)</li>
* <li>Identity public key (Base64 encoded)</li>
* <li>Identity private key (Base64 encoded)</li>
* <li>Identity ID (Base64 encoded)</li>
* </ol>
* <p>
* Whitespace and newlines are automatically stripped from the input string.
*
* @param sixParts the comma-separated six-parts authentication string
* @return a new WhatsappSixPartsKeys instance containing the parsed credentials
* @throws NullPointerException if sixParts is null
* @throws IllegalArgumentException if the string format is invalid, doesn't contain exactly six parts,
* or the phone number is malformed
*/
public static WhatsAppClientSixPartsKeys of(String sixParts) {
Objects.requireNonNull(sixParts, "Invalid six parts");
var parts = sixParts.trim()
.replaceAll("\n", "")
.split(",", 6);
if (parts.length != 6) {
throw new IllegalArgumentException("Malformed six parts: " + sixParts);
}
var phoneNumber = parsePhoneNumber(parts);
var noisePublicKey = SignalIdentityPublicKey.ofDirect(Base64.getDecoder().decode(parts[1]));
var noisePrivateKey = SignalIdentityPrivateKey.ofDirect(Base64.getDecoder().decode(parts[2]));
var identityPublicKey = SignalIdentityPublicKey.ofDirect(Base64.getDecoder().decode(parts[3]));
var identityPrivateKey = SignalIdentityPrivateKey.ofDirect(Base64.getDecoder().decode(parts[4]));
var identityId = Base64.getDecoder().decode(parts[5]);
var noiseKeyPair = new SignalIdentityKeyPair(noisePublicKey, noisePrivateKey);
var identityKeyPair = new SignalIdentityKeyPair(identityPublicKey, identityPrivateKey);
return new WhatsAppClientSixPartsKeys(phoneNumber, noiseKeyPair, identityKeyPair, identityId);
}
/**
* Parses the phone number from the first part of the six-parts array.
*
* @param parts the array of six parts
* @return the phone number as a long
* @throws IllegalArgumentException if the phone number is empty or contains invalid characters
*/
private static long parsePhoneNumber(String[] parts) {
var rawPhoneNumber = parts[0];
if(rawPhoneNumber.isEmpty()) {
throw new IllegalArgumentException("Invalid phone number: " + rawPhoneNumber);
}
try {
return Long.parseUnsignedLong(rawPhoneNumber, rawPhoneNumber.charAt(0) == '+' ? 1 : 0, rawPhoneNumber.length(), 10);
}catch (NumberFormatException exception) {
throw new IllegalArgumentException("Invalid phone number: " + rawPhoneNumber);
}
}
/**
* Converts this WhatsappSixPartsKeys instance back to its six-parts string representation.
* <p>
* The returned string contains six comma-separated Base64-encoded parts that can be used to
* reconstruct this instance using {@link #of(String)}.
*
* @return the six-parts string representation of these credentials
*/
@Override
public String toString() {
return String.valueOf(phoneNumber) +
',' +
Base64.getEncoder().encodeToString(noiseKeyPair.publicKey().toEncodedPoint()) +
',' +
Base64.getEncoder().encodeToString(noiseKeyPair.privateKey().toEncodedPoint()) +
',' +
Base64.getEncoder().encodeToString(identityKeyPair.publicKey().toEncodedPoint()) +
',' +
Base64.getEncoder().encodeToString(identityKeyPair.privateKey().toEncodedPoint()) +
',' +
Base64.getEncoder().encodeToString(identityId);
}
/**
* Returns the phone number associated with these credentials.
*
* @return the phone number as a long value
*/
public long phoneNumber() {
return phoneNumber;
}
/**
* Returns the Noise protocol key pair used for secure channel establishment.
* <p>
* The Noise protocol is used during the initial handshake phase to establish an encrypted
* connection with WhatsApp servers.
*
* @return the Noise protocol key pair
*/
public SignalIdentityKeyPair noiseKeyPair() {
return noiseKeyPair;
}
/**
* Returns the Signal identity key pair used for end-to-end encryption.
* <p>
* This key pair is used for the Signal protocol implementation that provides end-to-end
* encryption for all WhatsApp messages.
*
* @return the Signal identity key pair
*/
public SignalIdentityKeyPair identityKeyPair() {
return identityKeyPair;
}
/**
* Returns the unique identity identifier for this account.
* <p>
* Note: The returned array is the actual internal array, not a copy. Modifications to the
* returned array will affect this instance.
*
* @return the identity ID as a byte array
*/
public byte[] identityId() {
return identityId;
}
/**
* Compares this WhatsappSixPartsKeys instance with another object for equality.
* <p>
* Two WhatsappSixPartsKeys instances are considered equal if they have the same phone number,
* noise key pair, identity key pair, and identity ID.
*
* @param o the object to compare with
* @return true if the objects are equal, false otherwise
*/
@Override
public boolean equals(Object o) {
return o instanceof WhatsAppClientSixPartsKeys that
&& Objects.equals(phoneNumber, that.phoneNumber) &&
Objects.equals(noiseKeyPair, that.noiseKeyPair) &&
Objects.equals(identityKeyPair, that.identityKeyPair) &&
Objects.deepEquals(identityId, that.identityId);
}
/**
* Returns a hash code value for this WhatsappSixPartsKeys instance.
*
* @return the hash code value
*/
@Override
public int hashCode() {
return Objects.hash(phoneNumber, noiseKeyPair, identityKeyPair, Arrays.hashCode(identityId));
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientType.java
================================================
package com.github.auties00.cobalt.client;
import it.auties.protobuf.annotation.ProtobufEnum;
/**
* Represents the different types of WhatsApp clients that can be initialized in this API.
* <p>
* This enumeration defines the various platforms where a WhatsApp client can operate.
* Each client type might have specific capabilities, limitations, and connection protocols
* that affect how messages are sent, received, and processed within the system.
* <p>
*/
@ProtobufEnum
public enum WhatsAppClientType {
/**
* Represents a web-based WhatsApp client that connects through <a href="https://web.whatsapp.com">web.whatsapp.com</a>.
* <p>
* This client type emulates the official WhatsApp Web application.
*/
WEB,
/**
* Represents a mobile application client for WhatsApp.
* <p>
* This client type emulates the behavior of the official WhatsApp mobile application.
*/
MOBILE
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppClientVerificationHandler.java
================================================
package com.github.auties00.cobalt.client;
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.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static com.google.zxing.client.j2se.MatrixToImageWriter.writeToPath;
import static java.lang.System.Logger.Level.INFO;
import static java.nio.file.Files.createTempFile;
/**
* A sealed interface that defines handlers for verification methods in WhatsApp.
* This interface provides mechanisms for handling both Web and Mobile verification processes.
*/
public sealed interface WhatsAppClientVerificationHandler {
/**
* A sealed interface that represents verification methods for WhatsApp Web Client.
* Provides handling for QR codes and pairing codes used in the WhatsApp Web verification process.
*/
sealed interface Web extends WhatsAppClientVerificationHandler {
/**
* Handles the verification value provided by WhatsApp Web.
*
* @param value The verification value to be processed (either QR code data or pairing code)
*/
void handle(String value);
/**
* An interface for handling QR codes sent by WhatsApp Web during authentication.
* Provides various methods to process and display QR codes in different formats.
*/
@FunctionalInterface
non-sealed interface QrCode extends Web {
/**
* Creates a handler that prints the QR code to the terminal.
*
* @return A QrCode handler that renders the QR code to the console
* @apiNote If your terminal doesn't support UTF characters, the output may appear as random characters
*/
static QrCode toTerminal() {
return qr -> {
var matrix = createMatrix(qr, 10, 0);
System.out.println(QrTerminal.toString(matrix, true));
};
}
/**
* Creates a BitMatrix representation of a QR code from a value.
*
* @param qr The QR code content to encode
* @param size The size of the QR code in pixels
* @param margin The margin size around the QR code
* @return A BitMatrix representing the QR code
* @throws UnsupportedOperationException if the QR code cannot be created
*/
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 code", exception);
}
}
/**
* Creates a handler that saves the QR code to a temporary file and processes it with the provided consumer.
*
* @param fileConsumer The consumer to process the created file path
* @return A QrCode handler that saves the QR code to a temporary file
* @throws UncheckedIOException if the temporary file cannot be created
*/
static QrCode toFile(QrCode.ToFile 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);
}
}
/**
* Creates a handler that saves the QR code to a specified path and processes it with the provided consumer.
*
* @param path The destination path where the QR code image will be saved
* @param fileConsumer The consumer to process the file path after creation
* @return A QrCode handler that saves the QR code to the specified path
*/
static QrCode toFile(Path path, QrCode.ToFile 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 code to file", exception);
}
};
}
/**
* An interface for consuming a file path containing a saved QR code.
* Provides various methods to process the file path.
*/
interface ToFile extends Consumer<Path> {
/**
* Creates a consumer that discards the file path, taking no action.
*
* @return A ToFile consumer that ignores the file path
*/
static QrCode.ToFile discard() {
return ignored -> {};
}
/**
* Creates a consumer that logs the file path to the terminal using the system logger.
*
* @return A ToFile consumer that prints the file location to the console
*/
static QrCode.ToFile toTerminal() {
return path -> System.getLogger(QrCode.class.getName())
.log(INFO, "Saved QR code at %s".formatted(path));
}
/**
* Creates a consumer that opens the QR code file using the default desktop application.
*
* @return A ToFile consumer that opens the file with the desktop
* @throws RuntimeException if the file cannot be opened with the desktop
*/
static QrCode.ToFile toDesktop() {
return path -> {
try {
if (!Desktop.isDesktopSupported()) {
return;
}
Desktop.getDesktop().open(path.toFile());
} catch (Throwable throwable) {
throw new RuntimeException("Cannot open file with desktop", throwable);
}
};
}
}
}
/**
* An interface for handling pairing codes sent by WhatsApp Web during authentication.
* Provides methods to process and display pairing codes.
*/
@FunctionalInterface
non-sealed interface PairingCode extends Web {
/**
* Creates a handler that prints the pairing code to the terminal.
*
* @return A PairingCode handler that outputs the code to the console
*/
static PairingCode toTerminal() {
return System.out::println;
}
}
}
/**
* An interface that represents verification methods for WhatsApp Mobile Client.
* Handles verification processes for mobile authentication through various channels.
*/
non-sealed interface Mobile extends WhatsAppClientVerificationHandler {
/**
* Returns the preferred verification method to be requested.
*
* @return An Optional containing the verification method name, or empty if no specific method is requested
*/
Optional<String> requestMethod();
/**
* Returns the verification code to be used for authentication.
*
* @return The verification code value
*/
String verificationCode();
/**
* Creates a Mobile verification handler with no specific request method.
* The verification code is obtained from the provided supplier.
*
* @param supplier A non-null supplier that provides the verification code
* @return A Mobile verification handler with no specific request method
* @throws NullPointerException if the supplier is null
*/
static Mobile none(Supplier<String> supplier) {
Objects.requireNonNull(supplier, "supplier cannot be null");
return new Mobile() {
@Override
public Optional<String> requestMethod() {
return Optional.empty();
}
@Override
public String verificationCode() {
var value = supplier.get();
if(value == null) {
throw new IllegalArgumentException("Cannot send verification code: no value");
}
return value;
}
};
}
/**
* Creates a Mobile verification handler that requests verification via SMS.
* The verification code is obtained from the provided supplier.
*
* @param supplier A non-null supplier that provides the verification code
* @return A Mobile verification handler for SMS verification
* @throws NullPointerException if the supplier is null
*/
static Mobile sms(Supplier<String> supplier) {
Objects.requireNonNull(supplier, "supplier cannot be null");
return new Mobile() {
@Override
public Optional<String> requestMethod() {
return Optional.of("sms");
}
@Override
public String verificationCode() {
var value = supplier.get();
if(value == null) {
throw new IllegalArgumentException("Cannot send verification code: no value");
}
return value;
}
};
}
/**
* Creates a Mobile verification handler that requests verification via phone call.
* The verification code is obtained from the provided supplier.
*
* @param supplier A non-null supplier that provides the verification code
* @return A Mobile verification handler for voice call verification
* @throws NullPointerException if the supplier is null
*/
static Mobile call(Supplier<String> supplier) {
Objects.requireNonNull(supplier, "supplier cannot be null");
return new Mobile() {
@Override
public Optional<String> requestMethod() {
return Optional.of("voice");
}
@Override
public String verificationCode() {
var value = supplier.get();
if(value == null) {
throw new IllegalArgumentException("Cannot send verification code: no value");
}
return value;
}
};
}
/**
* Creates a Mobile verification handler that requests verification via WhatsApp.
* The verification code is obtained from the provided supplier.
*
* @param supplier A non-null supplier that provides the verification code
* @return A Mobile verification handler for WhatsApp verification
* @throws NullPointerException if the supplier is null
*/
static Mobile whatsapp(Supplier<String> supplier) {
Objects.requireNonNull(supplier, "supplier cannot be null");
return new Mobile() {
@Override
public Optional<String> requestMethod() {
return Optional.of("wa_old");
}
@Override
public String verificationCode() {
var value = supplier.get();
if(value == null) {
throw new IllegalArgumentException("Cannot send verification code: no value");
}
return value;
}
};
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/WhatsAppWebClientHistory.java
================================================
package com.github.auties00.cobalt.client;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* Represents a policy configuration for WhatsApp Web history synchronization during the initial connection.
* <p>
* This class defines how much chat history WhatsApp should send when a Web client first connects or scans a QR code.
* The policy controls both the amount of message history to sync and whether newsletters should be included.
* </p>
* <p>
* The history sync process affects:
* <ul>
* <li>System resource usage (memory, bandwidth, storage)</li>
* <li>Initial connection time</li>
* <li>Available offline message access</li>
* <li>Newsletter content synchronization</li>
* </ul>
* </p>
* <p>
* This class is immutable and thread-safe. It uses a factory pattern with pre-configured instances
* for common use cases, as well as support for custom configurations.
* </p>
*
* @see WhatsAppClient
* @see WhatsAppClientBuilder
* @since 1.0
*/
@ProtobufMessage
public final class WhatsAppWebClientHistory {
private static final WhatsAppWebClientHistory ZERO = new WhatsAppWebClientHistory(0, false);
private static final WhatsAppWebClientHistory ZERO_WITH_NEWSLETTERS = new WhatsAppWebClientHistory(0, true);
private static final WhatsAppWebClientHistory STANDARD = new WhatsAppWebClientHistory(59206, false);
private static final WhatsAppWebClientHistory STANDARD_WITH_NEWSLETTERS = new WhatsAppWebClientHistory(59206, true);
private static final WhatsAppWebClientHistory EXTENDED = new WhatsAppWebClientHistory(Integer.MAX_VALUE, false);
private static final WhatsAppWebClientHistory EXTENDED_WITH_NEWSLETTERS = new WhatsAppWebClientHistory(Integer.MAX_VALUE, true);
@ProtobufProperty(index = 1, type = ProtobufType.INT32)
final int size;
@ProtobufProperty(index = 2, type = ProtobufType.BOOL)
final boolean newsletters;
WhatsAppWebClientHistory(int size, boolean newsletters) {
this.size = size;
this.newsletters = newsletters;
}
/**
* Creates a policy that discards all chat history, keeping only new messages from session creation onwards.
* <p>
* This is the most resource-efficient option but provides no access to historical messages.
* Recommended for applications that only need real-time messaging capabilities.
* </p>
*
* @param newsletters whether newsletters should be synchronized during the initial connection
* @return a policy that discards all previous chat history
*/
public static WhatsAppWebClientHistory discard(boolean newsletters) {
return newsletters ? ZERO_WITH_NEWSLETTERS : ZERO;
}
/**
* Creates a policy using WhatsApp Web's default history synchronization settings.
* <p>
* This policy provides a balanced approach between resource usage and message availability,
* syncing approximately the last few weeks of chat history. This is the recommended setting
* for most applications as it matches the official WhatsApp Web behavior.
* </p>
*
* @param newsletters whether newsletters should be synchronized during the initial connection
* @return a policy using standard WhatsApp Web history limits
*/
public static WhatsAppWebClientHistory standard(boolean newsletters) {
return newsletters ? STANDARD_WITH_NEWSLETTERS : STANDARD;
}
/**
* Creates a policy that attempts to synchronize most available chat history.
* <p>
* This policy requests the maximum amount of chat history that WhatsApp allows,
* which may include several months or years of messages depending on account age.
* <strong>Warning:</strong> This can consume significant system resources and bandwidth.
* </p>
*
* @param newsletters whether newsletters should be synchronized during the initial connection
* @return a policy that requests extended chat history
*/
public static WhatsAppWebClientHistory extended(boolean newsletters) {
return newsletters ? EXTENDED_WITH_NEWSLETTERS : EXTENDED;
}
/**
* Creates a policy with a custom history size limit.
* <p>
* Allows fine-grained control over the amount of history to synchronize.
* The actual amount of history received may be less than requested if the account
* doesn't have enough historical data or if WhatsApp imposes server-side limits.
* </p>
*
* @param size the maximum value of historical items to synchronize (must be non-negative)
* @param newsletters whether newsletters should be synchronized during the initial connection
* @return a policy with the specified custom size limit
* @throws IllegalArgumentException if size is negative
*/
public static WhatsAppWebClientHistory custom(int size, boolean newsletters) {
return new WhatsAppWebClientHistory(size, newsletters);
}
/**
* Checks if this policy discards all chat history.
* <p>
* A zero-size policy means no historical messages will be synchronized,
* and only new messages from session creation onwards will be available.
* </p>
*
* @return {@code true} if this policy discards all history, {@code false} otherwise
*/
public boolean isZero() {
return size == 0;
}
/**
* Checks if this policy requests extended chat history beyond the standard amount.
* <p>
* Extended policies typically result in longer sync times and higher resource usage
* but provide access to more historical messages.
* </p>
*
* @return {@code true} if this policy requests more than the standard amount of history
*/
public boolean isExtended() {
return size > STANDARD.size();
}
/**
* Returns the maximum value of historical items this policy will attempt to synchronize.
* <p>
* This represents the upper limit of history items to request from WhatsApp's servers.
* The actual amount synchronized may be less due to server limitations or account history.
* </p>
*
* @return the history size limit, or {@link Integer#MAX_VALUE} for unlimited requests
*/
public int size() {
return size;
}
/**
* Checks if this policy includes newsletter synchronization.
* <p>
* When enabled, newsletters and their associated metadata will be synchronized
* along with regular chat history during the initial connection.
* </p>
*
* @return {@code true} if newsletters should be synchronized, {@code false} otherwise
*/
public boolean hasNewsletters() {
return newsletters;
}
@Override
public boolean equals(Object o) {
return o == this || o instanceof WhatsAppWebClientHistory that
&& size == that.size
&& newsletters == that.newsletters;
}
@Override
public int hashCode() {
return Objects.hash(size, newsletters);
}
@Override
public String toString() {
return "WhatsappWebHistorySetting[" +
"size=" + size + ", " +
"newsletters=" + newsletters + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/info/WhatsAppAndroidClientInfo.java
================================================
package com.github.auties00.cobalt.client.info;
import com.github.auties00.cobalt.model.auth.Version;
import net.dongliu.apk.parser.ByteArrayApkFile;
import net.dongliu.apk.parser.bean.ApkSigner;
import net.dongliu.apk.parser.bean.CertificateMeta;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.util.Base64;
import java.util.Collection;
import java.util.NoSuchElementException;
final class WhatsAppAndroidClientInfo implements WhatsAppMobileClientInfo {
private static final byte[] MOBILE_ANDROID_SALT = Base64.getDecoder().decode("PkTwKSZqUfAUyR0rPQ8hYJ0wNsQQ3dW1+3SCnyTXIfEAxxS75FwkDf47wNv/c8pP3p0GXKR6OOQmhyERwx74fw1RYSU10I4r1gyBVDbRJ40pidjM41G1I1oN");
private static final URI MOBILE_PERSONAL_ANDROID_URL = URI.create("https://www.whatsapp.com/android/current/WhatsApp.apk");
private static final URI MOBILE_BUSINESS_ANDROID_URL = URI.create("https://d.cdnpure.com/b/APK/com.whatsapp.w4b?version=latest");
private static final String MOBILE_ANDROID_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
private static volatile WhatsAppAndroidClientInfo personalApkInfo;
private static final Object personalApkInfoLock = new Object();
private static volatile WhatsAppAndroidClientInfo businessApkInfo;
private static final Object businessApkInfoLock = new Object();
private final Version version;
private final byte[] md5Hash;
private final SecretKeySpec secretKey;
private final byte[][] certificates;
private final boolean business;
private WhatsAppAndroidClientInfo(Version version, byte[] md5Hash, SecretKeySpec secretKey, byte[][] certificates, boolean business) {
this.version = version;
this.md5Hash = md5Hash;
this.secretKey = secretKey;
this.certificates = certificates;
this.business = business;
}
public static WhatsAppAndroidClientInfo ofPersonal() {
if (personalApkInfo == null) {
synchronized (personalApkInfoLock) {
if(personalApkInfo == null) {
personalApkInfo = queryApkInfo(false);
}
}
}
return personalApkInfo;
}
public static WhatsAppAndroidClientInfo ofBusiness() {
if (businessApkInfo == null) {
synchronized (businessApkInfoLock) {
if(businessApkInfo == null) {
businessApkInfo = queryApkInfo(true);
}
}
}
return businessApkInfo;
}
private static WhatsAppAndroidClientInfo queryApkInfo(boolean business) {
try(var httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()) {
var request = HttpRequest.newBuilder()
.uri(business ? MOBILE_BUSINESS_ANDROID_URL : MOBILE_PERSONAL_ANDROID_URL)
.GET()
.header("User-Agent", MOBILE_ANDROID_USER_AGENT)
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Sec-Fetch-Dest", "document")
.header("Sec-Fetch-Mode", "navigate")
.header("Sec-Fetch-Site", "none")
.header("Sec-Fetch-User", "?1")
.build();
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
throw new IOException("HTTP request failed with status code: " + response.statusCode());
}
try (var apkFile = new ByteArrayApkFile(response.body())) {
var version = Version.of(apkFile.getApkMeta().getVersionName());
var digest = MessageDigest.getInstance("MD5");
digest.update(apkFile.getFileData("classes.dex"));
var md5Hash = digest.digest();
var secretKey = getSecretKey(apkFile.getApkMeta().getPackageName(), getAboutLogo(apkFile));
var certificates = getCertificates(apkFile);
return new WhatsAppAndroidClientInfo(version, md5Hash, secretKey, certificates, business);
}
} catch (IOException | GeneralSecurityException | InterruptedException exception) {
throw new RuntimeException("Cannot extract data from APK", exception);
}
}
private static byte[] getAboutLogo(ByteArrayApkFile apkFile) throws IOException {
var resource = apkFile.getFileData("res/drawable-hdpi/about_logo.png");
if (resource != null) {
return resource;
}
var resourceV4 = apkFile.getFileData("res/drawable-hdpi-v4/about_logo.png");
if (resourceV4 != null) {
return resourceV4;
}
var xxResourceV4 = apkFile.getFileData("res/drawable-xxhdpi-v4/about_logo.png");
if (xxResourceV4 != null) {
return xxResourceV4;
}
throw new NoSuchElementException("Missing about_logo.png from apk");
}
private static byte[][] getCertificates(ByteArrayApkFile apkFile) throws IOException, CertificateException {
return apkFile.getApkSingers()
.stream()
.map(ApkSigner::getCertificateMetas)
.flatMap(Collection::stream)
.map(CertificateMeta::getData)
.toArray(byte[][]::new);
}
private static SecretKeySpec getSecretKey(String packageName, byte[] resource) throws IOException, GeneralSecurityException {
var packageBytes = packageName.getBytes(StandardCharsets.UTF_8);
var password = new byte[packageBytes.length + resource.length];
System.arraycopy(packageBytes, 0, password, 0, packageBytes.length);
System.arraycopy(resource, 0, password, packageBytes.length, resource.length);
var mac = Mac.getInstance("HmacSHA1");
var keySpec = new SecretKeySpec(password, mac.getAlgorithm());
mac.init(keySpec);
var keySize = 64;
var macLen = mac.getMacLength();
var iterations = 128;
var blocks = (keySize + macLen - 1) / macLen;
var out = new byte[keySize];
var state = new byte[macLen];
var iBuf = new byte[4];
var offset = 0;
for (var block = 1; block <= blocks; ++block) {
mac.update(MOBILE_ANDROID_SALT);
iBuf[0] = (byte) (block >>> 24);
iBuf[1] = (byte) (block >>> 16);
iBuf[2] = (byte) (block >>> 8);
iBuf[3] = (byte) (block);
mac.update(iBuf, 0, iBuf.length);
mac.doFinal(state, 0);
var toCopy = Math.min(macLen, keySize - offset);
System.arraycopy(state, 0, out, offset, toCopy);
for (var cnt = 1; cnt < iterations; ++cnt) {
mac.update(state, 0, macLen);
mac.doFinal(state, 0);
for (var j = 0; j < toCopy; ++j) {
out[offset + j] ^= state[j];
}
}
offset += toCopy;
}
return new SecretKeySpec(out, 0, out.length, "PBKDF2");
}
@Override
public Version version() {
return version;
}
@Override
public boolean business() {
return business;
}
@Override
public String computeRegistrationToken(long nationalPhoneNumber) {
try {
var mac = Mac.getInstance("HMACSHA1");
mac.init(secretKey);
for (var certificate : certificates) {
mac.update(certificate);
}
mac.update(md5Hash);
mac.update(String.valueOf(nationalPhoneNumber).getBytes(StandardCharsets.UTF_8));
return URLEncoder.encode(Base64.getEncoder().encodeToString(mac.doFinal()), StandardCharsets.UTF_8);
}catch (GeneralSecurityException exception) {
throw new InternalError("Cannot compute registration token", exception);
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/info/WhatsAppClientInfo.java
================================================
package com.github.auties00.cobalt.client.info;
import com.github.auties00.cobalt.model.auth.UserAgent;
import com.github.auties00.cobalt.model.auth.Version;
public sealed interface WhatsAppClientInfo
permits WhatsAppWebClientInfo, WhatsAppMobileClientInfo {
static WhatsAppClientInfo of(UserAgent.PlatformType platform) {
return switch (platform) {
case ANDROID -> WhatsAppAndroidClientInfo.ofPersonal();
case IOS -> WhatsAppIosClientInfo.ofPersonal();
case ANDROID_BUSINESS -> WhatsAppAndroidClientInfo.ofBusiness();
case IOS_BUSINESS -> WhatsAppIosClientInfo.ofBusiness();
case WINDOWS, MACOS -> WhatsAppWebClientInfo.of();
};
}
Version version();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/info/WhatsAppIosClientInfo.java
================================================
package com.github.auties00.cobalt.client.info;
import com.alibaba.fastjson2.JSON;
import com.github.auties00.cobalt.model.auth.Version;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
final class WhatsAppIosClientInfo implements WhatsAppMobileClientInfo {
private static final URI MOBILE_PERSONAL_IOS_URL = URI.create("https://itunes.apple.com/lookup?bundleId=net.whatsapp.WhatsApp");
private static final URI MOBILE_BUSINESS_IOS_URL = URI.create("https://itunes.apple.com/lookup?bundleId=net.whatsapp.WhatsAppSMB");
private static final String MOBILE_IOS_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1";
private static volatile WhatsAppIosClientInfo personalIpaInfo;
private static final Object personalIpaInfoLock = new Object();
private static volatile WhatsAppIosClientInfo businessIpaInfo;
private static final Object businessIpaInfoLock = new Object();
private static final String MOBILE_IOS_STATIC = "0a1mLfGUIBVrMKF1RdvLI5lkRBvof6vn0fD2QRSM";
private static final String MOBILE_BUSINESS_IOS_STATIC = "USUDuDYDeQhY4RF2fCSp5m3F6kJ1M2J8wS7bbNA2";
private final Version version;
private final boolean business;
private WhatsAppIosClientInfo(Version version, boolean business) {
this.version = version;
this.business = business;
}
public static WhatsAppIosClientInfo ofPersonal() {
if (personalIpaInfo == null) {
synchronized (personalIpaInfoLock) {
if(personalIpaInfo == null) {
personalIpaInfo = queryIpaInfo(false);
}
}
}
return personalIpaInfo;
}
public static WhatsAppIosClientInfo ofBusiness() {
if (businessIpaInfo == null) {
synchronized (businessIpaInfoLock) {
if(businessIpaInfo == null) {
businessIpaInfo = queryIpaInfo(true);
}
}
}
return businessIpaInfo;
}
private static WhatsAppIosClientInfo queryIpaInfo(boolean business) {
try(var httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()) {
var request = HttpRequest.newBuilder()
.uri(business ? MOBILE_BUSINESS_IOS_URL : MOBILE_PERSONAL_IOS_URL)
.header("User-Agent", MOBILE_IOS_USER_AGENT)
.GET()
.build();
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
throw new IOException("HTTP request failed with status code: " + response.statusCode());
}
var jsonObject = JSON.parseObject(response.body());
var results = jsonObject.getJSONArray("results");
if (results == null || results.isEmpty()) {
return null;
}
var result = results.getJSONObject(0);
var version = result.getString("version");
if (version == null) {
return null;
}
if (!version.startsWith("2.")) {
version = "2." + version;
}
var parsedVersion = Version.of(version);
return new WhatsAppIosClientInfo(parsedVersion, business);
} catch (IOException | InterruptedException e) {
throw new RuntimeException("Cannot query iOS version", e);
}
}
@Override
public Version version() {
return version;
}
@Override
public boolean business() {
return business;
}
@Override
public String computeRegistrationToken(long nationalPhoneNumber) {
try {
var staticToken = business ? MOBILE_BUSINESS_IOS_STATIC : MOBILE_IOS_STATIC;
var token = staticToken + HexFormat.of().formatHex(version.toHash()) + nationalPhoneNumber;
var digest = MessageDigest.getInstance("MD5");
digest.update(token.getBytes());
var result = digest.digest();
return HexFormat.of().formatHex(result);
} catch (NoSuchAlgorithmException exception) {
throw new UnsupportedOperationException("Missing md5 implementation", exception);
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/info/WhatsAppMobileClientInfo.java
================================================
package com.github.auties00.cobalt.client.info;
import com.github.auties00.cobalt.model.auth.UserAgent;
public sealed interface WhatsAppMobileClientInfo
extends WhatsAppClientInfo
permits WhatsAppAndroidClientInfo, WhatsAppIosClientInfo {
static WhatsAppMobileClientInfo of(UserAgent.PlatformType platform) {
return switch (platform) {
case ANDROID -> WhatsAppAndroidClientInfo.ofPersonal();
case IOS -> WhatsAppIosClientInfo.ofPersonal();
case ANDROID_BUSINESS -> WhatsAppAndroidClientInfo.ofBusiness();
case IOS_BUSINESS -> WhatsAppIosClientInfo.ofBusiness();
case WINDOWS, MACOS -> throw new IllegalArgumentException("Cannot create WhatsappClientInfo for web");
};
}
boolean business();
String computeRegistrationToken(long nationalPhoneNumber);
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/info/WhatsAppWebClientInfo.java
================================================
package com.github.auties00.cobalt.client.info;
import com.github.auties00.cobalt.model.auth.Version;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
final class WhatsAppWebClientInfo implements WhatsAppClientInfo {
private static volatile WhatsAppWebClientInfo webInfo;
private static final Object webInfoLock = new Object();
private static final String WEB_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
private static final URI WEB_UPDATE_URL = URI.create("https://web.whatsapp.com");
private static final char[] WEB_UPDATE_PATTERN = "\"client_revision\":".toCharArray();
private final Version version;
private WhatsAppWebClientInfo(Version version) {
this.version = version;
}
public static WhatsAppWebClientInfo of() {
if (webInfo == null) {
synchronized (webInfoLock) {
if(webInfo == null) {
webInfo = queryWebInfo();
}
}
}
return webInfo;
}
private static WhatsAppWebClientInfo queryWebInfo() {
try(var httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()) {
var request = HttpRequest.newBuilder()
.uri(WEB_UPDATE_URL)
.GET()
.header("User-Agent", WEB_USER_AGENT)
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Sec-Fetch-Dest", "document")
.header("Sec-Fetch-Mode", "navigate")
.header("Sec-Fetch-Site", "none")
.header("Sec-Fetch-User", "?1")
.build();
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if(response.statusCode() != 200) {
throw new IllegalStateException("Cannot query web version: status code " + response.statusCode());
}
try (var inputStream = response.body()) {
var patternIndex = 0;
int value;
while ((value = inputStream.read()) != -1) {
if (value == WEB_UPDATE_PATTERN[patternIndex]) {
if (++patternIndex == WEB_UPDATE_PATTERN.length) {
var clientVersion = 0;
while ((value = inputStream.read()) != -1 && Character.isDigit(value)) {
clientVersion *= 10;
clientVersion += value - '0';
}
var version = new Version(2, 3000, clientVersion);
return new WhatsAppWebClientInfo(version);
}
} else {
patternIndex = 0;
if (value == WEB_UPDATE_PATTERN[0]) {
patternIndex = 1;
}
}
}
throw new IllegalStateException("Cannot find client_revision in web update response");
}
} catch (IOException | InterruptedException exception) {
throw new RuntimeException("Cannot query web version", exception);
}
}
@Override
public Version version() {
return version;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/registration/WhatsAppAndroidClientRegistration.java
================================================
package com.github.auties00.cobalt.client.registration;
import com.github.auties00.cobalt.client.WhatsAppClientVerificationHandler;
import com.github.auties00.cobalt.store.WhatsappStore;
import java.net.URI;
import java.net.http.HttpRequest;
import java.util.Locale;
import java.util.UUID;
public final class WhatsAppAndroidClientRegistration extends WhatsAppMobileClientRegistration {
public WhatsAppAndroidClientRegistration(WhatsappStore store, WhatsAppClientVerificationHandler.Mobile verification) {
super(store, verification);
}
@Override
protected HttpRequest createRequest(String path, String body) {
return HttpRequest.newBuilder()
.uri(URI.create("%s%s".formatted(MOBILE_REGISTRATION_ENDPOINT, path)))
.POST(HttpRequest.BodyPublishers.ofString("ENC=" + body))
.header("User-Agent", store.device().toUserAgent(store.clientVersion()))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "text/json")
.header("WaMsysRequest", "1")
.header("request_token", UUID.randomUUID().toString())
.build();
}
@Override
protected String[] getRequestVerificationCodeParameters(String method) {
return new String[]{
"method", method,
"sim_mcc", "000",
"sim_mnc", "000",
"reason", "",
"mcc", "000",
"mnc", "000",
"feo2_query_status", "error_security_exception",
"db", "1",
"sim_type", "0",
"recaptcha", "%7B%22stage%22%3A%22ABPROP_DISABLED%22%7D",
"network_radio_type", "1",
"prefer_sms_over_flash", "false",
"simnum", "0",
"airplane_mode_type", "0",
"client_metrics", "%7B%22attempts%22%3A20%2C%22app_campaign_download_source%22%3A%22google-play%7Cunknown%22%7D",
"mistyped", "7",
"advertising_id", store.advertisingId().toString(),
"hasinrc", "1",
"roaming_type", "0",
"device_ram", "3.57",
"education_screen_displayed", "false",
"pid", String.valueOf(ProcessHandle.current().pid()),
"gpia", "",
"cellular_strength", "5",
"_gg", "",
"_gi", "",
"_gp", "",
"backup_token", toUrlHex(store.backupToken()),
"hasav", "2"
};
}
@Override
protected String generateFdid() {
return store.fdid().toString().toLowerCase(Locale.ROOT);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/registration/WhatsAppIosClientRegistration.java
================================================
package com.github.auties00.cobalt.client.registration;
import com.github.auties00.cobalt.client.WhatsAppClientVerificationHandler;
import com.github.auties00.cobalt.store.WhatsappStore;
import java.net.URI;
import java.net.http.HttpRequest;
import java.util.Locale;
public final class WhatsAppIosClientRegistration extends WhatsAppMobileClientRegistration {
public WhatsAppIosClientRegistration(WhatsappStore store, WhatsAppClientVerificationHandler.Mobile verification) {
super(store, verification);
}
@Override
protected HttpRequest createRequest(String path, String body) {
return HttpRequest.newBuilder()
.uri(URI.create("%s%s".formatted(MOBILE_REGISTRATION_ENDPOINT, path)))
.POST(HttpRequest.BodyPublishers.ofString("ENC=" + body))
.header("User-Agent", store.device().toUserAgent(store.clientVersion()))
.header("Content-Type", "application/x-www-form-urlencoded")
.build();
}
@Override
protected String[] getRequestVerificationCodeParameters(String method) {
return new String[]{
"method", method,
"sim_mcc", "000",
"sim_mnc", "000",
"reason", "",
"cellular_strength", "1"
};
}
@Override
protected String generateFdid() {
return store.fdid().toString().toUpperCase(Locale.ROOT);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/client/registration/WhatsAppMobileClientRegistration.java
================================================
package com.github.auties00.cobalt.client.registration;
import com.alibaba.fastjson2.JSON;
import com.github.auties00.cobalt.client.WhatsAppClientVerificationHandler;
import com.github.auties00.cobalt.client.info.WhatsAppMobileClientInfo;
import com.github.auties00.cobalt.exception.MobileRegistrationException;
import com.github.auties00.cobalt.model.business.BusinessVerifiedNameCertificateBuilder;
import com.github.auties00.cobalt.model.business.BusinessVerifiedNameCertificateSpec;
import com.github.auties00.cobalt.model.business.BusinessVerifiedNameDetailsBuilder;
import com.github.auties00.cobalt.model.business.BusinessVerifiedNameDetailsSpec;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.store.WhatsappStore;
import com.github.auties00.cobalt.util.SecureBytes;
import com.github.auties00.curve25519.Curve25519;
import com.github.auties00.libsignal.key.SignalIdentityKeyPair;
import com.github.auties00.libsignal.key.SignalIdentityPublicKey;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.*;
public abstract sealed class WhatsAppMobileClientRegistration implements AutoCloseable
permits WhatsAppAndroidClientRegistration, WhatsAppIosClientRegistration {
public static final String MOBILE_REGISTRATION_ENDPOINT = "https://v.whatsapp.net/v2";
private static final byte[] REGISTRATION_PUBLIC_KEY = HexFormat.of().parseHex("8e8c0f74c3ebc5d7a6865c6c3c843856b06121cce8ea774d22fb6f122512302d");
private static final String SIGNAL_PUBLIC_KEY_TYPE = Base64.getUrlEncoder().encodeToString(new byte[]{SignalIdentityPublicKey.type()});
protected final HttpClient httpClient;
protected final WhatsappStore store;
protected final WhatsAppClientVerificationHandler.Mobile verification;
protected WhatsAppMobileClientRegistration(WhatsappStore store, WhatsAppClientVerificationHandler.Mobile verification) {
Objects.requireNonNull(store, "store cannot be null");
Objects.requireNonNull(verification, "verification cannot be null");
this.store = store;
this.verification = verification;
this.httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
}
public static WhatsAppMobileClientRegistration of(WhatsappStore store, WhatsAppClientVerificationHandler.Mobile verification) {
return switch (store.device().platform()) {
case ANDROID -> new WhatsAppAndroidClientRegistration(store, verification);
case IOS -> new WhatsAppIosClientRegistration(store, verification);
default -> throw new IllegalArgumentException("Unsupported platform: " + store.device().platform());
};
}
protected abstract String[] getRequestVerificationCodeParameters(String method);
protected abstract String generateFdid();
protected abstract HttpRequest createRequest(String path, String body);
public void register() {
try {
assertRegistrationKeys();
requestVerificationCodeIfNecessary();
sendVerificationCode();
} catch (IOException | InterruptedException exception) {
throw new MobileRegistrationException(exception);
}
}
// Pretty much this method checks if an account with the exact keys we provide already exists
// If the api answers "incorrect" it means that no account with those keys exists so we can register
private void assertRegistrationKeys() throws IOException, InterruptedException {
// Get request data
var attrs = getRegistrationOptions(false);
// First attempt
var result = sendRequest("/exist", attrs);
var response = JSON.parseObject(result);
if (Objects.equals(response.getString("reason"), "incorrect")) {
return;
}
// Second attempt
result = sendRequest("/exist", attrs);
response = JSON.parseObject(result);
if (Objects.equals(response.getString("reason"), "incorrect")) {
return;
}
// Error
throw new MobileRegistrationException("Cannot get account data", new String(result));
}
private void requestVerificationCodeIfNecessary() throws IOException, InterruptedException {
var codeResult = verification.requestMethod();
if (codeResult.isEmpty()) {
return;
}
requestVerificationCode(codeResult.get());
saveRegistrationStatus(false);
}
private void requestVerificationCode(String method) throws IOException, InterruptedException {
String lastError = null;
while (true) {
var params = getRequestVerificationCodeParameters(method);
var attrs = getRegistrationOptions(true, params);
var result = sendRequest("/code", attrs);
var response = JSON.parseObject(result);
var status = response.getString("status");
if (isSuccessful(status)) {
return;
}
var reason = response.getString("reason");
if(isTooRecent(reason)) {
throw new MobileRegistrationException("Please wait before trying to register this phone value again. Don't spam!", new String(result));
}
if(isRegistrationBlocked(reason)) {
var resultJson = new String(result);
if(method.equals("wa_old")) {
throw new MobileRegistrationException("The registration attempt was blocked by Whatsapp: you might want to change platform(iOS/Android) or try using a residential proxy (don't spam)", resultJson);
}else {
throw new MobileRegistrationException("The registration attempt was blocked by Whatsapp: please try using a Whatsapp OTP as a verification method", resultJson);
}
}
if (Objects.equals(reason, lastError)) {
throw new MobileRegistrationException("An error occurred while registering: " + reason, new String(result));
}
lastError = reason;
}
}
private boolean isTooRecent(String reason) {
return reason.equalsIgnoreCase("too_recent")
|| reason.equalsIgnoreCase("too_many")
|| reason.equalsIgnoreCase("too_many_guesses")
|| reason.equalsIgnoreCase("too_many_all_methods");
}
private boolean isRegistrationBlocked(String reason) {
return reason.equalsIgnoreCase("no_routes");
}
public void sendVerificationCode() throws IOException, InterruptedException {
var code = verification.verificationCode();
var attrs = getRegistrationOptions(true, "code", normalizeCodeResult(code));
var result = sendRequest("/register", attrs);
var response = JSON.parseObject(result);
var status = response.getString("status");
if (isSuccessful(status)) {
saveRegistrationStatus(true);
return;
}
throw new MobileRegistrationException("Cannot confirm registration", new String(result));
}
private void saveRegistrationStatus(boolean registered) {
store.setRegistered(registered);
if (registered) {
var phoneNumber = store.phoneNumber()
.orElseThrow(() -> new MobileRegistrationException("Phone number wasn't set"));
var jid = Jid.of(phoneNumber);
store.setJid(jid);
}
store.serialize();
}
private String normalizeCodeResult(String code) {
return code.replaceAll("-", "")
.trim();
}
private boolean isSuccessful(String status) {
return status.equalsIgnoreCase("ok")
|| status.equalsIgnoreCase("sent")
|| status.equalsIgnoreCase("verified");
}
private byte[] sendRequest(String path, String params) throws IOException, InterruptedException {
try {
var keypair = SignalIdentityKeyPair.random();
var key = Curve25519.sharedKey(keypair.privateKey().toEncodedPoint(), REGISTRATION_PUBLIC_KEY);
var cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(
Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "AES"),
new GCMParameterSpec(128, new byte[12])
);
var result = cipher.doFinal(params.getBytes(StandardCharsets.UTF_8));
var cipheredParameters = Base64.getUrlEncoder().encodeToString(SecureBytes.concat(keypair.publicKey().toEncodedPoint(), result));
var requestBuilder = createRequest(path, cipheredParameters);
var response = httpClient.send(requestBuilder, HttpResponse.BodyHandlers.ofByteArray());
if(response.statusCode() != 200) {
throw new RuntimeException("Cannot send request to " + path + ": status code" + response.statusCode());
}
return response.body();
} catch (GeneralSecurityException exception) {
throw new RuntimeException("Cannot encrypt request", exception);
}
}
private String getRegistrationOptions(boolean useToken, String... attributes) {
var phoneNumber = getPhoneNumber(store);
var token = getToken(phoneNumber, useToken);
var certificate = generateBusinessCertificate();
var fdid = generateFdid();
var registrationParams = toFormParams(
"cc", String.valueOf(phoneNumber.getCountryCode()),
"in", String.valueOf(phoneNumber.getNationalNumber()),
"rc", String.valueOf(store.releaseChannel().index()),
"lg", "en",
"lc", "US",
"authkey", Base64.getUrlEncoder().encodeToString(store.noiseKeyPair().publicKey().toEncodedPoint()),
"vname", certificate,
"e_regid", Base64.getUrlEncoder().encodeToString(SecureBytes.intToBytes(store.registrationId(), 4)),
"e_keytype", SIGNAL_PUBLIC_KEY_TYPE,
"e_ident", Base64.getUrlEncoder().encodeToString(store.identityKeyPair().publicKey().toEncodedPoint()),
"e_skey_id", Base64.getUrlEncoder().encodeToString(SecureBytes.intToBytes(store.signedKeyPair().id(), 3)),
"e_skey_val", Base64.getUrlEncoder().encodeToString(store.signedKeyPair().publicKey().toEncodedPoint()),
"e_skey_sig", Base64.getUrlEncoder().encodeToString(store.signedKeyPair().signature()),
"fdid", fdid,
"expid", Base64.getUrlEncoder().encodeToString(store.deviceId()),
"id", toUrlHex(store.identityId()),
"token", useToken ? token : null
);
var additionalParams = toFormParams(attributes);
if (additionalParams.isEmpty()) {
return registrationParams;
} else if(registrationParams.isEmpty()) {
return additionalParams;
} else {
return registrationParams + "&" + additionalParams;
}
}
private String getToken(PhoneNumber phoneNumber, boolean useToken) {
if (!useToken) {
return null;
}
var info = WhatsAppMobileClientInfo.of(store.device().platform());
return info.computeRegistrationToken(phoneNumber.getNationalNumber());
}
protected String generateBusinessCertificate() {
if(!store.device().platform().isBusiness()) {
return null;
}
var details = new BusinessVerifiedNameDetailsBuilder()
.name("")
.issuer("smb:wa")
.serial(Math.abs(new SecureRandom().nextLong()))
.build();
var encodedDetails = BusinessVerifiedNameDetailsSpec.encode(details);
var certificate = new BusinessVerifiedNameCertificateBuilder()
.encodedDetails(encodedDetails)
.signature(Curve25519.sign(store.identityKeyPair().privateKey().toEncodedPoint(), encodedDetails))
.build();
return Base64.getUrlEncoder().encodeToString(BusinessVerifiedNameCertificateSpec.encode(certificate));
}
protected static PhoneNumber getPhoneNumber(WhatsappStore store) {
var phoneNumber = store.phoneNumber()
.orElseThrow(() -> new MobileRegistrationException("Phone number wasn't set"));
try {
return PhoneNumberUtil.getInstance()
.parse("+" + phoneNumber, null);
}catch (NumberParseException exception) {
throw new MobileRegistrationException("Malformed phone number: " + phoneNumber);
}
}
protected String toUrlHex(byte[] buffer) {
var id = new StringBuilder();
for (var x : buffer) {
id.append(String.format("%%%02x", x));
}
return id.toString().toUpperCase(Locale.ROOT);
}
private String toFormParams(String... entries) {
if (entries == null) {
return "";
}
var length = entries.length;
if ((length & 1) != 0) {
throw new IllegalArgumentException("Odd form patches");
}
var result = new StringJoiner("&");
for (var i = 0; i < length; i += 2) {
if (entries[i + 1] == null) {
continue;
}
result.add(entries[i] + "=" + entries[i + 1]);
}
return result.toString();
}
@Override
public void close() {
httpClient.close();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/HmacValidationException.java
================================================
package com.github.auties00.cobalt.exception;
/**
* A security exception that is thrown when HMAC (Hash-based Message Authentication Code) signature validation fails.
* <p>
* This exception indicates that data integrity verification has failed during cryptographic operations,
* which typically occurs in the following scenarios:
* <ul>
* <li>Media file decryption when the ciphertext MAC doesn't match the expected value</li>
* <li>Web app state synchronization when patch or snapshot MACs fail validation</li>
* <li>Device identity verification when signature validation fails during login</li>
* <li>Mutation record decoding when index or message MACs don't match</li>
* </ul>
* <p>
* The exception's message refers to the location where the failure occurred.
*
* @see SecurityException
*/
public final class HmacValidationException extends SecurityException {
/**
* Constructs a new HMAC validation exception with the specified location identifier.
*
* @param location a string identifying where the HMAC validation failure occurred
*/
public HmacValidationException(String location) {
super(location);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MalformedJidException.java
================================================
package com.github.auties00.cobalt.exception;
import com.github.auties00.cobalt.model.jid.Jid;
/**
* Signals that an attempt to parse or construct a JID has failed
* due to invalid format or content.
*
* <p>This exception is typically thrown when:
* <ul>
* <li>A JID string contains unexpected characters or tokens</li>
* <li>Required JID components are missing or malformed</li>
* <li>Numeric values in the JID are out of valid range</li>
* <li>The JID structure does not conform to the expected format</li>
* </ul>
*
* @see Jid
* @see RuntimeException
*/
public class MalformedJidException extends RuntimeException {
/**
* Constructs a new malformed JID exception with the specified detail message.
*
* @param message the detail message explaining why the JID is malformed
*/
public MalformedJidException(String message) {
super(message);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MalformedNodeException.java
================================================
package com.github.auties00.cobalt.exception;
import com.github.auties00.cobalt.client.WhatsAppClientErrorHandler;
/**
* A runtime exception that is thrown when a malformed or invalid node is encountered in the WhatsApp protocol stream.
* <p>
* This exception typically occurs when:
* <ul>
* <li>The XML structure of a received node is not well-formed</li>
* <li>A node fails to meet the required structure</li>
* <li>The node structure is corrupted or incomplete</li>
* </ul>
*
* @see WhatsAppClientErrorHandler
*/
public class MalformedNodeException extends RuntimeException {
/**
* Constructs a new {@code MalformedNodeException} with no detail message.
* <p>
* This constructor is typically used when the error context is self-evident
* from the call stack or when additional details are not available.
* </p>
*/
public MalformedNodeException() {
super();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MediaDownloadException.java
================================================
package com.github.auties00.cobalt.exception;
public final class MediaDownloadException extends MediaException {
public MediaDownloadException(String message) {
super(message);
}
public MediaDownloadException(String message, Throwable cause) {
super(message, cause);
}
public MediaDownloadException(Throwable cause) {
super(cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MediaException.java
================================================
package com.github.auties00.cobalt.exception;
public sealed class MediaException extends RuntimeException
permits MediaDownloadException, MediaProcessingException, MediaUploadException {
public MediaException(String message) {
super(message);
}
public MediaException(String message, Throwable cause) {
super(message, cause);
}
public MediaException(Throwable cause) {
super(cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MediaProcessingException.java
================================================
package com.github.auties00.cobalt.exception;
public final class MediaProcessingException extends MediaException {
public MediaProcessingException(String message) {
super(message);
}
public MediaProcessingException(String message, Throwable cause) {
super(message, cause);
}
public MediaProcessingException(Throwable cause) {
super(cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MediaUploadException.java
================================================
package com.github.auties00.cobalt.exception;
public final class MediaUploadException extends MediaException {
public MediaUploadException(String message) {
super(message);
}
public MediaUploadException(String message, Throwable cause) {
super(message, cause);
}
public MediaUploadException(Throwable cause) {
super(cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/MobileRegistrationException.java
================================================
package com.github.auties00.cobalt.exception;
import java.util.Optional;
/**
* An exception thrown when mobile phone number registration with the WhatsApp API fails.
* <p>
* This exception occurs during various stages of the mobile registration process, including:
* <ul>
* <li>Phone number validation failures (malformed or invalid phone numbers)</li>
* <li>Rate limiting when too many registration attempts are made in a short period</li>
* <li>Registration blocking by WhatsApp's anti-spam mechanisms</li>
* <li>Verification code request failures (when requesting SMS or call verification)</li>
* <li>Verification code submission failures (invalid or expired codes)</li>
* <li>Network or I/O errors during registration API calls</li>
* <li>Unsupported platform configurations for mobile registration</li>
* </ul>
* <p>
* When available, the exception may contain the raw JSON response from the WhatsApp registration
* API, which can be retrieved via {@link #erroneousResponse()} for debugging purposes.
*/
public class MobileRegistrationException extends RuntimeException {
private final String erroneousResponse;
/**
* Constructs a new mobile registration exception with a descriptive message and the raw API response.
* <p>
* This constructor should be used when a registration failure occurs and the WhatsApp API
* returns an error response that may contain additional diagnostic information.
*
* @param message a descriptive error message explaining the registration failure
* @param erroneousResponse the raw response from the WhatsApp registration API (typically JSON format)
*/
public MobileRegistrationException(String message, String erroneousResponse) {
super(message);
this.erroneousResponse = erroneousResponse;
}
/**
* Constructs a new mobile registration exception with a descriptive message.
* <p>
* This constructor should be used for registration failures that occur before or without
* communication with the WhatsApp API, such as validation errors or unsupported configurations.
*
* @param message a descriptive error message explaining the registration failure
*/
public MobileRegistrationException(String message) {
super(message);
this.erroneousResponse = null;
}
/**
* Constructs a new mobile registration exception that wraps an underlying cause.
* <p>
* This constructor should be used when a registration failure is caused by an underlying
* exception, such as network errors, I/O failures, or interrupted operations.
*
* @param cause the underlying exception that caused the registration to fail
*/
public MobileRegistrationException(Throwable cause) {
super(cause);
this.erroneousResponse = null;
}
/**
* Returns the raw API response that caused this exception, if available.
* <p>
* The response, when present, typically contains a JSON-formatted error message from the
* WhatsApp registration API with details such as error codes, reasons, and additional metadata.
*
* @return an {@link Optional} containing the erroneous API response, or empty if no response is available
*/
public Optional<String> erroneousResponse() {
return Optional.ofNullable(erroneousResponse);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/NodeTimeoutException.java
================================================
package com.github.auties00.cobalt.exception;
import com.github.auties00.cobalt.node.Node;
/**
* A runtime exception thrown when a WhatsApp protocol node request does not receive a response
* within the expected timeout period.
*
* <p>This exception occurs during socket communication when a {@link Node} is sent to the WhatsApp
* server but no response is received within the configured timeout duration (typically 60 seconds).
* The exception captures the original node that timed out, which can be useful for debugging
* and error handling purposes.
*
* <p>Common scenarios that may trigger this exception include:
* <ul>
* <li>Network connectivity issues preventing communication with WhatsApp servers</li>
* <li>Server-side delays or unavailability</li>
* <li>Invalid or malformed requests that the server ignores</li>
* <li>Authentication or session problems</li>
* </ul>
*
* @see Node
*/
public class NodeTimeoutException extends RuntimeException {
private final Node node;
/**
* Constructs a new {@code NodeTimeoutException} with the node that timed out.
*
* @param node the WhatsApp protocol node that did not receive a response in time;
* must not be {@code null}
*/
public NodeTimeoutException(Node node) {
this.node = node;
}
/**
* Returns the WhatsApp protocol node that did not receive a response within the timeout period.
*
* @return the node that timed out; never {@code null}
*/
public Node node() {
return node;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/SessionBadMacException.java
================================================
package com.github.auties00.cobalt.exception;
/**
* A security exception that is thrown when Message Authentication Code (MAC) validation fails
* during active session communication with the WhatsApp server.
* <p>
* This exception indicates a critical cryptographic failure in the protocol stream, typically occurring when:
* <ul>
* <li>The session's read/write counter goes out of sync with the server</li>
* <li>Encrypted frames cannot be properly authenticated during stream communication</li>
* <li>The server sends a "bad-mac" error node in response to invalid message authentication</li>
* </ul>
* <p>
* This exception differs from {@link HmacValidationException} in that it specifically relates to
* stream-level cryptographic failures rather than data payload integrity issues. When thrown,
* it typically results in session termination and requires re-authentication.
*
* @see HmacValidationException
* @see SessionConflictException
*/
public class SessionBadMacException extends RuntimeException {
/**
* Constructs a new session bad MAC exception.
* <p>
* This constructor is invoked when the WhatsApp stream error handler detects
* a "bad-mac" error node from the server, indicating that MAC validation has
* failed for the current session.
*/
public SessionBadMacException() {
super();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/SessionClosedException.java
================================================
package com.github.auties00.cobalt.exception;
public final class SessionClosedException extends RuntimeException {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/SessionConflictException.java
================================================
package com.github.auties00.cobalt.exception;
/**
* A session security exception that is thrown when a concurrent authentication conflict is detected
* on the WhatsApp server.
* <p>
* This exception indicates that another client has successfully authenticated using the same session
* credentials (cryptographic keys and device identity) that the current session is attempting to use.
* This typically occurs when the user logs into the same account from another device or application instance.
* <p>
* When this exception is thrown, the current session is typically terminated immediately and cannot
* be recovered. The user must re-authenticate to establish a new session. This behavior enforces
* WhatsApp's policy of maintaining exclusive session ownership and prevents session hijacking attempts.
* <p>
* Unlike {@link SessionBadMacException}, which indicates cryptographic validation failures,
* this exception specifically signals that the session credentials are valid but are being
* actively used by another client instance.
*
* @see SessionBadMacException
* @see HmacValidationException
*/
public class SessionConflictException extends RuntimeException {
/**
* Constructs a new session conflict exception.
* <p>
* This constructor is invoked when the WhatsApp stream error handler detects
* either a "conflict" error node or a "replace" stream error from the server,
* indicating that another client has taken over the session.
*/
public SessionConflictException() {
super();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/WebAppStateSyncException.java
================================================
package com.github.auties00.cobalt.exception;
/**
* Exception thrown when WhatsApp Web App State synchronization fails.
*/
public abstract sealed class WebAppStateSyncException
extends RuntimeException
permits WebAppStateSyncFatalException, WebAppStateSyncGenericRetryableException {
public WebAppStateSyncException(Throwable cause) {
super(cause);
}
/**
* Creates a new WebAppStateSyncException with the specified message.
*
* @param message the detail message
*/
public WebAppStateSyncException(String message) {
super(message);
}
/**
* Creates a new WebAppStateSyncException with the specified message and cause.
*
* @param message the detail message
* @param cause the cause of this exception
*/
public WebAppStateSyncException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/WebAppStateSyncFatalException.java
================================================
package com.github.auties00.cobalt.exception;
public final class WebAppStateSyncFatalException
extends WebAppStateSyncException {
public WebAppStateSyncFatalException(String message) {
super(message);
}
public WebAppStateSyncFatalException(String message, Throwable cause) {
super(message, cause);
}
public WebAppStateSyncFatalException(Throwable cause) {
super(cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/WebAppStateSyncGenericRetryableException.java
================================================
package com.github.auties00.cobalt.exception;
public sealed class WebAppStateSyncGenericRetryableException
extends WebAppStateSyncException permits WebAppStateSyncMissingKeyException {
public WebAppStateSyncGenericRetryableException(String message) {
super(message);
}
public WebAppStateSyncGenericRetryableException(String message, Throwable cause) {
super(message, cause);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/exception/WebAppStateSyncMissingKeyException.java
================================================
package com.github.auties00.cobalt.exception;
import java.util.HexFormat;
import java.util.Objects;
public final class WebAppStateSyncMissingKeyException
extends WebAppStateSyncGenericRetryableException {
private final byte[] keyId;
public WebAppStateSyncMissingKeyException(byte[] keyId) {
Objects.requireNonNull(keyId, "keyId cannot be null");
super("Missing key with id " + HexFormat.of().formatHex(keyId));
this.keyId = keyId;
}
public byte[] keyId() {
return keyId;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/media/MediaConnection.java
================================================
package com.github.auties00.cobalt.media;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.github.auties00.cobalt.exception.MediaDownloadException;
import com.github.auties00.cobalt.exception.MediaException;
import com.github.auties00.cobalt.exception.MediaUploadException;
import com.github.auties00.cobalt.model.media.MediaProvider;
import com.github.auties00.cobalt.util.Clock;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;
import java.util.SequencedCollection;
public final class MediaConnection {
private final String auth;
private final int ttl;
private final int maxBuckets;
private final long timestamp;
private final SequencedCollection<? extends MediaHost> hosts;
public MediaConnection(String auth, int ttl, int maxBuckets, long timestamp, SequencedCollection<? extends MediaHost> hosts) {
this.auth = auth;
this.ttl = ttl;
this.maxBuckets = maxBuckets;
this.timestamp = timestamp;
this.hosts = hosts;
}
public boolean upload(MediaProvider provider, InputStream inputStream) throws MediaException {
Objects.requireNonNull(provider, "provider cannot be null");
Objects.requireNonNull(inputStream, "inputStream cannot be null");
var path = provider.mediaPath()
.path();
if (path.isEmpty()) {
return false;
}
try(var client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()) {
var uploadStream = MediaUploadInputStream.of(provider, inputStream);
var tempFile = Files.createTempFile("upload", ".tmp");
try (uploadStream; var outputStream = Files.newOutputStream(tempFile)) {
uploadStream.transferTo(outputStream);
}
var timestamp = Clock.nowSeconds();
var fileSha256 = uploadStream.fileSha256();
var fileEncSha256 = uploadStream.fileEncSha256()
.orElse(null);
var mediaKey = uploadStream.fileKey()
.orElse(null);
var fileLength = uploadStream.fileLength();
for (var host : hosts) {
if(!host.canUpload(provider)) {
continue;
}
var uploadResult = tryUpload(client, host.hostname(), path.get(), fileEncSha256, fileSha256, tempFile)
.or(() -> host.fallbackHostname().flatMap(fallbackHostname -> tryUpload(client, fallbackHostname, path.get(), fileEncSha256, fileSha256, tempFile)));
if(uploadResult.isPresent()) {
var directPath = uploadResult.get()
.getString("direct_path");
var url = uploadResult.get()
.getString("url");
// var handle = jsonObject.getString("handle");
provider.setMediaSha256(fileSha256);
provider.setMediaEncryptedSha256(fileEncSha256);
provider.setMediaKey(mediaKey);
provider.setMediaSize(fileLength);
provider.setMediaDirectPath(directPath);
provider.setMediaUrl(url);
provider.setMediaKeyTimestamp(timestamp);
return true;
}
}
throw new MediaUploadException("Cannot upload media: no hosts available");
}catch (IOException exception) {
throw new MediaUploadException("Cannot upload media", exception);
}
}
private Optional<JSONObject> tryUpload(HttpClient client, String hostname, String path, byte[] fileEncSha256, byte[] fileSha256, Path body) {
try {
var auth = URLEncoder.encode(this.auth, StandardCharsets.UTF_8);
var token = Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(Objects.requireNonNullElse(fileEncSha256, fileSha256));
var uri = URI.create("https://%s/%s/%s?auth=%s&token=%s".formatted(hostname, path, token, auth, token));
var requestBuilder = HttpRequest.newBuilder()
.uri(uri)
.POST(HttpRequest.BodyPublishers.ofFile(body));
var request = requestBuilder.header("Content-Type", "application/octet-stream")
.header("Accept", "application/json")
.headers("Origin", "https://web.whatsapp.com")
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
throw new MediaUploadException("Cannot upload media: status code " + response.statusCode());
}
var jsonObject = JSON.parseObject(response.body());
return Optional.ofNullable(jsonObject);
}catch (Throwable _) {
return Optional.empty();
}
}
public InputStream download(MediaProvider provider) throws MediaException {
Objects.requireNonNull(provider, "provider cannot be null");
var defaultUploadUrl = provider.mediaUrl();
if(defaultUploadUrl.isPresent()) {
var result = tryDownload(provider, defaultUploadUrl.get());
if(result.isPresent()) {
return result.get();
}
}
var defaultDirectPath = provider.mediaDirectPath()
.orElseThrow(() -> new MediaDownloadException("Missing direct path from media"));
for(var host : hosts) {
if(!host.canDownload(provider)) {
continue;
}
var uploadUrl = "https://" + host.hostname() + defaultDirectPath;
var result = tryDownload(provider, uploadUrl);
if(result.isPresent()) {
return result.get();
}
}
throw new MediaDownloadException("Cannot download media: no hosts available");
}
public Optional<InputStream> tryDownload(MediaProvider provider, String uploadUrl) throws MediaException {
var client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
var request = HttpRequest.newBuilder()
.uri(URI.create(uploadUrl))
.build();
try {
var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
throw new MediaDownloadException("Cannot download media: status code " + response.statusCode());
}
var payloadLength = response.headers()
.firstValueAsLong("Content-Length")
.orElseThrow(() -> new MediaDownloadException("Unknown content length"));
var rawInputStream = response.body();
return Optional.of(new MediaDownloadInputStream(client, rawInputStream, payloadLength, provider));
} catch (Throwable throwable) {
client.close();
return Optional.empty();
}
}
public String auth() {
return auth;
}
public int ttl() {
return ttl;
}
public int maxBuckets() {
return maxBuckets;
}
public long timestamp() {
return timestamp;
}
public SequencedCollection<? extends MediaHost> hosts() {
return hosts;
}
@Override
public String toString() {
return "MediaConnection[" +
"auth=" + auth + ", " +
"ttl=" + ttl + ", " +
"maxBuckets=" + maxBuckets + ", " +
"timestamp=" + timestamp + ", " +
"hosts=" + hosts + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/media/MediaDownloadInputStream.java
================================================
package com.github.auties00.cobalt.media;
import com.github.auties00.cobalt.exception.HmacValidationException;
import com.github.auties00.cobalt.exception.MediaDownloadException;
import com.github.auties00.cobalt.exception.MediaException;
import com.github.auties00.cobalt.model.media.MediaProvider;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.net.http.HttpClient;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Objects;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
final class MediaDownloadInputStream extends MediaInputStream {
private final HttpClient client;
private final Inflater inflater;
private final byte[] buffer;
private int bufferOffset;
private int bufferLimit;
private final byte[] inflatedBuffer;
private int inflatedOffset;
private int inflatedLimit;
private final byte[] macBuffer;
private int macBufferOffset;
private final MessageDigest plaintextDigest;
private final byte[] expectedPlaintextSha256;
private final MessageDigest ciphertextDigest;
private final byte[] expectedCiphertextSha256;
private final Cipher cipher;
private final Mac mac;
private long remainingText;
private State state;
MediaDownloadInputStream(HttpClient client, InputStream rawInputStream, long payloadLength, MediaProvider provider) throws MediaException {
super(rawInputStream);
Objects.requireNonNull(client, "client cannot be null");
Objects.requireNonNull(rawInputStream, "rawInputStream must not be null");
Objects.requireNonNull(provider, "provider must not be null");
this.client = client;
this.inflater = provider.mediaPath().inflatable() ? new Inflater() : null;
this.buffer = new byte[BUFFER_LENGTH];
this.inflatedBuffer = isInflatable() ? new byte[BUFFER_LENGTH] : null;
this.expectedPlaintextSha256 = provider.mediaSha256().orElse(null);
this.plaintextDigest = expectedPlaintextSha256 != null ? newHash() : null;
var hasKeyName = provider.mediaPath().keyName().isPresent();
var hasMediaKey = provider.mediaKey().isPresent();
if (hasKeyName != hasMediaKey) {
throw new MediaDownloadException("Media key and key name must both be present or both be absent");
} else if (hasKeyName) {
this.expectedCiphertextSha256 = provider.mediaEncryptedSha256().orElse(null);
this.ciphertextDigest = expectedCiphertextSha256 != null ? newHash() : null;
var mediaKey = provider.mediaKey()
.orElseThrow(() -> new MediaDownloadException("Media key must be present"));
var keyName = provider.mediaPath().keyName()
.orElseThrow(() -> new MediaDownloadException("Key name must be present"));
var expanded = deriveMediaKeyData(mediaKey, keyName);
var iv = new IvParameterSpec(expanded, 0, IV_LENGTH);
var cipherKey = new SecretKeySpec(expanded, IV_LENGTH, KEY_LENGTH, "AES");
var macKey = new SecretKeySpec(expanded, IV_LENGTH + KEY_LENGTH, KEY_LENGTH, "HmacSHA256");
this.cipher = newCipher(Cipher.DECRYPT_MODE, cipherKey, iv);
this.mac = newMac(macKey);
this.mac.update(expanded, 0, IV_LENGTH);
this.remainingText = payloadLength - MAC_LENGTH;
this.macBuffer = new byte[MAC_LENGTH];
} else {
this.expectedCiphertextSha256 = null;
this.ciphertextDigest = null;
this.cipher = null;
this.mac = null;
this.macBuffer = null;
this.remainingText = payloadLength;
}
this.state = State.READ_DATA;
}
@Override
public int read() throws MediaDownloadException {
if (isDone()) {
return -1;
} else if (isInflatable()) {
return inflatedBuffer[inflatedOffset++] & 0xFF;
} else {
return buffer[bufferOffset++] & 0xFF;
}
}
@Override
public int read(byte[] b, int off, int len) throws MediaDownloadException {
if (isDone()) {
return -1;
} else if (isInflatable()) {
var toRead = Math.min(len, inflatedLimit - inflatedOffset);
System.arraycopy(inflatedBuffer, inflatedOffset, b, off, toRead);
inflatedOffset += toRead;
return toRead;
} else {
var toRead = Math.min(len, bufferLimit - bufferOffset);
System.arraycopy(buffer, bufferOffset, b, off, toRead);
bufferOffset += toRead;
return toRead;
}
}
private boolean isDone() throws MediaDownloadException {
try {
var inflatable = isInflatable();
while ((inflatable ? inflatedOffset >= inflatedLimit : bufferOffset >= bufferLimit) && state != State.DONE) {
if(inflatable && !inflater.needsInput() && !inflater.finished()) {
inflatedOffset = 0;
inflatedLimit = inflater.inflate(inflatedBuffer);
}else {
switch (state) {
case READ_DATA -> {
if (remainingText > 0) {
var toRead = (int) Math.min(buffer.length, remainingText);
var read = rawInputStream.read(buffer, 0, toRead);
if (read == -1) {
throw new MediaDownloadException("Unexpected end of stream: expected " + remainingText + " more bytes");
}
remainingText -= read;
if (isEncrypted()) {
if (ciphertextDigest != null) {
ciphertextDigest.update(buffer, 0, read);
}
mac.update(buffer, 0, read);
bufferOffset = 0;
bufferLimit = cipher.update(buffer, 0, read, buffer, 0);
} else {
bufferOffset = 0;
bufferLimit = read;
}
if(plaintextDigest != null) {
plaintextDigest.update(buffer, 0, bufferLimit);
}
if(inflatable) {
inflater.setInput(buffer, 0, bufferLimit);
inflatedOffset = 0;
inflatedLimit = inflater.inflate(inflatedBuffer);
}
} else {
if (isEncrypted()) {
bufferOffset = 0;
bufferLimit = cipher.doFinal(buffer, 0);
if (plaintextDigest != null) {
plaintextDigest.update(buffer, 0, bufferLimit);
}
if(inflatable) {
inflater.setInput(buffer, 0, bufferLimit);
inflatedOffset = 0;
inflatedLimit = inflater.inflate(inflatedBuffer);
}
state = State.READ_MAC;
} else {
if (!inflatable || inflater.finished()) {
state = State.VALIDATE_ALL;
}
}
}
}
case READ_MAC -> {
var toRead = MAC_LENGTH - macBufferOffset;
if(toRead > 0) {
var read = rawInputStream.read(macBuffer, macBufferOffset, toRead);
if (read == -1) {
throw new MediaDownloadException("Unexpected end of stream: expected " + toRead + " more bytes");
}
macBufferOffset += read;
}
if (macBufferOffset == MAC_LENGTH) {
if (ciphertextDigest != null) {
ciphertextDigest.update(macBuffer);
}
if (!inflatable || inflater.finished()) {
state = State.VALIDATE_ALL;
}
}
}
case VALIDATE_ALL -> {
if (isEncrypted()) {
if (ciphertextDigest != null) {
var actualCiphertextSha256 = ciphertextDigest.digest();
if (!Arrays.equals(expectedCiphertextSha256, actualCiphertextSha256)) {
throw new MediaDownloadException("Ciphertext SHA256 hash doesn't match the expected value");
}
}
var actualCiphertextMac = mac.doFinal();
if (!Arrays.equals(macBuffer, 0, MAC_LENGTH, actualCiphertextMac, 0, MAC_LENGTH)) {
throw new HmacValidationException("media_decryption");
}
}
if (plaintextDigest != null) {
var actualPlaintextSha256 = plaintextDigest.digest();
if (!Arrays.equals(expectedPlaintextSha256, actualPlaintextSha256)) {
throw new MediaDownloadException("Plaintext SHA256 hash doesn't match the expected value");
}
}
state = State.DONE;
}
}
}
}
return state == State.DONE;
} catch (IOException exception) {
throw new MediaDownloadException("Cannot read data", exception);
} catch (GeneralSecurityException exception) {
throw new MediaDownloadException("Cannot decrypt data", exception);
} catch (DataFormatException exception) {
throw new MediaDownloadException("Cannot inflate data", exception);
}
}
private boolean isEncrypted() {
return cipher != null;
}
private boolean isInflatable() {
return inflater != null;
}
@Override
public void close() throws IOException {
super.close();
client.close();
if (inflater != null) {
inflater.end();
}
}
private enum State {
READ_DATA,
READ_MAC,
VALIDATE_ALL,
DONE
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/media/MediaHost.java
================================================
package com.github.auties00.cobalt.media;
import com.github.auties00.cobalt.model.media.MediaPath;
import com.github.auties00.cobalt.model.media.MediaProvider;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public sealed interface MediaHost {
String hostname();
Optional<String> fallbackHostname();
boolean canDownload(MediaProvider provider);
boolean canUpload(MediaProvider provider);
record Primary(
String hostname,
Optional<String> fallbackHostname,
String ip4,
String fallbackIp4,
String ip6,
String fallbackIp6,
Set<MediaPath> download,
Set<MediaPath> upload
) implements MediaHost {
@Override
public boolean canUpload(MediaProvider provider) {
Objects.requireNonNull(provider, "provider cannot be null");
return download.contains(provider.mediaPath());
}
@Override
public boolean canDownload(MediaProvider provider) {
Objects.requireNonNull(provider, "provider cannot be null");
return upload.contains(provider.mediaPath());
}
}
record Fallback(String hostname) implements MediaHost {
@Override
public boolean canDownload(MediaProvider provider) {
Objects.requireNonNull(provider, "provider cannot be null");
return true;
}
@Override
public boolean canUpload(MediaProvider provider) {
Objects.requireNonNull(provider, "provider cannot be null");
return true;
}
@Override
public Optional<String> fallbackHostname() {
return Optional.empty();
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/media/MediaInputStream.java
================================================
package com.github.auties00.cobalt.media;
import com.github.auties00.cobalt.exception.MediaException;
import javax.crypto.Cipher;
import javax.crypto.KDF;
import javax.crypto.Mac;
import javax.crypto.spec.HKDFParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Objects;
abstract class MediaInputStream extends InputStream {
static final int BUFFER_LENGTH = 8192;
static final int MAC_LENGTH = 10;
static final int EXPANDED_SIZE = 112;
static final int KEY_LENGTH = 32;
static final int IV_LENGTH = 16;
final InputStream rawInputStream;
MediaInputStream(InputStream rawInputStream) {
this.rawInputStream = Objects.requireNonNull(rawInputStream, "rawInputStream must not be null");
}
byte[] deriveMediaKeyData(byte[] mediaKey, String mediaKeyName) throws MediaException {
try {
var hkdf = KDF.getInstance("HKDF-SHA256");
var params = HKDFParameterSpec.ofExtract()
.addIKM(new SecretKeySpec(mediaKey, "AES"))
.thenExpand(mediaKeyName.getBytes(), EXPANDED_SIZE);
return hkdf.deriveData(params);
}catch (GeneralSecurityException e) {
throw new MediaException("Cannot derive media key data", e);
}
}
MessageDigest newHash() throws MediaException {
try {
return MessageDigest.getInstance("SHA-256");
}catch (GeneralSecurityException exception) {
throw new MediaException("Cannot create new hash", exception);
}
}
Cipher newCipher(int mode, SecretKeySpec key, IvParameterSpec iv) throws MediaException {
try {
var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(mode, key, iv);
return cipher;
}catch (GeneralSecurityException exception) {
throw new MediaException("Cannot create new cipher", exception);
}
}
Mac newMac(SecretKeySpec key) throws MediaException {
try {
var mac = Mac.getInstance("HmacSHA256");
mac.init(key);
return mac;
}catch (GeneralSecurityException exception) {
throw new MediaException("Cannot create new mac", exception);
}
}
@Override
public void close() throws IOException {
rawInputStream.close();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/media/MediaUploadInputStream.java
================================================
package com.github.auties00.cobalt.media;
import com.github.auties00.cobalt.exception.MediaException;
import com.github.auties00.cobalt.model.media.MediaProvider;
import com.github.auties00.cobalt.util.SecureBytes;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Optional;
public abstract sealed class MediaUploadInputStream extends MediaInputStream {
static final int BUFFER_LENGTH = 8192;
static final int MAC_LENGTH = 10;
MediaUploadInputStream(InputStream rawInputStream) {
super(rawInputStream);
}
public abstract long fileLength();
public abstract byte[] fileSha256();
public abstract Optional<byte[]> fileEncSha256();
public abstract Optional<byte[]> fileKey();
static MediaUploadInputStream of(MediaProvider provider, InputStream inputStream) throws MediaException {
var keyName = provider.mediaPath()
.keyName();
if(keyName.isPresent()) {
return new Ciphertext(inputStream, keyName.get());
}else {
return new Plaintext(inputStream);
}
}
private static final class Ciphertext extends MediaUploadInputStream {
private final MessageDigest plaintextDigest;
private final MessageDigest ciphertextDigest;
private final Mac ciphertextMac;
private final Cipher cipher;
private final byte[] plaintextBuffer;
private final byte[] ciphertextBuffer;
private final byte[] outputBuffer;
private final byte[] mediaKey;
private byte[] plaintextHash;
private byte[] ciphertextHash;
private long plaintextLength;
private boolean finalized;
private int outputPosition;
private int outputLimit;
public Ciphertext(InputStream rawInputStream, String keyName) throws MediaException {
super(rawInputStream);
this.plaintextDigest = newHash();
this.ciphertextDigest = newHash();
this.mediaKey = SecureBytes.random(32);
var expanded = deriveMediaKeyData(mediaKey, keyName);
var iv = new IvParameterSpec(expanded, 0, IV_LENGTH);
var cipherKey = new SecretKeySpec(expanded, IV_LENGTH, KEY_LENGTH, "AES");
var macKey = new SecretKeySpec(expanded, IV_LENGTH + KEY_LENGTH, KEY_LENGTH, "HmacSHA256");
this.cipher = newCipher(Cipher.ENCRYPT_MODE, cipherKey, iv);
this.ciphertextMac = newMac(macKey);
ciphertextMac.update(expanded, 0, IV_LENGTH);
this.plaintextBuffer = new byte[BUFFER_LENGTH];
this.ciphertextBuffer = new byte[BUFFER_LENGTH + cipher.getBlockSize()];
this.outputBuffer = new byte[BUFFER_LENGTH];
this.plaintextLength = 0;
}
@Override
public int read() throws IOException {
ensureDataAvailable();
if (outputPosition >= outputLimit) {
return -1;
}
return outputBuffer[outputPosition++] & 0xFF;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
ensureDataAvailable();
if (outputPosition >= outputLimit) {
return -1;
}
var available = outputLimit - outputPosition;
var toRead = Math.min(len, available);
System.arraycopy(outputBuffer, outputPosition, b, off, toRead);
outputPosition += toRead;
return toRead;
}
private void ensureDataAvailable() throws IOException {
try {
while (outputPosition >= outputLimit && !finalized) {
this.outputPosition = 0;
this.outputLimit = 0;
var plaintextRead = rawInputStream.read(plaintextBuffer, 0, plaintextBuffer.length);
if (plaintextRead == -1) {
rawInputStream.close();
var finalCiphertextLen = cipher.doFinal(ciphertextBuffer, 0);
processChunk(finalCiphertextLen);
var mac = ciphertextMac.doFinal();
ciphertextDigest.update(mac, 0, MAC_LENGTH);
var macSpace = outputBuffer.length - outputLimit;
var macToCopy = Math.min(MAC_LENGTH, macSpace);
System.arraycopy(mac, 0, outputBuffer, outputLimit, macToCopy);
outputLimit += macToCopy;
plaintextHash = plaintextDigest.digest();
ciphertextHash = ciphertextDigest.digest();
finalized = true;
break;
}
plaintextDigest.update(plaintextBuffer, 0, plaintextRead);
plaintextLength += plaintextRead;
var ciphertextLen = cipher.update(plaintextBuffer, 0, plaintextRead, ciphertextBuffer, 0);
processChunk(ciphertextLen);
}
} catch (GeneralSecurityException exception) {
throw new IOException("Cannot encrypt data", exception);
}
}
private void processChunk(int length) {
if (length <= 0) {
return;
}
ciphertextDigest.update(ciphertextBuffer, 0, length);
ciphertextMac.update(ciphertextBuffer, 0, length);
var toCopy = Math.min(length, outputBuffer.length);
System.arraycopy(ciphertextBuffer, 0, outputBuffer, 0, toCopy);
outputLimit = toCopy;
}
@Override
public long fileLength() {
return plaintextLength;
}
@Override
public byte[] fileSha256() {
if(plaintextHash == null) {
throw new IllegalStateException("Cannot get file SHA-256 hash before the file has been fully read");
}
return plaintextHash;
}
@Override
public Optional<byte[]> fileEncSha256() {
if(ciphertextHash == null) {
throw new IllegalStateException("Cannot get file encrypted SHA-256 hash before the file has been fully read");
}
return Optional.of(ciphertextHash);
}
@Override
public Optional<byte[]> fileKey() {
return Optional.of(mediaKey);
}
}
private static final class Plaintext extends MediaUploadInputStream {
private final MessageDigest plaintextDigest;
private long plaintextLength;
private byte[] plaintextHash;
private boolean finalized;
public Plaintext(InputStream rawInputStream) {
super(rawInputStream);
try {
this.plaintextDigest = MessageDigest.getInstance("SHA-256");
this.plaintextLength = 0;
}catch (GeneralSecurityException exception) {
throw new InternalError("Cannot initialize stream", exception);
}
}
@Override
public int read() throws IOException {
var ch = rawInputStream.read();
if (ch != -1) {
plaintextDigest.update((byte) ch);
plaintextLength++;
}else if(!finalized) {
finalized = true;
plaintextHash = plaintextDigest.digest();
}
return ch;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
var result = rawInputStream.read(b, off, len);
if (result != -1) {
plaintextDigest.update(b, off, result);
plaintextLength += result;
}else if(!finalized) {
finalized = true;
plaintextHash = plaintextDigest.digest();
}
return result;
}
@Override
public long fileLength() {
return plaintextLength;
}
@Override
public byte[] fileSha256() {
if(plaintextHash == null) {
throw new IllegalStateException("Cannot get file SHA-256 hash before the file has been fully read");
}
return plaintextHash;
}
@Override
public Optional<byte[]> fileEncSha256() {
return Optional.empty();
}
@Override
public Optional<byte[]> fileKey() {
return Optional.empty();
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/Action.java
================================================
package com.github.auties00.cobalt.model.action;
/**
* A model interface that represents an action
*/
public sealed interface Action permits AgentAction, AndroidUnsupportedActions, ArchiveChatAction, ChatAssignmentAction, ChatAssignmentOpenedStatusAction, ClearChatAction, ContactAction, DeleteChatAction, DeleteMessageForMeAction, LabelAssociationAction, LabelEditAction, MarkChatAsReadAction, MuteAction, NuxAction, PinAction, PrimaryVersionAction, QuickReplyAction, RecentEmojiWeightsAction, RemoveRecentStickerAction, StarAction, StickerAction, SubscriptionAction, TimeFormatAction, UserStatusMuteAction {
/**
* The name of this action
*
* @return a non-null value
*/
String indexName();
/**
* The version of this action
*
* @return a non-null int
*/
int actionVersion();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/AgentAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
/**
* A model clas that represents an agent
*/
@ProtobufMessage(name = "SyncActionValue.AgentAction")
public final class AgentAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 2, type = ProtobufType.INT32)
final Integer deviceId;
@ProtobufProperty(index = 3, type = ProtobufType.BOOL)
final boolean deleted;
AgentAction(
String name,
Integer deviceId,
boolean deleted
) {
this.name = name;
this.deviceId = deviceId;
this.deleted = deleted;
}
@Override
public String indexName() {
return "deviceAgent";
}
@Override
public int actionVersion() {
return 7;
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
public OptionalInt deviceId() {
return deviceId == null ? OptionalInt.empty() : OptionalInt.of(deviceId);
}
public boolean deleted() {
return deleted;
}
@Override
public boolean equals(Object o) {
return o instanceof AgentAction that
&& deviceId.equals(that.deviceId)
&& deleted == that.deleted
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, deviceId, deleted);
}
@Override
public String toString() {
return "AgentAction[" +
"name=" + name + ", " +
"deviceId=" + deviceId + ", " +
"deleted=" + deleted + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/AndroidUnsupportedActions.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents unsupported actions for android
*/
@ProtobufMessage(name = "SyncActionValue.AndroidUnsupportedActions")
public final class AndroidUnsupportedActions implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean allowed;
AndroidUnsupportedActions(boolean allowed) {
this.allowed = allowed;
}
@Override
public String indexName() {
return "android_unsupported_actions";
}
@Override
public int actionVersion() {
return 4;
}
public boolean allowed() {
return allowed;
}
@Override
public boolean equals(Object o) {
return o instanceof AndroidUnsupportedActions that
&& allowed == that.allowed;
}
@Override
public int hashCode() {
return Objects.hashCode(allowed);
}
@Override
public String toString() {
return "AndroidUnsupportedActions[" +
"allowed=" + allowed + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/ArchiveChatAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.sync.ActionMessageRangeSync;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents an archived chat
*/
@ProtobufMessage(name = "SyncActionValue.ArchiveChatAction")
public final class ArchiveChatAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean archived;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final ActionMessageRangeSync messageRange;
ArchiveChatAction(boolean archived, ActionMessageRangeSync messageRange) {
this.archived = archived;
this.messageRange = messageRange;
}
@Override
public String indexName() {
return "archive";
}
@Override
public int actionVersion() {
return 3;
}
public boolean archived() {
return archived;
}
public Optional<ActionMessageRangeSync> messageRange() {
return Optional.ofNullable(messageRange);
}
@Override
public boolean equals(Object o) {
return o instanceof ArchiveChatAction that
&& archived == that.archived
&& Objects.equals(messageRange, that.messageRange);
}
@Override
public int hashCode() {
return Objects.hash(archived, messageRange);
}
@Override
public String toString() {
return "ArchiveChatAction[" +
"archived=" + archived + ", " +
"messageRange=" + messageRange + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/ChatAssignmentAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents the assignment of a chat
*/
@ProtobufMessage(name = "SyncActionValue.ChatAssignmentAction")
public final class ChatAssignmentAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String deviceAgentId;
ChatAssignmentAction(String deviceAgentId) {
this.deviceAgentId = deviceAgentId;
}
@Override
public String indexName() {
return "agentChatAssignment";
}
@Override
public int actionVersion() {
return 7;
}
public Optional<String> deviceAgentId() {
return Optional.ofNullable(deviceAgentId);
}
@Override
public boolean equals(Object o) {
return o instanceof ChatAssignmentAction that
&& Objects.equals(deviceAgentId, that.deviceAgentId);
}
@Override
public int hashCode() {
return Objects.hashCode(deviceAgentId);
}
@Override
public String toString() {
return "ChatAssignmentAction[" +
"deviceAgentId=" + deviceAgentId + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/ChatAssignmentOpenedStatusAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents the assignment of a chat as opened
*/
@ProtobufMessage(name = "SyncActionValue.ChatAssignmentOpenedStatusAction")
public final class ChatAssignmentOpenedStatusAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean chatOpened;
ChatAssignmentOpenedStatusAction(boolean chatOpened) {
this.chatOpened = chatOpened;
}
@Override
public String indexName() {
return "agentChatAssignmentOpenedStatus";
}
@Override
public int actionVersion() {
return 7;
}
public boolean chatOpened() {
return chatOpened;
}
@Override
public boolean equals(Object o) {
return o instanceof ChatAssignmentOpenedStatusAction that
&& chatOpened == that.chatOpened;
}
@Override
public int hashCode() {
return Objects.hashCode(chatOpened);
}
@Override
public String toString() {
return "ChatAssignmentOpenedStatusAction[" +
"chatOpened=" + chatOpened + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/ClearChatAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.sync.ActionMessageRangeSync;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a cleared chat
*/
@ProtobufMessage(name = "SyncActionValue.ClearChatAction")
public final class ClearChatAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final ActionMessageRangeSync messageRange;
ClearChatAction(ActionMessageRangeSync messageRange) {
this.messageRange = messageRange;
}
@Override
public String indexName() {
return "clearChat";
}
@Override
public int actionVersion() {
return 6;
}
public Optional<ActionMessageRangeSync> messageRange() {
return Optional.ofNullable(messageRange);
}
@Override
public boolean equals(Object o) {
return o instanceof ClearChatAction that
&& Objects.equals(messageRange, that.messageRange);
}
@Override
public int hashCode() {
return Objects.hashCode(messageRange);
}
@Override
public String toString() {
return "ClearChatAction[" +
"messageRange=" + messageRange + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/ContactAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.jid.Jid;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a new contact push name
*/
@ProtobufMessage(name = "SyncActionValue.ContactAction")
public final class ContactAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String fullName;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String firstName;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final Jid lid;
ContactAction(String fullName, String firstName, Jid lid) {
this.fullName = fullName;
this.firstName = firstName;
this.lid = lid;
}
@Override
public String indexName() {
return "contact";
}
@Override
public int actionVersion() {
return 2;
}
public Optional<String> name() {
return Optional.ofNullable(fullName)
.or(this::firstName);
}
public Optional<String> fullName() {
return Optional.ofNullable(fullName);
}
public Optional<String> firstName() {
return Optional.ofNullable(firstName);
}
public Optional<Jid> lid() {
return Optional.ofNullable(lid);
}
@Override
public boolean equals(Object o) {
return o instanceof ContactAction that
&& Objects.equals(fullName, that.fullName)
&& Objects.equals(firstName, that.firstName)
&& Objects.equals(lid, that.lid);
}
@Override
public int hashCode() {
return Objects.hash(fullName, firstName, lid);
}
@Override
public String toString() {
return "ContactAction[" +
"fullName=" + fullName + ", " +
"firstName=" + firstName + ", " +
"lidJid=" + lid + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/DeleteChatAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.sync.ActionMessageRangeSync;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a deleted chat
*/
@ProtobufMessage(name = "SyncActionValue.DeleteChatAction")
public final class DeleteChatAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final ActionMessageRangeSync messageRange;
DeleteChatAction(ActionMessageRangeSync messageRange) {
this.messageRange = messageRange;
}
@Override
public String indexName() {
return "deleteChat";
}
@Override
public int actionVersion() {
return 6;
}
public Optional<ActionMessageRangeSync> messageRange() {
return Optional.ofNullable(messageRange);
}
@Override
public boolean equals(Object o) {
return o instanceof DeleteChatAction that
&& Objects.equals(messageRange, that.messageRange);
}
@Override
public int hashCode() {
return Objects.hashCode(messageRange);
}
@Override
public String toString() {
return "DeleteChatAction[" +
"messageRange=" + messageRange + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/DeleteMessageForMeAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a message deleted for this client
*/
@ProtobufMessage(name = "SyncActionValue.DeleteMessageForMeAction")
public final class DeleteMessageForMeAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean deleteMedia;
@ProtobufProperty(index = 2, type = ProtobufType.INT64)
final long messageTimestampSeconds;
DeleteMessageForMeAction(boolean deleteMedia, long messageTimestampSeconds) {
this.deleteMedia = deleteMedia;
this.messageTimestampSeconds = messageTimestampSeconds;
}
@Override
public String indexName() {
return "deleteMessageForMe";
}
@Override
public int actionVersion() {
return 3;
}
public boolean deleteMedia() {
return deleteMedia;
}
public long messageTimestampSeconds() {
return messageTimestampSeconds;
}
public Optional<ZonedDateTime> messageTimestamp() {
return Clock.parseSeconds(messageTimestampSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof DeleteMessageForMeAction that
&& deleteMedia == that.deleteMedia
&& messageTimestampSeconds == that.messageTimestampSeconds;
}
@Override
public int hashCode() {
return Objects.hash(deleteMedia, messageTimestampSeconds);
}
@Override
public String toString() {
return "DeleteMessageForMeAction[" +
"deleteMedia=" + deleteMedia + ", " +
"messageTimestampSeconds=" + messageTimestampSeconds + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/LabelAssociationAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents a label association
*/
@ProtobufMessage(name = "SyncActionValue.LabelAssociationAction")
public final class LabelAssociationAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean labeled;
LabelAssociationAction(boolean labeled) {
this.labeled = labeled;
}
@Override
public String indexName() {
return "label_message";
}
@Override
public int actionVersion() {
return 3;
}
public boolean labeled() {
return labeled;
}
@Override
public boolean equals(Object o) {
return o instanceof LabelAssociationAction that
&& labeled == that.labeled;
}
@Override
public int hashCode() {
return Objects.hashCode(labeled);
}
@Override
public String toString() {
return "LabelAssociationAction[" +
"labeled=" + labeled + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/LabelEditAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.preferences.Label;
import com.github.auties00.cobalt.model.preferences.LabelBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
/**
* A model clas that represents an edit to a label
*/
@ProtobufMessage(name = "SyncActionValue.LabelEditAction")
public final class LabelEditAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 2, type = ProtobufType.INT32)
final Integer color;
@ProtobufProperty(index = 3, type = ProtobufType.INT32)
final int id;
@ProtobufProperty(index = 4, type = ProtobufType.BOOL)
final boolean deleted;
LabelEditAction(String name, Integer color, int id, boolean deleted) {
this.name = name;
this.color = color;
this.id = id;
this.deleted = deleted;
}
@Override
public String indexName() {
return "label_edit";
}
@Override
public int actionVersion() {
return 3;
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
public OptionalInt color() {
return color == null ? OptionalInt.empty() : OptionalInt.of(color);
}
public int id() {
return id;
}
public boolean deleted() {
return deleted;
}
public Label toLabel() {
return new LabelBuilder()
.id(id)
.name(Objects.requireNonNullElse(name, ""))
.color(Objects.requireNonNullElse(color, 0))
.build();
}
@Override
public boolean equals(Object o) {
return o instanceof LabelEditAction that
&& Objects.equals(color, that.color)
&& id == that.id
&& deleted == that.deleted
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name, color, id, deleted);
}
@Override
public String toString() {
return "LabelEditAction[" +
"name=" + name + ", " +
"color=" + color + ", " +
"id=" + id + ", " +
"deleted=" + deleted + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/MarkChatAsReadAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.sync.ActionMessageRangeSync;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a new read status for a chat
*/
@ProtobufMessage(name = "SyncActionValue.MarkChatAsReadAction")
public final class MarkChatAsReadAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean read;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final ActionMessageRangeSync messageRange;
MarkChatAsReadAction(boolean read, ActionMessageRangeSync messageRange) {
this.read = read;
this.messageRange = messageRange;
}
@Override
public String indexName() {
return "markChatAsRead";
}
@Override
public int actionVersion() {
return 3;
}
public boolean read() {
return read;
}
public Optional<ActionMessageRangeSync> messageRange() {
return Optional.ofNullable(messageRange);
}
@Override
public boolean equals(Object o) {
return o instanceof MarkChatAsReadAction that
&& read == that.read
&& Objects.equals(messageRange, that.messageRange);
}
@Override
public int hashCode() {
return Objects.hash(read, messageRange);
}
@Override
public String toString() {
return "MarkChatAsReadAction[" +
"read=" + read + ", " +
"messageRange=" + messageRange + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/MuteAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a new mute status for a chat
*/
@ProtobufMessage(name = "SyncActionValue.MuteAction")
public final class MuteAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean muted;
@ProtobufProperty(index = 2, type = ProtobufType.INT64)
final long muteEndTimestampSeconds;
@ProtobufProperty(index = 3, type = ProtobufType.BOOL)
final boolean autoMuted;
MuteAction(boolean muted, long muteEndTimestampSeconds, boolean autoMuted) {
this.muted = muted;
this.muteEndTimestampSeconds = muteEndTimestampSeconds;
this.autoMuted = autoMuted;
}
@Override
public String indexName() {
return "mute";
}
@Override
public int actionVersion() {
return 2;
}
public boolean muted() {
return muted;
}
public long muteEndTimestampSeconds() {
return muteEndTimestampSeconds;
}
public Optional<ZonedDateTime> muteEndTimestamp() {
return Clock.parseSeconds(muteEndTimestampSeconds);
}
public boolean autoMuted() {
return autoMuted;
}
@Override
public boolean equals(Object o) {
return o instanceof MuteAction that
&& muted == that.muted
&& muteEndTimestampSeconds == that.muteEndTimestampSeconds
&& autoMuted == that.autoMuted;
}
@Override
public int hashCode() {
return Objects.hash(muted, muteEndTimestampSeconds, autoMuted);
}
@Override
public String toString() {
return "MuteAction[" +
"muted=" + muted + ", " +
"muteEndTimestampSeconds=" + muteEndTimestampSeconds + ", " +
"autoMuted=" + autoMuted + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/NuxAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* Unknown
*/
@ProtobufMessage(name = "SyncActionValue.NuxAction")
public final class NuxAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean acknowledged;
NuxAction(boolean acknowledged) {
this.acknowledged = acknowledged;
}
@Override
public String indexName() {
return "nux";
}
@Override
public int actionVersion() {
return 7;
}
public boolean acknowledged() {
return acknowledged;
}
@Override
public boolean equals(Object o) {
return o instanceof NuxAction nuxAction
&& acknowledged == nuxAction.acknowledged;
}
@Override
public int hashCode() {
return Objects.hashCode(acknowledged);
}
@Override
public String toString() {
return "NuxAction[" +
"acknowledged=" + acknowledged + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/PinAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents a new pin status for a chat
*/
@ProtobufMessage(name = "SyncActionValue.PinAction")
public final class PinAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean pinned;
PinAction(boolean pinned) {
this.pinned = pinned;
}
@Override
public String indexName() {
return "pin_v1";
}
@Override
public int actionVersion() {
return 5;
}
public boolean pinned() {
return pinned;
}
@Override
public boolean equals(Object o) {
return o instanceof PinAction pinAction
&& pinned == pinAction.pinned;
}
@Override
public int hashCode() {
return Objects.hashCode(pinned);
}
@Override
public String toString() {
return "PinAction[" +
"pinned=" + pinned + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/PrimaryVersionAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that contains the main Whatsapp version being used
*/
@ProtobufMessage(name = "SyncActionValue.PrimaryVersionAction")
public final class PrimaryVersionAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String version;
PrimaryVersionAction(String version) {
this.version = version;
}
@Override
public String indexName() {
return "primary_version";
}
@Override
public int actionVersion() {
return 7;
}
public Optional<String> version() {
return Optional.ofNullable(version);
}
@Override
public boolean equals(Object o) {
return o instanceof PrimaryVersionAction that
&& Objects.equals(version, that.version);
}
@Override
public int hashCode() {
return Objects.hashCode(version);
}
@Override
public String toString() {
return "PrimaryVersionAction[" +
"version=" + version + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/QuickReplyAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.preferences.QuickReply;
import com.github.auties00.cobalt.model.preferences.QuickReplyBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents the addition or deletion of a quick reply
*/
@ProtobufMessage(name = "SyncActionValue.QuickReplyAction")
public final class QuickReplyAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String shortcut;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String message;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final List<String> keywords;
@ProtobufProperty(index = 4, type = ProtobufType.INT32)
final int count;
@ProtobufProperty(index = 5, type = ProtobufType.BOOL)
final boolean deleted;
QuickReplyAction(String shortcut, String message, List<String> keywords, int count, boolean deleted) {
this.shortcut = shortcut;
this.message = message;
this.keywords = keywords;
this.count = count;
this.deleted = deleted;
}
@Override
public String indexName() {
return "quick_reply";
}
@Override
public int actionVersion() {
return 2;
}
public Optional<String> shortcut() {
return Optional.ofNullable(shortcut);
}
public Optional<String> message() {
return Optional.ofNullable(message);
}
public List<String> keywords() {
return keywords;
}
public int count() {
return count;
}
public boolean deleted() {
return deleted;
}
public QuickReply toQuickReply() {
return new QuickReplyBuilder()
.shortcut(shortcut)
.message(message)
.keywords(keywords)
.count(count)
.build();
}
@Override
public boolean equals(Object o) {
return o instanceof QuickReplyAction that
&& count == that.count
&& deleted == that.deleted
&& Objects.equals(shortcut, that.shortcut)
&& Objects.equals(message, that.message)
&& Objects.equals(keywords, that.keywords);
}
@Override
public int hashCode() {
return Objects.hash(shortcut, message, keywords, count, deleted);
}
@Override
public String toString() {
return "QuickReplyAction[" +
"shortcut=" + shortcut + ", " +
"message=" + message + ", " +
"keywords=" + keywords + ", " +
"count=" + count + ", " +
"deleted=" + deleted + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/RecentEmojiWeightsAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.sync.RecentEmojiWeight;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* A model clas that represents a change in the weight of recent emojis
*/
@ProtobufMessage(name = "SyncActionValue.RecentEmojiWeightsAction")
public final class RecentEmojiWeightsAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final List<RecentEmojiWeight> weights;
RecentEmojiWeightsAction(List<RecentEmojiWeight> weights) {
this.weights = weights;
}
@Override
public String indexName() {
throw new UnsupportedOperationException("Cannot send action");
}
@Override
public int actionVersion() {
throw new UnsupportedOperationException("Cannot send action");
}
public List<RecentEmojiWeight> weights() {
return weights;
}
@Override
public boolean equals(Object o) {
return o instanceof RecentEmojiWeightsAction that
&& Objects.equals(weights, that.weights);
}
@Override
public int hashCode() {
return Objects.hashCode(weights);
}
@Override
public String toString() {
return "RecentEmojiWeightsAction[" +
"weights=" + weights + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/RemoveRecentStickerAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents the deletion of a sticker from the recent list
*/
@ProtobufMessage(name = "SyncActionValue.RemoveRecentStickerAction")
public final class RemoveRecentStickerAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.INT64)
final long lastStickerSentTimestampSeconds;
RemoveRecentStickerAction(long lastStickerSentTimestampSeconds) {
this.lastStickerSentTimestampSeconds = lastStickerSentTimestampSeconds;
}
@Override
public String indexName() {
return "removeRecentSticker";
}
@Override
public int actionVersion() {
return 7;
}
public long lastStickerSentTimestampSeconds() {
return lastStickerSentTimestampSeconds;
}
public Optional<ZonedDateTime> lastStickerSentTimestamp() {
return Clock.parseSeconds(lastStickerSentTimestampSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof RemoveRecentStickerAction that
&& lastStickerSentTimestampSeconds == that.lastStickerSentTimestampSeconds;
}
@Override
public int hashCode() {
return Objects.hashCode(lastStickerSentTimestampSeconds);
}
@Override
public String toString() {
return "RemoveRecentStickerAction[" +
"lastStickerSentTimestampSeconds=" + lastStickerSentTimestampSeconds + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/StarAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents a new star status for a message
*/
@ProtobufMessage(name = "SyncActionValue.StarAction")
public final class StarAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean starred;
StarAction(boolean starred) {
this.starred = starred;
}
@Override
public String indexName() {
return "star";
}
@Override
public int actionVersion() {
return 2;
}
public boolean starred() {
return starred;
}
@Override
public boolean equals(Object o) {
return o instanceof StarAction that
&& starred == that.starred;
}
@Override
public int hashCode() {
return Objects.hashCode(starred);
}
@Override
public String toString() {
return "StarAction[" +
"starred=" + starred + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/StickerAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.model.media.MediaPath;
import com.github.auties00.cobalt.model.media.MediaProvider;
import com.github.auties00.cobalt.model.preferences.Sticker;
import com.github.auties00.cobalt.model.preferences.StickerBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.*;
/**
* A model clas that represents a sticker
*/
@ProtobufMessage(name = "SyncActionValue.StickerAction")
public final class StickerAction implements Action, MediaProvider {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
String mediaUrl;
@ProtobufProperty(index = 2, type = ProtobufType.BYTES)
byte[] fileEncSha256;
@ProtobufProperty(index = 3, type = ProtobufType.BYTES)
byte[] mediaKey;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final String mimetype;
@ProtobufProperty(index = 5, type = ProtobufType.UINT32)
final Integer height;
@ProtobufProperty(index = 6, type = ProtobufType.UINT32)
final Integer width;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
String mediaDirectPath;
@ProtobufProperty(index = 8, type = ProtobufType.UINT64)
Long mediaSize;
@ProtobufProperty(index = 9, type = ProtobufType.BOOL)
final boolean favorite;
@ProtobufProperty(index = 10, type = ProtobufType.UINT32)
final Integer deviceIdHint;
StickerAction(String mediaUrl, byte[] fileEncSha256, byte[] mediaKey, String mimetype, Integer height, Integer width, String mediaDirectPath, Long mediaSize, boolean favorite, Integer deviceIdHint) {
this.mediaUrl = mediaUrl;
this.fileEncSha256 = fileEncSha256;
this.mediaKey = mediaKey;
this.mimetype = mimetype;
this.height = height;
this.width = width;
this.mediaDirectPath = mediaDirectPath;
this.mediaSize = mediaSize;
this.favorite = favorite;
this.deviceIdHint = deviceIdHint;
}
@Override
public String indexName() {
throw new UnsupportedOperationException("Cannot send action");
}
@Override
public int actionVersion() {
throw new UnsupportedOperationException("Cannot send action");
}
@Override
public Optional<byte[]> mediaKey() {
return Optional.ofNullable(mediaKey);
}
@Override
public void setMediaKey(byte[] mediaKey) {
this.mediaKey = mediaKey;
}
@Override
public void setMediaKeyTimestamp(Long timestamp) {
}
@Override
public Optional<byte[]> mediaSha256() {
return Optional.empty();
}
@Override
public void setMediaSha256(byte[] bytes) {
}
@Override
public Optional<byte[]> mediaEncryptedSha256() {
return Optional.ofNullable(fileEncSha256);
}
@Override
public void setMediaEncryptedSha256(byte[] fileEncSha256) {
this.fileEncSha256 = fileEncSha256;
}
public Optional<String> mimetype() {
return Optional.ofNullable(mimetype);
}
public OptionalInt height() {
return height == null ? OptionalInt.empty() : OptionalInt.of(height);
}
public OptionalInt width() {
return width == null ? OptionalInt.empty() : OptionalInt.of(width);
}
@Override
public Optional<String> mediaUrl() {
return Optional.ofNullable(mediaUrl);
}
@Override
public void setMediaUrl(String mediaUrl) {
this.mediaUrl = mediaUrl;
}
@Override
public Optional<String> mediaDirectPath() {
return Optional.ofNullable(mediaDirectPath);
}
@Override
public void setMediaDirectPath(String mediaDirectPath) {
this.mediaDirectPath = mediaDirectPath;
}
@Override
public OptionalLong mediaSize() {
return mediaSize == null ? OptionalLong.empty() : OptionalLong.of(mediaSize);
}
@Override
public void setMediaSize(long mediaSize) {
this.mediaSize = mediaSize;
}
@Override
public MediaPath mediaPath() {
return MediaPath.STICKER;
}
public boolean favorite() {
return favorite;
}
public OptionalInt deviceIdHint() {
return deviceIdHint == null ? OptionalInt.empty() : OptionalInt.of(deviceIdHint);
}
public Sticker toSticker() {
return new StickerBuilder()
.mediaUrl(mediaUrl)
.fileEncSha256(fileEncSha256)
.mediaKey(mediaKey)
.mediaKey(mediaKey)
.mimetype(mimetype)
.height(height)
.width(width)
.mediaDirectPath(mediaDirectPath)
.mediaSize(mediaSize)
.favorite(favorite)
.deviceIdHint(deviceIdHint)
.build();
}
@Override
public boolean equals(Object o) {
return o instanceof StickerAction that
&& Objects.equals(height, that.height)
&& Objects.equals(width, that.width)
&& Objects.equals(mediaSize, that.mediaSize)
&& favorite == that.favorite
&& Objects.equals(mediaUrl, that.mediaUrl)
&& Objects.deepEquals(fileEncSha256, that.fileEncSha256)
&& Objects.deepEquals(mediaKey, that.mediaKey)
&& Objects.equals(mimetype, that.mimetype)
&& Objects.equals(mediaDirectPath, that.mediaDirectPath)
&& Objects.equals(deviceIdHint, that.deviceIdHint);
}
@Override
public int hashCode() {
return Objects.hash(mediaUrl, Arrays.hashCode(fileEncSha256), Arrays.hashCode(mediaKey), mimetype, height, width, mediaDirectPath, mediaSize, favorite, deviceIdHint);
}
@Override
public String toString() {
return "StickerAction[" +
"url=" + mediaUrl + ", " +
"fileEncSha256=" + Arrays.toString(fileEncSha256) + ", " +
"mediaKey=" + Arrays.toString(mediaKey) + ", " +
"mimetype=" + mimetype + ", " +
"height=" + height + ", " +
"width=" + width + ", " +
"directPath=" + mediaDirectPath + ", " +
"mediaSize=" + mediaSize + ", " +
"favorite=" + favorite + ", " +
"deviceIdHint=" + deviceIdHint + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/SubscriptionAction.java
================================================
package com.github.auties00.cobalt.model.action;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model clas that represents a subscription
*/
@ProtobufMessage(name = "SyncActionValue.SubscriptionAction")
public final class SubscriptionAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean deactivated;
@ProtobufProperty(index = 2, type = ProtobufType.BOOL)
final boolean autoRenewing;
@ProtobufProperty(index = 3, type = ProtobufType.INT64)
final long expirationDateSeconds;
SubscriptionAction(boolean deactivated, boolean autoRenewing, long expirationDateSeconds) {
this.deactivated = deactivated;
this.autoRenewing = autoRenewing;
this.expirationDateSeconds = expirationDateSeconds;
}
@Override
public String indexName() {
return "subscription";
}
@Override
public int actionVersion() {
return 1;
}
public boolean deactivated() {
return deactivated;
}
public boolean autoRenewing() {
return autoRenewing;
}
public long expirationDateSeconds() {
return expirationDateSeconds;
}
public Optional<ZonedDateTime> expirationDate() {
return Clock.parseSeconds(expirationDateSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof SubscriptionAction that
&& deactivated == that.deactivated
&& autoRenewing == that.autoRenewing
&& expirationDateSeconds == that.expirationDateSeconds;
}
@Override
public int hashCode() {
return Objects.hash(deactivated, autoRenewing, expirationDateSeconds);
}
@Override
public String toString() {
return "SubscriptionAction[" +
"deactivated=" + deactivated + ", " +
"autoRenewing=" + autoRenewing + ", " +
"expirationDateSeconds=" + expirationDateSeconds + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/TimeFormatAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents the time format used by the companion
*/
@ProtobufMessage(name = "SyncActionValue.TimeFormatAction")
public final class TimeFormatAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean twentyFourHourFormatEnabled;
TimeFormatAction(boolean twentyFourHourFormatEnabled) {
this.twentyFourHourFormatEnabled = twentyFourHourFormatEnabled;
}
@Override
public String indexName() {
return "time_format";
}
@Override
public int actionVersion() {
return 7;
}
public boolean twentyFourHourFormatEnabled() {
return twentyFourHourFormatEnabled;
}
@Override
public boolean equals(Object o) {
return o instanceof TimeFormatAction that
&& twentyFourHourFormatEnabled == that.twentyFourHourFormatEnabled;
}
@Override
public int hashCode() {
return Objects.hashCode(twentyFourHourFormatEnabled);
}
@Override
public String toString() {
return "TimeFormatAction[" +
"twentyFourHourFormatEnabled=" + twentyFourHourFormatEnabled + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/action/UserStatusMuteAction.java
================================================
package com.github.auties00.cobalt.model.action;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model clas that represents whether a user was muted
*/
@ProtobufMessage(name = "SyncActionValue.UserStatusMuteAction")
public final class UserStatusMuteAction implements Action {
@ProtobufProperty(index = 1, type = ProtobufType.BOOL)
final boolean muted;
UserStatusMuteAction(boolean muted) {
this.muted = muted;
}
@Override
public String indexName() {
return "userStatusMute";
}
@Override
public int actionVersion() {
return 7;
}
public boolean muted() {
return muted;
}
@Override
public boolean equals(Object o) {
return o instanceof UserStatusMuteAction that
&& muted == that.muted;
}
@Override
public int hashCode() {
return Objects.hashCode(muted);
}
@Override
public String toString() {
return "UserStatusMuteAction[" +
"muted=" + muted + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/ClientFinish.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.BYTES;
@ProtobufMessage(name = "HandshakeMessage.ClientFinish")
public record ClientFinish(@ProtobufProperty(index = 1, type = BYTES) byte[] _static,
@ProtobufProperty(index = 2, type = BYTES) byte[] payload) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/ClientHello.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.BYTES;
@ProtobufMessage(name = "HandshakeMessage.ClientHello")
public record ClientHello(@ProtobufProperty(index = 1, type = BYTES) byte[] ephemeral,
@ProtobufProperty(index = 2, type = BYTES) byte[] _static,
@ProtobufProperty(index = 3, type = BYTES) byte[] payload) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/ClientPayload.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import java.util.List;
import static it.auties.protobuf.model.ProtobufType.*;
@ProtobufMessage(name = "ClientPayload")
public record ClientPayload(@ProtobufProperty(index = 1, type = UINT64) Long username,
@ProtobufProperty(index = 3, type = BOOL) Boolean passive,
@ProtobufProperty(index = 5, type = MESSAGE) UserAgent userAgent,
@ProtobufProperty(index = 6, type = MESSAGE) WebInfo webInfo,
@ProtobufProperty(index = 7, type = STRING) String pushName,
@ProtobufProperty(index = 9, type = SFIXED32) Integer sessionId,
@ProtobufProperty(index = 10, type = BOOL) Boolean shortConnect,
@ProtobufProperty(index = 12, type = ENUM) ClientPayloadConnectType connectType,
@ProtobufProperty(index = 13, type = ENUM) ClientPayloadConnectReason connectReason,
@ProtobufProperty(index = 14, type = INT32) List<Integer> shards,
@ProtobufProperty(index = 15, type = MESSAGE) DNSSource dnsSource,
@ProtobufProperty(index = 16, type = UINT32) Integer connectAttemptCount,
@ProtobufProperty(index = 18, type = UINT32) Integer device,
@ProtobufProperty(index = 19, type = MESSAGE) CompanionRegistrationData regData,
@ProtobufProperty(index = 20, type = ENUM) ClientPayloadProduct product,
@ProtobufProperty(index = 21, type = BYTES) byte[] fbCat,
@ProtobufProperty(index = 22, type = BYTES) byte[] fbUserAgent,
@ProtobufProperty(index = 23, type = BOOL) Boolean oc,
@ProtobufProperty(index = 24, type = INT32) Integer lc,
@ProtobufProperty(index = 30, type = ENUM) ClientPayloadIOSAppExtension iosAppExtension,
@ProtobufProperty(index = 31, type = UINT64) Long fbAppId,
@ProtobufProperty(index = 32, type = BYTES) byte[] fbDeviceId,
@ProtobufProperty(index = 33, type = BOOL) Boolean pull) {
@ProtobufEnum
public enum ClientPayloadConnectType {
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);
ClientPayloadConnectType(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
@ProtobufEnum
public enum ClientPayloadConnectReason {
PUSH(0),
USER_ACTIVATED(1),
SCHEDULED(2),
ERROR_RECONNECT(3),
NETWORK_SWITCH(4),
PING_RECONNECT(5);
ClientPayloadConnectReason(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
@ProtobufEnum
public enum ClientPayloadProduct {
WHATSAPP(0),
MESSENGER(1);
ClientPayloadProduct(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
@ProtobufEnum
public enum ClientPayloadIOSAppExtension {
SHARE_EXTENSION(0),
SERVICE_EXTENSION(1),
INTENTS_EXTENSION(2);
ClientPayloadIOSAppExtension(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/CompanionProperties.java
================================================
package com.github.auties00.cobalt.model.auth;
import com.github.auties00.cobalt.model.sync.HistorySyncConfig;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.*;
@ProtobufMessage(name = "DeviceProps")
public record CompanionProperties(@ProtobufProperty(index = 1, type = STRING) String os,
@ProtobufProperty(index = 2, type = MESSAGE) Version version,
@ProtobufProperty(index = 3, type = ENUM) PlatformType platformType,
@ProtobufProperty(index = 4, type = BOOL) boolean requireFullSync,
@ProtobufProperty(index = 5, type = MESSAGE) HistorySyncConfig historySyncConfig) {
@ProtobufEnum
public 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);
PlatformType(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/CompanionRegistrationData.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "ClientPayload.DevicePairingRegistrationData")
public record CompanionRegistrationData(@ProtobufProperty(index = 1, type = ProtobufType.BYTES) byte[] eRegid,
@ProtobufProperty(index = 2, type = ProtobufType.BYTES) byte[] eKeytype,
@ProtobufProperty(index = 3, type = ProtobufType.BYTES) byte[] eIdent,
@ProtobufProperty(index = 4, type = ProtobufType.BYTES) byte[] eSkeyId,
@ProtobufProperty(index = 5, type = ProtobufType.BYTES) byte[] eSkeyVal,
@ProtobufProperty(index = 6, type = ProtobufType.BYTES) byte[] eSkeySig,
@ProtobufProperty(index = 7, type = ProtobufType.BYTES) byte[] buildHash,
@ProtobufProperty(index = 8, type = ProtobufType.BYTES) byte[] companionProps) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/DeviceIdentity.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "ADVDeviceIdentity")
public record DeviceIdentity(@ProtobufProperty(index = 1, type = ProtobufType.UINT32)
int rawId,
@ProtobufProperty(index = 2, type = ProtobufType.UINT64)
long timestamp,
@ProtobufProperty(index = 3, type = ProtobufType.UINT32)
int keyIndex) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/DNSSource.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.BOOL;
import static it.auties.protobuf.model.ProtobufType.ENUM;
@ProtobufMessage(name = "ClientPayload.DNSSource")
public record DNSSource(@ProtobufProperty(index = 15, type = ENUM) ResolutionMethod dnsMethod,
@ProtobufProperty(index = 16, type = BOOL) boolean appCached) {
@ProtobufEnum(name = "ClientPayload.DNSSource.DNSResolutionMethod")
public enum ResolutionMethod {
SYSTEM(0),
GOOGLE(1),
HARDCODED(2),
OVERRIDE(3),
FALLBACK(4);
ResolutionMethod(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/HandshakeMessage.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.MESSAGE;
@ProtobufMessage(name = "HandshakeMessage")
public record HandshakeMessage(@ProtobufProperty(index = 2, type = MESSAGE) ClientHello clientHello,
@ProtobufProperty(index = 3, type = MESSAGE) ServerHello serverHello,
@ProtobufProperty(index = 4, type = MESSAGE) ClientFinish clientFinish) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/KeyIndexList.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
@ProtobufMessage(name = "ADVKeyIndexList")
public record KeyIndexList(@ProtobufProperty(index = 1, type = ProtobufType.UINT32) int rawId,
@ProtobufProperty(index = 2, type = ProtobufType.UINT64) long timestamp,
@ProtobufProperty(index = 3, type = ProtobufType.UINT32) int currentIndex,
@ProtobufProperty(index = 4, type = ProtobufType.UINT32) List<Integer> validIndexes) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/NoiseCertificate.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "NoiseCertificate")
public record NoiseCertificate(@ProtobufProperty(index = 1, type = ProtobufType.BYTES) byte[] details,
@ProtobufProperty(index = 2, type = ProtobufType.BYTES) byte[] signature) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/ServerHello.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.BYTES;
@ProtobufMessage(name = "HandshakeMessage.ServerHello")
public record ServerHello(@ProtobufProperty(index = 1, type = BYTES) byte[] ephemeral,
@ProtobufProperty(index = 2, type = BYTES) byte[] staticText,
@ProtobufProperty(index = 3, type = BYTES) byte[] payload) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/SignedDeviceIdentity.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "ADVSignedDeviceIdentity")
public record SignedDeviceIdentity(@ProtobufProperty(index = 1, type = ProtobufType.BYTES) byte[] details,
@ProtobufProperty(index = 2, type = ProtobufType.BYTES) byte[] accountSignatureKey,
@ProtobufProperty(index = 3, type = ProtobufType.BYTES) byte[] accountSignature,
@ProtobufProperty(index = 4, type = ProtobufType.BYTES) byte[] deviceSignature) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/SignedDeviceIdentityHMAC.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "ADVSignedDeviceIdentityHMAC")
public record SignedDeviceIdentityHMAC(@ProtobufProperty(index = 1, type = ProtobufType.BYTES) byte[] details,
@ProtobufProperty(index = 2, type = ProtobufType.BYTES) byte[] hmac) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/SignedKeyIndexList.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
@ProtobufMessage(name = "ADVSignedKeyIndexList")
public record SignedKeyIndexList(@ProtobufProperty(index = 1, type = ProtobufType.BYTES) byte[] details,
@ProtobufProperty(index = 2, type = ProtobufType.BYTES) byte[] accountSignature) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/UserAgent.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.*;
@ProtobufMessage(name = "ClientPayload.UserAgent")
public record UserAgent(@ProtobufProperty(index = 1, type = ENUM) PlatformType platform,
@ProtobufProperty(index = 2, type = MESSAGE) Version appVersion,
@ProtobufProperty(index = 3, type = STRING) String mcc,
@ProtobufProperty(index = 4, type = STRING) String mnc,
@ProtobufProperty(index = 5, type = STRING) String osVersion,
@ProtobufProperty(index = 6, type = STRING) String manufacturer,
@ProtobufProperty(index = 7, type = STRING) String device,
@ProtobufProperty(index = 8, type = STRING) String osBuildNumber,
@ProtobufProperty(index = 9, type = STRING) String phoneId,
@ProtobufProperty(index = 10, type = ENUM) ReleaseChannel releaseChannel,
@ProtobufProperty(index = 11, type = STRING) String localeLanguageIso6391,
@ProtobufProperty(index = 12, type = STRING) String localeCountryIso31661Alpha2,
@ProtobufProperty(index = 13, type = STRING) String deviceBoard,
@ProtobufProperty(index = 15, type = ENUM) DeviceType deviceType,
@ProtobufProperty(index = 16, type = STRING) String deviceModelType) {
@ProtobufEnum(name = "ClientPayload.UserAgent.Platform")
public enum PlatformType {
ANDROID("Android", 0),
IOS("iOS", 1),
ANDROID_BUSINESS("Android", 10),
IOS_BUSINESS("iOS", 12),
WINDOWS("Windows", 13),
MACOS("MacOS", 24);
PlatformType(String platformName, @ProtobufEnumIndex int index) {
this.platformName = platformName;
this.index = index;
}
final String platformName;
final int index;
public int index() {
return this.index;
}
public String platformName() {
return platformName;
}
public boolean isAndroid() {
return this == ANDROID || this == ANDROID_BUSINESS;
}
public boolean isIOS() {
return this == IOS || this == IOS_BUSINESS;
}
public boolean isBusiness() {
return this == ANDROID_BUSINESS || this == IOS_BUSINESS;
}
public boolean isMobile() {
return isAndroid() || isIOS();
}
public PlatformType toPersonal() {
return switch (this) {
case ANDROID_BUSINESS -> ANDROID;
case IOS_BUSINESS -> IOS;
default -> this;
};
}
public PlatformType toBusiness() {
return switch (this) {
case ANDROID -> ANDROID_BUSINESS;
case IOS -> IOS_BUSINESS;
default -> this;
};
}
}
@ProtobufEnum(name = "ClientPayload.UserAgent.ReleaseChannel")
public enum ReleaseChannel {
RELEASE(0),
BETA(1),
ALPHA(2),
DEBUG(3);
ReleaseChannel(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
@ProtobufEnum(name = "ClientPayload.UserAgent.DeviceType")
public enum DeviceType {
PHONE(0),
TABLET(1),
DESKTOP(2),
WEARABLE(3),
VR(4);
DeviceType(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/Version.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import static java.lang.Integer.parseInt;
@ProtobufMessage(name = "ClientPayload.UserAgent.AppVersion")
public record Version(
@ProtobufProperty(index = 1, type = ProtobufType.UINT32)
Integer primary,
@ProtobufProperty(index = 2, type = ProtobufType.UINT32)
Integer secondary,
@ProtobufProperty(index = 3, type = ProtobufType.UINT32)
Integer tertiary,
@ProtobufProperty(index = 4, type = ProtobufType.UINT32)
Integer quaternary,
@ProtobufProperty(index = 5, type = ProtobufType.UINT32)
Integer quinary
) {
public Version(int primary) {
this(primary, null, null, null, null);
}
public Version(int primary, int secondary, int tertiary) {
this(primary, secondary, tertiary, null, null);
}
public static Version of(String version) {
var tokens = version.split("\\.", 5);
if (tokens.length > 5) {
throw new IllegalArgumentException("Invalid value of tokens for version %s: %s".formatted(version, tokens));
}
var primary = tokens.length > 0 ? parseInt(tokens[0]) : null;
var secondary = tokens.length > 1 ? parseInt(tokens[1]) : null;
var tertiary = tokens.length > 2 ? parseInt(tokens[2]) : null;
var quaternary = tokens.length > 3 ? parseInt(tokens[3]) : null;
var quinary = tokens.length > 4 ? parseInt(tokens[4]) : null;
return new Version(primary, secondary, tertiary, quaternary, quinary);
}
public byte[] toHash() {
try {
var digest = MessageDigest.getInstance("MD5");
digest.update(toString().getBytes());
return digest.digest();
} catch (NoSuchAlgorithmException exception) {
throw new UnsupportedOperationException("Missing md5 implementation", exception);
}
}
@Override
public String toString() {
var result = new StringBuilder();
if(primary != null) {
result.append(primary);
}
if(secondary != null) {
if(!result.isEmpty()) {
result.append('.');
}
result.append(secondary);
}
if(tertiary != null) {
if(!result.isEmpty()) {
result.append('.');
}
result.append(tertiary);
}
if(quaternary != null) {
if(!result.isEmpty()) {
result.append('.');
}
result.append(quaternary);
}
if(quinary != null) {
if(!result.isEmpty()) {
result.append('.');
}
result.append(quinary);
}
return result.toString();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/WebFeatures.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.ENUM;
@ProtobufMessage(name = "WebFeatures")
public record WebFeatures(@ProtobufProperty(index = 1, type = ENUM) WebFeaturesFlag labelsDisplay,
@ProtobufProperty(index = 2, type = ENUM) WebFeaturesFlag voipIndividualOutgoing,
@ProtobufProperty(index = 3, type = ENUM) WebFeaturesFlag groupsV3,
@ProtobufProperty(index = 4, type = ENUM) WebFeaturesFlag groupsV3Create,
@ProtobufProperty(index = 5, type = ENUM) WebFeaturesFlag changeNumberV2,
@ProtobufProperty(index = 6, type = ENUM) WebFeaturesFlag queryStatusV3Thumbnail,
@ProtobufProperty(index = 7, type = ENUM) WebFeaturesFlag liveLocations,
@ProtobufProperty(index = 8, type = ENUM) WebFeaturesFlag queryVname,
@ProtobufProperty(index = 9, type = ENUM) WebFeaturesFlag voipIndividualIncoming,
@ProtobufProperty(index = 10, type = ENUM) WebFeaturesFlag quickRepliesQuery,
@ProtobufProperty(index = 11, type = ENUM) WebFeaturesFlag payments,
@ProtobufProperty(index = 12, type = ENUM) WebFeaturesFlag stickerPackQuery,
@ProtobufProperty(index = 13, type = ENUM) WebFeaturesFlag liveLocationsFinal,
@ProtobufProperty(index = 14, type = ENUM) WebFeaturesFlag labelsEdit,
@ProtobufProperty(index = 15, type = ENUM) WebFeaturesFlag mediaUpload,
@ProtobufProperty(index = 18, type = ENUM) WebFeaturesFlag mediaUploadRichQuickReplies,
@ProtobufProperty(index = 19, type = ENUM) WebFeaturesFlag vnameV2,
@ProtobufProperty(index = 20, type = ENUM) WebFeaturesFlag videoPlaybackUrl,
@ProtobufProperty(index = 21, type = ENUM) WebFeaturesFlag statusRanking,
@ProtobufProperty(index = 22, type = ENUM) WebFeaturesFlag voipIndividualVideo,
@ProtobufProperty(index = 23, type = ENUM) WebFeaturesFlag thirdPartyStickers,
@ProtobufProperty(index = 24, type = ENUM) WebFeaturesFlag frequentlyForwardedSetting,
@ProtobufProperty(index = 25, type = ENUM) WebFeaturesFlag groupsV4JoinPermission,
@ProtobufProperty(index = 26, type = ENUM) WebFeaturesFlag recentStickers,
@ProtobufProperty(index = 27, type = ENUM) WebFeaturesFlag catalog,
@ProtobufProperty(index = 28, type = ENUM) WebFeaturesFlag starredStickers,
@ProtobufProperty(index = 29, type = ENUM) WebFeaturesFlag voipGroupCall,
@ProtobufProperty(index = 30, type = ENUM) WebFeaturesFlag templateMessage,
@ProtobufProperty(index = 31, type = ENUM) WebFeaturesFlag templateMessageInteractivity,
@ProtobufProperty(index = 32, type = ENUM) WebFeaturesFlag ephemeralMessages,
@ProtobufProperty(index = 33, type = ENUM) WebFeaturesFlag e2ENotificationSync,
@ProtobufProperty(index = 34, type = ENUM) WebFeaturesFlag recentStickersV2,
@ProtobufProperty(index = 36, type = ENUM) WebFeaturesFlag recentStickersV3,
@ProtobufProperty(index = 37, type = ENUM) WebFeaturesFlag userNotice,
@ProtobufProperty(index = 39, type = ENUM) WebFeaturesFlag support,
@ProtobufProperty(index = 40, type = ENUM) WebFeaturesFlag groupUiiCleanup,
@ProtobufProperty(index = 41, type = ENUM) WebFeaturesFlag groupDogfoodingInternalOnly,
@ProtobufProperty(index = 42, type = ENUM) WebFeaturesFlag settingsSync,
@ProtobufProperty(index = 43, type = ENUM) WebFeaturesFlag archiveV2,
@ProtobufProperty(index = 44, type = ENUM) WebFeaturesFlag ephemeralAllowGroupMembers,
@ProtobufProperty(index = 45, type = ENUM) WebFeaturesFlag ephemeral24HDuration,
@ProtobufProperty(index = 46, type = ENUM) WebFeaturesFlag mdForceUpgrade,
@ProtobufProperty(index = 47, type = ENUM) WebFeaturesFlag disappearingMode,
@ProtobufProperty(index = 48, type = ENUM) WebFeaturesFlag externalMdOptInAvailable,
@ProtobufProperty(index = 49, type = ENUM) WebFeaturesFlag noDeleteMessageTimeLimit) {
@ProtobufEnum
public enum WebFeaturesFlag {
NOT_STARTED(0),
FORCE_UPGRADE(1),
DEVELOPMENT(2),
PRODUCTION(3);
WebFeaturesFlag(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/WebInfo.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.*;
@ProtobufMessage(name = "ClientPayload.WebInfo")
public record WebInfo(@ProtobufProperty(index = 1, type = STRING) String refToken,
@ProtobufProperty(index = 2, type = STRING) String version,
@ProtobufProperty(index = 3, type = MESSAGE) WebPayload webPayload,
@ProtobufProperty(index = 4, type = ENUM) Platform webSubPlatform) {
@ProtobufEnum(name = "ClientPayload.UserAgent.Platform")
public enum Platform {
WEB_BROWSER(0),
APP_STORE(1),
WIN_STORE(2),
DARWIN(3),
WIN32(4);
Platform(@ProtobufEnumIndex int index) {
this.index = index;
}
final int index;
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/auth/WebPayload.java
================================================
package com.github.auties00.cobalt.model.auth;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import static it.auties.protobuf.model.ProtobufType.*;
@ProtobufMessage(name = "ClientPayload.WebInfo.WebdPayload")
public record WebPayload(@ProtobufProperty(index = 1, type = BOOL) boolean usesParticipantInKey,
@ProtobufProperty(index = 2, type = BOOL) boolean supportsStarredMessages,
@ProtobufProperty(index = 3, type = BOOL) boolean supportsDocumentMessages,
@ProtobufProperty(index = 4, type = BOOL) boolean supportsUrlMessages,
@ProtobufProperty(index = 5, type = BOOL) boolean supportsMediaRetry,
@ProtobufProperty(index = 6, type = BOOL) boolean supportsE2EImage,
@ProtobufProperty(index = 7, type = BOOL) boolean supportsE2EVideo,
@ProtobufProperty(index = 8, type = BOOL) boolean supportsE2EAudio,
@ProtobufProperty(index = 9, type = BOOL) boolean supportsE2EDocument,
@ProtobufProperty(index = 10, type = STRING) String documentTypes,
@ProtobufProperty(index = 11, type = BYTES) byte[] features) {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessAccountPayload.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds a payload about a business account.
*/
@ProtobufMessage(name = "BizAccountPayload")
public final class BusinessAccountPayload {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final BusinessVerifiedNameCertificate certificate;
@ProtobufProperty(index = 2, type = ProtobufType.BYTES)
final byte[] info;
BusinessAccountPayload(BusinessVerifiedNameCertificate certificate, byte[] info) {
this.certificate = certificate;
this.info = info;
}
public Optional<BusinessVerifiedNameCertificate> certificate() {
return Optional.ofNullable(certificate);
}
public Optional<byte[]> info() {
return Optional.ofNullable(info);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessAccountPayload that
&& Objects.equals(certificate, that.certificate)
&& Arrays.equals(info, that.info);
}
@Override
public int hashCode() {
return Objects.hash(certificate, Arrays.hashCode(info));
}
@Override
public String toString() {
return "BusinessAccountPayload{" +
"certificate=" + certificate +
", info=" + Arrays.toString(info) +
'}';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessCatalogEntry.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.node.Node;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.net.URI;
import java.util.Arrays;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* A model class that represents a business catalog entry.
*/
@ProtobufMessage
public final class BusinessCatalogEntry {
private static final Map<String, BusinessItemAvailability> PRETTY_NAME_TO_AVAILABILITY = Arrays.stream(BusinessItemAvailability.values())
.collect(Collectors.toMap(entry -> entry.name().toLowerCase().replaceAll("_", " "), Function.identity()));
private static final Map<String, BusinessReviewStatus> PRETTY_NAME_TO_REVIEW_STATUS = Arrays.stream(BusinessReviewStatus.values())
.collect(Collectors.toMap(entry -> entry.name().toLowerCase(), Function.identity()));
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final URI encryptedImage;
@ProtobufProperty(index = 3, type = ProtobufType.ENUM)
final BusinessReviewStatus reviewStatus;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
final BusinessItemAvailability availability;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 6, type = ProtobufType.STRING)
final String sellerId;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final URI uri;
@ProtobufProperty(index = 8, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 9, type = ProtobufType.INT64)
final long price;
@ProtobufProperty(index = 10, type = ProtobufType.STRING)
final String currency;
@ProtobufProperty(index = 11, type = ProtobufType.BOOL)
final boolean hidden;
BusinessCatalogEntry(String id, URI encryptedImage, BusinessReviewStatus reviewStatus, BusinessItemAvailability availability, String name, String sellerId, URI uri, String description, long price, String currency, boolean hidden) {
this.id = Objects.requireNonNull(id, "id cannot be null in");
this.encryptedImage = Objects.requireNonNull(encryptedImage, "encryptedImage cannot be null");
this.reviewStatus = Objects.requireNonNull(reviewStatus, "reviewStatus cannot be null");
this.availability = Objects.requireNonNull(availability, "availability cannot be null");
this.name = Objects.requireNonNull(name, "name cannot be null");
this.sellerId = Objects.requireNonNull(sellerId, "sellerId cannot be null");
this.uri = Objects.requireNonNull(uri, "uri cannot be null");
this.description = Objects.requireNonNull(description, "description cannot be null");
this.price = price;
this.currency = Objects.requireNonNull(currency, "currency cannot be null");
this.hidden = hidden;
}
public static BusinessCatalogEntry of(Node node) {
var id = node.getRequiredAttributeAsString("id");
var hidden = node.getAttributeAsBool("is_hidden", false);
var name = node.getChild("name")
.flatMap(Node::toContentString)
.orElseThrow(() -> new NoSuchElementException("Missing name for catalog entry"));
var encryptedImage = node.getChild("media")
.flatMap(entry -> entry.getChild("original_image_url"))
.flatMap(Node::toContentString)
.map(URI::create)
.orElseThrow(() -> new NoSuchElementException("Missing image for catalog entry"));
var statusInfo = node.getChild("status_info")
.flatMap(entry -> entry.getChild("status"))
.flatMap(Node::toContentString)
.map(prettyName -> PRETTY_NAME_TO_REVIEW_STATUS.get(prettyName.toLowerCase()))
.orElse(BusinessReviewStatus.NO_REVIEW);
var availability = node.getChild("availability")
.flatMap(Node::toContentString)
.map(prettyName -> PRETTY_NAME_TO_AVAILABILITY.get(prettyName.toLowerCase()))
.orElse(BusinessItemAvailability.UNKNOWN);
var sellerId = node.getChild("retailer_id")
.flatMap(Node::toContentString)
.orElseThrow(() -> new NoSuchElementException("Missing seller id for catalog entry"));
var uri = node.getChild("url")
.flatMap(Node::toContentString)
.map(URI::create)
.orElseThrow(() -> new NoSuchElementException("Missing uri for catalog entry"));
var description = node.getChild("description")
.flatMap(Node::toContentString)
.orElse("");
var price = node.getChild("price")
.flatMap(Node::toContentString)
.map(Long::parseUnsignedLong)
.orElseThrow(() -> new NoSuchElementException("Missing price for catalog entry"));
var currency = node.getChild("currency")
.flatMap(Node::toContentString)
.orElseThrow(() -> new NoSuchElementException("Missing currency for catalog entry"));
return new BusinessCatalogEntry(id, encryptedImage, statusInfo, availability, name, sellerId, uri, description, price, currency, hidden);
}
public String id() {
return id;
}
public URI encryptedImage() {
return encryptedImage;
}
public BusinessReviewStatus reviewStatus() {
return reviewStatus;
}
public BusinessItemAvailability availability() {
return availability;
}
public String name() {
return name;
}
public String sellerId() {
return sellerId;
}
public URI uri() {
return uri;
}
public String description() {
return description;
}
public long price() {
return price;
}
public String currency() {
return currency;
}
public boolean hidden() {
return hidden;
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessCatalogEntry that
&& price == that.price
&& hidden == that.hidden
&& Objects.equals(id, that.id)
&& Objects.equals(encryptedImage, that.encryptedImage)
&& reviewStatus == that.reviewStatus
&& availability == that.availability
&& Objects.equals(name, that.name)
&& Objects.equals(sellerId, that.sellerId)
&& Objects.equals(uri, that.uri)
&& Objects.equals(description, that.description)
&& Objects.equals(currency, that.currency);
}
@Override
public int hashCode() {
return Objects.hash(id, encryptedImage, reviewStatus, availability, name, sellerId, uri, description, price, currency, hidden);
}
@Override
public String toString() {
return "BusinessCatalogEntry[" +
"id='" + id + '\'' +
", encryptedImage=" + encryptedImage +
", reviewStatus=" + reviewStatus +
", availability=" + availability +
", name='" + name + '\'' +
", sellerId='" + sellerId + '\'' +
", uri=" + uri +
", description='" + description + '\'' +
", price=" + price +
", currency='" + currency + '\'' +
", hidden=" + hidden +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessCategory.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.node.Node;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a business category
*/
@ProtobufMessage
public final class BusinessCategory {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String name;
BusinessCategory(String id, String name) {
this.id = id;
this.name = name;
}
public static BusinessCategory of(Node node) {
var id = node.getRequiredAttributeAsString("id");
var name = URLDecoder.decode(node.toContentString().orElseThrow(), StandardCharsets.UTF_8);
return new BusinessCategory(id, name);
}
public String id() {
return id;
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessCategory that
&& Objects.equals(id, that.id)
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "BusinessCategory[" +
"id=" + id +
", name=" + name + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessCollectionEntry.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.node.Node;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* Record class representing a business collection entry.
*/
@ProtobufMessage
public final class BusinessCollectionEntry {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final List<BusinessCatalogEntry> products;
BusinessCollectionEntry(String id, String name, List<BusinessCatalogEntry> products) {
this.id = id;
this.name = name;
this.products = Objects.requireNonNullElse(products, List.of());
}
public static BusinessCollectionEntry of(Node node) {
var id = node.getChild("id")
.flatMap(Node::toContentString)
.orElseThrow(() -> new NoSuchElementException("Missing id from business collections"));
var name = node.getChild("name")
.flatMap(Node::toContentString)
.orElseThrow(() -> new NoSuchElementException("Missing name from business collections"));
var products = node.streamChildren("product")
.map(BusinessCatalogEntry::of)
.toList();
return new BusinessCollectionEntry(id, name, products);
}
public String id() {
return id;
}
public String name() {
return name;
}
public List<BusinessCatalogEntry> products() {
return products;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (BusinessCollectionEntry) obj;
return Objects.equals(this.id, that.id) &&
Objects.equals(this.name, that.name) &&
Objects.equals(this.products, that.products);
}
@Override
public int hashCode() {
return Objects.hash(id, name, products);
}
@Override
public String toString() {
return "BusinessCollectionEntry[" +
"id=" + id + ", " +
"name=" + name + ", " +
"products=" + products + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessHours.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* A business hours representation that contains the business' time zone and a list of business hour
* patches.
*/
@ProtobufMessage
public final class BusinessHours {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String timeZone;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final List<BusinessHoursEntry> entries;
BusinessHours(String timeZone, List<BusinessHoursEntry> entries) {
this.timeZone = Objects.requireNonNull(timeZone, "timeZone cannot be null");
this.entries = Objects.requireNonNullElse(entries, List.of());
}
public String timeZone() {
return timeZone;
}
public List<BusinessHoursEntry> entries() {
return entries;
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessHours that
&& Objects.equals(timeZone, that.timeZone)
&& Objects.equals(entries, that.entries);
}
@Override
public int hashCode() {
return Objects.hash(timeZone, entries);
}
@Override
public String toString() {
return "BusinessHours[" +
"timeZone=" + timeZone + ", " +
"patches=" + entries + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessHoursEntry.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.node.Node;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A business hours entry that represents the hours of operation for a single day of the week.
*/
@ProtobufMessage
public final class BusinessHoursEntry {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String day;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String mode;
@ProtobufProperty(index = 3, type = ProtobufType.INT64)
final long openTime;
@ProtobufProperty(index = 4, type = ProtobufType.INT64)
final long closeTime;
BusinessHoursEntry(String day, String mode, long openTime, long closeTime) {
this.day = day;
this.mode = mode;
this.openTime = openTime;
this.closeTime = closeTime;
}
public static BusinessHoursEntry of(Node node) {
return new BusinessHoursEntry(
node.getRequiredAttributeAsString("day_of_week"),
node.getRequiredAttributeAsString("mode"),
node.getAttributeAsLong("open_time", 0),
node.getAttributeAsLong("close_time", 0)
);
}
public String day() {
return day;
}
public String mode() {
return mode;
}
public long openTime() {
return openTime;
}
public long closeTime() {
return closeTime;
}
public boolean alwaysOpen() {
return openTime == 0 && closeTime == 0;
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessHoursEntry that
&& openTime == that.openTime
&& closeTime == that.closeTime
&& Objects.equals(day, that.day)
&& Objects.equals(mode, that.mode);
}
@Override
public int hashCode() {
return Objects.hash(day, mode, openTime, closeTime);
}
@Override
public String toString() {
return "BusinessHoursEntry[" +
"day=" + day + ", " +
"mode=" + mode + ", " +
"openTime=" + openTime + ", " +
"closeTime=" + closeTime + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessItemAvailability.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufEnum;
/**
* An enumeration of possible Availabilities.
*/
@ProtobufEnum
public enum BusinessItemAvailability {
/**
* Indicates an unknown availability.
*/
UNKNOWN,
/**
* Indicates that the item is in stock.
*/
IN_STOCK,
/**
* Indicates that the item is out of stock.
*/
OUT_OF_STOCK
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessLocalizedName.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a time a localizable name
*/
@ProtobufMessage(name = "LocalizedName")
public final class BusinessLocalizedName {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String lg;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String lc;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String name;
BusinessLocalizedName(String lg, String lc, String name) {
this.lg = lg;
this.lc = lc;
this.name = name;
}
public Optional<String> lg() {
return Optional.ofNullable(lg);
}
public Optional<String> lc() {
return Optional.ofNullable(lc);
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessLocalizedName that
&& Objects.equals(lg, that.lg)
&& Objects.equals(lc, that.lc)
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(lg, lc, name);
}
@Override
public String toString() {
return "BusinessLocalizedName[" +
"lg=" + lg +
", lc=" + lc +
", name=" + name + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessPrivacyStatus.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* The constants of this enumerated type describe the various types of business privacy
*/
@ProtobufEnum(name = "WebMessageInfo.BizPrivacyStatus")
public enum BusinessPrivacyStatus {
/**
* End-to-end encryption
*/
E2EE(0),
/**
* Bsp encryption
*/
BSP(1),
/**
* Facebook encryption
*/
FACEBOOK(2),
/**
* Facebook and bsp encryption
*/
BSP_AND_FB(3);
final int index;
BusinessPrivacyStatus(@ProtobufEnumIndex int index) {
this.index = index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessProfile.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.node.Node;
import com.github.auties00.cobalt.model.jid.Jid;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* This model class represents the metadata of a business profile
*/
@ProtobufMessage
public final class BusinessProfile {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String address;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final String email;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final BusinessHours hours;
@ProtobufProperty(index = 6, type = ProtobufType.BOOL)
final boolean cartEnabled;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final List<URI> websites;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
final List<BusinessCategory> categories;
BusinessProfile(Jid jid, String description, String address, String email, BusinessHours hours, boolean cartEnabled, List<URI> websites, List<BusinessCategory> categories) {
this.jid = Objects.requireNonNull(jid, "value cannot be null");
this.description = description;
this.address = address;
this.email = email;
this.hours = hours;
this.cartEnabled = cartEnabled;
this.websites = Objects.requireNonNullElse(websites, List.of());
this.categories = Objects.requireNonNullElse(categories, List.of());
}
public static BusinessProfile of(Node node) {
var jid = node.getRequiredAttributeAsJid("value");
var address = node.getChild("address")
.flatMap(Node::toContentString)
.orElse(null);
var description = node.getChild("description")
.flatMap(Node::toContentString)
.orElse(null);
var websites = node.streamChildren("website")
.flatMap(Node::streamContentString)
.map(URI::create)
.toList();
var email = node.getChild("email")
.flatMap(Node::toContentString)
.orElse(null);
var categories = node.streamChildren("categories")
.flatMap(entry -> entry.streamChild("category"))
.map(BusinessCategory::of)
.toList();
var cartEnabled = node.getChild("profile_options")
.flatMap(entry -> entry.getChild("cart_enabled"))
.flatMap(Node::toContentBool)
.orElse(!node.hasChild("profile_options"));
var hours = node.getChild("business_hours")
.flatMap(attributes -> attributes.getAttributeAsString("timezone"))
.map(timezone -> getBusinessHours(node, timezone))
.orElse(null);
return new BusinessProfile(jid, description, address, email, hours, cartEnabled, websites, categories);
}
private static BusinessHours getBusinessHours(Node node, String timezone) {
var entries = node.streamChild("business_hours")
.flatMap(entry -> entry.streamChildren("business_hours_config"))
.map(BusinessHoursEntry::of)
.toList();
return new BusinessHours(timezone, entries);
}
public Jid jid() {
return jid;
}
public Optional<String> description() {
return Optional.ofNullable(description);
}
public Optional<String> address() {
return Optional.ofNullable(address);
}
public Optional<String> email() {
return Optional.ofNullable(email);
}
public Optional<BusinessHours> hours() {
return Optional.ofNullable(hours);
}
public boolean cartEnabled() {
return cartEnabled;
}
public List<URI> websites() {
return websites;
}
public List<BusinessCategory> categories() {
return categories;
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessProfile that
&& cartEnabled == that.cartEnabled
&& Objects.equals(jid, that.jid)
&& Objects.equals(description, that.description)
&& Objects.equals(address, that.address)
&& Objects.equals(email, that.email)
&& Objects.equals(hours, that.hours)
&& Objects.equals(websites, that.websites)
&& Objects.equals(categories, that.categories);
}
@Override
public int hashCode() {
return Objects.hash(jid, description, address, email, hours, cartEnabled, websites, categories);
}
@Override
public String toString() {
return "BusinessProfile[" +
"value=" + jid + ", " +
"description=" + description + ", " +
"address=" + address + ", " +
"email=" + email + ", " +
"hours=" + hours + ", " +
"cartEnabled=" + cartEnabled + ", " +
"websites=" + websites + ", " +
"categories=" + categories + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessReviewStatus.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufEnum;
/**
* An enumeration of possible ReviewStatuses.
*/
@ProtobufEnum
public enum BusinessReviewStatus {
/**
* Indicates that no review has been performed.
*/
NO_REVIEW,
/**
* Indicates that the review is pending.
*/
PENDING,
/**
* Indicates that the review was rejected.
*/
REJECTED,
/**
* Indicates that the review was approved.
*/
APPROVED,
/**
* Indicates that the review is outdated.
*/
OUTDATED
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessVerifiedNameCertificate.java
================================================
package com.github.auties00.cobalt.model.business;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a business certificate
*/
@ProtobufMessage(name = "VerifiedNameCertificate")
public final class BusinessVerifiedNameCertificate {
@ProtobufProperty(index = 1, type = ProtobufType.BYTES)
final byte[] encodedDetails;
@ProtobufProperty(index = 2, type = ProtobufType.BYTES)
final byte[] signature;
@ProtobufProperty(index = 3, type = ProtobufType.BYTES)
final byte[] serverSignature;
BusinessVerifiedNameCertificate(byte[] encodedDetails, byte[] signature, byte[] serverSignature) {
this.encodedDetails = encodedDetails;
this.signature = signature;
this.serverSignature = serverSignature;
}
public Optional<byte[]> encodedDetails() {
return Optional.ofNullable(encodedDetails);
}
public Optional<byte[]> signature() {
return Optional.ofNullable(signature);
}
public Optional<byte[]> serverSignature() {
return Optional.ofNullable(serverSignature);
}
public BusinessVerifiedNameDetails details() {
return BusinessVerifiedNameDetailsSpec.decode(encodedDetails);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessVerifiedNameCertificate that
&& Arrays.equals(encodedDetails, that.encodedDetails)
&& Arrays.equals(signature, that.signature)
&& Arrays.equals(serverSignature, that.serverSignature);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(encodedDetails), Arrays.hashCode(signature), Arrays.hashCode(serverSignature));
}
@Override
public String toString() {
return "BusinessVerifiedNameCertificate[" +
"encodedDetails=" + Arrays.toString(encodedDetails) +
", signature=" + Arrays.toString(signature) +
", serverSignature=" + Arrays.toString(serverSignature) +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/business/BusinessVerifiedNameDetails.java
================================================
package com.github.auties00.cobalt.model.business;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a verified name
*/
@ProtobufMessage(name = "VerifiedNameCertificate.Details")
public final class BusinessVerifiedNameDetails {
@ProtobufProperty(index = 1, type = ProtobufType.UINT64)
final long serial;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String issuer;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
final List<BusinessLocalizedName> localizedNames;
@ProtobufProperty(index = 10, type = ProtobufType.UINT64)
final long issueTimeSeconds;
BusinessVerifiedNameDetails(long serial, String issuer, String name, List<BusinessLocalizedName> localizedNames, long issueTimeSeconds) {
this.serial = serial;
this.issuer = issuer;
this.name = name;
this.localizedNames = localizedNames;
this.issueTimeSeconds = issueTimeSeconds;
}
public long serial() {
return serial;
}
public Optional<String> issuer() {
return Optional.ofNullable(issuer);
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
public List<BusinessLocalizedName> localizedNames() {
return Collections.unmodifiableList(localizedNames);
}
public long issueTimeSeconds() {
return issueTimeSeconds;
}
public Optional<ZonedDateTime> issueTime() {
return Clock.parseSeconds(issueTimeSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessVerifiedNameDetails that
&& serial == that.serial
&& Objects.equals(issuer, that.issuer)
&& Objects.equals(name, that.name)
&& Objects.equals(localizedNames, that.localizedNames)
&& issueTimeSeconds == that.issueTimeSeconds;
}
@Override
public int hashCode() {
return Objects.hash(serial, issuer, name, localizedNames, issueTimeSeconds);
}
@Override
public String toString() {
return "BusinessVerifiedNameDetails[" +
"serial=" + serial +
", issuer=" + issuer +
", name=" + name +
", localizedNames=" + localizedNames +
", issueTimeSeconds=" + issueTimeSeconds + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/Button.java
================================================
package com.github.auties00.cobalt.model.button.base;
import com.github.auties00.cobalt.model.button.base.ButtonBody.Type;
import com.github.auties00.cobalt.model.info.NativeFlowInfo;
import com.github.auties00.cobalt.util.SecureBytes;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.HexFormat;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a button
*/
@ProtobufMessage(name = "Message.ButtonsMessage.Button")
public final class Button {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final ButtonText bodyText;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final NativeFlowInfo bodyNativeFlow;
@ProtobufProperty(index = 3, type = ProtobufType.ENUM)
final Type bodyType;
Button(String id, ButtonText bodyText, NativeFlowInfo bodyNativeFlow, Type bodyType) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.bodyText = bodyText;
this.bodyNativeFlow = bodyNativeFlow;
this.bodyType = Objects.requireNonNull(bodyType, "bodyType cannot be null");
}
public String id() {
return id;
}
public Optional<ButtonText> bodyText() {
return Optional.ofNullable(bodyText);
}
public Optional<NativeFlowInfo> bodyNativeFlow() {
return Optional.ofNullable(bodyNativeFlow);
}
public Type bodyType() {
return bodyType;
}
/**
* Constructs a new button
*
* @param body the body of this button
* @return a non-null button
*/
public static Button of(ButtonBody body) {
var id = HexFormat.of().formatHex(SecureBytes.random(6));
return Button.of(id, body);
}
/**
* Constructs a new button
*
* @param id the non-null id of the button
* @param body the body of this button
* @return a non-null button
*/
public static Button of(String id, ButtonBody body) {
var builder = new ButtonBuilder()
.id(id);
switch (body) {
case ButtonText buttonText -> builder.bodyText(buttonText).bodyType(Type.TEXT);
case NativeFlowInfo flowInfo -> builder.bodyNativeFlow(flowInfo).bodyType(Type.NATIVE_FLOW);
case null -> builder.bodyType(Type.UNKNOWN);
}
return builder.build();
}
/**
* Returns the body of this button
*
* @return an optional
*/
public Optional<? extends ButtonBody> body() {
return bodyText != null ? Optional.of(bodyText) : Optional.ofNullable(bodyNativeFlow);
}
@Override
public boolean equals(Object o) {
return o instanceof Button that
&& Objects.equals(id, that.id)
&& Objects.equals(bodyText, that.bodyText)
&& Objects.equals(bodyNativeFlow, that.bodyNativeFlow)
&& Objects.equals(bodyType, that.bodyType);
}
@Override
public int hashCode() {
return Objects.hash(id, bodyText, bodyNativeFlow, bodyType);
}
@Override
public String toString() {
return "Button[" +
"id=" + id + ", " +
"bodyText=" + bodyText + ", " +
"bodyNativeFlow=" + bodyNativeFlow + ", " +
"bodyType=" + bodyType + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonActionLink.java
================================================
package com.github.auties00.cobalt.model.button.base;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* An action link for a button
*/
@ProtobufMessage(name = "ActionLink")
public final class ButtonActionLink {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String url;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String buttonTitle;
ButtonActionLink(String url, String buttonTitle) {
this.url = Objects.requireNonNull(url, "url cannot be null");
this.buttonTitle = Objects.requireNonNull(buttonTitle, "buttonTitle cannot be null");
}
public String url() {
return url;
}
public String buttonTitle() {
return buttonTitle;
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonActionLink that
&& Objects.equals(url, that.url)
&& Objects.equals(buttonTitle, that.buttonTitle);
}
@Override
public int hashCode() {
return Objects.hash(url, buttonTitle);
}
@Override
public String toString() {
return "ButtonActionLink[" +
"url=" + url + ", " +
"buttonTitle=" + buttonTitle + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonBody.java
================================================
package com.github.auties00.cobalt.model.button.base;
import com.github.auties00.cobalt.model.info.NativeFlowInfo;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents the body of a button
*/
public sealed interface ButtonBody permits ButtonText, NativeFlowInfo {
/**
* Returns the type of this body
*
* @return a non-null type
*/
Type bodyType();
@ProtobufEnum(name = "Message.ButtonsMessage.Button.Type")
enum Type {
UNKNOWN(0),
TEXT(1),
NATIVE_FLOW(2);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonOpaqueData.java
================================================
package com.github.auties00.cobalt.model.button.base;
import com.github.auties00.cobalt.model.poll.PollOption;
import com.github.auties00.cobalt.model.poll.PollUpdateEncryptedMetadata;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents data about a button
*/
@ProtobufMessage(name = "MsgOpaqueData")
public final class ButtonOpaqueData {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String body;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String caption;
@ProtobufProperty(index = 5, type = ProtobufType.DOUBLE)
final double longitude;
@ProtobufProperty(index = 7, type = ProtobufType.DOUBLE)
final double latitude;
@ProtobufProperty(index = 8, type = ProtobufType.INT32)
final int paymentAmount1000;
@ProtobufProperty(index = 9, type = ProtobufType.STRING)
final String paymentNote;
@ProtobufProperty(index = 10, type = ProtobufType.STRING)
final String canonicalUrl;
@ProtobufProperty(index = 11, type = ProtobufType.STRING)
final String matchedText;
@ProtobufProperty(index = 12, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 13, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 6, type = ProtobufType.BOOL)
final boolean live;
@ProtobufProperty(index = 14, type = ProtobufType.BYTES)
final byte[] futureProofBuffer;
@ProtobufProperty(index = 15, type = ProtobufType.STRING)
final String clientUrl;
@ProtobufProperty(index = 16, type = ProtobufType.STRING)
final String loc;
@ProtobufProperty(index = 17, type = ProtobufType.STRING)
final String pollName;
@ProtobufProperty(index = 18, type = ProtobufType.MESSAGE)
final List<PollOption> pollOptions;
@ProtobufProperty(index = 20, type = ProtobufType.UINT32)
final int pollSelectableOptionsCount;
@ProtobufProperty(index = 21, type = ProtobufType.BYTES)
final byte[] messageSecret;
@ProtobufProperty(index = 51, type = ProtobufType.STRING)
final String originalSelfAuthor;
@ProtobufProperty(index = 22, type = ProtobufType.INT64)
final long senderTimestampMilliseconds;
@ProtobufProperty(index = 23, type = ProtobufType.STRING)
final String pollUpdateParentKey;
@ProtobufProperty(index = 24, type = ProtobufType.MESSAGE)
final PollUpdateEncryptedMetadata encPollVote;
@ProtobufProperty(index = 25, type = ProtobufType.STRING)
final String encReactionTargetMessageKey;
@ProtobufProperty(index = 26, type = ProtobufType.BYTES)
final byte[] encReactionEncPayload;
@ProtobufProperty(index = 27, type = ProtobufType.BYTES)
final byte[] encReactionEncIv;
ButtonOpaqueData(String body, String caption, double longitude, double latitude, int paymentAmount1000, String paymentNote, String canonicalUrl, String matchedText, String title, String description, boolean live, byte[] futureProofBuffer, String clientUrl, String loc, String pollName, List<PollOption> pollOptions, int pollSelectableOptionsCount, byte[] messageSecret, String originalSelfAuthor, long senderTimestampMilliseconds, String pollUpdateParentKey, PollUpdateEncryptedMetadata encPollVote, String encReactionTargetMessageKey, byte[] encReactionEncPayload, byte[] encReactionEncIv) {
this.body = body;
this.caption = caption;
this.longitude = longitude;
this.latitude = latitude;
this.paymentAmount1000 = paymentAmount1000;
this.paymentNote = paymentNote;
this.canonicalUrl = canonicalUrl;
this.matchedText = matchedText;
this.title = title;
this.description = description;
this.live = live;
this.futureProofBuffer = futureProofBuffer;
this.clientUrl = clientUrl;
this.loc = loc;
this.pollName = pollName;
this.pollOptions = Objects.requireNonNullElse(pollOptions, List.of());
this.pollSelectableOptionsCount = pollSelectableOptionsCount;
this.messageSecret = messageSecret;
this.originalSelfAuthor = originalSelfAuthor;
this.senderTimestampMilliseconds = senderTimestampMilliseconds;
this.pollUpdateParentKey = pollUpdateParentKey;
this.encPollVote = encPollVote;
this.encReactionTargetMessageKey = encReactionTargetMessageKey;
this.encReactionEncPayload = encReactionEncPayload;
this.encReactionEncIv = encReactionEncIv;
}
public Optional<String> body() {
return Optional.ofNullable(body);
}
public Optional<String> caption() {
return Optional.ofNullable(caption);
}
public double longitude() {
return longitude;
}
public double latitude() {
return latitude;
}
public int paymentAmount1000() {
return paymentAmount1000;
}
public Optional<String> paymentNote() {
return Optional.ofNullable(paymentNote);
}
public Optional<String> canonicalUrl() {
return Optional.ofNullable(canonicalUrl);
}
public Optional<String> matchedText() {
return Optional.ofNullable(matchedText);
}
public Optional<String> title() {
return Optional.ofNullable(title);
}
public Optional<String> description() {
return Optional.ofNullable(description);
}
public boolean live() {
return live;
}
public Optional<byte[]> futureProofBuffer() {
return Optional.ofNullable(futureProofBuffer);
}
public Optional<String> clientUrl() {
return Optional.ofNullable(clientUrl);
}
public Optional<String> loc() {
return Optional.ofNullable(loc);
}
public Optional<String> pollName() {
return Optional.ofNullable(pollName);
}
public List<PollOption> pollOptions() {
return pollOptions;
}
public int pollSelectableOptionsCount() {
return pollSelectableOptionsCount;
}
public Optional<byte[]> messageSecret() {
return Optional.ofNullable(messageSecret);
}
public Optional<String> originalSelfAuthor() {
return Optional.ofNullable(originalSelfAuthor);
}
public long senderTimestampMilliseconds() {
return senderTimestampMilliseconds;
}
public Optional<ZonedDateTime> senderTimestamp() {
return Clock.parseMilliseconds(senderTimestampMilliseconds);
}
public Optional<String> pollUpdateParentKey() {
return Optional.ofNullable(pollUpdateParentKey);
}
public Optional<PollUpdateEncryptedMetadata> encPollVote() {
return Optional.ofNullable(encPollVote);
}
public Optional<String> encReactionTargetMessageKey() {
return Optional.ofNullable(encReactionTargetMessageKey);
}
public Optional<byte[]> encReactionEncPayload() {
return Optional.ofNullable(encReactionEncPayload);
}
public Optional<byte[]> encReactionEncIv() {
return Optional.ofNullable(encReactionEncIv);
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonOpaqueData that
&& longitude == that.longitude
&& latitude == that.latitude
&& paymentAmount1000 == that.paymentAmount1000
&& live == that.live
&& pollSelectableOptionsCount == that.pollSelectableOptionsCount
&& senderTimestampMilliseconds == that.senderTimestampMilliseconds
&& Objects.equals(body, that.body)
&& Objects.equals(caption, that.caption)
&& Objects.equals(paymentNote, that.paymentNote)
&& Objects.equals(canonicalUrl, that.canonicalUrl)
&& Objects.equals(matchedText, that.matchedText)
&& Objects.equals(title, that.title)
&& Objects.equals(description, that.description)
&& Arrays.equals(futureProofBuffer, that.futureProofBuffer)
&& Objects.equals(clientUrl, that.clientUrl)
&& Objects.equals(loc, that.loc)
&& Objects.equals(pollName, that.pollName)
&& Objects.equals(pollOptions, that.pollOptions)
&& Arrays.equals(messageSecret, that.messageSecret)
&& Objects.equals(originalSelfAuthor, that.originalSelfAuthor)
&& Objects.equals(pollUpdateParentKey, that.pollUpdateParentKey)
&& Objects.equals(encPollVote, that.encPollVote)
&& Objects.equals(encReactionTargetMessageKey, that.encReactionTargetMessageKey)
&& Arrays.equals(encReactionEncPayload, that.encReactionEncPayload)
&& Arrays.equals(encReactionEncIv, that.encReactionEncIv);
}
@Override
public int hashCode() {
return Objects.hash(body, caption, longitude, latitude, paymentAmount1000, paymentNote,
canonicalUrl, matchedText, title, description, live, Arrays.hashCode(futureProofBuffer),
clientUrl, loc, pollName, pollOptions, pollSelectableOptionsCount,
Arrays.hashCode(messageSecret), originalSelfAuthor, senderTimestampMilliseconds,
pollUpdateParentKey, encPollVote, encReactionTargetMessageKey,
Arrays.hashCode(encReactionEncPayload), Arrays.hashCode(encReactionEncIv));
}
@Override
public String toString() {
return "ButtonOpaqueData[" +
"body=" + body +
", caption=" + caption +
", longitude=" + longitude +
", latitude=" + latitude +
", paymentAmount1000=" + paymentAmount1000 +
", paymentNote=" + paymentNote +
", canonicalUrl=" + canonicalUrl +
", matchedText=" + matchedText +
", title=" + title +
", description=" + description +
", isLive=" + live +
", futureProofBuffer=" + Arrays.toString(futureProofBuffer) +
", clientUrl=" + clientUrl +
", loc=" + loc +
", pollName=" + pollName +
", pollOptions=" + pollOptions +
", pollSelectableOptionsCount=" + pollSelectableOptionsCount +
", messageSecret=" + Arrays.toString(messageSecret) +
", originalSelfAuthor=" + originalSelfAuthor +
", senderTimestampMs=" + senderTimestampMilliseconds +
", pollUpdateParentKey=" + pollUpdateParentKey +
", encPollVote=" + encPollVote +
", encReactionTargetMessageKey=" + encReactionTargetMessageKey +
", encReactionEncPayload=" + Arrays.toString(encReactionEncPayload) +
", encReactionEncIv=" + Arrays.toString(encReactionEncIv) +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonRow.java
================================================
package com.github.auties00.cobalt.model.button.base;
import com.github.auties00.cobalt.util.SecureBytes;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.HexFormat;
import java.util.Objects;
/**
* A model class that represents a row of buttons
*/
@ProtobufMessage(name = "Message.ListMessage.Row")
public final class ButtonRow {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String id;
ButtonRow(String title, String description, String id) {
this.title = Objects.requireNonNull(title, "title cannot be null");
this.description = Objects.requireNonNull(description, "description cannot be null");
this.id = Objects.requireNonNull(id, "id cannot be null");
}
public static ButtonRow of(String title, String description) {
return new ButtonRow(title, description, HexFormat.of().formatHex(SecureBytes.random(5)));
}
public String title() {
return title;
}
public String description() {
return description;
}
public String id() {
return id;
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonRow that
&& Objects.equals(title, that.title)
&& Objects.equals(description, that.description)
&& Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(title, description, id);
}
@Override
public String toString() {
return "ButtonRow[" +
"title=" + title + ", " +
"description=" + description + ", " +
"id=" + id + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonRowOpaqueData.java
================================================
package com.github.auties00.cobalt.model.button.base;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents data about a row
*/
@ProtobufMessage(name = "MsgRowOpaqueData")
public final class ButtonRowOpaqueData {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final ButtonOpaqueData currentMessage;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final ButtonOpaqueData quotedMessage;
ButtonRowOpaqueData(ButtonOpaqueData currentMessage, ButtonOpaqueData quotedMessage) {
this.currentMessage = currentMessage;
this.quotedMessage = quotedMessage;
}
public Optional<ButtonOpaqueData> currentMessage() {
return Optional.ofNullable(currentMessage);
}
public Optional<ButtonOpaqueData> quotedMessage() {
return Optional.ofNullable(quotedMessage);
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonRowOpaqueData that
&& Objects.equals(currentMessage, that.currentMessage)
&& Objects.equals(quotedMessage, that.quotedMessage);
}
@Override
public int hashCode() {
return Objects.hash(currentMessage, quotedMessage);
}
@Override
public String toString() {
return "ButtonRowOpaqueData[" +
"currentMessage=" + currentMessage + ", " +
"quotedMessage=" + quotedMessage + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonSection.java
================================================
package com.github.auties00.cobalt.model.button.base;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a section of buttons
*/
@ProtobufMessage(name = "Message.ListMessage.Section")
public final class ButtonSection {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final List<ButtonRow> rows;
ButtonSection(String title, List<ButtonRow> rows) {
this.title = title;
this.rows = Objects.requireNonNullElse(rows, List.of());
}
public Optional<String> title() {
return Optional.ofNullable(title);
}
public List<ButtonRow> rows() {
return rows;
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonSection that
&& Objects.equals(title, that.title)
&& Objects.equals(rows, that.rows);
}
@Override
public int hashCode() {
return Objects.hash(title, rows);
}
@Override
public String toString() {
return "ButtonSection[" +
"title=" + title + ", " +
"rows=" + rows + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/ButtonText.java
================================================
package com.github.auties00.cobalt.model.button.base;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the text of a button
*/
@ProtobufMessage
public final class ButtonText implements ButtonBody {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String content;
ButtonText(String content) {
this.content = Objects.requireNonNull(content, "content cannot be null");
}
public String content() {
return content;
}
@Override
public Type bodyType() {
return Type.TEXT;
}
@Override
public boolean equals(Object o) {
return o instanceof ButtonText that
&& Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
@Override
public String toString() {
return "ButtonText[" +
"content=" + content + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/SingleSelectReplyButton.java
================================================
package com.github.auties00.cobalt.model.button.base;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the selection of a row
*/
@ProtobufMessage(name = "Message.ListResponseMessage.SingleSelectReply")
public final class SingleSelectReplyButton {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String rowId;
SingleSelectReplyButton(String rowId) {
this.rowId = Objects.requireNonNull(rowId, "rowId cannot be null");
}
public String rowId() {
return rowId;
}
@Override
public boolean equals(Object o) {
return o instanceof SingleSelectReplyButton that
&& Objects.equals(rowId, that.rowId);
}
@Override
public int hashCode() {
return Objects.hash(rowId);
}
@Override
public String toString() {
return "SingleSelectReplyButton[" +
"rowId=" + rowId + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/base/TemplateFormatter.java
================================================
package com.github.auties00.cobalt.model.button.base;
import com.github.auties00.cobalt.model.button.template.highlyStructured.HighlyStructuredFourRowTemplate;
import com.github.auties00.cobalt.model.button.template.hydrated.HydratedFourRowTemplate;
import com.github.auties00.cobalt.model.message.button.InteractiveMessage;
import com.github.auties00.cobalt.model.message.button.TemplateMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A formatter used to structure a button message
*/
public sealed interface TemplateFormatter permits HighlyStructuredFourRowTemplate, HydratedFourRowTemplate, InteractiveMessage {
/**
* Returns the type of this formatter
*
* @return a non-null type
*/
Type templateType();
/**
* The constant of this enumerated type define the various of types of visual formats for a
* {@link TemplateMessage}
*/
@ProtobufEnum
enum Type {
/**
* No format
*/
NONE(0),
/**
* Four row template
*/
FOUR_ROW(1),
/**
* Hydrated four row template
*/
HYDRATED_FOUR_ROW(2),
/**
* Interactive message
*/
INTERACTIVE(3);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
public int index() {
return this.index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveBody.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the body of a product
*/
@ProtobufMessage(name = "Message.InteractiveMessage.Body")
public final class InteractiveBody {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String content;
InteractiveBody(String content) {
this.content = Objects.requireNonNull(content, "content cannot be null");
}
public String content() {
return content;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveBody that
&& Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
@Override
public String toString() {
return "InteractiveBody[" +
"content=" + content + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveButton.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a native flow button
*/
@ProtobufMessage(name = "Message.InteractiveMessage.NativeFlowMessage.NativeFlowButton")
public final class InteractiveButton {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String parameters;
InteractiveButton(String name, String parameters) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.parameters = parameters;
}
public String name() {
return name;
}
public Optional<String> parameters() {
return Optional.ofNullable(parameters);
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveButton that
&& Objects.equals(name, that.name)
&& Objects.equals(parameters, that.parameters);
}
@Override
public int hashCode() {
return Objects.hash(name, parameters);
}
@Override
public String toString() {
return "InteractiveButton[" +
"name=" + name + ", " +
"parameters=" + parameters + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveCollection.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.button.InteractiveMessageContent;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a business collection
*/
@ProtobufMessage(name = "Message.InteractiveMessage.CollectionMessage")
public final class InteractiveCollection implements InteractiveMessageContent {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid business;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 3, type = ProtobufType.INT32)
final int version;
InteractiveCollection(Jid business, String id, int version) {
this.business = Objects.requireNonNull(business, "business cannot be null");
this.id = Objects.requireNonNull(id, "id cannot be null");
this.version = version;
}
public Jid business() {
return business;
}
public String id() {
return id;
}
public int version() {
return version;
}
@Override
public Type contentType() {
return Type.COLLECTION;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveCollection that
&& Objects.equals(business, that.business)
&& Objects.equals(id, that.id)
&& version == that.version;
}
@Override
public int hashCode() {
return Objects.hash(business, id, version);
}
@Override
public String toString() {
return "InteractiveCollection[" +
"business=" + business + ", " +
"id=" + id + ", " +
"version=" + version + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveFooter.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the footer of a product
*/
@ProtobufMessage(name = "Message.InteractiveMessage.Footer")
public final class InteractiveFooter {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String content;
InteractiveFooter(String content) {
this.content = Objects.requireNonNull(content, "content cannot be null");
}
public String content() {
return content;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveFooter that
&& Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
@Override
public String toString() {
return "InteractiveFooter[" +
"content=" + content + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveHeader.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents the header of a product
*/
@ProtobufMessage(name = "Message.InteractiveMessage.Header")
public final class InteractiveHeader {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String subtitle;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final DocumentMessage attachmentDocument;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final ImageMessage attachmentImage;
@ProtobufProperty(index = 5, type = ProtobufType.BOOL)
final boolean mediaAttachment;
@ProtobufProperty(index = 6, type = ProtobufType.BYTES)
final InteractiveHeaderThumbnail attachmentThumbnail;
@ProtobufProperty(index = 7, type = ProtobufType.MESSAGE)
final VideoOrGifMessage attachmentVideo;
InteractiveHeader(String title, String subtitle, DocumentMessage attachmentDocument,
ImageMessage attachmentImage, boolean mediaAttachment,
InteractiveHeaderThumbnail attachmentThumbnail, VideoOrGifMessage attachmentVideo) {
this.title = title;
this.subtitle = subtitle;
this.attachmentDocument = attachmentDocument;
this.attachmentImage = attachmentImage;
this.mediaAttachment = mediaAttachment;
this.attachmentThumbnail = attachmentThumbnail;
this.attachmentVideo = attachmentVideo;
}
@ProtobufBuilder(className = "InteractiveHeaderSimpleBuilder")
static InteractiveHeader simpleBuilder(String title, String subtitle, InteractiveHeaderAttachment attachment) {
var builder = new InteractiveHeaderBuilder()
.title(title)
.subtitle(subtitle);
switch (attachment) {
case DocumentMessage documentMessage -> builder.attachmentDocument(documentMessage);
case ImageMessage imageMessage -> builder.attachmentImage(imageMessage);
case InteractiveHeaderThumbnail productHeaderThumbnail ->
builder.attachmentThumbnail(productHeaderThumbnail);
case VideoOrGifMessage videoMessage -> builder.attachmentVideo(videoMessage);
case null -> {}
}
builder.mediaAttachment(attachment != null);
return builder.build();
}
public Optional<String> title() {
return Optional.ofNullable(title);
}
public Optional<String> subtitle() {
return Optional.ofNullable(subtitle);
}
public Optional<DocumentMessage> attachmentDocument() {
return Optional.ofNullable(attachmentDocument);
}
public Optional<ImageMessage> attachmentImage() {
return Optional.ofNullable(attachmentImage);
}
public boolean mediaAttachment() {
return mediaAttachment;
}
public Optional<InteractiveHeaderThumbnail> attachmentThumbnail() {
return Optional.ofNullable(attachmentThumbnail);
}
public Optional<VideoOrGifMessage> attachmentVideo() {
return Optional.ofNullable(attachmentVideo);
}
/**
* Returns the type of attachment of this message
*
* @return a non-null attachment type
*/
public InteractiveHeaderAttachment.Type attachmentType() {
return attachment()
.map(InteractiveHeaderAttachment::interactiveHeaderType)
.orElse(InteractiveHeaderAttachment.Type.NONE);
}
/**
* Returns the attachment of this message if present
*
* @return a non-null attachment type
*/
public Optional<? extends InteractiveHeaderAttachment> attachment() {
if (attachmentDocument != null) {
return Optional.of(attachmentDocument);
}
if (attachmentImage != null) {
return Optional.of(attachmentImage);
}
if (attachmentThumbnail != null) {
return Optional.of(attachmentThumbnail);
}
return Optional.ofNullable(attachmentVideo);
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveHeader that
&& Objects.equals(title, that.title)
&& Objects.equals(subtitle, that.subtitle)
&& Objects.equals(attachmentDocument, that.attachmentDocument)
&& Objects.equals(attachmentImage, that.attachmentImage)
&& mediaAttachment == that.mediaAttachment
&& Objects.equals(attachmentThumbnail, that.attachmentThumbnail)
&& Objects.equals(attachmentVideo, that.attachmentVideo);
}
@Override
public int hashCode() {
return Objects.hash(title, subtitle, attachmentDocument, attachmentImage,
mediaAttachment, attachmentThumbnail, attachmentVideo);
}
@Override
public String toString() {
return "InteractiveHeader[" +
"title=" + title +
", subtitle=" + subtitle +
", attachmentDocument=" + attachmentDocument +
", attachmentImage=" + attachmentImage +
", mediaAttachment=" + mediaAttachment +
", attachmentThumbnail=" + attachmentThumbnail +
", attachmentVideo=" + attachmentVideo + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveHeaderAttachment.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A sealed class that describes the various types of headers
*/
public sealed interface InteractiveHeaderAttachment permits DocumentMessage, ImageMessage, InteractiveHeaderThumbnail, VideoOrGifMessage {
Type interactiveHeaderType();
/**
* The constants of this enumerated type describe the various types of attachment that a product
* header can have
*/
@ProtobufEnum
enum Type {
/**
* No attachment
*/
NONE(0),
/**
* Document message
*/
DOCUMENT(3),
/**
* Image attachment
*/
IMAGE(4),
/**
* Jpeg attachment
*/
THUMBNAIL(6),
/**
* Video attachment
*/
VIDEO(7);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveHeaderThumbnail.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
/**
* A model that represents the jpeg thumbnail of a {@link InteractiveHeader}
*
* @param thumbnail the non-null jpeg thumbnail
*/
public record InteractiveHeaderThumbnail(byte[] thumbnail) implements InteractiveHeaderAttachment {
@ProtobufDeserializer(builderBehaviour = ProtobufDeserializer.BuilderBehaviour.DISCARD)
public static InteractiveHeaderThumbnail of(byte[] thumbnail) {
return new InteractiveHeaderThumbnail(thumbnail);
}
@ProtobufSerializer
@Override
public byte[] thumbnail() {
return thumbnail;
}
@Override
public Type interactiveHeaderType() {
return Type.THUMBNAIL;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveLocation.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* This model class describes a Location
*/
@ProtobufMessage(name = "Location")
public final class InteractiveLocation {
@ProtobufProperty(index = 1, type = ProtobufType.DOUBLE)
final double latitude;
@ProtobufProperty(index = 2, type = ProtobufType.DOUBLE)
final double longitude;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String name;
InteractiveLocation(double latitude, double longitude, String name) {
this.latitude = latitude;
this.longitude = longitude;
this.name = Objects.requireNonNull(name, "name cannot be null");
}
public double latitude() {
return latitude;
}
public double longitude() {
return longitude;
}
public String name() {
return name;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveLocation that
&& Double.compare(latitude, that.latitude) == 0
&& Double.compare(longitude, that.longitude) == 0
&& Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(latitude, longitude, name);
}
@Override
public String toString() {
return "InteractiveLocation[" +
"latitude=" + latitude + ", " +
"longitude=" + longitude + ", " +
"name=" + name + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveLocationAnnotation.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* A model class that describes an interactive annotation linked to a message
*/
@ProtobufMessage(name = "InteractiveAnnotation")
public final class InteractiveLocationAnnotation {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final List<InteractivePoint> polygonVertices;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final InteractiveLocation location;
InteractiveLocationAnnotation(List<InteractivePoint> polygonVertices, InteractiveLocation location) {
this.polygonVertices = Objects.requireNonNullElse(polygonVertices, List.of());
this.location = location;
}
public List<InteractivePoint> polygonVertices() {
return polygonVertices;
}
public InteractiveLocation location() {
return location;
}
/**
* Returns the type of sync
*
* @return a non-null Action
*/
public Action type() {
return location != null ? Action.LOCATION : Action.UNKNOWN;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveLocationAnnotation that
&& Objects.equals(polygonVertices, that.polygonVertices)
&& Objects.equals(location, that.location);
}
@Override
public int hashCode() {
return Objects.hash(polygonVertices, location);
}
@Override
public String toString() {
return "InteractiveLocationAnnotation[" +
"polygonVertices=" + polygonVertices + ", " +
"location=" + location + ']';
}
/**
* The constants of this enumerated type describe the various types of sync that an interactive
* annotation can provide
*/
@ProtobufEnum
public enum Action {
/**
* Unknown
*/
UNKNOWN(0),
/**
* Location
*/
LOCATION(2);
final int index;
Action(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveNativeFlow.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import com.github.auties00.cobalt.model.message.button.InteractiveMessageContent;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* A model class that represents a native flow
* <a href="https://docs.360dialog.com/partner/messaging/interactive-messages/beta-receive-whatsapp-payments-via-stripe">Here</a>> is an explanation on how to use this kind of message
*/
@ProtobufMessage(name = "Message.InteractiveMessage.NativeFlowMessage")
public final class InteractiveNativeFlow implements InteractiveMessageContent {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final List<InteractiveButton> buttons;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String parameters;
@ProtobufProperty(index = 3, type = ProtobufType.INT32)
final int version;
InteractiveNativeFlow(List<InteractiveButton> buttons, String parameters, int version) {
this.buttons = Objects.requireNonNullElse(buttons, List.of());
this.parameters = parameters;
this.version = version;
}
public List<InteractiveButton> buttons() {
return buttons;
}
public String parameters() {
return parameters;
}
public int version() {
return version;
}
@Override
public Type contentType() {
return Type.NATIVE_FLOW;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveNativeFlow that
&& Objects.equals(buttons, that.buttons)
&& Objects.equals(parameters, that.parameters)
&& version == that.version;
}
@Override
public int hashCode() {
return Objects.hash(buttons, parameters, version);
}
@Override
public String toString() {
return "InteractiveNativeFlow[" +
"buttons=" + buttons + ", " +
"parameters=" + parameters + ", " +
"version=" + version + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractivePoint.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* This model class describes a Point in space
*/
@ProtobufMessage(name = "Point")
public final class InteractivePoint {
@ProtobufProperty(index = 1, type = ProtobufType.INT32)
final Integer xDeprecated;
@ProtobufProperty(index = 2, type = ProtobufType.INT32)
final Integer yDeprecated;
@ProtobufProperty(index = 3, type = ProtobufType.DOUBLE)
final Double x;
@ProtobufProperty(index = 4, type = ProtobufType.DOUBLE)
final Double y;
InteractivePoint(Integer xDeprecated, Integer yDeprecated, Double x, Double y) {
this.xDeprecated = xDeprecated;
this.yDeprecated = yDeprecated;
this.x = x;
this.y = y;
}
public int xDeprecated() {
return xDeprecated;
}
public int yDeprecated() {
return yDeprecated;
}
public double x() {
if (x != null) {
return x;
} else if (xDeprecated != null) {
return xDeprecated;
} else {
return 0;
}
}
public double y() {
if (y != null) {
return y;
} else if (yDeprecated != null) {
return yDeprecated;
} else {
return 0;
}
}
@Override
public boolean equals(Object o) {
return o instanceof InteractivePoint that
&& Objects.equals(xDeprecated, that.xDeprecated)
&& Objects.equals(yDeprecated, that.yDeprecated)
&& Objects.equals(x, that.x)
&& Objects.equals(y, that.y);
}
@Override
public int hashCode() {
return Objects.hash(xDeprecated, yDeprecated, x, y);
}
@Override
public String toString() {
return "InteractivePoint[" +
"xDeprecated=" + xDeprecated + ", " +
"yDeprecated=" + yDeprecated + ", " +
"x=" + x + ", " +
"y=" + y + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveResponseBody.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the body of a product
*/
@ProtobufMessage(name = "Message.InteractiveResponseMessage.Body")
public final class InteractiveResponseBody {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String content;
InteractiveResponseBody(String content) {
this.content = Objects.requireNonNull(content, "content cannot be null");
}
public String content() {
return content;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveResponseBody that
&& Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
@Override
public String toString() {
return "InteractiveResponseBody[" +
"content=" + content + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/interactive/InteractiveShop.java
================================================
package com.github.auties00.cobalt.model.button.interactive;
import com.github.auties00.cobalt.model.message.button.InteractiveMessageContent;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a shop
*/
@ProtobufMessage(name = "Message.InteractiveMessage.ShopMessage")
public final class InteractiveShop implements InteractiveMessageContent {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.ENUM)
final SurfaceType surfaceType;
@ProtobufProperty(index = 3, type = ProtobufType.INT32)
final int version;
InteractiveShop(String id, SurfaceType surfaceType, int version) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.surfaceType = Objects.requireNonNull(surfaceType, "surfaceType cannot be null");
this.version = version;
}
public String id() {
return id;
}
public SurfaceType surfaceType() {
return surfaceType;
}
public int version() {
return version;
}
@Override
public Type contentType() {
return Type.SHOP;
}
@Override
public boolean equals(Object o) {
return o instanceof InteractiveShop that
&& Objects.equals(id, that.id)
&& Objects.equals(surfaceType, that.surfaceType)
&& version == that.version;
}
@Override
public int hashCode() {
return Objects.hash(id, surfaceType, version);
}
@Override
public String toString() {
return "InteractiveShop[" +
"id=" + id + ", " +
"surfaceType=" + surfaceType + ", " +
"version=" + version + ']';
}
/**
* The constants of this enumerated type describe the various types of surfaces that a
* {@link InteractiveShop} can have
*/
@ProtobufEnum(name = "Message.InteractiveMessage.ShopMessage.Surface")
public enum SurfaceType {
/**
* Unknown
*/
UNKNOWN_SURFACE(0),
/**
* Facebook
*/
FACEBOOK(1),
/**
* Instagram
*/
INSTAGRAM(2),
/**
* Whatsapp
*/
WHATSAPP(3);
final int index;
SurfaceType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredButton.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents all types of hydrated buttons
*/
public sealed interface HighlyStructuredButton permits HighlyStructuredCallButton, HighlyStructuredQuickReplyButton, HighlyStructuredURLButton {
/**
* Returns the text of this button
*
* @return a non-null structure if the protobuf isn't corrupted
*/
HighlyStructuredMessage text();
/**
* Returns the type of this button
*
* @return a non-null type
*/
Type buttonType();
/**
* The constants of this enumerated type describe the various types of buttons that a template can
* wrap
*/
@ProtobufEnum
enum Type {
/**
* No button
*/
NONE(0),
/**
* Quick reply button
*/
QUICK_REPLY(1),
/**
* Url button
*/
URL(2),
/**
* Call button
*/
CALL(3);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredButtonTemplate.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a template for a button
*/
@ProtobufMessage(name = "HydratedTemplateButton")
public final class HighlyStructuredButtonTemplate {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredQuickReplyButton highlyStructuredQuickReplyButton;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredURLButton highlyStructuredUrlButton;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final HighlyStructuredCallButton highlyStructuredCallButton;
@ProtobufProperty(index = 4, type = ProtobufType.UINT32)
final int index;
HighlyStructuredButtonTemplate(HighlyStructuredQuickReplyButton highlyStructuredQuickReplyButton,
HighlyStructuredURLButton highlyStructuredUrlButton,
HighlyStructuredCallButton highlyStructuredCallButton,
int index) {
this.highlyStructuredQuickReplyButton = highlyStructuredQuickReplyButton;
this.highlyStructuredUrlButton = highlyStructuredUrlButton;
this.highlyStructuredCallButton = highlyStructuredCallButton;
this.index = index;
}
/**
* Constructs a new template
*
* @param highlyStructuredButton the button
* @return a non-null button template
*/
public static HighlyStructuredButtonTemplate of(HighlyStructuredButton highlyStructuredButton) {
return of(-1, highlyStructuredButton);
}
/**
* Constructs a new template
*
* @param index the index
* @param highlyStructuredButton the button
* @return a non-null button template
*/
public static HighlyStructuredButtonTemplate of(int index, HighlyStructuredButton highlyStructuredButton) {
var builder = new HighlyStructuredButtonTemplateBuilder()
.index(index);
switch (highlyStructuredButton) {
case HighlyStructuredQuickReplyButton highlyStructuredQuickReplyButton ->
builder.highlyStructuredQuickReplyButton(highlyStructuredQuickReplyButton);
case HighlyStructuredURLButton highlyStructuredURLButton ->
builder.highlyStructuredUrlButton(highlyStructuredURLButton);
case HighlyStructuredCallButton highlyStructuredCallButton ->
builder.highlyStructuredCallButton(highlyStructuredCallButton);
case null -> {
}
}
return builder.build();
}
public Optional<HighlyStructuredQuickReplyButton> highlyStructuredQuickReplyButton() {
return Optional.ofNullable(highlyStructuredQuickReplyButton);
}
public Optional<HighlyStructuredURLButton> highlyStructuredUrlButton() {
return Optional.ofNullable(highlyStructuredUrlButton);
}
public Optional<HighlyStructuredCallButton> highlyStructuredCallButton() {
return Optional.ofNullable(highlyStructuredCallButton);
}
public int index() {
return index;
}
/**
* Returns this button
*
* @return a non-null optional
*/
public Optional<? extends HighlyStructuredButton> button() {
if (highlyStructuredQuickReplyButton != null) {
return Optional.of(highlyStructuredQuickReplyButton);
}
if (highlyStructuredUrlButton != null) {
return Optional.of(highlyStructuredUrlButton);
}
return Optional.ofNullable(highlyStructuredCallButton);
}
/**
* Returns the type of button that this message wraps
*
* @return a non-null button type
*/
public HighlyStructuredButton.Type buttonType() {
return button().map(HighlyStructuredButton::buttonType)
.orElse(HighlyStructuredButton.Type.NONE);
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredButtonTemplate that
&& Objects.equals(highlyStructuredQuickReplyButton, that.highlyStructuredQuickReplyButton)
&& Objects.equals(highlyStructuredUrlButton, that.highlyStructuredUrlButton)
&& Objects.equals(highlyStructuredCallButton, that.highlyStructuredCallButton)
&& index == that.index;
}
@Override
public int hashCode() {
return Objects.hash(highlyStructuredQuickReplyButton, highlyStructuredUrlButton, highlyStructuredCallButton, index);
}
@Override
public String toString() {
return "HighlyStructuredButtonTemplate[" +
"highlyStructuredQuickReplyButton=" + highlyStructuredQuickReplyButton + ", " +
"highlyStructuredUrlButton=" + highlyStructuredUrlButton + ", " +
"highlyStructuredCallButton=" + highlyStructuredCallButton + ", " +
"index=" + index + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredCallButton.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a button that can start a phone call
*/
@ProtobufMessage(name = "TemplateButton.CallButton")
public final class HighlyStructuredCallButton implements HighlyStructuredButton {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage text;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage phoneNumber;
HighlyStructuredCallButton(HighlyStructuredMessage text, HighlyStructuredMessage phoneNumber) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.phoneNumber = Objects.requireNonNull(phoneNumber, "phoneNumber cannot be null");
}
public HighlyStructuredMessage text() {
return text;
}
public HighlyStructuredMessage phoneNumber() {
return phoneNumber;
}
@Override
public Type buttonType() {
return Type.CALL;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredCallButton that
&& Objects.equals(text, that.text)
&& Objects.equals(phoneNumber, that.phoneNumber);
}
@Override
public int hashCode() {
return Objects.hash(text, phoneNumber);
}
@Override
public String toString() {
return "HighlyStructuredCallButton[" +
"text=" + text + ", " +
"phoneNumber=" + phoneNumber + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredCurrency.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a currency
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMCurrency")
public final class HighlyStructuredCurrency implements HighlyStructuredLocalizableParameterValue {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String currencyCode;
@ProtobufProperty(index = 2, type = ProtobufType.INT64)
final long amount1000;
HighlyStructuredCurrency(String currencyCode, long amount1000) {
this.currencyCode = Objects.requireNonNull(currencyCode, "currencyCode cannot be null");
this.amount1000 = amount1000;
}
public String currencyCode() {
return currencyCode;
}
public long amount1000() {
return amount1000;
}
@Override
public Type parameterType() {
return Type.CURRENCY;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredCurrency that
&& Objects.equals(currencyCode, that.currencyCode)
&& amount1000 == that.amount1000;
}
@Override
public int hashCode() {
return Objects.hash(currencyCode, amount1000);
}
@Override
public String toString() {
return "HighlyStructuredCurrency[" +
"currencyCode=" + currencyCode + ", " +
"amount1000=" + amount1000 + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredDateTime.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a time
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime")
public final class HighlyStructuredDateTime implements HighlyStructuredLocalizableParameterValue {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredDateTimeComponent dateComponent;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredDateTimeUnixEpoch dateUnixEpoch;
HighlyStructuredDateTime(HighlyStructuredDateTimeComponent dateComponent, HighlyStructuredDateTimeUnixEpoch dateUnixEpoch) {
this.dateComponent = dateComponent;
this.dateUnixEpoch = dateUnixEpoch;
}
/**
* Constructs a new date time using a component
*
* @param dateComponent the non-null component
* @return a non-null date time
*/
public static HighlyStructuredDateTime of(HighlyStructuredDateTimeValue dateComponent) {
return switch (dateComponent) {
case HighlyStructuredDateTimeComponent highlyStructuredDateTimeComponent ->
new HighlyStructuredDateTime(highlyStructuredDateTimeComponent, null);
case HighlyStructuredDateTimeUnixEpoch highlyStructuredDateTimeUnixEpoch ->
new HighlyStructuredDateTime(null, highlyStructuredDateTimeUnixEpoch);
case null -> new HighlyStructuredDateTime(null, null);
};
}
/**
* Returns the type of date of this component
*
* @return a non-null date type
*/
public HighlyStructuredDateTimeValue.Type dateType() {
return date().map(HighlyStructuredDateTimeValue::dateType)
.orElse(HighlyStructuredDateTimeValue.Type.NONE);
}
/**
* Returns the date of this component
*
* @return a non-null date type
*/
public Optional<? extends HighlyStructuredDateTimeValue> date() {
return Optional.ofNullable(dateComponent != null ? dateComponent : dateUnixEpoch);
}
public Optional<HighlyStructuredDateTimeComponent> dateComponent() {
return Optional.ofNullable(dateComponent);
}
public Optional<HighlyStructuredDateTimeUnixEpoch> dateUnixEpoch() {
return Optional.ofNullable(dateUnixEpoch);
}
@Override
public Type parameterType() {
return Type.DATE_TIME;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredDateTime that
&& Objects.equals(dateComponent, that.dateComponent)
&& Objects.equals(dateUnixEpoch, that.dateUnixEpoch);
}
@Override
public int hashCode() {
return Objects.hash(dateComponent, dateUnixEpoch);
}
@Override
public String toString() {
return "HighlyStructuredDateTime[" +
"dateComponent=" + dateComponent + ", " +
"dateUnixEpoch=" + dateUnixEpoch + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredDateTimeComponent.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a time component
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent")
public final class HighlyStructuredDateTimeComponent implements HighlyStructuredDateTimeValue {
@ProtobufProperty(index = 1, type = ProtobufType.ENUM)
final DayOfWeek dayOfWeek;
@ProtobufProperty(index = 2, type = ProtobufType.UINT32)
final int year;
@ProtobufProperty(index = 3, type = ProtobufType.UINT32)
final int month;
@ProtobufProperty(index = 4, type = ProtobufType.UINT32)
final int dayOfMonth;
@ProtobufProperty(index = 5, type = ProtobufType.UINT32)
final int hour;
@ProtobufProperty(index = 6, type = ProtobufType.UINT32)
final int minute;
@ProtobufProperty(index = 7, type = ProtobufType.ENUM)
final CalendarType calendar;
HighlyStructuredDateTimeComponent(DayOfWeek dayOfWeek, int year, int month, int dayOfMonth, int hour, int minute, CalendarType calendar) {
this.dayOfWeek = Objects.requireNonNull(dayOfWeek, "dayOfWeek cannot be null");
this.year = year;
this.month = month;
this.dayOfMonth = dayOfMonth;
this.hour = hour;
this.minute = minute;
this.calendar = Objects.requireNonNull(calendar, "calendar cannot be null");
}
public DayOfWeek dayOfWeek() {
return dayOfWeek;
}
public int year() {
return year;
}
public int month() {
return month;
}
public int dayOfMonth() {
return dayOfMonth;
}
public int hour() {
return hour;
}
public int minute() {
return minute;
}
public CalendarType calendar() {
return calendar;
}
@Override
public Type dateType() {
return Type.COMPONENT;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredDateTimeComponent that
&& Objects.equals(dayOfWeek, that.dayOfWeek)
&& year == that.year
&& month == that.month
&& dayOfMonth == that.dayOfMonth
&& hour == that.hour
&& minute == that.minute
&& Objects.equals(calendar, that.calendar);
}
@Override
public int hashCode() {
return Objects.hash(dayOfWeek, year, month, dayOfMonth, hour, minute, calendar);
}
@Override
public String toString() {
return "HighlyStructuredDateTimeComponent[" +
"dayOfWeek=" + dayOfWeek + ", " +
"year=" + year + ", " +
"month=" + month + ", " +
"dayOfMonth=" + dayOfMonth + ", " +
"hour=" + hour + ", " +
"minute=" + minute + ", " +
"calendar=" + calendar + ']';
}
/**
* The constants of this enumerated type describe the supported calendar types
*/
@ProtobufEnum(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent.CalendarType")
public enum CalendarType {
/**
* Gregorian calendar
*/
GREGORIAN(1),
/**
* Solar calendar
*/
SOLAR_HIJRI(2);
final int index;
CalendarType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
/**
* The constants of this enumerated type describe the days of the week
*/
@ProtobufEnum(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeComponent.DayOfWeekType")
public enum DayOfWeek {
/**
* Monday
*/
MONDAY(1),
/**
* Tuesday
*/
TUESDAY(2),
/**
* Wednesday
*/
WEDNESDAY(3),
/**
* Thursday
*/
THURSDAY(4),
/**
* Friday
*/
FRIDAY(5),
/**
* Saturday
*/
SATURDAY(6),
/**
* Sunday
*/
SUNDAY(7);
final int index;
DayOfWeek(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredDateTimeUnixEpoch.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a time as a unix epoch
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter.HSMDateTime.HSMDateTimeUnixEpoch")
public final class HighlyStructuredDateTimeUnixEpoch implements HighlyStructuredDateTimeValue {
@ProtobufProperty(index = 1, type = ProtobufType.INT64)
final long timestampSeconds;
HighlyStructuredDateTimeUnixEpoch(long timestampSeconds) {
this.timestampSeconds = timestampSeconds;
}
/**
* Returns the timestampSeconds as a zoned date time
*
* @return an optional
*/
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
public long timestampSeconds() {
return timestampSeconds;
}
@Override
public Type dateType() {
return Type.UNIX_EPOCH;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredDateTimeUnixEpoch that
&& timestampSeconds == that.timestampSeconds;
}
@Override
public int hashCode() {
return Objects.hash(timestampSeconds);
}
@Override
public String toString() {
return "HighlyStructuredDateTimeUnixEpoch[" +
"timestampSeconds=" + timestampSeconds + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredDateTimeValue.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model class that represents the value of a localizable parameter
*/
public sealed interface HighlyStructuredDateTimeValue permits HighlyStructuredDateTimeComponent, HighlyStructuredDateTimeUnixEpoch {
/**
* Returns the type of date
*
* @return a non-null type
*/
Type dateType();
/**
* The constants of this enumerated type describe the various type of date types that a date time can wrap
*/
@ProtobufEnum
enum Type {
/**
* No date
*/
NONE(0),
/**
* Component date
*/
COMPONENT(1),
/**
* Unix epoch date
*/
UNIX_EPOCH(2);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredFourRowTemplate.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import com.github.auties00.cobalt.model.button.base.TemplateFormatter;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a four row template
*/
@ProtobufMessage(name = "Message.TemplateMessage.FourRowTemplate")
public final class HighlyStructuredFourRowTemplate implements TemplateFormatter {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final DocumentMessage titleDocument;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage titleHighlyStructured;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final ImageMessage titleImage;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final VideoOrGifMessage titleVideo;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final LocationMessage titleLocation;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage content;
@ProtobufProperty(index = 7, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage footer;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
final List<HighlyStructuredButtonTemplate> buttons;
HighlyStructuredFourRowTemplate(DocumentMessage titleDocument, HighlyStructuredMessage titleHighlyStructured,
ImageMessage titleImage, VideoOrGifMessage titleVideo, LocationMessage titleLocation,
HighlyStructuredMessage content, HighlyStructuredMessage footer,
List<HighlyStructuredButtonTemplate> buttons) {
this.titleDocument = titleDocument;
this.titleHighlyStructured = titleHighlyStructured;
this.titleImage = titleImage;
this.titleVideo = titleVideo;
this.titleLocation = titleLocation;
this.content = Objects.requireNonNull(content, "content cannot be null");
this.footer = footer;
this.buttons = Objects.requireNonNullElse(buttons, List.of());
}
@ProtobufBuilder(className = "HighlyStructuredFourRowTemplateSimpleBuilder")
static HighlyStructuredFourRowTemplate simpleBuilder(HighlyStructuredFourRowTemplateTitle title, HighlyStructuredMessage content, HighlyStructuredMessage footer, List<HighlyStructuredButtonTemplate> buttons) {
var builder = new HighlyStructuredFourRowTemplateBuilder()
.buttons(getIndexedButtons(buttons))
.content(content)
.footer(footer);
switch (title) {
case DocumentMessage documentMessage -> builder.titleDocument(documentMessage);
case HighlyStructuredMessage highlyStructuredMessage ->
builder.titleHighlyStructured(highlyStructuredMessage);
case ImageMessage imageMessage -> builder.titleImage(imageMessage);
case VideoOrGifMessage videoMessage -> builder.titleVideo(videoMessage);
case LocationMessage locationMessage -> builder.titleLocation(locationMessage);
case null -> {}
}
return builder.build();
}
private static List<HighlyStructuredButtonTemplate> getIndexedButtons(List<HighlyStructuredButtonTemplate> buttons) {
var list = new ArrayList<HighlyStructuredButtonTemplate>(buttons.size());
for (var index = 0; index < buttons.size(); index++) {
var button = buttons.get(index);
var highlyStructuredQuickReplyButton = button.highlyStructuredQuickReplyButton()
.orElse(null);
var highlyStructuredUrlButton = button.highlyStructuredUrlButton()
.orElse(null);
var highlyStructuredCallButton = button.highlyStructuredCallButton()
.orElse(null);
var apply = new HighlyStructuredButtonTemplate(highlyStructuredQuickReplyButton, highlyStructuredUrlButton, highlyStructuredCallButton, index + 1);
list.set(index, apply);
}
return list;
}
public Optional<DocumentMessage> titleDocument() {
return Optional.ofNullable(titleDocument);
}
public Optional<HighlyStructuredMessage> titleHighlyStructured() {
return Optional.ofNullable(titleHighlyStructured);
}
public Optional<ImageMessage> titleImage() {
return Optional.ofNullable(titleImage);
}
public Optional<VideoOrGifMessage> titleVideo() {
return Optional.ofNullable(titleVideo);
}
public Optional<LocationMessage> titleLocation() {
return Optional.ofNullable(titleLocation);
}
public HighlyStructuredMessage content() {
return content;
}
public Optional<HighlyStructuredMessage> footer() {
return Optional.ofNullable(footer);
}
public List<HighlyStructuredButtonTemplate> buttons() {
return buttons;
}
public HighlyStructuredFourRowTemplateTitle.Type titleType() {
return title().map(HighlyStructuredFourRowTemplateTitle::titleType)
.orElse(HighlyStructuredFourRowTemplateTitle.Type.NONE);
}
public Optional<? extends HighlyStructuredFourRowTemplateTitle> title() {
if (titleDocument != null) {
return Optional.of(titleDocument);
}
if (titleHighlyStructured != null) {
return Optional.of(titleHighlyStructured);
}
if (titleImage != null) {
return Optional.of(titleImage);
}
if (titleVideo != null) {
return Optional.of(titleVideo);
}
return Optional.ofNullable(titleLocation);
}
@Override
public Type templateType() {
return Type.FOUR_ROW;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredFourRowTemplate that
&& Objects.equals(titleDocument, that.titleDocument)
&& Objects.equals(titleHighlyStructured, that.titleHighlyStructured)
&& Objects.equals(titleImage, that.titleImage)
&& Objects.equals(titleVideo, that.titleVideo)
&& Objects.equals(titleLocation, that.titleLocation)
&& Objects.equals(content, that.content)
&& Objects.equals(footer, that.footer)
&& Objects.equals(buttons, that.buttons);
}
@Override
public int hashCode() {
return Objects.hash(titleDocument, titleHighlyStructured, titleImage, titleVideo, titleLocation, content, footer, buttons);
}
@Override
public String toString() {
return "HighlyStructuredFourRowTemplate[" +
"titleDocument=" + titleDocument + ", " +
"titleHighlyStructured=" + titleHighlyStructured + ", " +
"titleImage=" + titleImage + ", " +
"titleVideo=" + titleVideo + ", " +
"titleLocation=" + titleLocation + ", " +
"content=" + content + ", " +
"footer=" + footer + ", " +
"buttons=" + buttons + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredFourRowTemplateTitle.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents the title of a {@link HighlyStructuredFourRowTemplate}
*/
public sealed interface HighlyStructuredFourRowTemplateTitle permits DocumentMessage, HighlyStructuredMessage, ImageMessage, VideoOrGifMessage, LocationMessage {
/**
* Return the type of this title
*
* @return a non-null type
*/
Type titleType();
/**
* The constants of this enumerated type describe the various types of title that a template can
* have
*/
@ProtobufEnum
enum Type {
/**
* No title
*/
NONE(0),
/**
* Document title
*/
DOCUMENT(1),
/**
* Highly structured message title
*/
HIGHLY_STRUCTURED(2),
/**
* Image title
*/
IMAGE(3),
/**
* Video title
*/
VIDEO(4),
/**
* Location title
*/
LOCATION(5);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredLocalizableParameter.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a time a localizable parameter
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage.HSMLocalizableParameter")
public final class HighlyStructuredLocalizableParameter {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String defaultValue;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredCurrency parameterCurrency;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final HighlyStructuredDateTime parameterDateTime;
HighlyStructuredLocalizableParameter(String defaultValue, HighlyStructuredCurrency parameterCurrency, HighlyStructuredDateTime parameterDateTime) {
this.defaultValue = Objects.requireNonNull(defaultValue, "defaultValue cannot be null");
this.parameterCurrency = parameterCurrency;
this.parameterDateTime = parameterDateTime;
}
/**
* Constructs a new localizable parameter with a default value and a parameter
*
* @param defaultValue the default value
* @param parameter the parameter
* @return a non-null localizable parameter
*/
public static HighlyStructuredLocalizableParameter of(String defaultValue, HighlyStructuredLocalizableParameterValue parameter) {
var builder = new HighlyStructuredLocalizableParameterBuilder()
.defaultValue(defaultValue);
switch (parameter) {
case HighlyStructuredCurrency highlyStructuredCurrency ->
builder.parameterCurrency(highlyStructuredCurrency);
case HighlyStructuredDateTime businessDateTime -> builder.parameterDateTime(businessDateTime);
case null -> {}
}
return builder.build();
}
public String defaultValue() {
return defaultValue;
}
public Optional<HighlyStructuredCurrency> parameterCurrency() {
return Optional.ofNullable(parameterCurrency);
}
public Optional<HighlyStructuredDateTime> parameterDateTime() {
return Optional.ofNullable(parameterDateTime);
}
/**
* Returns the type of parameter that this message wraps
*
* @return a non-null parameter type
*/
public HighlyStructuredLocalizableParameterValue.Type parameterType() {
return parameter()
.map(HighlyStructuredLocalizableParameterValue::parameterType)
.orElse(HighlyStructuredLocalizableParameterValue.Type.NONE);
}
/**
* Returns the parameter that this message wraps
*
* @return a non-null optional
*/
public Optional<? extends HighlyStructuredLocalizableParameterValue> parameter() {
if(parameterCurrency != null) {
return Optional.of(parameterCurrency);
}else if (parameterDateTime != null) {
return Optional.of(parameterDateTime);
}else {
return Optional.empty();
}
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredLocalizableParameter that
&& Objects.equals(defaultValue, that.defaultValue)
&& Objects.equals(parameterCurrency, that.parameterCurrency)
&& Objects.equals(parameterDateTime, that.parameterDateTime);
}
@Override
public int hashCode() {
return Objects.hash(defaultValue, parameterCurrency, parameterDateTime);
}
@Override
public String toString() {
return "HighlyStructuredLocalizableParameter[" +
"defaultValue=" + defaultValue + ", " +
"parameterCurrency=" + parameterCurrency + ", " +
"parameterDateTime=" + parameterDateTime + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredLocalizableParameterValue.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model class that represents the value of a localizable parameter
*/
public sealed interface HighlyStructuredLocalizableParameterValue permits HighlyStructuredCurrency, HighlyStructuredDateTime {
/**
* Returns the type of parameter
*
* @return a non-null type
*/
Type parameterType();
@ProtobufEnum
enum Type {
/**
* No parameter
*/
NONE(0),
/**
* Currency parameter
*/
CURRENCY(2),
/**
* Date time parameter
*/
DATE_TIME(3);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredMessage.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import com.github.auties00.cobalt.model.message.button.TemplateMessage;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import com.github.auties00.cobalt.model.message.model.Message;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a message that contains a highly structured message inside. Not
* really clear how this could be used, contributions are welcomed.
*/
@ProtobufMessage(name = "Message.HighlyStructuredMessage")
public final class HighlyStructuredMessage implements ButtonMessage, HighlyStructuredFourRowTemplateTitle {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String namespace;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String elementName;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final List<String> params;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final String fallbackLg;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final String fallbackLc;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final List<HighlyStructuredLocalizableParameter> localizableParameters;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String deterministicLg;
@ProtobufProperty(index = 8, type = ProtobufType.STRING)
final String deterministicLc;
@ProtobufProperty(index = 9, type = ProtobufType.MESSAGE)
final TemplateMessage templateMessage;
HighlyStructuredMessage(String namespace, String elementName, List<String> params, String fallbackLg, String fallbackLc, List<HighlyStructuredLocalizableParameter> localizableParameters, String deterministicLg, String deterministicLc, TemplateMessage templateMessage) {
this.namespace = Objects.requireNonNull(namespace, "namespace cannot be null");
this.elementName = Objects.requireNonNull(elementName, "elementName cannot be null");
this.params = Objects.requireNonNullElse(params, List.of());
this.fallbackLg = fallbackLg;
this.fallbackLc = fallbackLc;
this.localizableParameters = Objects.requireNonNullElse(localizableParameters, List.of());
this.deterministicLg = deterministicLg;
this.deterministicLc = deterministicLc;
this.templateMessage = Objects.requireNonNull(templateMessage, "templateMessage cannot be null");
}
public String namespace() {
return namespace;
}
public String elementName() {
return elementName;
}
public List<String> params() {
return params;
}
public Optional<String> fallbackLg() {
return Optional.ofNullable(fallbackLg);
}
public Optional<String> fallbackLc() {
return Optional.ofNullable(fallbackLc);
}
public List<HighlyStructuredLocalizableParameter> localizableParameters() {
return localizableParameters;
}
public Optional<String> deterministicLg() {
return Optional.ofNullable(deterministicLg);
}
public Optional<String> deterministicLc() {
return Optional.ofNullable(deterministicLc);
}
public TemplateMessage templateMessage() {
return templateMessage;
}
@Override
public Message.Type type() {
return Message.Type.HIGHLY_STRUCTURED;
}
@Override
public HighlyStructuredFourRowTemplateTitle.Type titleType() {
return HighlyStructuredFourRowTemplateTitle.Type.HIGHLY_STRUCTURED;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredMessage that
&& Objects.equals(namespace, that.namespace)
&& Objects.equals(elementName, that.elementName)
&& Objects.equals(params, that.params)
&& Objects.equals(fallbackLg, that.fallbackLg)
&& Objects.equals(fallbackLc, that.fallbackLc)
&& Objects.equals(localizableParameters, that.localizableParameters)
&& Objects.equals(deterministicLg, that.deterministicLg)
&& Objects.equals(deterministicLc, that.deterministicLc)
&& Objects.equals(templateMessage, that.templateMessage);
}
@Override
public int hashCode() {
return Objects.hash(namespace, elementName, params, fallbackLg, fallbackLc, localizableParameters, deterministicLg, deterministicLc, templateMessage);
}
@Override
public String toString() {
return "HighlyStructuredMessage[" +
"namespace=" + namespace + ", " +
"elementName=" + elementName + ", " +
"params=" + params + ", " +
"fallbackLg=" + fallbackLg + ", " +
"fallbackLc=" + fallbackLc + ", " +
"localizableParameters=" + localizableParameters + ", " +
"deterministicLg=" + deterministicLg + ", " +
"deterministicLc=" + deterministicLc + ", " +
"templateMessage=" + templateMessage + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredQuickReplyButton.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a quick reply button
*/
@ProtobufMessage(name = "TemplateButton.QuickReplyButton")
public final class HighlyStructuredQuickReplyButton implements HighlyStructuredButton {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage text;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String id;
HighlyStructuredQuickReplyButton(HighlyStructuredMessage text, String id) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.id = Objects.requireNonNull(id, "id cannot be null");
}
public HighlyStructuredMessage text() {
return text;
}
public String id() {
return id;
}
public Type buttonType() {
return Type.QUICK_REPLY;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredQuickReplyButton that
&& Objects.equals(text, that.text)
&& Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(text, id);
}
@Override
public String toString() {
return "HighlyStructuredQuickReplyButton[" +
"text=" + text + ", " +
"id=" + id + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/highlyStructured/HighlyStructuredURLButton.java
================================================
package com.github.auties00.cobalt.model.button.template.highlyStructured;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents an url button
*/
@ProtobufMessage(name = "TemplateButton.URLButton")
public final class HighlyStructuredURLButton implements HighlyStructuredButton {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage text;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage url;
HighlyStructuredURLButton(HighlyStructuredMessage text, HighlyStructuredMessage url) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.url = Objects.requireNonNull(url, "url cannot be null");
}
public HighlyStructuredMessage text() {
return text;
}
public HighlyStructuredMessage url() {
return url;
}
@Override
public Type buttonType() {
return Type.URL;
}
@Override
public boolean equals(Object o) {
return o instanceof HighlyStructuredURLButton that
&& Objects.equals(text, that.text)
&& Objects.equals(url, that.url);
}
@Override
public int hashCode() {
return Objects.hash(text, url);
}
@Override
public String toString() {
return "HighlyStructuredURLButton[" +
"text=" + text + ", " +
"url=" + url + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedButton.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents all types of hydrated buttons
*/
public sealed interface HydratedButton permits HydratedCallButton, HydratedQuickReplyButton, HydratedURLButton {
/**
* Returns the text of this button
*
* @return a non-null value if the protobuf isn't corrupted
*/
String text();
/**
* Returns the type of this button
*
* @return a non-null type
*/
Type buttonType();
/**
* The constants of this enumerated type describe the various types of buttons that a template can
* wrap
*/
@ProtobufEnum
enum Type {
/**
* No button
*/
NONE(0),
/**
* Quick reply button
*/
QUICK_REPLY(1),
/**
* Url button
*/
URL(2),
/**
* Call button
*/
CALL(3);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedCallButton.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a hydrated button that can start a phone call
*/
@ProtobufMessage(name = "HydratedTemplateButton.HydratedCallButton")
public final class HydratedCallButton implements HydratedButton {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String text;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String phoneNumber;
HydratedCallButton(String text, String phoneNumber) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.phoneNumber = Objects.requireNonNull(phoneNumber, "phoneNumber cannot be null");
}
public String text() {
return text;
}
public String phoneNumber() {
return phoneNumber;
}
@Override
public Type buttonType() {
return Type.CALL;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedCallButton that
&& Objects.equals(text, that.text)
&& Objects.equals(phoneNumber, that.phoneNumber);
}
@Override
public int hashCode() {
return Objects.hash(text, phoneNumber);
}
@Override
public String toString() {
return "HydratedCallButton[" +
"text=" + text + ", " +
"phoneNumber=" + phoneNumber + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedFourRowTemplate.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import com.github.auties00.cobalt.model.button.base.TemplateFormatter;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;
/**
* A model class that represents a hydrated four row template
*/
@ProtobufMessage(name = "Message.TemplateMessage.HydratedFourRowTemplate")
public final class HydratedFourRowTemplate implements TemplateFormatter {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final DocumentMessage titleDocument;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final HydratedFourRowTemplateTextTitle titleText;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final ImageMessage titleImage;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final VideoOrGifMessage titleVideo;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final LocationMessage titleLocation;
@ProtobufProperty(index = 6, type = ProtobufType.STRING)
final String body;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String footer;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
final List<HydratedTemplateButton> hydratedButtons;
@ProtobufProperty(index = 9, type = ProtobufType.STRING)
final String templateId;
HydratedFourRowTemplate(DocumentMessage titleDocument, HydratedFourRowTemplateTextTitle titleText,
ImageMessage titleImage, VideoOrGifMessage titleVideo, LocationMessage titleLocation,
String body, String footer, List<HydratedTemplateButton> hydratedButtons, String templateId) {
this.titleDocument = titleDocument;
this.titleText = titleText;
this.titleImage = titleImage;
this.titleVideo = titleVideo;
this.titleLocation = titleLocation;
this.body = body;
this.footer = footer;
this.hydratedButtons = Objects.requireNonNullElse(hydratedButtons, List.of());
this.templateId = templateId;
}
@ProtobufBuilder(className = "HydratedFourRowTemplateSimpleBuilder")
static HydratedFourRowTemplate customBuilder(HydratedFourRowTemplateTitle title, String body, String footer, List<HydratedTemplateButton> buttons, String templateId) {
var builder = new HydratedFourRowTemplateBuilder()
.templateId(templateId)
.body(body)
.hydratedButtons(getIndexedButtons(buttons))
.footer(footer);
switch (title) {
case DocumentMessage documentMessage -> builder.titleDocument(documentMessage);
case HydratedFourRowTemplateTextTitle hydratedFourRowTemplateTextTitle ->
builder.titleText(hydratedFourRowTemplateTextTitle);
case ImageMessage imageMessage -> builder.titleImage(imageMessage);
case VideoOrGifMessage videoMessage -> builder.titleVideo(videoMessage);
case LocationMessage locationMessage -> builder.titleLocation(locationMessage);
case null -> {
}
}
return builder.build();
}
private static List<HydratedTemplateButton> getIndexedButtons(List<HydratedTemplateButton> buttons) {
return IntStream.range(0, buttons.size()).mapToObj(index -> {
var button = buttons.get(index);
return new HydratedTemplateButton(button.quickReplyButton(), button.urlButton(), button.callButton(), index + 1);
}).toList();
}
public Optional<DocumentMessage> titleDocument() {
return Optional.ofNullable(titleDocument);
}
public Optional<HydratedFourRowTemplateTextTitle> titleText() {
return Optional.ofNullable(titleText);
}
public Optional<ImageMessage> titleImage() {
return Optional.ofNullable(titleImage);
}
public Optional<VideoOrGifMessage> titleVideo() {
return Optional.ofNullable(titleVideo);
}
public Optional<LocationMessage> titleLocation() {
return Optional.ofNullable(titleLocation);
}
public String body() {
return body;
}
public Optional<String> footer() {
return Optional.ofNullable(footer);
}
public List<HydratedTemplateButton> hydratedButtons() {
return hydratedButtons;
}
public String templateId() {
return templateId;
}
/**
* Returns the type of title that this template wraps
*
* @return a non-null title type
*/
public HydratedFourRowTemplateTitle.Type titleType() {
return title().map(HydratedFourRowTemplateTitle::hydratedTitleType)
.orElse(HydratedFourRowTemplateTitle.Type.NONE);
}
/**
* Returns the title of this template
*
* @return an optional
*/
public Optional<? extends HydratedFourRowTemplateTitle> title() {
if (titleDocument != null) {
return Optional.of(titleDocument);
}else if (titleText != null) {
return Optional.of(titleText);
}else if (titleImage != null) {
return Optional.of(titleImage);
}else if (titleVideo != null) {
return Optional.of(titleVideo);
}else if(titleLocation != null){
return Optional.of(titleLocation);
}else {
return Optional.empty();
}
}
@Override
public Type templateType() {
return Type.HYDRATED_FOUR_ROW;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedFourRowTemplate that
&& Objects.equals(titleDocument, that.titleDocument)
&& Objects.equals(titleText, that.titleText)
&& Objects.equals(titleImage, that.titleImage)
&& Objects.equals(titleVideo, that.titleVideo)
&& Objects.equals(titleLocation, that.titleLocation)
&& Objects.equals(body, that.body)
&& Objects.equals(footer, that.footer)
&& Objects.equals(hydratedButtons, that.hydratedButtons)
&& Objects.equals(templateId, that.templateId);
}
@Override
public int hashCode() {
return Objects.hash(titleDocument, titleText, titleImage, titleVideo, titleLocation,
body, footer, hydratedButtons, templateId);
}
@Override
public String toString() {
return "HydratedFourRowTemplate[" +
"titleDocument=" + titleDocument +
", titleText=" + titleText +
", titleImage=" + titleImage +
", titleVideo=" + titleVideo +
", titleLocation=" + titleLocation +
", body=" + body +
", footer=" + footer +
", hydratedButtons=" + hydratedButtons +
", templateId=" + templateId + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedFourRowTemplateTextTitle.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
import java.util.Objects;
/**
* A model class that represents a hydrated four row template
*/
public final class HydratedFourRowTemplateTextTitle implements HydratedFourRowTemplateTitle {
final String text;
HydratedFourRowTemplateTextTitle(String text) {
this.text = Objects.requireNonNull(text, "text cannot be null");
}
@ProtobufDeserializer
public static HydratedFourRowTemplateTextTitle of(String text) {
return new HydratedFourRowTemplateTextTitle(text);
}
@ProtobufSerializer
public String text() {
return text;
}
@Override
public Type hydratedTitleType() {
return Type.TEXT;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedFourRowTemplateTextTitle that
&& Objects.equals(text, that.text);
}
@Override
public int hashCode() {
return Objects.hash(text);
}
@Override
public String toString() {
return "HydratedFourRowTemplateTextTitle[" +
"text=" + text + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedFourRowTemplateTitle.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents the title of a {@link HydratedFourRowTemplate}
*/
public sealed interface HydratedFourRowTemplateTitle permits DocumentMessage, HydratedFourRowTemplateTextTitle, ImageMessage, VideoOrGifMessage, LocationMessage {
/**
* Return the type of this title
*
* @return a non-null type
*/
Type hydratedTitleType();
/**
* The constants of this enumerated type describe the various types of title that a template can
* wrap
*/
@ProtobufEnum
enum Type {
/**
* No title
*/
NONE(0),
/**
* Document title
*/
DOCUMENT(1),
/**
* Text title
*/
TEXT(2),
/**
* Image title
*/
IMAGE(3),
/**
* Video title
*/
VIDEO(4),
/**
* Location title
*/
LOCATION(5);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedQuickReplyButton.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import com.github.auties00.cobalt.util.SecureBytes;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a hydrated quick reply button
*/
@ProtobufMessage(name = "HydratedTemplateButton.HydratedQuickReplyButton")
public final class HydratedQuickReplyButton implements HydratedButton {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String text;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String id;
HydratedQuickReplyButton(String text, String id) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.id = Objects.requireNonNullElseGet(id, () -> SecureBytes.randomHex(6));
}
public String text() {
return text;
}
public String id() {
return id;
}
@Override
public Type buttonType() {
return Type.QUICK_REPLY;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedQuickReplyButton that
&& Objects.equals(text, that.text)
&& Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(text, id);
}
@Override
public String toString() {
return "HydratedQuickReplyButton[" +
"text=" + text + ", " +
"id=" + id + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedTemplateButton.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a hydrated template for a button
*/
@ProtobufMessage(name = "HydratedTemplateButton")
public final class HydratedTemplateButton {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HydratedQuickReplyButton quickReplyButton;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HydratedURLButton urlButton;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final HydratedCallButton callButton;
@ProtobufProperty(index = 4, type = ProtobufType.UINT32)
final int index;
HydratedTemplateButton(HydratedQuickReplyButton quickReplyButton, HydratedURLButton urlButton, HydratedCallButton callButton, int index) {
this.quickReplyButton = quickReplyButton;
this.urlButton = urlButton;
this.callButton = callButton;
this.index = index;
}
/**
* Constructs a new template button
*
* @param button the non-null button
* @return a non-null button template
*/
public static HydratedTemplateButton of(HydratedButton button) {
return of(-1, button);
}
/**
* Constructs a new template button
*
* @param index the index
* @param button the non-null button
* @return a non-null button template
*/
public static HydratedTemplateButton of(int index, HydratedButton button) {
var builder = new HydratedTemplateButtonBuilder()
.index(index);
switch (button) {
case HydratedQuickReplyButton hydratedQuickReplyButton ->
builder.quickReplyButton(hydratedQuickReplyButton);
case HydratedURLButton hydratedURLButton -> builder.urlButton(hydratedURLButton);
case HydratedCallButton hydratedCallButton -> builder.callButton(hydratedCallButton);
case null -> {}
}
return builder.build();
}
/**
* Returns this button
*
* @return a non-null optional
*/
public Optional<HydratedButton> button() {
if (quickReplyButton != null) {
return Optional.of(quickReplyButton);
}
if (urlButton != null) {
return Optional.of(urlButton);
}
if (callButton != null) {
return Optional.of(callButton);
}
return Optional.empty();
}
/**
* Returns the type of button that this message wraps
*
* @return a non-null button type
*/
public HydratedButton.Type buttonType() {
return button()
.map(HydratedButton::buttonType)
.orElse(HydratedButton.Type.NONE);
}
public HydratedQuickReplyButton quickReplyButton() {
return quickReplyButton;
}
public HydratedURLButton urlButton() {
return urlButton;
}
public HydratedCallButton callButton() {
return callButton;
}
public int index() {
return index;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedTemplateButton that
&& Objects.equals(quickReplyButton, that.quickReplyButton)
&& Objects.equals(urlButton, that.urlButton)
&& Objects.equals(callButton, that.callButton)
&& index == that.index;
}
@Override
public int hashCode() {
return Objects.hash(quickReplyButton, urlButton, callButton, index);
}
@Override
public String toString() {
return "HydratedTemplateButton[" +
"quickReplyButton=" + quickReplyButton + ", " +
"urlButton=" + urlButton + ", " +
"callButton=" + callButton + ", " +
"index=" + index + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/button/template/hydrated/HydratedURLButton.java
================================================
package com.github.auties00.cobalt.model.button.template.hydrated;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents a hydrated url button
*/
@ProtobufMessage(name = "HydratedTemplateButton.HydratedURLButton")
public final class HydratedURLButton implements HydratedButton {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String text;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String url;
HydratedURLButton(String text, String url) {
this.text = Objects.requireNonNull(text, "text cannot be null");
this.url = Objects.requireNonNull(url, "url cannot be null");
}
public String text() {
return text;
}
public String url() {
return url;
}
@Override
public Type buttonType() {
return Type.URL;
}
@Override
public boolean equals(Object o) {
return o instanceof HydratedURLButton that
&& Objects.equals(text, that.text)
&& Objects.equals(url, that.url);
}
@Override
public int hashCode() {
return Objects.hash(text, url);
}
@Override
public String toString() {
return "HydratedURLButton[" +
"text=" + text + ", " +
"url=" + url + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/call/Call.java
================================================
package com.github.auties00.cobalt.model.call;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
@ProtobufMessage
public final class Call {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid chatJid;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final Jid callerJid;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 4, type = ProtobufType.UINT64)
final long timestampSeconds;
@ProtobufProperty(index = 5, type = ProtobufType.BOOL)
final boolean video;
@ProtobufProperty(index = 6, type = ProtobufType.ENUM)
final CallStatus status;
@ProtobufProperty(index = 7, type = ProtobufType.BOOL)
final boolean offline;
Call(Jid chatJid, Jid callerJid, String id, long timestampSeconds, boolean video, CallStatus status, boolean offline) {
this.chatJid = Objects.requireNonNull(chatJid, "chatJid cannot be null");
this.callerJid = Objects.requireNonNull(callerJid, "callerJid cannot be null");
this.id = Objects.requireNonNull(id, "id cannot be null");
this.timestampSeconds = timestampSeconds;
this.video = video;
this.status = Objects.requireNonNull(status, "status cannot be null");
this.offline = offline;
}
public Jid chatJid() {
return chatJid;
}
public Jid callerJid() {
return callerJid;
}
public String id() {
return id;
}
public long timestampSeconds() {
return timestampSeconds;
}
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
public boolean video() {
return video;
}
public CallStatus status() {
return status;
}
public boolean offline() {
return offline;
}
@Override
public boolean equals(Object o) {
return o instanceof Call call
&& timestampSeconds == call.timestampSeconds
&& video == call.video
&& offline == call.offline
&& Objects.equals(chatJid, call.chatJid)
&& Objects.equals(callerJid, call.callerJid)
&& Objects.equals(id, call.id)
&& status == call.status;
}
@Override
public int hashCode() {
return Objects.hash(chatJid, callerJid, id, timestampSeconds, video, status, offline);
}
@Override
public String toString() {
return "Call[" +
"chat=" + chatJid + ", " +
"caller=" + callerJid + ", " +
"id=" + id + ", " +
"timestampSeconds=" + timestampSeconds + ", " +
"video=" + video + ", " +
"status=" + status + ", " +
"offline=" + offline + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/call/CallStatus.java
================================================
package com.github.auties00.cobalt.model.call;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
@ProtobufEnum
public enum CallStatus {
RINGING(0),
ACCEPTED(1),
REJECTED(2),
TIMED_OUT(3);
final int index;
CallStatus(@ProtobufEnumIndex int index) {
this.index = index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/Chat.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.contact.ContactStatus;
import com.github.auties00.cobalt.model.info.ChatMessageInfo;
import com.github.auties00.cobalt.model.info.MessageInfo;
import com.github.auties00.cobalt.model.info.MessageInfoParent;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.jid.JidProvider;
import com.github.auties00.cobalt.model.jid.JidServer;
import com.github.auties00.cobalt.model.media.MediaVisibility;
import com.github.auties00.cobalt.model.sync.HistorySyncMessage;
import com.github.auties00.cobalt.util.Clock;
import com.github.auties00.collections.ConcurrentLinkedHashMap;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/**
* A model class that represents a Chat. A chat can be of two types: a conversation with a contact
* or a group. This class is only a model, this means that changing its values will have no real
* effect on WhatsappWeb's servers
*/
@ProtobufMessage(name = "Conversation")
@SuppressWarnings({"unused", "UnusedReturnValue"})
public final class Chat implements MessageInfoParent {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final Messages messages;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
Jid newJid;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
Jid oldJid;
@ProtobufProperty(index = 6, type = ProtobufType.UINT32)
int unreadMessagesCount;
@ProtobufProperty(index = 8, type = ProtobufType.BOOL)
boolean endOfHistoryTransfer;
@ProtobufProperty(index = 9, type = ProtobufType.UINT32)
ChatEphemeralTimer ephemeralMessageDuration;
@ProtobufProperty(index = 10, type = ProtobufType.INT64)
long ephemeralMessagesToggleTimeSeconds;
@ProtobufProperty(index = 11, type = ProtobufType.ENUM)
EndOfHistoryTransferType endOfHistoryTransferType;
@ProtobufProperty(index = 12, type = ProtobufType.UINT64)
long timestampSeconds;
@ProtobufProperty(index = 13, type = ProtobufType.STRING)
String name;
@ProtobufProperty(index = 15, type = ProtobufType.BOOL)
boolean notSpam;
@ProtobufProperty(index = 16, type = ProtobufType.BOOL)
boolean archived;
@ProtobufProperty(index = 17, type = ProtobufType.MESSAGE)
ChatDisappear disappearInitiator;
@ProtobufProperty(index = 19, type = ProtobufType.BOOL)
boolean markedAsUnread;
@ProtobufProperty(index = 24, type = ProtobufType.UINT32)
int pinnedTimestampSeconds;
@ProtobufProperty(index = 25, type = ProtobufType.UINT64)
ChatMute mute;
@ProtobufProperty(index = 26, type = ProtobufType.MESSAGE)
ChatWallpaper wallpaper;
@ProtobufProperty(index = 27, type = ProtobufType.ENUM)
MediaVisibility mediaVisibility;
@ProtobufProperty(index = 29, type = ProtobufType.BOOL)
boolean suspended;
@ProtobufProperty(index = 30, type = ProtobufType.BOOL)
boolean terminated;
@ProtobufProperty(index = 34, type = ProtobufType.BOOL)
boolean support;
@ProtobufProperty(index = 38, type = ProtobufType.STRING)
String displayName;
@ProtobufProperty(index = 39, type = ProtobufType.STRING)
Jid phoneJid;
@ProtobufProperty(index = 40, type = ProtobufType.BOOL)
boolean shareOwnPhoneNumber;
@ProtobufProperty(index = 41, type = ProtobufType.BOOL)
boolean phoneDuplicateLidThread;
@ProtobufProperty(index = 42, type = ProtobufType.STRING)
Jid lid;
@ProtobufProperty(index = 999, type = ProtobufType.MAP, mapKeyType = ProtobufType.STRING, mapValueType = ProtobufType.ENUM)
final ConcurrentHashMap<Jid, ContactStatus> presences;
Chat(Jid jid, Messages messages, Jid newJid, Jid oldJid, int unreadMessagesCount, boolean endOfHistoryTransfer, ChatEphemeralTimer ephemeralMessageDuration, long ephemeralMessagesToggleTimeSeconds, EndOfHistoryTransferType endOfHistoryTransferType, long timestampSeconds, String name, boolean notSpam, boolean archived, ChatDisappear disappearInitiator, boolean markedAsUnread, int pinnedTimestampSeconds, ChatMute mute, ChatWallpaper wallpaper, MediaVisibility mediaVisibility, boolean suspended, boolean terminated, boolean support, String displayName, Jid phoneJid, boolean shareOwnPhoneNumber, boolean phoneDuplicateLidThread, Jid lid, ConcurrentHashMap<Jid, ContactStatus> presences) {
this.jid = jid;
this.messages = messages;
this.newJid = newJid;
this.oldJid = oldJid;
this.unreadMessagesCount = unreadMessagesCount;
this.endOfHistoryTransfer = endOfHistoryTransfer;
this.ephemeralMessageDuration = Objects.requireNonNullElse(ephemeralMessageDuration, ChatEphemeralTimer.OFF);
this.ephemeralMessagesToggleTimeSeconds = ephemeralMessagesToggleTimeSeconds;
this.endOfHistoryTransferType = endOfHistoryTransferType;
this.timestampSeconds = timestampSeconds;
this.name = name;
this.notSpam = notSpam;
this.archived = archived;
this.disappearInitiator = disappearInitiator;
this.markedAsUnread = markedAsUnread;
this.pinnedTimestampSeconds = pinnedTimestampSeconds;
this.mute = Objects.requireNonNullElse(mute, ChatMute.notMuted());
this.wallpaper = wallpaper;
this.mediaVisibility = mediaVisibility;
this.suspended = suspended;
this.terminated = terminated;
this.support = support;
this.displayName = displayName;
this.phoneJid = phoneJid;
this.shareOwnPhoneNumber = shareOwnPhoneNumber;
this.phoneDuplicateLidThread = phoneDuplicateLidThread;
this.lid = lid;
this.presences = presences;
}
/**
* Returns the JID associated with this chat.
*
* @return a non-null Jid
*/
public Jid jid() {
return jid;
}
public Optional<Jid> newJid() {
return Optional.ofNullable(newJid);
}
public Optional<Jid> oldJid() {
return Optional.ofNullable(oldJid);
}
public int unreadMessagesCount() {
return unreadMessagesCount;
}
public boolean endOfHistoryTransfer() {
return endOfHistoryTransfer;
}
public ChatEphemeralTimer ephemeralMessageDuration() {
return ephemeralMessageDuration;
}
public long ephemeralMessagesToggleTimeSeconds() {
return ephemeralMessagesToggleTimeSeconds;
}
public Optional<EndOfHistoryTransferType> endOfHistoryTransferType() {
return Optional.ofNullable(endOfHistoryTransferType);
}
public long timestampSeconds() {
return timestampSeconds;
}
public boolean notSpam() {
return notSpam;
}
public boolean archived() {
return archived;
}
public Optional<ChatDisappear> disappearInitiator() {
return Optional.ofNullable(disappearInitiator);
}
public boolean markedAsUnread() {
return markedAsUnread;
}
public int pinnedTimestampSeconds() {
return pinnedTimestampSeconds;
}
public ChatMute mute() {
return mute;
}
public Optional<ChatWallpaper> wallpaper() {
return Optional.ofNullable(wallpaper);
}
public MediaVisibility mediaVisibility() {
return mediaVisibility;
}
public boolean suspended() {
return suspended;
}
public boolean terminated() {
return terminated;
}
public boolean support() {
return support;
}
public Optional<Jid> phoneJid() {
return Optional.ofNullable(phoneJid);
}
public boolean phoneDuplicateLidThread() {
return phoneDuplicateLidThread;
}
public Optional<Jid> lidJid() {
return Optional.ofNullable(lid);
}
public Optional<ContactStatus> getPresence(JidProvider jid) {
return Optional.ofNullable(presences.get(jid.toJid()));
}
public void addPresence(JidProvider jid, ContactStatus status) {
presences.put(jid.toJid(), status);
}
public boolean removePresence(JidProvider jid) {
return presences.remove(jid.toJid()) != null;
}
public boolean hasName() {
return name != null;
}
public boolean shareOwnPhoneNumber() {
return shareOwnPhoneNumber;
}
public void setUnreadMessagesCount(int unreadMessagesCount) {
this.unreadMessagesCount = unreadMessagesCount;
}
public void setEndOfHistoryTransfer(boolean endOfHistoryTransfer) {
this.endOfHistoryTransfer = endOfHistoryTransfer;
}
public void setEphemeralMessageDuration(ChatEphemeralTimer ephemeralMessageDuration) {
this.ephemeralMessageDuration = ephemeralMessageDuration;
}
public void setEphemeralMessagesToggleTimeSeconds(long ephemeralMessagesToggleTimeSeconds) {
this.ephemeralMessagesToggleTimeSeconds = ephemeralMessagesToggleTimeSeconds;
}
public void setEndOfHistoryTransferType(EndOfHistoryTransferType endOfHistoryTransferType) {
this.endOfHistoryTransferType = endOfHistoryTransferType;
}
public void setTimestampSeconds(long timestampSeconds) {
this.timestampSeconds = timestampSeconds;
}
public void setName(String name) {
this.name = name;
}
public void setNotSpam(boolean notSpam) {
this.notSpam = notSpam;
}
public void setArchived(boolean archived) {
this.archived = archived;
}
public void setDisappearInitiator(ChatDisappear disappearInitiator) {
this.disappearInitiator = disappearInitiator;
}
public void setMarkedAsUnread(boolean markedAsUnread) {
this.markedAsUnread = markedAsUnread;
}
public void setPinnedTimestampSeconds(int pinnedTimestampSeconds) {
this.pinnedTimestampSeconds = pinnedTimestampSeconds;
}
public void setMute(ChatMute mute) {
this.mute = mute;
}
public void setWallpaper(ChatWallpaper wallpaper) {
this.wallpaper = wallpaper;
}
public void setMediaVisibility(MediaVisibility mediaVisibility) {
this.mediaVisibility = mediaVisibility;
}
public void setSuspended(boolean suspended) {
this.suspended = suspended;
}
public void setTerminated(boolean terminated) {
this.terminated = terminated;
}
public void setSupport(boolean support) {
this.support = support;
}
public void setPhoneJid(Jid phoneJid) {
this.phoneJid = phoneJid;
}
public void setShareOwnPhoneNumber(boolean shareOwnPhoneNumber) {
this.shareOwnPhoneNumber = shareOwnPhoneNumber;
}
public void setPhoneDuplicateLidThread(boolean phoneDuplicateLidThread) {
this.phoneDuplicateLidThread = phoneDuplicateLidThread;
}
public void setLid(Jid lid) {
this.lid = lid;
}
/**
* Returns the name of this chat
*
* @return a non-null value
*/
public String name() {
if (name != null) {
return name;
}
if (displayName != null) {
return displayName;
}
return jid.user();
}
/**
* Returns a boolean to represent whether this chat is a group or not
*
* @return true if this chat is a group
*/
public boolean isGroupOrCommunity() {
return jid.server().type() == JidServer.Type.GROUP_OR_COMMUNITY;
}
/**
* Returns a boolean to represent whether this chat is pinned or not
*
* @return true if this chat is pinned
*/
public boolean isPinned() {
return pinnedTimestampSeconds != 0;
}
/**
* Returns a boolean to represent whether ephemeral messages are enabled for this chat
*
* @return true if ephemeral messages are enabled for this chat
*/
public boolean isEphemeral() {
return ephemeralMessageDuration != ChatEphemeralTimer.OFF && ephemeralMessagesToggleTimeSeconds != 0;
}
/**
* Returns all the unread messages in this chat
*
* @return a non-null collection
*/
public Collection<ChatMessageInfo> unreadMessages() {
if (!hasUnreadMessages()) {
return List.of();
}
return messages.streamMessages()
.limit(unreadMessagesCount())
.toList();
}
/**
* Returns a boolean to represent whether this chat has unread messages
*
* @return true if this chat has unread messages
*/
public boolean hasUnreadMessages() {
return unreadMessagesCount > 0;
}
/**
* Returns an optional value containing the seconds this chat was pinned
*
* @return an optional
*/
public Optional<ZonedDateTime> pinnedTimestamp() {
return Clock.parseSeconds(pinnedTimestampSeconds);
}
/**
* Returns the timestampSeconds for the creation of this chat in seconds since
* {@link Instant#EPOCH}
*
* @return an optional
*/
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
/**
* Returns an optional value containing the seconds in seconds since
* {@link Instant#EPOCH} when ephemeral messages were turned on
*
* @return an optional
*/
public Optional<ZonedDateTime> ephemeralMessagesToggleTime() {
return Clock.parseSeconds(ephemeralMessagesToggleTimeSeconds);
}
/**
* Returns an optional value containing the latest message in chronological terms for this chat
*
* @return an optional
*/
@Override
public Optional<ChatMessageInfo> newestMessage() {
return messages.getLast();
}
/**
* Returns an optional value containing the first message in chronological terms for this chat
*
* @return an optional
*/
@Override
public Optional<ChatMessageInfo> oldestMessage() {
return messages.getFirst();
}
/**
* Returns all the starred messages in this chat
*
* @return a non-null list of messages
*/
public SequencedCollection<ChatMessageInfo> starredMessages() {
return messages.streamMessages()
.filter(ChatMessageInfo::starred)
.toList();
}
/**
* Adds a message to the chat in the most recent slot available
*
* @param info the message to add to the chat
*/
public void addMessage(ChatMessageInfo info) {
Objects.requireNonNull(info, "info cannot be null");
messages.add(info);
updateChatTimestamp(info);
}
/**
* Remove a message from the chat
*
* @param info the message to remove
* @return whether the message was removed
*/
@Override
public boolean removeMessage(String info) {
if(!messages.removeById(info)) {
return false;
}
refreshChatTimestamp();
return true;
}
private void refreshChatTimestamp() {
var message = newestMessage();
if (message.isEmpty()) {
this.timestampSeconds = 0L;
}else {
updateChatTimestamp(message.get());
}
}
private void updateChatTimestamp(ChatMessageInfo info) {
if (info.timestampSeconds().isEmpty()) {
return;
}
var newTimestamp = info.timestampSeconds()
.getAsLong();
var oldTimeStamp = newestMessage()
.map(value -> value.timestampSeconds().orElse(0L))
.orElse(0L);
if (oldTimeStamp > newTimestamp) {
return;
}
this.timestampSeconds = newTimestamp;
}
/**
* Removes all messages from the chat
*/
@Override
public void removeMessages() {
messages.clear();
}
/**
* Returns an immutable list of messages wrapped in history syncs
* This is useful for the proto
*
* @return a non-null collection
*/
public SequencedCollection<ChatMessageInfo> messages() {
return messages.toUnmodifiableSequencedView();
}
/**
* Returns this object as a value
*
* @return a non-null value
*/
@Override
public Jid toJid() {
return jid();
}
@Override
public boolean equals(Object o) {
return o instanceof Chat chat &&
unreadMessagesCount == chat.unreadMessagesCount &&
endOfHistoryTransfer == chat.endOfHistoryTransfer &&
ephemeralMessagesToggleTimeSeconds == chat.ephemeralMessagesToggleTimeSeconds &&
timestampSeconds == chat.timestampSeconds &&
notSpam == chat.notSpam &&
archived == chat.archived &&
markedAsUnread == chat.markedAsUnread &&
pinnedTimestampSeconds == chat.pinnedTimestampSeconds &&
suspended == chat.suspended &&
terminated == chat.terminated &&
support == chat.support &&
shareOwnPhoneNumber == chat.shareOwnPhoneNumber &&
phoneDuplicateLidThread == chat.phoneDuplicateLidThread &&
Objects.equals(jid, chat.jid) &&
Objects.equals(messages, chat.messages) &&
Objects.equals(newJid, chat.newJid) &&
Objects.equals(oldJid, chat.oldJid) &&
ephemeralMessageDuration == chat.ephemeralMessageDuration &&
endOfHistoryTransferType == chat.endOfHistoryTransferType &&
Objects.equals(name, chat.name) &&
Objects.equals(disappearInitiator, chat.disappearInitiator) &&
Objects.equals(mute, chat.mute) &&
Objects.equals(wallpaper, chat.wallpaper) &&
mediaVisibility == chat.mediaVisibility &&
Objects.equals(displayName, chat.displayName) &&
Objects.equals(phoneJid, chat.phoneJid) &&
Objects.equals(lid, chat.lid) &&
Objects.equals(presences, chat.presences);
}
@Override
public int hashCode() {
return Objects.hash(jid, messages, newJid, oldJid, unreadMessagesCount, endOfHistoryTransfer, ephemeralMessageDuration, ephemeralMessagesToggleTimeSeconds, endOfHistoryTransferType, timestampSeconds, name, notSpam, archived, disappearInitiator, markedAsUnread, pinnedTimestampSeconds, mute, wallpaper, mediaVisibility, suspended, terminated, support, displayName, phoneJid, shareOwnPhoneNumber, phoneDuplicateLidThread, lid, presences);
}
@Override
public Optional<ChatMessageInfo> getMessageById(String id) {
return messages.getById(id);
}
/**
* The constants of this enumerated type describe the various types of transfers that can regard a
* chat history sync
*/
@ProtobufEnum(name = "Conversation.EndOfHistoryTransferType")
public enum EndOfHistoryTransferType {
/**
* Complete, but more messages remain on the phone
*/
COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY(0),
/**
* Complete and no more messages remain on the phone
*/
COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY(1);
final int index;
EndOfHistoryTransferType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
static final class Messages extends AbstractCollection<HistorySyncMessage> {
private final ConcurrentLinkedHashMap<String, ChatMessageInfo> backing;
Messages() {
this.backing = new ConcurrentLinkedHashMap<>();
}
@Override
public boolean add(HistorySyncMessage historySyncMessage) {
if(historySyncMessage == null) {
return false;
}else {
backing.put(historySyncMessage.messageInfo().id(), historySyncMessage.messageInfo());
return true;
}
}
public boolean add(ChatMessageInfo messageInfo) {
if(messageInfo == null) {
return false;
}else {
backing.put(messageInfo.id(), messageInfo);
return true;
}
}
public Optional<ChatMessageInfo> getById(String id) {
return Optional.ofNullable(backing.get(id));
}
public Optional<ChatMessageInfo> getFirst() {
return Optional.ofNullable(backing.firstEntry())
.map(Map.Entry::getValue);
}
public Optional<ChatMessageInfo> getLast() {
return Optional.ofNullable(backing.lastEntry())
.map(Map.Entry::getValue);
}
public boolean removeById(String id) {
return backing.remove(id) != null;
}
public Stream<ChatMessageInfo> streamMessages() {
return backing.sequencedValues()
.stream();
}
@Override
public Iterator<HistorySyncMessage> iterator() {
throw new UnsupportedOperationException();
}
@Override
public int size() {
return backing.size();
}
public SequencedCollection<ChatMessageInfo> toUnmodifiableSequencedView() {
return new UnmodifiableSequencedView();
}
private class UnmodifiableSequencedView implements SequencedCollection<ChatMessageInfo> {
@Override
public SequencedCollection<ChatMessageInfo> reversed() {
return backing.sequencedValues().reversed();
}
@Override
public int size() {
return backing.size();
}
@Override
public boolean isEmpty() {
return backing.isEmpty();
}
@Override
public boolean contains(Object o) {
return o instanceof ChatMessageInfo chatMessageInfo
&& backing.containsKey(chatMessageInfo.id());
}
@Override
public Iterator<ChatMessageInfo> iterator() {
return backing
.sequencedValues()
.iterator();
}
@Override
public Object[] toArray() {
return backing.sequencedValues()
.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return backing.sequencedValues()
.toArray(a);
}
@Override
public boolean add(ChatMessageInfo chatMessageInfo) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException();
}
@Override
public boolean containsAll(Collection<?> collection) {
Objects.requireNonNull(collection);
for (var entry : collection) {
if (!(entry instanceof MessageInfo messageInfo)) {
return false;
}
if (!backing.containsKey(messageInfo.id())) {
return false;
}
}
return true;
}
@Override
public boolean addAll(Collection<? extends ChatMessageInfo> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean retainAll(Collection<?> c) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
throw new UnsupportedOperationException();
}
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatDisappear.java
================================================
package com.github.auties00.cobalt.model.chat;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model that represents a chat disappear mode
*/
@ProtobufMessage(name = "DisappearingMode")
public final class ChatDisappear {
@ProtobufProperty(index = 1, type = ProtobufType.ENUM)
final Initiator initiator;
ChatDisappear(Initiator initiator) {
this.initiator = Objects.requireNonNullElse(initiator, Initiator.UNKNOWN);
}
public Initiator initiator() {
return initiator;
}
@Override
public boolean equals(Object o) {
return o instanceof ChatDisappear that
&& Objects.equals(initiator, that.initiator);
}
@Override
public int hashCode() {
return Objects.hash(initiator);
}
@Override
public String toString() {
return "ChatDisappear[" +
"initiator=" + initiator + ']';
}
/**
* The constants of this enumerated type describe the various actors that can initialize
* disappearing messages in a chat
*/
@ProtobufEnum(name = "DisappearingMode.Initiator")
public enum Initiator {
/**
* Unknown
*/
UNKNOWN(999),
/**
* Changed in chat
*/
CHANGED_IN_CHAT(0),
/**
* Initiated by me
*/
INITIATED_BY_ME(1),
/**
* Initiated by other
*/
INITIATED_BY_OTHER(2);
final int index;
Initiator(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatEphemeralTimer.java
================================================
package com.github.auties00.cobalt.model.chat;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
import java.time.Duration;
import java.util.Arrays;
/**
* Enum representing the ChatEphemeralTimer period. Each constant is associated with a specific
* duration period.
*/
public enum ChatEphemeralTimer {
/**
* ChatEphemeralTimer with duration of 0 days.
*/
OFF(Duration.ofDays(0)),
/**
* ChatEphemeralTimer with duration of 1 day.
*/
ONE_DAY(Duration.ofDays(1)),
/**
* ChatEphemeralTimer with duration of 7 days.
*/
ONE_WEEK(Duration.ofDays(7)),
/**
* ChatEphemeralTimer with duration of 90 days.
*/
THREE_MONTHS(Duration.ofDays(90));
private final Duration period;
ChatEphemeralTimer(Duration period) {
this.period = period;
}
public Duration period() {
return period;
}
@ProtobufDeserializer
public static ChatEphemeralTimer of(int value) {
return Arrays.stream(values())
.filter(entry -> entry.period().toSeconds() == value || entry.period().toDays() == value)
.findFirst()
.orElse(OFF);
}
@ProtobufSerializer
public int periodSeconds() {
return (int) period.toSeconds();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatMute.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Optional;
/**
* An immutable model class that represents a mute
*
* @param endTimeStamp the end date of the mute associated with this object stored as second since
* {@link Instant#EPOCH}
*/
public record ChatMute(long endTimeStamp) {
/**
* Not muted flag
*/
private static final long NOT_MUTED_FLAG = 0;
/**
* Muted flag
*/
private static final long MUTED_INDEFINITELY_FLAG = -1;
/**
* Not muted constant
*/
private static final ChatMute NOT_MUTED = new ChatMute(NOT_MUTED_FLAG);
/**
* Muted constant
*/
private static final ChatMute MUTED_INDEFINITELY = new ChatMute(MUTED_INDEFINITELY_FLAG);
/**
* Constructs a new not muted ChatMute
*
* @return a non-null mute
*/
public static ChatMute notMuted() {
return NOT_MUTED;
}
/**
* Constructs a new muted ChatMute
*
* @return a non-null mute
*/
public static ChatMute muted() {
return MUTED_INDEFINITELY;
}
/**
* Constructs a new mute that lasts eight hours
*
* @return a non-null mute
*/
public static ChatMute mutedForEightHours() {
return muted(ZonedDateTime.now().plusHours(8).toEpochSecond());
}
/**
* Constructs a new mute for a duration in endTimeStamp
*
* @param seconds can be null and is considered as not muted
* @return a non-null mute
*/
@ProtobufDeserializer
public static ChatMute muted(Long seconds) {
if (seconds == null || seconds == NOT_MUTED_FLAG) {
return NOT_MUTED;
}
if (seconds == MUTED_INDEFINITELY_FLAG) {
return MUTED_INDEFINITELY;
}
return new ChatMute(seconds);
}
/**
* Constructs a new mute that lasts one week
*
* @return a non-null mute
*/
public static ChatMute mutedForOneWeek() {
return muted(ZonedDateTime.now().plusWeeks(1).toEpochSecond());
}
/**
* Returns whether the chat associated with this object is muted or not.
*
* @return true if the chat associated with this object is muted
*/
public boolean isMuted() {
return type() != Type.NOT_MUTED;
}
/**
* Returns a non-null enum that describes the type of mute for this object
*
* @return a non-null enum that describes the type of mute for this object
*/
public Type type() {
if (endTimeStamp == MUTED_INDEFINITELY_FLAG) {
return Type.MUTED_INDEFINITELY;
}
if (endTimeStamp == NOT_MUTED_FLAG) {
return Type.NOT_MUTED;
}
return Type.MUTED_FOR_TIMEFRAME;
}
/**
* Returns the date when this mute expires if the chat is muted and not indefinitely
*
* @return a non-empty optional date if {@link ChatMute#endTimeStamp} > 0
*/
public Optional<ZonedDateTime> end() {
return Clock.parseSeconds(endTimeStamp);
}
@ProtobufSerializer
public long endTimeStamp() {
return endTimeStamp;
}
/**
* The constants of this enumerated type describe the various types of mute a {@link ChatMute} can
* describe
*/
public enum Type {
/**
* This constant describes a {@link ChatMute} that holds a seconds greater than 0 Simply put,
* {@link ChatMute#endTimeStamp()} > 0
*/
MUTED_FOR_TIMEFRAME,
/**
* This constant describes a {@link ChatMute} that holds a seconds equal to -1 Simply put,
* {@link ChatMute#endTimeStamp()} == -1
*/
MUTED_INDEFINITELY,
/**
* This constant describes a {@link ChatMute} that holds a seconds equal to 0 Simply put,
* {@link ChatMute#endTimeStamp()} == 0
*/
NOT_MUTED
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatParticipant.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.jid.Jid;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
@ProtobufMessage
public final class ChatParticipant {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.ENUM)
ChatRole role;
@ProtobufProperty(index = 3, type = ProtobufType.ENUM)
Type type;
ChatParticipant(Jid jid, ChatRole role, Type type) {
this.jid = jid;
this.role = role;
this.type = type;
}
public static ChatParticipant ofGroup(Jid jid, ChatRole role) {
return new ChatParticipant(jid, role, Type.GROUP);
}
public static ChatParticipant ofCommunity(Jid jid) {
return new ChatParticipant(jid, ChatRole.USER, Type.COMMUNITY);
}
public Jid jid() {
return jid;
}
public ChatRole role() {
return role;
}
public void setRole(ChatRole role) {
switch (type) {
case GROUP -> this.role = role;
case COMMUNITY -> {}
}
}
@Override
public String toString() {
return "ChatParticipant[" +
"value=" + jid +
", role=" + role +
']';
}
@Override
public int hashCode() {
return Objects.hash(jid, role, type);
}
@ProtobufEnum
public enum Type {
GROUP,
COMMUNITY
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatPastParticipant.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* Class representing a past participant in a chat
*/
@ProtobufMessage(name = "PastParticipant")
public final class ChatPastParticipant {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.ENUM)
final Reason reason;
@ProtobufProperty(index = 3, type = ProtobufType.UINT64)
final long timestampSeconds;
ChatPastParticipant(Jid jid, Reason reason, long timestampSeconds) {
this.jid = Objects.requireNonNull(jid, "value cannot be null");
this.reason = Objects.requireNonNull(reason, "reason cannot be null");
this.timestampSeconds = timestampSeconds;
}
public Jid jid() {
return jid;
}
/**
* Returns when the past participant left the chat
*
* @return an optional
*/
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
public Reason reason() {
return reason;
}
public long timestampSeconds() {
return timestampSeconds;
}
@Override
public boolean equals(Object o) {
return o instanceof ChatPastParticipant that
&& Objects.equals(jid, that.jid)
&& Objects.equals(reason, that.reason)
&& timestampSeconds == that.timestampSeconds;
}
@Override
public int hashCode() {
return Objects.hash(jid, reason, timestampSeconds);
}
@Override
public String toString() {
return "ChatPastParticipant[" +
"value=" + jid + ", " +
"reason=" + reason + ", " +
"timestampSeconds=" + timestampSeconds + ']';
}
/**
* Enum representing the errorReason for a past participant leaving the chat.
*/
@ProtobufEnum(name = "PastParticipant.LeaveReason")
public enum Reason {
/**
* The past participant left the chat voluntarily.
*/
LEFT(0),
/**
* The past participant was removed from the chat.
*/
REMOVED(1);
final int index;
Reason(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatRole.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.client.WhatsAppClient;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* The constants of this enumerated type describe the various roles that a {@link ChatParticipant}
* can have in a group. Said roles can be changed using various methods in {@link WhatsAppClient}.
*/
@ProtobufEnum(name = "GroupParticipant.Rank")
public enum ChatRole {
/**
* A participant of the group with no special powers
*/
USER(0, null),
/**
* A participant of the group with administration powers
*/
ADMIN(1, "admin"),
/**
* The founder of the group, also known as super admin
*/
FOUNDER(2, "superadmin");
final int index;
private final String data;
ChatRole(@ProtobufEnumIndex int index, String data) {
this.index = index;
this.data = data;
}
public static ChatRole of(String input) {
return Arrays.stream(values())
.filter(entry -> Objects.equals(entry.data(), input))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("Cannot find GroupRole for %s".formatted(input)));
}
public int index() {
return index;
}
public String data() {
return data;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatSetting.java
================================================
package com.github.auties00.cobalt.model.chat;
/**
* Common interface for chat settings
*/
public sealed interface ChatSetting permits GroupSetting, CommunitySetting {
int index();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatSettingPolicy.java
================================================
package com.github.auties00.cobalt.model.chat;
import it.auties.protobuf.annotation.ProtobufEnum;
/**
* The constants of this enumerated type describe the various policies that can be enforced for a {@link GroupSetting} or {@link CommunitySetting} in a {@link Chat}
*/
@ProtobufEnum
public enum ChatSettingPolicy {
/**
* Allows both admins and users
*/
ANYONE,
/**
* Allows only admins
*/
ADMINS;
/**
* Returns a GroupPolicy based on a boolean value obtained from Whatsapp
*
* @param input the boolean value obtained from Whatsapp
* @return a non-null GroupPolicy
*/
public static ChatSettingPolicy of(boolean input) {
return input ? ADMINS : ANYONE;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/ChatWallpaper.java
================================================
package com.github.auties00.cobalt.model.chat;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that represents the wallpaper of a chat.
*/
@ProtobufMessage(name = "WallpaperSettings")
public final class ChatWallpaper {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String filename;
@ProtobufProperty(index = 2, type = ProtobufType.UINT32)
final int opacity;
ChatWallpaper(String filename, int opacity) {
this.filename = Objects.requireNonNull(filename, "filename cannot be null");
this.opacity = opacity;
}
public String filename() {
return filename;
}
public int opacity() {
return opacity;
}
@Override
public boolean equals(Object o) {
return o instanceof ChatWallpaper that
&& Objects.equals(filename, that.filename)
&& opacity == that.opacity;
}
@Override
public int hashCode() {
return Objects.hash(filename, opacity);
}
@Override
public String toString() {
return "ChatWallpaper[" +
"filename=" + filename +
", opacity=" + opacity +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/CommunityLinkedGroup.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.jid.Jid;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.OptionalInt;
/**
* A model class that represents a group linked to a community
*/
@ProtobufMessage(name = "CommunityLinkedGroup")
public final class CommunityLinkedGroup {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.UINT32)
final Integer participants;
CommunityLinkedGroup(Jid jid, Integer participants) {
this.jid = jid;
this.participants = participants;
}
public Jid jid() {
return jid;
}
public OptionalInt participants() {
return OptionalInt.of(participants);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/CommunitySetting.java
================================================
package com.github.auties00.cobalt.model.chat;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* The constants of this enumerated type describe the various settings that can be toggled for a community
*/
@ProtobufEnum
public enum CommunitySetting implements ChatSetting {
/**
* Who can add/remove groups to/from a community
*/
MODIFY_GROUPS(20),
/**
* Who can add/remove participants to/from a community
*/
ADD_PARTICIPANTS(21);
final int index;
CommunitySetting(@ProtobufEnumIndex int index) {
this.index = index;
}
@Override
public int index() {
return index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/GroupAction.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.client.WhatsAppClient;
import com.github.auties00.cobalt.model.contact.Contact;
import it.auties.protobuf.annotation.ProtobufEnum;
/**
* The constants of this enumerated type describe the various actions that can be executed on a
* {@link Contact} in a {@link Chat}. Said chat should be a group: {@link Chat#isGroupOrCommunity()}. Said
* actions can be executed using various methods in {@link WhatsAppClient}.
*/
@ProtobufEnum
public enum GroupAction {
/**
* Adds a contact to a group
*/
ADD,
/**
* Removes a contact from a group
*/
REMOVE,
/**
* Promotes a contact to admin in a group
*/
PROMOTE,
/**
* Demotes a contact to user in a group
*/
DEMOTE;
/**
* Returns the name of this enumerated constant
*
* @return a lowercase non-null String
*/
public String data() {
return name().toLowerCase();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/GroupOrCommunityMetadata.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.*;
/**
* This model class represents the metadata of a group or community
*/
@ProtobufMessage
public final class GroupOrCommunityMetadata {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String subject;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final Jid subjectAuthorJid;
@ProtobufProperty(index = 4, type = ProtobufType.INT64)
final long subjectTimestampSeconds;
@ProtobufProperty(index = 5, type = ProtobufType.INT64)
final long foundationTimestampSeconds;
@ProtobufProperty(index = 6, type = ProtobufType.STRING)
final Jid founderJid;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 8, type = ProtobufType.STRING)
final String descriptionId;
@ProtobufProperty(index = 9, type = ProtobufType.MAP, mapKeyType = ProtobufType.UINT32, mapValueType = ProtobufType.ENUM)
final Map<Integer, ChatSettingPolicy> settings;
@ProtobufProperty(index = 10, type = ProtobufType.MESSAGE)
final SequencedSet<ChatParticipant> participants;
@ProtobufProperty(index = 12, type = ProtobufType.INT64)
final long ephemeralExpirationSeconds;
@ProtobufProperty(index = 13, type = ProtobufType.STRING)
final Jid parentCommunityJid;
@ProtobufProperty(index = 14, type = ProtobufType.BOOL)
final boolean isCommunity;
@ProtobufProperty(index = 15, type = ProtobufType.MESSAGE)
final SequencedSet<CommunityLinkedGroup> communityGroups;
GroupOrCommunityMetadata(Jid jid, String subject, Jid subjectAuthorJid, long subjectTimestampSeconds, long foundationTimestampSeconds, Jid founderJid, String description, String descriptionId, Map<Integer, ChatSettingPolicy> settings, SequencedSet<ChatParticipant> participants, long ephemeralExpirationSeconds, Jid parentCommunityJid, boolean isCommunity, SequencedSet<CommunityLinkedGroup> communityGroups) {
this.jid = Objects.requireNonNull(jid, "value cannot be null");
this.subject = Objects.requireNonNull(subject, "subject cannot be null");
this.subjectAuthorJid = subjectAuthorJid;
this.subjectTimestampSeconds = subjectTimestampSeconds;
this.foundationTimestampSeconds = foundationTimestampSeconds;
this.founderJid = founderJid;
this.description = description;
this.descriptionId = descriptionId;
this.settings = Objects.requireNonNullElseGet(settings, HashMap::new);
this.participants = Objects.requireNonNullElseGet(participants, LinkedHashSet::new);
this.ephemeralExpirationSeconds = ephemeralExpirationSeconds;
this.parentCommunityJid = parentCommunityJid;
this.isCommunity = isCommunity;
this.communityGroups = Objects.requireNonNullElseGet(communityGroups, LinkedHashSet::new);
}
public Jid jid() {
return jid;
}
public String subject() {
return subject;
}
public Optional<Jid> subjectAuthorJid() {
return Optional.ofNullable(subjectAuthorJid);
}
public long subjectTimestampSeconds() {
return subjectTimestampSeconds;
}
public Optional<ZonedDateTime> subjectTimestamp() {
return Clock.parseSeconds(subjectTimestampSeconds);
}
public long foundationTimestampSeconds() {
return foundationTimestampSeconds;
}
public Optional<ZonedDateTime> foundationTimestamp() {
return Clock.parseSeconds(foundationTimestampSeconds);
}
public Optional<Jid> founder() {
return Optional.ofNullable(founderJid);
}
public Optional<String> description() {
return Optional.ofNullable(description);
}
public Optional<String> descriptionId() {
return Optional.ofNullable(descriptionId);
}
public Optional<ChatSettingPolicy> getPolicy(ChatSetting setting) {
return Optional.ofNullable(settings.get(setting.index()));
}
public Set<ChatParticipant> participants() {
return Collections.unmodifiableSet(participants);
}
public void addParticipant(ChatParticipant participant) {
participants.add(participant);
}
public boolean removeParticipant(ChatParticipant participant) {
return participants.remove(participant);
}
public boolean removeParticipant(Jid jid) {
return participants.removeIf(participant -> participant.jid().equals(jid));
}
public long ephemeralExpirationSeconds() {
return ephemeralExpirationSeconds;
}
public Optional<ZonedDateTime> ephemeralExpiration() {
return Clock.parseSeconds(ephemeralExpirationSeconds);
}
public Optional<Jid> parentCommunityJid() {
return Optional.ofNullable(parentCommunityJid);
}
public boolean isCommunity() {
return isCommunity;
}
public SequencedSet<CommunityLinkedGroup> communityGroups() {
return Collections.unmodifiableSequencedSet(communityGroups);
}
public void addCommunityGroup(CommunityLinkedGroup group) {
communityGroups.add(group);
}
public boolean removeCommunityGroup(CommunityLinkedGroup group) {
return communityGroups.remove(group);
}
public boolean removeCommunityGroup(Jid jid) {
return communityGroups.removeIf(group -> group.jid().equals(jid));
}
@Override
public boolean equals(Object o) {
return o instanceof GroupOrCommunityMetadata that
&& Objects.equals(jid, that.jid)
&& Objects.equals(subject, that.subject)
&& Objects.equals(subjectAuthorJid, that.subjectAuthorJid)
&& Objects.equals(subjectTimestampSeconds, that.subjectTimestampSeconds)
&& Objects.equals(foundationTimestampSeconds, that.foundationTimestampSeconds)
&& Objects.equals(founderJid, that.founderJid)
&& Objects.equals(description, that.description)
&& Objects.equals(descriptionId, that.descriptionId)
&& Objects.equals(settings, that.settings)
&& Objects.equals(participants, that.participants)
&& Objects.equals(ephemeralExpirationSeconds, that.ephemeralExpirationSeconds)
&& Objects.equals(parentCommunityJid, that.parentCommunityJid)
&& isCommunity == that.isCommunity
&& Objects.equals(communityGroups, that.communityGroups);
}
@Override
public int hashCode() {
return Objects.hash(jid, subject, subjectAuthorJid, subjectTimestampSeconds, foundationTimestampSeconds,
founderJid, description, descriptionId, settings, participants,
ephemeralExpirationSeconds, parentCommunityJid, isCommunity, communityGroups);
}
@Override
public String toString() {
return "ChatMetadata[" +
"value=" + jid + ", " +
"subject=" + subject + ", " +
"subjectAuthor=" + subjectAuthorJid + ", " +
"subjectTimestamp=" + subjectTimestampSeconds + ", " +
"foundationTimestamp=" + foundationTimestampSeconds + ", " +
"founder=" + founderJid + ", " +
"description=" + description + ", " +
"descriptionId=" + descriptionId + ", " +
"settings=" + settings + ", " +
"participants=" + participants + ", " +
"ephemeralExpiration=" + ephemeralExpirationSeconds + ", " +
"parentCommunityJid=" + parentCommunityJid + ", " +
"isCommunity=" + isCommunity + ", " +
"communityGroups=" + communityGroups + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/GroupPastParticipants.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.model.jid.Jid;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* Class representing a list of past participants in a chat group
*/
@ProtobufMessage(name = "PastParticipants")
public final class GroupPastParticipants {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid groupJid;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final List<ChatPastParticipant> pastParticipants;
GroupPastParticipants(Jid groupJid, List<ChatPastParticipant> pastParticipants) {
this.groupJid = Objects.requireNonNull(groupJid, "groupJid cannot be null");
this.pastParticipants = Objects.requireNonNullElse(pastParticipants, List.of());
}
public Jid groupJid() {
return groupJid;
}
public List<ChatPastParticipant> pastParticipants() {
return pastParticipants;
}
@Override
public boolean equals(Object o) {
return o instanceof GroupPastParticipants that
&& Objects.equals(groupJid, that.groupJid)
&& Objects.equals(pastParticipants, that.pastParticipants);
}
@Override
public int hashCode() {
return Objects.hash(groupJid, pastParticipants);
}
@Override
public String toString() {
return "GroupPastParticipants[" +
"groupJid=" + groupJid + ", " +
"pastParticipants=" + pastParticipants + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/chat/GroupSetting.java
================================================
package com.github.auties00.cobalt.model.chat;
import com.github.auties00.cobalt.client.WhatsAppClient;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* The constants of this enumerated type describe the various settings that can be toggled for a
* group. Said settings can be changed using various methods in {@link WhatsAppClient}.
*/
@ProtobufEnum
public enum GroupSetting implements ChatSetting {
/**
* Who can edit the metadata of a group
*/
EDIT_GROUP_INFO(0),
/**
* Who can send messages in a group
*/
SEND_MESSAGES(1),
/**
* Who can add new members
*/
ADD_PARTICIPANTS(2),
/**
* Who can accept new members
*/
APPROVE_PARTICIPANTS(3);
final int index;
GroupSetting(@ProtobufEnumIndex int index) {
this.index = index;
}
@Override
public int index() {
return index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/contact/Contact.java
================================================
package com.github.auties00.cobalt.model.contact;
import com.github.auties00.cobalt.client.WhatsAppClient;
import com.github.auties00.cobalt.model.chat.Chat;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.jid.JidProvider;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a Contact. This class is only a model, this means that changing its
* values will have no real effect on WhatsappWeb's servers.
*/
@ProtobufMessage
public final class Contact implements JidProvider {
/**
* The non-null unique value used to identify this contact
*/
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final Jid jid;
/**
* The nullable name specified by this contact when he created a Whatsapp account. Theoretically,
* it should not be possible for this field to be null as it's required when registering for
* Whatsapp. Though it looks that it can be removed later, so it's nullable.
*/
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
String chosenName;
/**
* The nullable name associated with this contact on the phone connected with Whatsapp
*/
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
String fullName;
/**
* The nullable short name associated with this contact on the phone connected with Whatsapp If a
* name is available, theoretically, also a short name should be
*/
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
String shortName;
/**
* The nullable last known presence of this contact. This field is associated only with the
* presence of this contact in the corresponding conversation. If, for example, this contact is
* composing, recording or paused in a group this field will not be affected. Instead,
* {@link Chat#getPresence(JidProvider)} should be used. By default, Whatsapp will not send updates about a
* contact's status unless they send a message or are in the recent contacts. To force Whatsapp to
* send updates, use {@link WhatsAppClient#subscribeToPresence(JidProvider)}.
*/
@ProtobufProperty(index = 5, type = ProtobufType.ENUM)
ContactStatus lastKnownPresence;
/**
* The nullable last seconds this contact was seen available. Any contact can decide to hide this
* information in their privacy settings.
*/
@ProtobufProperty(index = 6, type = ProtobufType.UINT64)
long lastSeenSeconds;
/**
* Whether this contact is blocked
*/
@ProtobufProperty(index = 7, type = ProtobufType.BOOL)
boolean blocked;
/**
* Whether the status updates of the contact are muted
*/
@ProtobufProperty(index = 8, type = ProtobufType.BOOL)
boolean statusMuted;
Contact(Jid jid, String chosenName, String fullName, String shortName, ContactStatus lastKnownPresence, long lastSeenSeconds, boolean blocked, boolean statusMuted) {
this.jid = Objects.requireNonNull(jid, "value cannot be null");
this.chosenName = chosenName;
this.fullName = fullName;
this.shortName = shortName;
this.lastKnownPresence = Objects.requireNonNullElse(lastKnownPresence, ContactStatus.UNAVAILABLE);
this.lastSeenSeconds = lastSeenSeconds;
this.blocked = blocked;
this.statusMuted = statusMuted;
}
public Jid jid() {
return this.jid;
}
public String name() {
if (shortName != null) {
return shortName;
}
if (fullName != null) {
return fullName;
}
if (chosenName != null) {
return chosenName;
}
return jid().user();
}
public long lastSeenSeconds() {
return lastSeenSeconds;
}
public Optional<ZonedDateTime> lastSeen() {
return Clock.parseSeconds(lastSeenSeconds);
}
public Optional<String> chosenName() {
return Optional.ofNullable(this.chosenName);
}
public Optional<String> fullName() {
return Optional.ofNullable(this.fullName);
}
public Optional<String> shortName() {
return Optional.ofNullable(this.shortName);
}
public ContactStatus lastKnownPresence() {
return this.lastKnownPresence;
}
public boolean blocked() {
return this.blocked;
}
public void setChosenName(String chosenName) {
this.chosenName = chosenName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public void setShortName(String shortName) {
this.shortName = shortName;
}
public void setLastKnownPresence(ContactStatus lastKnownPresence) {
this.lastKnownPresence = lastKnownPresence;
}
public void setLastSeen(ZonedDateTime lastSeen) {
this.lastSeenSeconds = lastSeen.toEpochSecond();
}
public void setBlocked(boolean blocked) {
this.blocked = blocked;
}
public boolean statusMuted() {
return statusMuted;
}
public void setStatusMuted(boolean statusMuted) {
this.statusMuted = statusMuted;
}
@Override
public int hashCode() {
return Objects.hashCode(this.jid());
}
public boolean equals(Object other) {
return other instanceof Contact that && Objects.equals(this.jid(), that.jid());
}
@Override
public Jid toJid() {
return jid();
}
public boolean hasName(String name) {
return name != null
&& (name.equals(fullName) || name.equals(shortName) || name.equals(chosenName));
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/contact/ContactCard.java
================================================
package com.github.auties00.cobalt.model.contact;
import com.github.auties00.cobalt.model.jid.Jid;
import ezvcard.Ezvcard;
import ezvcard.VCard;
import ezvcard.VCardVersion;
import ezvcard.property.Telephone;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A model class to represent and build the vcard of a contact
*/
public sealed interface ContactCard {
String BUSINESS_NAME_VCARD_PROPERTY = "X-WA-BIZ-NAME";
String PHONE_NUMBER_VCARD_PROPERTY = "WAID";
String DEFAULT_NUMBER_VCARD_TYPE = "CELL";
/**
* Parses a vcard
* If the vCard dependency wasn't included, or a parsing error occurs, a raw representation is returned
*
* @param vcard the non-null vcard to parse
* @return a non-null vcard
*/
@ProtobufDeserializer
static ContactCard of(String vcard) {
try {
if(vcard == null) {
return null;
}
var parsed = Ezvcard.parse(vcard).first();
var version = Objects.requireNonNullElse(parsed.getVersion().getVersion(), VCardVersion.V3_0.getVersion());
var name = parsed.getFormattedName().getValue();
var phoneNumbers = parsed.getTelephoneNumbers()
.stream()
.filter(ContactCard::isValidPhoneNumber)
.collect(Collectors.toUnmodifiableMap(ContactCard::getPhoneType, ContactCard::getPhoneValue, ContactCard::joinPhoneNumbers));
var businessName = parsed.getExtendedProperty(BUSINESS_NAME_VCARD_PROPERTY);
return new Parsed(version, name, phoneNumbers, businessName != null ? businessName.getValue() : null);
} catch (Throwable ignored) {
return new Raw(vcard);
}
}
/**
* Creates a new vcard
*
* @param name the nullable name of the contact
* @param phoneNumber the non-null phone value of the contact
* @return a vcard
*/
static ContactCard of(String name, Jid phoneNumber) {
return of(name, phoneNumber, null);
}
/**
* Creates a new vcard
*
* @param name the nullable name of the contact
* @param phoneNumber the non-null phone value of the contact
* @param businessName the nullable business name of the contact
* @return a vcard
*/
static ContactCard of(String name, Jid phoneNumber, String businessName) {
return new Parsed(
VCardVersion.V3_0.getVersion(),
name,
Map.of(DEFAULT_NUMBER_VCARD_TYPE, List.of(Objects.requireNonNull(phoneNumber))),
businessName
);
}
private static boolean isValidPhoneNumber(Telephone entry) {
return getPhoneType(entry) != null && entry.getParameter(PHONE_NUMBER_VCARD_PROPERTY) != null;
}
private static String getPhoneType(Telephone entry) {
return entry.getParameters().getType();
}
private static List<Jid> getPhoneValue(Telephone entry) {
return List.of(Jid.of(entry.getParameter(PHONE_NUMBER_VCARD_PROPERTY)));
}
private static List<Jid> joinPhoneNumbers(List<Jid> first, List<Jid> second) {
return Stream.of(first, second).flatMap(Collection::stream).toList();
}
@ProtobufSerializer
String toVcard();
/**
* A parsed representation of the vcard
*/
final class Parsed implements ContactCard {
private final String version;
private final String name;
private final Map<String, List<Jid>> phoneNumbers;
private final String businessName;
private Parsed(String version, String name, Map<String, List<Jid>> phoneNumbers, String businessName) {
this.version = version;
this.name = name;
this.phoneNumbers = phoneNumbers;
this.businessName = businessName;
}
public String version() {
return version;
}
public Optional<String> name() {
return Optional.ofNullable(name);
}
public Optional<String> businessName() {
return Optional.ofNullable(businessName);
}
public List<Jid> phoneNumbers(String type) {
return phoneNumbers.getOrDefault(type, List.of());
}
public List<Jid> phoneNumbers() {
return Objects.requireNonNullElseGet(phoneNumbers.get(DEFAULT_NUMBER_VCARD_TYPE), List::of);
}
public void addPhoneNumber(Jid contact) {
addPhoneNumber(DEFAULT_NUMBER_VCARD_TYPE, contact);
}
public void addPhoneNumber(String category, Jid contact) {
var oldValue = phoneNumbers.get(category);
if (oldValue == null) {
phoneNumbers.put(category, List.of(contact));
return;
}
var values = new ArrayList<>(oldValue);
values.add(contact);
phoneNumbers.put(category, Collections.unmodifiableList(values));
}
/**
* Converts this object in a valid vcard
*
* @return a non-null String
*/
@Override
@ProtobufSerializer
public String toVcard() {
var vcard = new VCard();
vcard.setVersion(VCardVersion.valueOfByStr(version()));
vcard.setFormattedName(name);
phoneNumbers.forEach((type, contacts) -> {
for(var contact : contacts) {
contact.toPhoneNumber().ifPresent(phoneNumber -> {
var telephone = new Telephone(phoneNumber);
telephone.getParameters().setType(type);
telephone.getParameters().put(PHONE_NUMBER_VCARD_PROPERTY, contact.user());
vcard.addTelephoneNumber(telephone);
});
}
});
if(businessName != null) {
vcard.addExtendedProperty(BUSINESS_NAME_VCARD_PROPERTY, businessName);
}
return Ezvcard.write(vcard)
.go();
}
@Override
public boolean equals(Object o) {
return o instanceof Parsed parsed
&& Objects.equals(version, parsed.version)
&& Objects.equals(name, parsed.name)
&& Objects.equals(phoneNumbers, parsed.phoneNumbers)
&& Objects.equals(businessName, parsed.businessName);
}
@Override
public int hashCode() {
return Objects.hash(version, name, phoneNumbers, businessName);
}
@Override
public String toString() {
return "ContactCard[" +
"version=" + version + ", " +
"name=" + name + ", " +
"phoneNumbers=" + phoneNumbers + ", " +
"businessName=" + businessName + ']';
}
}
/**
* A raw representation of the vcard
*/
final class Raw implements ContactCard {
private final String toVcard;
private Raw(String toVcard) {
this.toVcard = toVcard;
}
@Override
@ProtobufSerializer
public String toVcard() {
return toVcard;
}
@Override
public boolean equals(Object o) {
return o instanceof Raw raw
&& Objects.equals(toVcard, raw.toVcard);
}
@Override
public int hashCode() {
return Objects.hashCode(toVcard);
}
@Override
public String toString() {
return "Raw[" +
"toVcard=" + toVcard + ']';
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/contact/ContactStatus.java
================================================
package com.github.auties00.cobalt.model.contact;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import java.util.Arrays;
import java.util.Optional;
/**
* The constants of this enumerated type describe the various status that a {@link Contact} can be
* in
*/
@ProtobufEnum
public enum ContactStatus {
/**
* When the contact is online
*/
AVAILABLE(0),
/**
* When the contact is offline
*/
UNAVAILABLE(1),
/**
* When the contact is writing a text message
*/
COMPOSING(2),
/**
* When the contact is recording an audio message
*/
RECORDING(3);
final int index;
ContactStatus(@ProtobufEnumIndex int index) {
this.index = index;
}
public static Optional<ContactStatus> of(String name) {
return Arrays.stream(values())
.filter(entry -> entry.name().equalsIgnoreCase(name))
.findFirst();
}
@Override
public String toString() {
return name().toLowerCase();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/AdReplyInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds the information related to an companion reply.
*/
@ProtobufMessage(name = "ContextInfo.AdReplyInfo")
public final class AdReplyInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String advertiserName;
@ProtobufProperty(index = 2, type = ProtobufType.ENUM)
final MediaType mediaType;
@ProtobufProperty(index = 16, type = ProtobufType.BYTES)
final byte[] thumbnail;
@ProtobufProperty(index = 17, type = ProtobufType.STRING)
final String caption;
AdReplyInfo(String advertiserName, MediaType mediaType, byte[] thumbnail, String caption) {
this.advertiserName = Objects.requireNonNull(advertiserName, "advertiserName cannot be null");
this.mediaType = Objects.requireNonNull(mediaType, "mediaType cannot be null");
this.thumbnail = thumbnail;
this.caption = caption;
}
public String advertiserName() {
return advertiserName;
}
public MediaType mediaType() {
return mediaType;
}
public Optional<byte[]> thumbnail() {
return Optional.ofNullable(thumbnail);
}
public Optional<String> caption() {
return Optional.ofNullable(caption);
}
@Override
public boolean equals(Object o) {
return o instanceof AdReplyInfo that
&& Objects.equals(advertiserName, that.advertiserName)
&& Objects.equals(mediaType, that.mediaType)
&& Arrays.equals(thumbnail, that.thumbnail)
&& Objects.equals(caption, that.caption);
}
@Override
public int hashCode() {
return Objects.hash(advertiserName, mediaType, Arrays.hashCode(thumbnail), caption);
}
@Override
public String toString() {
return "AdReplyInfo[" +
"advertiserName=" + advertiserName +
", mediaType=" + mediaType +
", thumbnail=" + Arrays.toString(thumbnail) +
", caption=" + caption +
']';
}
/**
* The constants of this enumerated type describe the various types of companion that a
* {@link AdReplyInfo} can link to
*/
@ProtobufEnum(name = "ContextInfo.AdReplyInfo.MediaType")
public enum MediaType {
/**
* Unknown type
*/
NONE(0),
/**
* Image type
*/
IMAGE(1),
/**
* Video type
*/
VIDEO(2);
final int index;
MediaType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/BusinessAccountLinkInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds a payload about a business link info.
*/
@ProtobufMessage(name = "BizAccountLinkInfo")
public final class BusinessAccountLinkInfo {
@ProtobufProperty(index = 1, type = ProtobufType.UINT64)
final long businessId;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String phoneNumber;
@ProtobufProperty(index = 3, type = ProtobufType.UINT64)
final long issueTimeSeconds;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
final HostStorageType hostStorage;
@ProtobufProperty(index = 5, type = ProtobufType.ENUM)
final AccountType accountType;
BusinessAccountLinkInfo(long businessId, String phoneNumber, long issueTimeSeconds, HostStorageType hostStorage, AccountType accountType) {
this.businessId = businessId;
this.phoneNumber = Objects.requireNonNull(phoneNumber, "phoneNumber cannot be null");
this.issueTimeSeconds = issueTimeSeconds;
this.hostStorage = Objects.requireNonNull(hostStorage, "hostStorage cannot be null");
this.accountType = Objects.requireNonNull(accountType, "accountType cannot be null");
}
public long businessId() {
return businessId;
}
public String phoneNumber() {
return phoneNumber;
}
public long issueTimeSeconds() {
return issueTimeSeconds;
}
public HostStorageType hostStorage() {
return hostStorage;
}
public AccountType accountType() {
return accountType;
}
/**
* Returns this object's timestampSeconds
*
* @return an optional
*/
public Optional<ZonedDateTime> issueTime() {
return Clock.parseSeconds(issueTimeSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessAccountLinkInfo that
&& businessId == that.businessId
&& Objects.equals(phoneNumber, that.phoneNumber)
&& issueTimeSeconds == that.issueTimeSeconds
&& Objects.equals(hostStorage, that.hostStorage)
&& Objects.equals(accountType, that.accountType);
}
@Override
public int hashCode() {
return Objects.hash(businessId, phoneNumber, issueTimeSeconds, hostStorage, accountType);
}
@Override
public String toString() {
return "BusinessAccountLinkInfo[" +
"businessId=" + businessId +
", phoneNumber=" + phoneNumber +
", issueTimeSeconds=" + issueTimeSeconds +
", hostStorage=" + hostStorage +
", accountType=" + accountType +
']';
}
/**
* The constants of this enumerated type describe the various types of business accounts
*/
@ProtobufEnum(name = "BizAccountLinkInfo.AccountType")
public enum AccountType {
/**
* Enterprise
*/
ENTERPRISE(0),
/**
* Page
*/
PAGE(1);
final int index;
AccountType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
@ProtobufEnum(name = "BizAccountLinkInfo.HostStorageType")
public enum HostStorageType {
/**
* Hosted on a private server ("On-Premise")
*/
ON_PREMISE(0),
/**
* Hosted by facebook
*/
FACEBOOK(1);
final int index;
HostStorageType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/BusinessIdentityInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.business.BusinessVerifiedNameCertificate;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds the information related to the identity of a business account.
*/
@ProtobufMessage(name = "BizIdentityInfo")
public final class BusinessIdentityInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.ENUM)
final VerifiedLevel level;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final BusinessVerifiedNameCertificate certificate;
@ProtobufProperty(index = 3, type = ProtobufType.BOOL)
final boolean signed;
@ProtobufProperty(index = 4, type = ProtobufType.BOOL)
final boolean revoked;
@ProtobufProperty(index = 5, type = ProtobufType.ENUM)
final HostStorageType hostStorage;
@ProtobufProperty(index = 6, type = ProtobufType.ENUM)
final ActorsType actualActors;
@ProtobufProperty(index = 7, type = ProtobufType.UINT64)
final long privacyModeTimestampSeconds;
@ProtobufProperty(index = 8, type = ProtobufType.UINT64)
final long featureControls;
BusinessIdentityInfo(VerifiedLevel level, BusinessVerifiedNameCertificate certificate, boolean signed, boolean revoked, HostStorageType hostStorage, ActorsType actualActors, long privacyModeTimestampSeconds, long featureControls) {
this.level = Objects.requireNonNullElse(level, VerifiedLevel.UNKNOWN);
this.certificate = Objects.requireNonNull(certificate, "certificate cannot be null");
this.signed = signed;
this.revoked = revoked;
this.hostStorage = Objects.requireNonNull(hostStorage, "hostStorage cannot be null");
this.actualActors = Objects.requireNonNull(actualActors, "actualActors cannot be null");
this.privacyModeTimestampSeconds = privacyModeTimestampSeconds;
this.featureControls = featureControls;
}
public VerifiedLevel level() {
return level;
}
public BusinessVerifiedNameCertificate certificate() {
return certificate;
}
public boolean signed() {
return signed;
}
public boolean revoked() {
return revoked;
}
public HostStorageType hostStorage() {
return hostStorage;
}
public ActorsType actualActors() {
return actualActors;
}
public long privacyModeTimestampSeconds() {
return privacyModeTimestampSeconds;
}
public long featureControls() {
return featureControls;
}
/**
* Returns the privacy mode timestampSeconds
*
* @return an optional
*/
public Optional<ZonedDateTime> privacyModeTimestamp() {
return Clock.parseSeconds(privacyModeTimestampSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof BusinessIdentityInfo that
&& Objects.equals(level, that.level)
&& Objects.equals(certificate, that.certificate)
&& signed == that.signed
&& revoked == that.revoked
&& Objects.equals(hostStorage, that.hostStorage)
&& Objects.equals(actualActors, that.actualActors)
&& privacyModeTimestampSeconds == that.privacyModeTimestampSeconds
&& featureControls == that.featureControls;
}
@Override
public int hashCode() {
return Objects.hash(level, certificate, signed, revoked, hostStorage, actualActors,
privacyModeTimestampSeconds, featureControls);
}
@Override
public String toString() {
return "BusinessIdentityInfo[" +
"level=" + level +
", certificate=" + certificate +
", signed=" + signed +
", revoked=" + revoked +
", hostStorage=" + hostStorage +
", actualActors=" + actualActors +
", privacyModeTimestampSeconds=" + privacyModeTimestampSeconds +
", featureControls=" + featureControls +
']';
}
/**
* The constants of this enumerated type describe the various types of actors of a business account
*/
@ProtobufEnum(name = "BizIdentityInfo.ActualActorsType")
public enum ActorsType {
/**
* Self
*/
SELF(0),
/**
* Bsp
*/
BSP(1);
final int index;
ActorsType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
/**
* The constants of this enumerated type describe the various types of verification that a business
* account can have
*/
@ProtobufEnum(name = "BizIdentityInfo.VerifiedLevelValue")
public enum VerifiedLevel {
/**
* Unknown
*/
UNKNOWN(0),
/**
* Low
*/
LOW(1),
/**
* High
*/
HIGH(2);
final int index;
VerifiedLevel(@ProtobufEnumIndex int index) {
this.index = index;
}
}
@ProtobufEnum(name = "BizIdentityInfo.HostStorageType")
public enum HostStorageType {
/**
* Hosted on a private server ("On-Premise")
*/
ON_PREMISE(0),
/**
* Hosted by facebook
*/
FACEBOOK(1);
final int index;
HostStorageType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/ChatMessageInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.business.BusinessPrivacyStatus;
import com.github.auties00.cobalt.model.chat.Chat;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.media.MediaData;
import com.github.auties00.cobalt.model.message.model.*;
import com.github.auties00.cobalt.model.message.model.*;
import com.github.auties00.cobalt.model.message.model.*;
import com.github.auties00.cobalt.model.message.standard.LiveLocationMessage;
import com.github.auties00.cobalt.model.message.standard.ReactionMessage;
import com.github.auties00.cobalt.model.poll.PollAdditionalMetadata;
import com.github.auties00.cobalt.model.poll.PollUpdate;
import com.github.auties00.cobalt.model.sync.PhotoChange;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.*;
import static java.util.Objects.requireNonNullElseGet;
/**
* A model class that holds the information related to a {@link Message}.
*/
@ProtobufMessage(name = "WebMessageInfo")
public final class ChatMessageInfo implements MessageInfo { // TODO: Check me
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final ChatMessageKey key;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
MessageContainer message;
@ProtobufProperty(index = 3, type = ProtobufType.UINT64)
final long timestampSeconds;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
MessageStatus status;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final Jid senderJid;
@ProtobufProperty(index = 6, type = ProtobufType.UINT64)
final long messageC2STimestamp;
@ProtobufProperty(index = 16, type = ProtobufType.BOOL)
boolean ignore;
@ProtobufProperty(index = 17, type = ProtobufType.BOOL)
boolean starred;
@ProtobufProperty(index = 18, type = ProtobufType.BOOL)
final boolean broadcast;
@ProtobufProperty(index = 19, type = ProtobufType.STRING)
final String pushName;
@ProtobufProperty(index = 20, type = ProtobufType.BYTES)
final byte[] mediaCiphertextSha256;
@ProtobufProperty(index = 21, type = ProtobufType.BOOL)
final boolean multicast;
@ProtobufProperty(index = 22, type = ProtobufType.BOOL)
final boolean urlText;
@ProtobufProperty(index = 23, type = ProtobufType.BOOL)
final boolean urlNumber;
@ProtobufProperty(index = 24, type = ProtobufType.ENUM)
final MessageInfoStubType stubType;
@ProtobufProperty(index = 25, type = ProtobufType.BOOL)
final boolean clearMedia;
@ProtobufProperty(index = 26, type = ProtobufType.STRING)
final List<String> stubParameters;
@ProtobufProperty(index = 27, type = ProtobufType.UINT32)
final int duration;
@ProtobufProperty(index = 28, type = ProtobufType.STRING)
final List<String> labels;
@ProtobufProperty(index = 29, type = ProtobufType.MESSAGE)
final PaymentInfo paymentInfo;
@ProtobufProperty(index = 30, type = ProtobufType.MESSAGE)
final LiveLocationMessage finalLiveLocation;
@ProtobufProperty(index = 31, type = ProtobufType.MESSAGE)
final PaymentInfo quotedPaymentInfo;
@ProtobufProperty(index = 32, type = ProtobufType.UINT64)
final long ephemeralStartTimestamp;
@ProtobufProperty(index = 33, type = ProtobufType.UINT32)
final int ephemeralDuration;
@ProtobufProperty(index = 34, type = ProtobufType.BOOL)
final boolean enableEphemeral;
@ProtobufProperty(index = 35, type = ProtobufType.BOOL)
final boolean ephemeralOutOfSync;
@ProtobufProperty(index = 36, type = ProtobufType.ENUM)
final BusinessPrivacyStatus businessPrivacyStatus;
@ProtobufProperty(index = 37, type = ProtobufType.STRING)
final String businessVerifiedName;
@ProtobufProperty(index = 38, type = ProtobufType.MESSAGE)
final MediaData mediaData;
@ProtobufProperty(index = 39, type = ProtobufType.MESSAGE)
final PhotoChange photoChange;
@ProtobufProperty(index = 40, type = ProtobufType.MESSAGE)
final MessageReceipt receipt;
@ProtobufProperty(index = 41, type = ProtobufType.MESSAGE)
final List<ReactionMessage> reactions;
@ProtobufProperty(index = 42, type = ProtobufType.MESSAGE)
final MediaData quotedStickerData;
@ProtobufProperty(index = 43, type = ProtobufType.BYTES)
final byte[] futureProofData;
@ProtobufProperty(index = 44, type = ProtobufType.MESSAGE)
final PublicServiceAnnouncementStatus psaStatus;
@ProtobufProperty(index = 45, type = ProtobufType.MESSAGE)
final List<PollUpdate> pollUpdates;
@ProtobufProperty(index = 46, type = ProtobufType.MESSAGE)
PollAdditionalMetadata pollAdditionalMetadata;
@ProtobufProperty(index = 47, type = ProtobufType.STRING)
final String agentId;
@ProtobufProperty(index = 48, type = ProtobufType.BOOL)
final boolean statusAlreadyViewed;
@ProtobufProperty(index = 49, type = ProtobufType.BYTES)
byte[] messageSecret;
@ProtobufProperty(index = 50, type = ProtobufType.MESSAGE)
final KeepInChat keepInChat;
@ProtobufProperty(index = 51, type = ProtobufType.STRING)
final Jid originalSender;
@ProtobufProperty(index = 52, type = ProtobufType.UINT64)
long revokeTimestampSeconds;
private Chat chat;
private Contact sender;
ChatMessageInfo(ChatMessageKey key, MessageContainer message, long timestampSeconds, MessageStatus status, Jid senderJid, long messageC2STimestamp, boolean ignore, boolean starred, boolean broadcast, String pushName, byte[] mediaCiphertextSha256, boolean multicast, boolean urlText, boolean urlNumber, MessageInfoStubType stubType, boolean clearMedia, List<String> stubParameters, int duration, List<String> labels, PaymentInfo paymentInfo, LiveLocationMessage finalLiveLocation, PaymentInfo quotedPaymentInfo, long ephemeralStartTimestamp, int ephemeralDuration, boolean enableEphemeral, boolean ephemeralOutOfSync, BusinessPrivacyStatus businessPrivacyStatus, String businessVerifiedName, MediaData mediaData, PhotoChange photoChange, MessageReceipt receipt, List<ReactionMessage> reactions, MediaData quotedStickerData, byte[] futureProofData, PublicServiceAnnouncementStatus psaStatus, List<PollUpdate> pollUpdates, PollAdditionalMetadata pollAdditionalMetadata, String agentId, boolean statusAlreadyViewed, byte[] messageSecret, KeepInChat keepInChat, Jid originalSender, long revokeTimestampSeconds) {
this.key = key;
this.message = Objects.requireNonNullElseGet(message, MessageContainer::empty);
this.timestampSeconds = timestampSeconds;
this.status = status;
this.senderJid = senderJid;
this.messageC2STimestamp = messageC2STimestamp;
this.ignore = ignore;
this.starred = starred;
this.broadcast = broadcast;
this.pushName = pushName;
this.mediaCiphertextSha256 = mediaCiphertextSha256;
this.multicast = multicast;
this.urlText = urlText;
this.urlNumber = urlNumber;
this.stubType = stubType;
this.clearMedia = clearMedia;
this.stubParameters = stubParameters;
this.duration = duration;
this.labels = labels;
this.paymentInfo = paymentInfo;
this.finalLiveLocation = finalLiveLocation;
this.quotedPaymentInfo = quotedPaymentInfo;
this.ephemeralStartTimestamp = ephemeralStartTimestamp;
this.ephemeralDuration = ephemeralDuration;
this.enableEphemeral = enableEphemeral;
this.ephemeralOutOfSync = ephemeralOutOfSync;
this.businessPrivacyStatus = businessPrivacyStatus;
this.businessVerifiedName = businessVerifiedName;
this.mediaData = mediaData;
this.photoChange = photoChange;
this.receipt = Objects.requireNonNullElseGet(receipt, MessageReceipt::new);
this.reactions = reactions;
this.quotedStickerData = quotedStickerData;
this.futureProofData = futureProofData;
this.psaStatus = psaStatus;
this.pollUpdates = pollUpdates;
this.pollAdditionalMetadata = pollAdditionalMetadata;
this.agentId = agentId;
this.statusAlreadyViewed = statusAlreadyViewed;
this.messageSecret = messageSecret;
this.keepInChat = keepInChat;
this.originalSender = originalSender;
this.revokeTimestampSeconds = revokeTimestampSeconds;
}
/**
* Determines whether the message was sent by you or by someone else
*
* @return a boolean
*/
public boolean fromMe() {
return key.fromMe();
}
/**
* Returns the name of the chat where this message is or its pretty value
*
* @return a non-null String
*/
public String chatName() {
if (chat != null) {
return chat.name();
}
return chatJid().user();
}
/**
* Returns the value of the contact or group that sent the message.
*
* @return a non-null ContactJid
*/
public Jid chatJid() {
return key.chatJid();
}
/**
* Returns the name of the person that sent this message or its pretty value
*
* @return a non-null String
*/
public String senderName() {
return sender().map(Contact::name).orElseGet(senderJid()::user);
}
/**
* Returns the timestampSeconds for this message
*
* @return an optional
*/
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
/**
* Returns the timestampSeconds for this message
*
* @return an optional
*/
public Optional<ZonedDateTime> revokeTimestamp() {
return Clock.parseSeconds(revokeTimestampSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof ChatMessageInfo that &&
timestampSeconds == that.timestampSeconds &&
messageC2STimestamp == that.messageC2STimestamp &&
ignore == that.ignore &&
starred == that.starred &&
broadcast == that.broadcast &&
multicast == that.multicast &&
urlText == that.urlText &&
urlNumber == that.urlNumber &&
clearMedia == that.clearMedia &&
duration == that.duration &&
ephemeralStartTimestamp == that.ephemeralStartTimestamp &&
ephemeralDuration == that.ephemeralDuration &&
enableEphemeral == that.enableEphemeral &&
ephemeralOutOfSync == that.ephemeralOutOfSync &&
statusAlreadyViewed == that.statusAlreadyViewed &&
revokeTimestampSeconds == that.revokeTimestampSeconds &&
Objects.equals(key, that.key) &&
Objects.equals(message, that.message) &&
status == that.status &&
Objects.equals(senderJid, that.senderJid) &&
Objects.equals(pushName, that.pushName) &&
Objects.deepEquals(mediaCiphertextSha256, that.mediaCiphertextSha256) &&
stubType == that.stubType &&
Objects.equals(stubParameters, that.stubParameters) &&
Objects.equals(labels, that.labels) &&
Objects.equals(paymentInfo, that.paymentInfo) &&
Objects.equals(finalLiveLocation, that.finalLiveLocation) &&
Objects.equals(quotedPaymentInfo, that.quotedPaymentInfo) &&
businessPrivacyStatus == that.businessPrivacyStatus &&
Objects.equals(businessVerifiedName, that.businessVerifiedName) &&
Objects.equals(mediaData, that.mediaData) &&
Objects.equals(photoChange, that.photoChange) &&
Objects.equals(receipt, that.receipt) &&
Objects.equals(reactions, that.reactions) &&
Objects.equals(quotedStickerData, that.quotedStickerData) &&
Objects.deepEquals(futureProofData, that.futureProofData) &&
Objects.equals(psaStatus, that.psaStatus) &&
Objects.equals(pollUpdates, that.pollUpdates) &&
Objects.equals(pollAdditionalMetadata, that.pollAdditionalMetadata) &&
Objects.equals(agentId, that.agentId) &&
Objects.deepEquals(messageSecret, that.messageSecret) &&
Objects.equals(keepInChat, that.keepInChat) &&
Objects.equals(originalSender, that.originalSender);
}
@Override
public int hashCode() {
return Objects.hash(key, message, timestampSeconds, status, senderJid, messageC2STimestamp, ignore, starred, broadcast, pushName, Arrays.hashCode(mediaCiphertextSha256), multicast, urlText, urlNumber, stubType, clearMedia, stubParameters, duration, labels, paymentInfo, finalLiveLocation, quotedPaymentInfo, ephemeralStartTimestamp, ephemeralDuration, enableEphemeral, ephemeralOutOfSync, businessPrivacyStatus, businessVerifiedName, mediaData, photoChange, receipt, reactions, quotedStickerData, Arrays.hashCode(futureProofData), psaStatus, pollUpdates, pollAdditionalMetadata, agentId, statusAlreadyViewed, Arrays.hashCode(messageSecret), keepInChat, originalSender, revokeTimestampSeconds);
}
/**
* Returns the id of the message
*
* @return a non-null String
*/
public String id() {
return key.id();
}
/**
* Returns the value of the sender
*
* @return a non-null ContactJid
*/
public Jid senderJid() {
return requireNonNullElseGet(senderJid, () -> key.senderJid().orElseGet(key::chatJid));
}
@Override
public Jid parentJid() {
return chatJid();
}
public ChatMessageKey key() {
return key;
}
@Override
public MessageContainer message() {
return message;
}
@Override
public void setMessage(MessageContainer message) {
this.message = message;
}
public OptionalLong timestampSeconds() {
return Clock.parseTimestamp(timestampSeconds);
}
@Override
public MessageStatus status() {
return status;
}
public long messageC2STimestamp() {
return messageC2STimestamp;
}
public boolean ignore() {
return ignore;
}
public void setIgnore(boolean ignore) {
this.ignore = ignore;
}
public boolean starred() {
return starred;
}
public boolean broadcast() {
return broadcast;
}
public Optional<String> pushName() {
return Optional.ofNullable(pushName);
}
public Optional<byte[]> mediaCiphertextSha256() {
return Optional.ofNullable(mediaCiphertextSha256);
}
public boolean multicast() {
return multicast;
}
public boolean urlText() {
return urlText;
}
public boolean urlNumber() {
return urlNumber;
}
public Optional<MessageInfoStubType> stubType() {
return Optional.ofNullable(stubType);
}
public boolean clearMedia() {
return clearMedia;
}
public List<String> stubParameters() {
return stubParameters;
}
public int duration() {
return duration;
}
public List<String> labels() {
return labels;
}
public Optional<PaymentInfo> paymentInfo() {
return Optional.ofNullable(paymentInfo);
}
public Optional<LiveLocationMessage> finalLiveLocation() {
return Optional.ofNullable(finalLiveLocation);
}
public Optional<PaymentInfo> quotedPaymentInfo() {
return Optional.ofNullable(quotedPaymentInfo);
}
public long ephemeralStartTimestamp() {
return ephemeralStartTimestamp;
}
public int ephemeralDuration() {
return ephemeralDuration;
}
public boolean enableEphemeral() {
return enableEphemeral;
}
public boolean ephemeralOutOfSync() {
return ephemeralOutOfSync;
}
public Optional<BusinessPrivacyStatus> businessPrivacyStatus() {
return Optional.ofNullable(businessPrivacyStatus);
}
public Optional<String> businessVerifiedName() {
return Optional.ofNullable(businessVerifiedName);
}
public Optional<MediaData> mediaData() {
return Optional.ofNullable(mediaData);
}
public Optional<PhotoChange> photoChange() {
return Optional.ofNullable(photoChange);
}
public MessageReceipt receipt() {
return receipt;
}
public List<ReactionMessage> reactions() {
return reactions;
}
public Optional<MediaData> quotedStickerData() {
return Optional.ofNullable(quotedStickerData);
}
public byte[] futureProofData() {
return futureProofData;
}
public Optional<PublicServiceAnnouncementStatus> psaStatus() {
return Optional.ofNullable(psaStatus);
}
public List<PollUpdate> pollUpdates() {
return pollUpdates;
}
public Optional<PollAdditionalMetadata> pollAdditionalMetadata() {
return Optional.ofNullable(pollAdditionalMetadata);
}
public void setPollAdditionalMetadata(PollAdditionalMetadata pollAdditionalMetadata) {
this.pollAdditionalMetadata = pollAdditionalMetadata;
}
public Optional<String> agentId() {
return Optional.ofNullable(agentId);
}
public boolean statusAlreadyViewed() {
return statusAlreadyViewed;
}
public Optional<byte[]> messageSecret() {
return Optional.ofNullable(messageSecret);
}
public void setMessageSecret(byte[] messageSecret) {
this.messageSecret = messageSecret;
}
public Optional<KeepInChat> keepInChat() {
return Optional.ofNullable(keepInChat);
}
public Optional<Jid> originalSender() {
return Optional.ofNullable(originalSender);
}
public long revokeTimestampSeconds() {
return revokeTimestampSeconds;
}
public Optional<Chat> chat() {
return Optional.ofNullable(chat);
}
public void setChat(Chat chat) {
this.chat = chat;
}
@Override
public Optional<MessageInfoParent> parent() {
return Optional.ofNullable(chat);
}
@Override
public void setParent(MessageInfoParent parent) {
if(parent == null) {
this.chat = null;
}else if(!(parent instanceof Chat parentChat)) {
throw new IllegalArgumentException("Parent is not a chat");
}else {
this.chat = parentChat;
}
}
@Override
public Optional<Contact> sender() {
return Optional.ofNullable(sender);
}
@Override
public void setSender(Contact sender) {
this.sender = sender;
}
@Override
public void setStatus(MessageStatus status) {
this.status = status;
}
public void setStarred(boolean starred) {
this.starred = starred;
}
public void setRevokeTimestampSeconds(long revokeTimestampSeconds) {
this.revokeTimestampSeconds = revokeTimestampSeconds;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/ContextInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.button.base.ButtonActionLink;
import com.github.auties00.cobalt.model.chat.ChatDisappear;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.ChatMessageKey;
import com.github.auties00.cobalt.model.message.model.MessageContainer;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* A model class that holds the information related to a {@link it.auties.whatsapp.model.message.model.ContextualMessage}.
*/
@ProtobufMessage(name = "ContextInfo")
public final class ContextInfo implements Info { // TODO: Check me
/**
* The value of the message that this ContextualMessage quotes
*/
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String quotedMessageId;
/**
* The value of the contact that sent the message that this ContextualMessage quotes
*/
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final Jid quotedMessageSenderJid;
/**
* The message container that this ContextualMessage quotes
*/
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final MessageContainer quotedMessage;
/**
* The value of the contact that sent the message that this ContextualMessage quotes
*/
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final Jid quotedMessageParentJid;
/**
* A list of the contacts' jids mentioned in this ContextualMessage
*/
@ProtobufProperty(index = 15, type = ProtobufType.STRING)
final List<Jid> mentions;
/**
* Conversation source
*/
@ProtobufProperty(index = 18, type = ProtobufType.STRING)
final String conversionSource;
/**
* Conversation data
*/
@ProtobufProperty(index = 19, type = ProtobufType.BYTES)
final byte[] conversionData;
/**
* Conversation delay in endTimeStamp
*/
@ProtobufProperty(index = 20, type = ProtobufType.UINT32)
final int conversionDelaySeconds;
/**
* Forwarding score
*/
@ProtobufProperty(index = 21, type = ProtobufType.UINT32)
final int forwardingScore;
/**
* Whether this ContextualMessage is forwarded
*/
@ProtobufProperty(index = 22, type = ProtobufType.BOOL)
final boolean forwarded;
/**
* The ad that this ContextualMessage quotes
*/
@ProtobufProperty(index = 23, type = ProtobufType.MESSAGE)
final AdReplyInfo quotedAd;
/**
* Placeholder key
*/
@ProtobufProperty(index = 24, type = ProtobufType.MESSAGE)
final ChatMessageKey placeholderKey;
/**
* The expiration in seconds for this ContextualMessage. Only valid if the chat where this message
* was sent is ephemeral.
*/
@ProtobufProperty(index = 25, type = ProtobufType.UINT32)
int ephemeralExpiration;
/**
* The timestampSeconds, that is the seconds in seconds since {@link java.time.Instant#EPOCH}, of the
* last modification to the ephemeral settings for the chat where this ContextualMessage was
* sent.
*/
@ProtobufProperty(index = 26, type = ProtobufType.INT64)
long ephemeralSettingTimestamp;
/**
* Ephemeral shared secret
*/
@ProtobufProperty(index = 27, type = ProtobufType.BYTES)
final byte[] ephemeralSharedSecret;
/**
* External ad reply
*/
@ProtobufProperty(index = 28, type = ProtobufType.MESSAGE)
final ExternalAdReplyInfo externalAdReply;
/**
* Entry point conversion source
*/
@ProtobufProperty(index = 29, type = ProtobufType.STRING)
final String entryPointConversionSource;
/**
* Entry point conversion app
*/
@ProtobufProperty(index = 30, type = ProtobufType.STRING)
final String entryPointConversionApp;
/**
* Entry point conversion delay in endTimeStamp
*/
@ProtobufProperty(index = 31, type = ProtobufType.UINT32)
final int entryPointConversionDelaySeconds;
/**
* Disappearing mode
*/
@ProtobufProperty(index = 32, type = ProtobufType.MESSAGE)
final ChatDisappear disappearingMode;
/**
* Action link
*/
@ProtobufProperty(index = 33, type = ProtobufType.MESSAGE)
final ButtonActionLink actionLink;
/**
* Group subject
*/
@ProtobufProperty(index = 34, type = ProtobufType.STRING)
final String groupSubject;
/**
* Parent group
*/
@ProtobufProperty(index = 35, type = ProtobufType.STRING)
final Jid parentGroup;
/**
* Trust banner type
*/
@ProtobufProperty(index = 37, type = ProtobufType.STRING)
final String trustBannerType;
/**
* Trust banner action
*/
@ProtobufProperty(index = 38, type = ProtobufType.UINT32)
final int trustBannerAction;
/**
* The contact that sent the message that this ContextualMessage quotes
*/
private Contact quotedMessageSender;
/**
* The parent who stores the message that this ContextualMessage quotes
*/
private MessageInfoParent quotedMessageParent;
ContextInfo(String quotedMessageId, Jid quotedMessageSenderJid, MessageContainer quotedMessage, Jid quotedMessageParentJid, List<Jid> mentions, String conversionSource, byte[] conversionData, int conversionDelaySeconds, int forwardingScore, boolean forwarded, AdReplyInfo quotedAd, ChatMessageKey placeholderKey, int ephemeralExpiration, long ephemeralSettingTimestamp, byte[] ephemeralSharedSecret, ExternalAdReplyInfo externalAdReply, String entryPointConversionSource, String entryPointConversionApp, int entryPointConversionDelaySeconds, ChatDisappear disappearingMode, ButtonActionLink actionLink, String groupSubject, Jid parentGroup, String trustBannerType, int trustBannerAction) {
this.quotedMessageId = quotedMessageId;
this.quotedMessageSenderJid = quotedMessageSenderJid;
this.quotedMessage = quotedMessage;
this.quotedMessageParentJid = quotedMessageParentJid;
this.mentions = mentions;
this.conversionSource = conversionSource;
this.conversionData = conversionData;
this.conversionDelaySeconds = conversionDelaySeconds;
this.forwardingScore = forwardingScore;
this.forwarded = forwarded;
this.quotedAd = quotedAd;
this.placeholderKey = placeholderKey;
this.ephemeralExpiration = ephemeralExpiration;
this.ephemeralSettingTimestamp = ephemeralSettingTimestamp;
this.ephemeralSharedSecret = ephemeralSharedSecret;
this.externalAdReply = externalAdReply;
this.entryPointConversionSource = entryPointConversionSource;
this.entryPointConversionApp = entryPointConversionApp;
this.entryPointConversionDelaySeconds = entryPointConversionDelaySeconds;
this.disappearingMode = disappearingMode;
this.actionLink = actionLink;
this.groupSubject = groupSubject;
this.parentGroup = parentGroup;
this.trustBannerType = trustBannerType;
this.trustBannerAction = trustBannerAction;
}
public static ContextInfo of(MessageInfo quotedMessage) {
return new ContextInfoBuilder()
.quotedMessageId(quotedMessage.id())
.quotedMessageSenderJid(quotedMessage.senderJid())
.quotedMessage(quotedMessage.message())
.quotedMessageParentJid(quotedMessage.parentJid())
.mentions(new ArrayList<>())
.build();
}
public static ContextInfo of(ContextInfo contextInfo, MessageInfo quotedMessage) {
return contextInfo == null ? of(quotedMessage) : new ContextInfoBuilder()
.quotedMessageId(quotedMessage.id())
.quotedMessageSenderJid(quotedMessage.senderJid())
.quotedMessage(quotedMessage.message())
.quotedMessageParentJid(quotedMessage.parentJid())
.mentions(new ArrayList<>())
.conversionSource(contextInfo.conversionSource)
.conversionData(contextInfo.conversionData)
.conversionDelaySeconds(contextInfo.conversionDelaySeconds)
.forwardingScore(contextInfo.forwardingScore)
.forwarded(contextInfo.forwarded)
.quotedAd(contextInfo.quotedAd)
.placeholderKey(contextInfo.placeholderKey)
.ephemeralExpiration(contextInfo.ephemeralExpiration)
.ephemeralSettingTimestamp(contextInfo.ephemeralSettingTimestamp)
.ephemeralSharedSecret(contextInfo.ephemeralSharedSecret)
.externalAdReply(contextInfo.externalAdReply)
.entryPointConversionSource(contextInfo.entryPointConversionSource)
.entryPointConversionApp(contextInfo.entryPointConversionApp)
.entryPointConversionDelaySeconds(contextInfo.entryPointConversionDelaySeconds)
.disappearingMode(contextInfo.disappearingMode)
.actionLink(contextInfo.actionLink)
.groupSubject(contextInfo.groupSubject)
.parentGroup(contextInfo.parentGroup)
.trustBannerType(contextInfo.trustBannerType)
.trustBannerAction(contextInfo.trustBannerAction)
.build();
}
public static ContextInfo empty() {
return new ContextInfoBuilder()
.mentions(new ArrayList<>())
.build();
}
/**
* Returns the sender of the quoted message
*
* @return an optional
*/
public Optional<Contact> quotedMessageSender() {
return Optional.ofNullable(quotedMessageSender);
}
public void setQuotedMessageSender(Contact quotedMessageSender) {
this.quotedMessageSender = quotedMessageSender;
}
/**
* Returns the chat value of the quoted message
*
* @return an optional
*/
public Optional<Jid> quotedMessageParentJid() {
return Optional.ofNullable(quotedMessageParentJid)
.or(this::quotedMessageSenderJid);
}
/**
* Returns the value of the sender of the quoted message
*
* @return an optional
*/
public Optional<Jid> quotedMessageSenderJid() {
return Optional.ofNullable(quotedMessageSenderJid);
}
/**
* Returns whether this context info has information about a quoted message
*
* @return a boolean
*/
public boolean hasQuotedMessage() {
return quotedMessageId().isPresent()
&& quotedMessage().isPresent()
&& quotedMessageParent().isPresent();
}
/**
* Returns the id of the quoted message
*
* @return an optional
*/
public Optional<String> quotedMessageId() {
return Optional.ofNullable(quotedMessageId);
}
/**
* Returns the quoted message
*
* @return an optional
*/
public Optional<MessageContainer> quotedMessage() {
return Optional.ofNullable(quotedMessage);
}
/**
* Returns the chat of the quoted message
*
* @return an optional
*/
public Optional<MessageInfoParent> quotedMessageParent() {
return Optional.ofNullable(quotedMessageParent);
}
public void setQuotedMessageParent(MessageInfoParent quotedMessageParent) {
this.quotedMessageParent = quotedMessageParent;
}
public List<Jid> mentions() {
return mentions;
}
public Optional<String> conversionSource() {
return Optional.ofNullable(conversionSource);
}
public Optional<byte[]> conversionData() {
return Optional.ofNullable(conversionData);
}
public int conversionDelaySeconds() {
return conversionDelaySeconds;
}
public int forwardingScore() {
return forwardingScore;
}
public boolean forwarded() {
return forwarded;
}
public Optional<AdReplyInfo> quotedAd() {
return Optional.ofNullable(quotedAd);
}
public Optional<ChatMessageKey> placeholderKey() {
return Optional.ofNullable(placeholderKey);
}
public int ephemeralExpiration() {
return ephemeralExpiration;
}
public void setEphemeralExpiration(int ephemeralExpiration) {
this.ephemeralExpiration = ephemeralExpiration;
}
public long ephemeralSettingTimestamp() {
return ephemeralSettingTimestamp;
}
public void setEphemeralSettingTimestamp(long ephemeralSettingTimestamp) {
this.ephemeralSettingTimestamp = ephemeralSettingTimestamp;
}
public Optional<byte[]> ephemeralSharedSecret() {
return Optional.ofNullable(ephemeralSharedSecret);
}
public Optional<ExternalAdReplyInfo> externalAdReply() {
return Optional.ofNullable(externalAdReply);
}
public Optional<String> entryPointConversionSource() {
return Optional.ofNullable(entryPointConversionSource);
}
public Optional<String> entryPointConversionApp() {
return Optional.ofNullable(entryPointConversionApp);
}
public int entryPointConversionDelaySeconds() {
return entryPointConversionDelaySeconds;
}
public Optional<ChatDisappear> disappearingMode() {
return Optional.ofNullable(disappearingMode);
}
public Optional<ButtonActionLink> actionLink() {
return Optional.ofNullable(actionLink);
}
public Optional<String> groupSubject() {
return Optional.ofNullable(groupSubject);
}
public Optional<Jid> parentGroup() {
return Optional.ofNullable(parentGroup);
}
public Optional<String> trustBannerType() {
return Optional.ofNullable(trustBannerType);
}
public int trustBannerAction() {
return trustBannerAction;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/DeviceContextInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.sync.DeviceListMetadata;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
@ProtobufMessage(name = "MessageContextInfo")
public final class DeviceContextInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final DeviceListMetadata deviceListMetadata;
@ProtobufProperty(index = 2, type = ProtobufType.INT32)
final int deviceListMetadataVersion;
@ProtobufProperty(index = 4, type = ProtobufType.BYTES)
byte[] paddingBytes;
@ProtobufProperty(index = 3, type = ProtobufType.BYTES)
byte[] messageSecret;
DeviceContextInfo(DeviceListMetadata deviceListMetadata, int deviceListMetadataVersion, byte[] messageSecret, byte[] paddingBytes) {
this.deviceListMetadata = deviceListMetadata;
this.deviceListMetadataVersion = deviceListMetadataVersion;
this.messageSecret = messageSecret;
this.paddingBytes = paddingBytes;
}
public Optional<DeviceListMetadata> deviceListMetadata() {
return Optional.ofNullable(deviceListMetadata);
}
public int deviceListMetadataVersion() {
return deviceListMetadataVersion;
}
public Optional<byte[]> messageSecret() {
return Optional.ofNullable(messageSecret);
}
public void setMessageSecret(byte[] messageSecret) {
this.messageSecret = messageSecret;
}
public Optional<byte[]> paddingBytes() {
return Optional.ofNullable(paddingBytes);
}
public void setPaddingBytes(byte[] paddingBytes) {
this.paddingBytes = paddingBytes;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/ExternalAdReplyInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds the information related to an advertisement.
*/
@ProtobufMessage(name = "ContextInfo.ExternalAdReplyInfo")
public final class ExternalAdReplyInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String body;
@ProtobufProperty(index = 3, type = ProtobufType.ENUM)
final MediaType mediaType;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final String thumbnailUrl;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final String mediaUrl;
@ProtobufProperty(index = 6, type = ProtobufType.BYTES)
final byte[] thumbnail;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String sourceType;
@ProtobufProperty(index = 8, type = ProtobufType.STRING)
final String sourceId;
@ProtobufProperty(index = 9, type = ProtobufType.STRING)
final String sourceUrl;
@ProtobufProperty(index = 10, type = ProtobufType.BOOL)
final boolean containsAutoReply;
@ProtobufProperty(index = 11, type = ProtobufType.BOOL)
final boolean renderLargerThumbnail;
@ProtobufProperty(index = 12, type = ProtobufType.BOOL)
final boolean showAdAttribution;
@ProtobufProperty(index = 13, type = ProtobufType.STRING)
final String ctwaClid;
ExternalAdReplyInfo(String title, String body, MediaType mediaType, String thumbnailUrl, String mediaUrl, byte[] thumbnail, String sourceType, String sourceId, String sourceUrl, boolean containsAutoReply, boolean renderLargerThumbnail, boolean showAdAttribution, String ctwaClid) {
this.title = title;
this.body = body;
this.mediaType = mediaType;
this.thumbnailUrl = thumbnailUrl;
this.mediaUrl = mediaUrl;
this.thumbnail = thumbnail;
this.sourceType = sourceType;
this.sourceId = sourceId;
this.sourceUrl = sourceUrl;
this.containsAutoReply = containsAutoReply;
this.renderLargerThumbnail = renderLargerThumbnail;
this.showAdAttribution = showAdAttribution;
this.ctwaClid = ctwaClid;
}
public Optional<String> title() {
return Optional.ofNullable(title);
}
public Optional<String> body() {
return Optional.ofNullable(body);
}
public Optional<MediaType> mediaType() {
return Optional.ofNullable(mediaType);
}
public Optional<String> thumbnailUrl() {
return Optional.ofNullable(thumbnailUrl);
}
public Optional<String> mediaUrl() {
return Optional.ofNullable(mediaUrl);
}
public Optional<byte[]> thumbnail() {
return Optional.ofNullable(thumbnail);
}
public Optional<String> sourceType() {
return Optional.ofNullable(sourceType);
}
public Optional<String> sourceId() {
return Optional.ofNullable(sourceId);
}
public Optional<String> sourceUrl() {
return Optional.ofNullable(sourceUrl);
}
public boolean containsAutoReply() {
return containsAutoReply;
}
public boolean renderLargerThumbnail() {
return renderLargerThumbnail;
}
public boolean showAdAttribution() {
return showAdAttribution;
}
public Optional<String> ctwaClid() {
return Optional.ofNullable(ctwaClid);
}
@Override
public boolean equals(Object o) {
return o instanceof ExternalAdReplyInfo that
&& Objects.equals(title, that.title)
&& Objects.equals(body, that.body)
&& Objects.equals(mediaType, that.mediaType)
&& Objects.equals(thumbnailUrl, that.thumbnailUrl)
&& Objects.equals(mediaUrl, that.mediaUrl)
&& Arrays.equals(thumbnail, that.thumbnail)
&& Objects.equals(sourceType, that.sourceType)
&& Objects.equals(sourceId, that.sourceId)
&& Objects.equals(sourceUrl, that.sourceUrl)
&& containsAutoReply == that.containsAutoReply
&& renderLargerThumbnail == that.renderLargerThumbnail
&& showAdAttribution == that.showAdAttribution
&& Objects.equals(ctwaClid, that.ctwaClid);
}
@Override
public int hashCode() {
return Objects.hash(title, body, mediaType, thumbnailUrl, mediaUrl, Arrays.hashCode(thumbnail), sourceType, sourceId, sourceUrl, containsAutoReply, renderLargerThumbnail, showAdAttribution, ctwaClid);
}
@Override
public String toString() {
return "ExternalAdReplyInfo[" +
"title=" + title +
", body=" + body +
", mediaType=" + mediaType +
", thumbnailUrl=" + thumbnailUrl +
", mediaUrl=" + mediaUrl +
", thumbnail=" + Arrays.toString(thumbnail) +
", sourceType=" + sourceType +
", sourceId=" + sourceId +
", sourceUrl=" + sourceUrl +
", containsAutoReply=" + containsAutoReply +
", renderLargerThumbnail=" + renderLargerThumbnail +
", showAdAttribution=" + showAdAttribution +
", ctwaClid=" + ctwaClid +
']';
}
/**
* The constants of this enumerated type describe the various types of media that an ad can wrap
*/
@ProtobufEnum(name = "ChatRowOpaqueData.DraftMessage.CtwaContextData.ContextInfoExternalAdReplyInfoMediaType")
public enum MediaType {
/**
* No media
*/
NONE(0),
/**
* Image
*/
IMAGE(1),
/**
* Video
*/
VIDEO(2);
final int index;
MediaType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/Info.java
================================================
package com.github.auties00.cobalt.model.info;
public sealed interface Info permits AdReplyInfo, BusinessIdentityInfo, ContextInfo, DeviceContextInfo, ExternalAdReplyInfo, MessageIndexInfo, MessageInfo, NativeFlowInfo, PaymentInfo, ProductListInfo {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/MessageIndexInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* An index that contains data about a setting change or an action
*/
@ProtobufMessage
public final class MessageIndexInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String type;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String targetId;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String messageId;
@ProtobufProperty(index = 4, type = ProtobufType.BOOL)
final boolean fromMe;
MessageIndexInfo(String type, String targetId, String messageId, boolean fromMe) {
this.type = Objects.requireNonNull(type, "type cannot be null");
this.targetId = targetId;
this.messageId = messageId;
this.fromMe = fromMe;
}
public String type() {
return type;
}
public Optional<String> targetId() {
return Optional.ofNullable(targetId);
}
public Optional<String> messageId() {
return Optional.ofNullable(messageId);
}
public boolean fromMe() {
return fromMe;
}
@Override
public boolean equals(Object o) {
return o instanceof MessageIndexInfo that
&& fromMe == that.fromMe
&& Objects.equals(type, that.type)
&& Objects.equals(targetId, that.targetId)
&& Objects.equals(messageId, that.messageId);
}
@Override
public int hashCode() {
return Objects.hash(type, targetId, messageId, fromMe);
}
@Override
public String toString() {
return "MessageIndexInfo[" +
"type=" + type + ", " +
"targetId=" + targetId + ", " +
"messageId=" + messageId + ", " +
"fromMe=" + fromMe + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/MessageInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import com.github.auties00.cobalt.model.message.model.MessageContainer;
import com.github.auties00.cobalt.model.message.model.MessageStatus;
import java.util.Optional;
import java.util.OptionalLong;
public sealed interface MessageInfo
extends Info
permits ChatMessageInfo, NewsletterMessageInfo, QuotedMessageInfo {
MessageStatus status();
void setStatus(MessageStatus status);
OptionalLong timestampSeconds();
String id();
Jid parentJid();
Optional<MessageInfoParent> parent();
void setParent(MessageInfoParent parent);
Jid senderJid();
Optional<Contact> sender();
void setSender(Contact sender);
MessageContainer message();
void setMessage(MessageContainer message);
default Optional<QuotedMessageInfo> quotedMessage() {
var message = message();
if(message == null) {
return Optional.empty();
}
return message.contentWithContext()
.flatMap(ContextualMessage::contextInfo)
.flatMap(QuotedMessageInfo::of);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/MessageInfoParent.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.chat.Chat;
import com.github.auties00.cobalt.model.jid.JidProvider;
import com.github.auties00.cobalt.model.newsletter.Newsletter;
import java.util.Optional;
import java.util.SequencedCollection;
public sealed interface MessageInfoParent
extends JidProvider
permits Chat, Newsletter {
SequencedCollection<? extends MessageInfo> messages();
Optional<? extends MessageInfo> getMessageById(String messageId);
boolean removeMessage(String messageId);
void removeMessages();
Optional<? extends MessageInfo> newestMessage();
Optional<? extends MessageInfo> oldestMessage();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/MessageInfoStubType.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.node.Node;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* The constants of this enumerated type describe the various types of a server message that a {@link ChatMessageInfo} can describe
*/
// TODO: Implement getParameters() and a contextual filter for constants that have the same notificationType and bodyType
@ProtobufEnum
public enum MessageInfoStubType {
UNKNOWN(0, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
REVOKE(1, "revoked", "sender") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CIPHERTEXT(2, "ciphertext", null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
FUTUREPROOF(3, null, "phone") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
NON_VERIFIED_TRANSITION(4, null, "non_verified_transition") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
UNVERIFIED_TRANSITION(5, null, "unverified_transition") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION(6, null, "verified_transition") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_LOW_UNKNOWN(7, null, "verified_low_unknown") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_HIGH(8, null, "verified_high") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_INITIAL_UNKNOWN(9, null, "verified_initial_unknown") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_INITIAL_LOW(10, null, "verified_initial_low") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_INITIAL_HIGH(11, null, "verified_initial_high") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_ANY_TO_NONE(12, null, "verified_transition_any_to_none") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_ANY_TO_HIGH(13, null, "verified_transition_any_to_high") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_HIGH_TO_LOW(14, null, "verified_transition_high_to_low") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_HIGH_TO_UNKNOWN(15, null, "verified_transition_high_to_unknown") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_UNKNOWN_TO_LOW(16, null, "verified_transition_unknown_to_low") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_LOW_TO_UNKNOWN(17, null, "verified_transition_low_to_unknown") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_NONE_TO_LOW(18, null, "verified_transition_none_to_low") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VERIFIED_TRANSITION_NONE_TO_UNKNOWN(19, null, "verified_transition_none_to_unknown") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CREATE(20, "gp2", "create") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_SUBJECT(21, "gp2", "subject") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_ICON(22, "gp2", "picture") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_INVITE_LINK(23, "gp2", "revoke_invite") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_DESCRIPTION(24, "gp2", "description") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_RESTRICT(25, "gp2", "restrict") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_ANNOUNCE(26, "gp2", "announce") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_ADD(27, "gp2", "add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_REMOVE(28, "gp2", "remove") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_PROMOTE(29, "gp2", "promote") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_DEMOTE(30, "gp2", "demote") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_INVITE(31, "gp2", "invite") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_LEAVE(32, "gp2", "leave") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_CHANGE_NUMBER(33, "gp2", "modify") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BROADCAST_CREATE(34, "broadcast_notification", "create") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BROADCAST_ADD(35, "broadcast_notification", "add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BROADCAST_REMOVE(36, "broadcast_notification", "remove") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GENERIC_NOTIFICATION(37, "notification", null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_IDENTITY_CHANGED(38, "e2e_notification", "identity") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_ENCRYPTED(39, "e2e_notification", "encrypt") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CALL_MISSED_VOICE(40, "call_log", "miss") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CALL_MISSED_VIDEO(41, "call_log", "miss_video") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
INDIVIDUAL_CHANGE_NUMBER(42, null, "change_number") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_DELETE(43, "gp2", "delete") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_ANNOUNCE_MODE_MESSAGE_BOUNCE(44, "gp2", "announce_msg_bounce") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CALL_MISSED_GROUP_VOICE(45, "call_log", "miss_group") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CALL_MISSED_GROUP_VIDEO(46, "call_log", "miss_group_video") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_CIPHERTEXT(47, "payment", "ciphertext") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_FUTUREPROOF(48, "payment", "futureproof") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_TRANSACTION_STATUS_UPDATE_FAILED(49, null, "payment_transaction_status_update_failed") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_TRANSACTION_STATUS_UPDATE_REFUNDED(50, null, "payment_transaction_status_update_refunded") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_TRANSACTION_STATUS_UPDATE_REFUND_FAILED(51, null, "payment_transaction_status_update_refund_failed") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_TRANSACTION_STATUS_RECEIVER_PENDING_SETUP(52, null, "payment_transaction_status_receiver_pending_setup") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_TRANSACTION_STATUS_RECEIVER_SUCCESS_AFTER_HICCUP(53, null, "payment_transaction_status_receiver_success_after_hiccup") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_ACCOUNT_SETUP_REMINDER(54, null, "payment_action_account_setup_reminder") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_SEND_PAYMENT_REMINDER(55, null, "payment_action_send_payment_reminder") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_SEND_PAYMENT_INVITATION(56, null, "payment_action_send_payment_invitation") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_REQUEST_DECLINED(57, null, "payment_action_request_declined") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_REQUEST_EXPIRED(58, null, "payment_action_request_expired") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_ACTION_REQUEST_CANCELLED(59, null, "payment_transaction_request_cancelled") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_VERIFIED_TRANSITION_TOP_TO_BOTTOM(60, null, "biz_verified_transition_top_to_bottom") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_VERIFIED_TRANSITION_BOTTOM_TO_TOP(61, null, "biz_verified_transition_bottom_to_top") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_INTRO_TOP(62, null, "biz_intro_top") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_INTRO_BOTTOM(63, null, "biz_intro_bottom") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_NAME_CHANGE(64, null, "biz_name_change") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_MOVE_TO_CONSUMER_APP(65, null, "biz_move_to_consumer_app") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_TWO_TIER_MIGRATION_TOP(66, null, "biz_two_tier_migration_top") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_TWO_TIER_MIGRATION_BOTTOM(67, null, "biz_two_tier_migration_bottom") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
OVERSIZED(68, "oversized", null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_NO_FREQUENTLY_FORWARDED(69, "gp2", "no_frequently_forwarded") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_V4_ADD_INVITE_SENT(70, "gp2", "v4_add_invite_sent") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_ADD_REQUEST_JOIN(71, "gp2", "v4_add_invite_join") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CHANGE_EPHEMERAL_SETTING(72, "gp2", "ephemeral") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_DEVICE_CHANGED(73, "e2e_notification", "device") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
VIEWED_ONCE(74, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_ENCRYPTED_NOW(75, "e2e_notification", "encrypt_now") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_TO_BSP_PREMISE(76, null, "blue_msg_bsp_fb_to_bsp_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_TO_SELF_FB(77, null, "blue_msg_bsp_fb_to_self_fb") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_TO_SELF_PREMISE(78, null, "blue_msg_bsp_fb_to_self_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_UNVERIFIED(79, null, "blue_msg_bsp_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(80, null, "blue_msg_bsp_fb_unverified_to_self_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_VERIFIED(81, null, "blue_msg_bsp_fb_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(82, null, "blue_msg_bsp_fb_verified_to_self_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_PREMISE_TO_SELF_PREMISE(83, null, "blue_msg_bsp_premise_to_self_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_PREMISE_UNVERIFIED(84, null, "blue_msg_bsp_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_PREMISE_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(85, null, "blue_msg_bsp_premise_unverified_to_self_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_PREMISE_VERIFIED(86, null, "blue_msg_bsp_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_PREMISE_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(87, null, "blue_msg_bsp_premise_verified_to_self_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_CONSUMER_TO_BSP_FB_UNVERIFIED(88, null, "blue_msg_consumer_to_bsp_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_CONSUMER_TO_BSP_PREMISE_UNVERIFIED(89, null, "blue_msg_consumer_to_bsp_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_CONSUMER_TO_SELF_FB_UNVERIFIED(90, null, "blue_msg_consumer_to_self_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_CONSUMER_TO_SELF_PREMISE_UNVERIFIED(91, null, "blue_msg_consumer_to_self_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_TO_BSP_PREMISE(92, null, "blue_msg_self_fb_to_bsp_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_TO_SELF_PREMISE(93, null, "blue_msg_self_fb_to_self_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_UNVERIFIED(94, null, "blue_msg_self_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_UNVERIFIED_TO_SELF_PREMISE_VERIFIED(95, null, "blue_msg_self_fb_unverified_to_self_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_VERIFIED(96, null, "blue_msg_self_fb_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_VERIFIED_TO_SELF_PREMISE_UNVERIFIED(97, null, "blue_msg_self_fb_verified_to_self_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_PREMISE_TO_BSP_PREMISE(98, null, "blue_msg_self_premise_to_bsp_premise") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_PREMISE_UNVERIFIED(99, null, "blue_msg_self_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_PREMISE_VERIFIED(100, null, "blue_msg_self_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_TO_BSP_FB(101, null, "blue_msg_to_bsp_fb") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_TO_CONSUMER(102, null, "blue_msg_to_consumer") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_TO_SELF_FB(103, null, "blue_msg_to_self_fb") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_UNVERIFIED_TO_BSP_FB_VERIFIED(104, null, "blue_msg_unverified_to_bsp_fb_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(105, null, "blue_msg_unverified_to_bsp_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_UNVERIFIED_TO_SELF_FB_VERIFIED(106, null, "blue_msg_unverified_to_self_fb_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_UNVERIFIED_TO_VERIFIED(107, null, "blue_msg_unverified_to_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_VERIFIED_TO_BSP_FB_UNVERIFIED(108, null, "blue_msg_verified_to_bsp_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(109, null, "blue_msg_verified_to_bsp_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_VERIFIED_TO_SELF_FB_UNVERIFIED(110, null, "blue_msg_verified_to_self_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_VERIFIED_TO_UNVERIFIED(111, null, "blue_msg_verified_to_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(112, null, "blue_msg_bsp_fb_unverified_to_bsp_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_UNVERIFIED_TO_SELF_FB_VERIFIED(113, null, "blue_msg_bsp_fb_unverified_to_self_fb_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(114, null, "blue_msg_bsp_fb_verified_to_bsp_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_BSP_FB_VERIFIED_TO_SELF_FB_UNVERIFIED(115, null, "blue_msg_bsp_fb_verified_to_self_fb_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_UNVERIFIED_TO_BSP_PREMISE_VERIFIED(116, null, "blue_msg_self_fb_unverified_to_bsp_premise_verified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLUE_MSG_SELF_FB_VERIFIED_TO_BSP_PREMISE_UNVERIFIED(117, null, "blue_msg_self_fb_verified_to_bsp_premise_unverified") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_IDENTITY_UNAVAILABLE(118, "e2e_notification", "e2e_identity_unavailable") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CREATING(119, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CREATE_FAILED(120, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_BOUNCED(121, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BLOCK_CONTACT(122, null, "block_contact") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
EPHEMERAL_SETTING_NOT_APPLIED(123, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SYNC_FAILED(124, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SYNCING(125, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_PRIVACY_MODE_INIT_FB(126, null, "biz_privacy_mode_init_fb") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_PRIVACY_MODE_INIT_BSP(127, null, "biz_privacy_mode_init_bsp") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_PRIVACY_MODE_TO_FB(128, null, "biz_privacy_mode_to_fb") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_PRIVACY_MODE_TO_BSP(129, null, "biz_privacy_mode_to_bsp") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
DISAPPEARING_MODE(130, null, "disappearing_mode") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
E2E_DEVICE_FETCH_FAILED(131, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
ADMIN_REVOKE(132, "revoked", "admin") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_INVITE_LINK_GROWTH_LOCKED(133, "gp2", "growth_locked") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_LINK_PARENT_GROUP(134, "gp2", "parent_group_link") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_LINK_SIBLING_GROUP(135, "gp2", "sibling_group_link") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_LINK_SUB_GROUP(136, "gp2", "sub_group_link") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_UNLINK_PARENT_GROUP(137, "gp2", "parent_group_unlink") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_UNLINK_SIBLING_GROUP(138, "gp2", "sibling_group_unlink") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_UNLINK_SUB_GROUP(139, "gp2", "sub_group_unlink") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_ACCEPT(140, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_LINKED_GROUP_JOIN(141, "gp2", "linked_group_join") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_CREATE(142, "gp2", "community_create") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
EPHEMERAL_KEEP_IN_CHAT(143, "gp2", "ephemeral_keep_in_chat") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST(144, "gp2", "membership_approval_request") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE(145, "gp2", "membership_approval_mode") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
INTEGRITY_UNLINK_PARENT_GROUP(146, "gp2", "integrity_parent_group_unlink") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_PARTICIPANT_PROMOTE(147, "gp2", "linked_group_promote") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_PARTICIPANT_DEMOTE(148, "gp2", "linked_group_demote") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_PARENT_GROUP_DELETED(149, "gp2", "delete_parent_group") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_LINK_PARENT_GROUP_MEMBERSHIP_APPROVAL(150, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_PARTICIPANT_JOINED_GROUP_AND_PARENT_GROUP(151, "gp2", "auto_add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
MASKED_THREAD_CREATED(152, null, "masked_thread_created") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
MASKED_THREAD_UNMASKED(153, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_CHAT_ASSIGNMENT(154, null, "chat_assignment") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CHAT_PSA(155, "e2e_notification", "chat_psa") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CHAT_POLL_CREATION_MESSAGE(156, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CAG_MASKED_THREAD_CREATED(157, null, "cag_masked_thread_created") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
// WhatsappWeb has this on "gp2" and "subject", but it would be a duplicate of GROUP_CHANGE_SUBJECT
COMMUNITY_PARENT_GROUP_SUBJECT_CHANGED(158, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CAG_INVITE_AUTO_ADD(159, "gp2", "invite_auto_add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_CHAT_ASSIGNMENT_UNASSIGN(160, null, "chat_assignment_unassign") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
// WhatsappWeb has this on gp2 and invite_auto_add, but it woudd be a duplicate of CAG_INVITE_AUTO_ADD
CAG_INVITE_AUTO_JOINED(161, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SCHEDULED_CALL_START_MESSAGE(162, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_INVITE_RICH(163, "gp2", "community_invite_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_INVITE_AUTO_ADD_RICH(164, "gp2", "community_invite_auto_add_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SUB_GROUP_INVITE_RICH(165, "gp2", "sub_group_invite_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SUB_GROUP_PARTICIPANT_ADD_RICH(166, "gp2", "sub_group_participant_add_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_LINK_PARENT_GROUP_RICH(167, "gp2", "community_link_parent_group_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_PARTICIPANT_ADD_RICH(168, "gp2", "community_participant_add_rich") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SILENCED_UNKNOWN_CALLER_AUDIO(169, "call_log", "silence") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
// WhatsappWeb has this on "call_log" and silence", but it would be a duplicate of SILENCED_UNKNOWN_CALLER_AUDIO
SILENCED_UNKNOWN_CALLER_VIDEO(170, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_MEMBER_ADD_MODE(171, "gp2", "member_add_mode") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD(172, "gp2", "created_membership_requests") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_CHANGE_DESCRIPTION(173, "gp2", "parent_group_description") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SENDER_INVITE(174, null, "sender_invite") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
RECEIVER_INVITE(175, null, "receiver_invite") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_ALLOW_MEMBER_ADDED_GROUPS(176, "gp2", "allow_non_admin_sub_group_creation") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PINNED_MESSAGE_IN_CHAT(177, "pinned_message", null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_INVITE_SETUP_INVITER(178, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_INVITE_SETUP_INVITEE_RECEIVE_ONLY(179, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAYMENT_INVITE_SETUP_INVITEE_SEND_AND_RECEIVE(180, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
LINKED_GROUP_CALL_START(181, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
REPORT_TO_ADMIN_ENABLED_STATUS(182, "gp2", "allow_admin_reports") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
EMPTY_SUBGROUP_CREATE(183, "gp2", "empty_subgroup_create") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SCHEDULED_CALL_CANCEL(184, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SUBGROUP_ADMIN_TRIGGERED_AUTO_ADD_RICH(185, "gp2", "subgroup_admin_triggered_auto_add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_CHANGE_RECENT_HISTORY_SHARING(186, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
PAID_MESSAGE_SERVER_CAMPAIGN_ID(187, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GENERAL_CHAT_CREATE(188, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GENERAL_CHAT_ADD(189, "gp2", "general_chat_add") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GENERAL_CHAT_AUTO_ADD_DISABLED(190, "gp2", "general_chat_auto_add_disabled") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SUGGESTED_SUBGROUP_ANNOUNCE(191, "gp2", "created_subgroup_suggestion") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_BOT_1P_MESSAGING_ENABLED(192, "notification_template", "biz_bot_1p_disclosure") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
CHANGE_USERNAME(193, null, "change_username") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_COEX_PRIVACY_INIT_SELF(194, "notification_template", "biz_me_account_type_is_hosted") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_COEX_PRIVACY_TRANSITION_SELF(195, "notification_template", "biz_me_account_type_is_hosted_transition") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
SUPPORT_AI_EDUCATION(196, "notification_template", "saga_init") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_BOT_3P_MESSAGING_ENABLED(197, "notification_template", "biz_bot_3p_disclosure") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
REMINDER_SETUP_MESSAGE(198, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
REMINDER_SENT_MESSAGE(199, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
REMINDER_CANCEL_MESSAGE(200, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_COEX_PRIVACY_INIT(201, "notification_template", "biz_account_type_is_hosted") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
BIZ_COEX_PRIVACY_TRANSITION(202, "notification_template", "biz_account_type_changed_to_hosted") {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
GROUP_DEACTIVATED(203, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
},
COMMUNITY_DEACTIVATE_SIBLING_GROUP(204, null, null) {
@Override
public List<String> getParameters(Node node) {
return List.of();
}
};
final int index;
private final String notificationType;
private final String bodyType;
private static final Map<String, Map<String, MessageInfoStubType>> typedConstantsLookup = new HashMap<>();
private static final Map<String, MessageInfoStubType> typedDefaultsLookup = new HashMap<>();
private static final Map<String, MessageInfoStubType> anyTypeConstantsLookup = new HashMap<>();
static {
for (var stubType : values()) {
if (stubType.notificationType != null) {
if (stubType.bodyType == null) {
var existing = typedDefaultsLookup.put(stubType.notificationType, stubType);
if(existing != null) {
throw new IllegalStateException("Conflict between stub type " + stubType + " and " + existing);
}
} else {
typedConstantsLookup.compute(stubType.notificationType, (key, value) -> {
if (value == null) {
value = new HashMap<>();
}
var existing = value.put(stubType.bodyType, stubType);
if (existing != null) {
throw new IllegalStateException("Conflict between stub type " + stubType + " and " + existing);
}
return value;
});
}
} else if (stubType.bodyType != null) {
var existing = anyTypeConstantsLookup.put(stubType.bodyType, stubType);
if(existing != null) {
throw new IllegalStateException("Conflict between stub type " + stubType + " and " + existing);
}
}
}
}
public static MessageInfoStubType getStubType(String notificationType, String bodyType) {
if(bodyType == null) {
return Objects.requireNonNullElse(typedDefaultsLookup.get(notificationType), UNKNOWN);
}
var subTypeLookup = typedConstantsLookup.get(notificationType);
if(subTypeLookup == null) {
return UNKNOWN;
}
var subTypeResult = subTypeLookup.get(bodyType);
if(subTypeResult != null) {
return subTypeResult;
}
return anyTypeConstantsLookup.getOrDefault(bodyType, UNKNOWN);
}
MessageInfoStubType(@ProtobufEnumIndex int index, String notificationType, String bodyType) {
this.index = index;
this.notificationType = notificationType;
this.bodyType = bodyType;
}
public abstract List<String> getParameters(Node node);
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/NativeFlowInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.button.base.ButtonBody;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A model class that holds the information related to a native flow.
*/
@ProtobufMessage(name = "Message.ButtonsMessage.Button.NativeFlowInfo")
public final class NativeFlowInfo implements Info, ButtonBody {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String parameters;
NativeFlowInfo(String name, String parameters) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.parameters = Objects.requireNonNull(parameters, "parameters cannot be null");
}
public String name() {
return name;
}
public String parameters() {
return parameters;
}
@Override
public Type bodyType() {
return Type.NATIVE_FLOW;
}
@Override
public boolean equals(Object o) {
return o instanceof NativeFlowInfo that
&& Objects.equals(name, that.name)
&& Objects.equals(parameters, that.parameters);
}
@Override
public int hashCode() {
return Objects.hash(name, parameters);
}
@Override
public String toString() {
return "NativeFlowInfo[" +
"name=" + name +
", parameters=" + parameters +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/NewsletterMessageInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.alibaba.fastjson2.JSONObject;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.MessageContainer;
import com.github.auties00.cobalt.model.message.model.MessageReceipt;
import com.github.auties00.cobalt.model.message.model.MessageStatus;
import com.github.auties00.cobalt.model.newsletter.Newsletter;
import com.github.auties00.cobalt.model.newsletter.NewsletterReaction;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.*;
// TODO: Add receipts
@ProtobufMessage
public final class NewsletterMessageInfo implements MessageInfo {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.INT32)
final int serverId;
@ProtobufProperty(index = 3, type = ProtobufType.UINT64)
final Long timestampSeconds;
@ProtobufProperty(index = 4, type = ProtobufType.UINT64)
final Long views;
@ProtobufProperty(index = 5, type = ProtobufType.MAP, mapKeyType = ProtobufType.STRING, mapValueType = ProtobufType.MESSAGE)
final Map<String, NewsletterReaction> reactions;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
MessageContainer message;
@ProtobufProperty(index = 7, type = ProtobufType.ENUM)
MessageStatus status;
@ProtobufProperty(index = 8, type = ProtobufType.BOOL)
boolean starred;
@ProtobufProperty(index = 9, type = ProtobufType.MESSAGE)
final MessageReceipt receipt;
Newsletter newsletter;
NewsletterMessageInfo(String id, int serverId, Long timestampSeconds, Long views, Map<String, NewsletterReaction> reactions, MessageContainer message, MessageStatus status, boolean starred, MessageReceipt receipt) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.serverId = serverId;
this.timestampSeconds = timestampSeconds;
this.views = views;
this.reactions = reactions;
this.message = message;
this.status = status;
this.starred = starred;
this.receipt = receipt;
}
public static Optional<NewsletterMessageInfo> ofJson(JSONObject jsonObject) {
if(jsonObject == null) {
return Optional.empty();
}
var id = jsonObject.getString("id");
if(id == null) {
return Optional.empty();
}
var serverId = jsonObject.getIntValue("serverId", -1);
var timestampSeconds = jsonObject.getLongValue("timestampSeconds", 0);
var views = jsonObject.getLongValue("views", 0);
var reactionsJsonObject = jsonObject.getJSONObject("reactions");
Map<String, NewsletterReaction> reactions = HashMap.newHashMap(reactionsJsonObject.size());
for(var reactionKey : reactionsJsonObject.sequencedKeySet()) {
var reactionJsonObject = reactionsJsonObject.getJSONObject(reactionKey);
NewsletterReaction.ofJson(reactionJsonObject)
.ifPresent(reaction -> reactions.put(reactionKey, reaction));
}
var message = MessageContainer.ofJson(jsonObject.getJSONObject("message"))
.orElse(MessageContainer.empty());
var status = MessageStatus.of(jsonObject.getString("status"))
.orElse(MessageStatus.ERROR);
// TODO
var starred = false;
var receipt = new MessageReceipt();
return Optional.of(new NewsletterMessageInfo(id, serverId, timestampSeconds, views, reactions, message, status, starred, receipt));
}
public void setNewsletter(Newsletter newsletter) {
Objects.requireNonNull(newsletter, "Newsletter cannot be null");
this.newsletter = newsletter;
}
public Jid newsletterJid() {
Objects.requireNonNull(newsletter, "Newsletter cannot be null");
return newsletter.jid();
}
@Override
public Jid parentJid() {
return newsletterJid();
}
@Override
public Optional<MessageInfoParent> parent() {
return Optional.ofNullable(newsletter);
}
@Override
public void setParent(MessageInfoParent parent) {
if(parent == null) {
this.newsletter = null;
}else if(!(parent instanceof Newsletter parentNewsletter)) {
throw new IllegalArgumentException("Parent is not a newsletter");
}else {
this.newsletter = parentNewsletter;
}
}
@Override
public Optional<Contact> sender() {
return Optional.empty();
}
@Override
public void setSender(Contact sender) {
}
@Override
public Jid senderJid() {
return newsletterJid();
}
public Newsletter newsletter() {
return Objects.requireNonNull(newsletter, "newsletter cannot be null when accessed");
}
public String id() {
return id;
}
public int serverId() {
return serverId;
}
@Override
public OptionalLong timestampSeconds() {
return timestampSeconds == null ? OptionalLong.empty() : OptionalLong.of(timestampSeconds);
}
public OptionalLong views() {
return views == null ? OptionalLong.empty() : OptionalLong.of(views);
}
public MessageContainer message() {
return message;
}
@Override
public void setMessage(MessageContainer message) {
this.message = message;
}
public Optional<ZonedDateTime> timestamp() {
return Clock.parseSeconds(timestampSeconds);
}
@Override
public MessageStatus status() {
return status;
}
@Override
public void setStatus(MessageStatus status) {
this.status = status;
}
public Collection<NewsletterReaction> reactions() {
return Collections.unmodifiableCollection(reactions.values());
}
public Optional<NewsletterReaction> findReaction(String value) {
return Optional.ofNullable(reactions.get(value));
}
public Optional<NewsletterReaction> addReaction(NewsletterReaction reaction) {
return Optional.ofNullable(reactions.put(reaction.content(), reaction));
}
public Optional<NewsletterReaction> removeReaction(String code) {
return Optional.ofNullable(reactions.remove(code));
}
public void incrementReaction(String code, boolean fromMe) {
findReaction(code).ifPresentOrElse(reaction -> {
reaction.setCount(reaction.count() + 1);
reaction.setFromMe(fromMe);
}, () -> {
var reaction = new NewsletterReaction(code, 1, fromMe);
addReaction(reaction);
});
}
public void decrementReaction(String code) {
findReaction(code).ifPresent(reaction -> {
if (reaction.count() <= 1) {
removeReaction(reaction.content());
return;
}
reaction.setCount(reaction.count() - 1);
reaction.setFromMe(false);
});
}
public boolean starred() {
return starred;
}
public void setStarred(boolean starred) {
this.starred = starred;
}
public MessageReceipt receipt() {
return receipt;
}
@Override
public String toString() {
return "NewsletterMessageInfo{" +
"newsletter=" + newsletter +
", id='" + id + '\'' +
", serverId=" + serverId +
", timestampSeconds=" + timestampSeconds +
", views=" + views +
", reactions=" + reactions +
", message=" + message +
", status=" + status +
'}';
}
@Override
public boolean equals(Object o) {
return o instanceof NewsletterMessageInfo that &&
serverId == that.serverId &&
Objects.equals(id, that.id) &&
Objects.equals(timestampSeconds, that.timestampSeconds) &&
Objects.equals(views, that.views) &&
Objects.equals(reactions, that.reactions) &&
Objects.equals(message, that.message) &&
Objects.equals(newsletter, that.newsletter) &&
status == that.status;
}
@Override
public int hashCode() {
return Objects.hash(id, serverId, timestampSeconds, views, reactions, message, newsletter, status);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/PaymentInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.ChatMessageKey;
import com.github.auties00.cobalt.model.payment.PaymentMoney;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that holds the information related to a payment.
*/
@ProtobufMessage(name = "PaymentInfo")
public final class PaymentInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.ENUM)
final Currency currencyDeprecated;
@ProtobufProperty(index = 2, type = ProtobufType.UINT64)
final long amount1000;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final Jid receiverJid;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
final Status status;
@ProtobufProperty(index = 5, type = ProtobufType.UINT64)
final long transactionTimestampSeconds;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final ChatMessageKey requestMessageKey;
@ProtobufProperty(index = 7, type = ProtobufType.UINT64)
final long expiryTimestampSeconds;
@ProtobufProperty(index = 8, type = ProtobufType.BOOL)
final boolean futureProofed;
@ProtobufProperty(index = 9, type = ProtobufType.STRING)
final String currency;
@ProtobufProperty(index = 10, type = ProtobufType.ENUM)
final TransactionStatus transactionStatus;
@ProtobufProperty(index = 11, type = ProtobufType.BOOL)
final boolean useNoviFormat;
@ProtobufProperty(index = 12, type = ProtobufType.MESSAGE)
final PaymentMoney primaryAmount;
@ProtobufProperty(index = 13, type = ProtobufType.MESSAGE)
final PaymentMoney exchangeAmount;
PaymentInfo(Currency currencyDeprecated, long amount1000, Jid receiverJid, Status status, long transactionTimestampSeconds, ChatMessageKey requestMessageKey, long expiryTimestampSeconds, boolean futureProofed, String currency, TransactionStatus transactionStatus, boolean useNoviFormat, PaymentMoney primaryAmount, PaymentMoney exchangeAmount) {
this.currencyDeprecated = Objects.requireNonNull(currencyDeprecated, "currencyDeprecated cannot be null");
this.amount1000 = amount1000;
this.receiverJid = Objects.requireNonNull(receiverJid, "receiverJid cannot be null");
this.status = Objects.requireNonNull(status, "status cannot be null");
this.transactionTimestampSeconds = transactionTimestampSeconds;
this.requestMessageKey = Objects.requireNonNull(requestMessageKey, "requestMessageKey cannot be null");
this.expiryTimestampSeconds = expiryTimestampSeconds;
this.futureProofed = futureProofed;
this.currency = Objects.requireNonNull(currency, "currency cannot be null");
this.transactionStatus = Objects.requireNonNull(transactionStatus, "transactionStatus cannot be null");
this.useNoviFormat = useNoviFormat;
this.primaryAmount = Objects.requireNonNull(primaryAmount, "primaryAmount cannot be null");
this.exchangeAmount = Objects.requireNonNull(exchangeAmount, "exchangeAmount cannot be null");
}
public Currency currencyDeprecated() {
return currencyDeprecated;
}
public long amount1000() {
return amount1000;
}
public Jid receiverJid() {
return receiverJid;
}
public Status status() {
return status;
}
public long transactionTimestampSeconds() {
return transactionTimestampSeconds;
}
public ChatMessageKey requestMessageKey() {
return requestMessageKey;
}
public long expiryTimestampSeconds() {
return expiryTimestampSeconds;
}
public boolean futureProofed() {
return futureProofed;
}
public String currency() {
return currency;
}
public TransactionStatus transactionStatus() {
return transactionStatus;
}
public boolean useNoviFormat() {
return useNoviFormat;
}
public PaymentMoney primaryAmount() {
return primaryAmount;
}
public PaymentMoney exchangeAmount() {
return exchangeAmount;
}
public Optional<ZonedDateTime> transactionTimestamp() {
return Clock.parseSeconds(transactionTimestampSeconds);
}
public Optional<ZonedDateTime> expiryTimestamp() {
return Clock.parseSeconds(expiryTimestampSeconds);
}
@Override
public boolean equals(Object o) {
return o instanceof PaymentInfo that
&& Objects.equals(currencyDeprecated, that.currencyDeprecated)
&& amount1000 == that.amount1000
&& Objects.equals(receiverJid, that.receiverJid)
&& Objects.equals(status, that.status)
&& transactionTimestampSeconds == that.transactionTimestampSeconds
&& Objects.equals(requestMessageKey, that.requestMessageKey)
&& expiryTimestampSeconds == that.expiryTimestampSeconds
&& futureProofed == that.futureProofed
&& Objects.equals(currency, that.currency)
&& Objects.equals(transactionStatus, that.transactionStatus)
&& useNoviFormat == that.useNoviFormat
&& Objects.equals(primaryAmount, that.primaryAmount)
&& Objects.equals(exchangeAmount, that.exchangeAmount);
}
@Override
public int hashCode() {
return Objects.hash(currencyDeprecated, amount1000, receiverJid, status, transactionTimestampSeconds,
requestMessageKey, expiryTimestampSeconds, futureProofed, currency, transactionStatus, useNoviFormat,
primaryAmount, exchangeAmount);
}
@Override
public String toString() {
return "PaymentInfo[" +
"currencyDeprecated=" + currencyDeprecated + ", " +
"amount1000=" + amount1000 + ", " +
"receiverJid=" + receiverJid + ", " +
"status=" + status + ", " +
"transactionTimestampSeconds=" + transactionTimestampSeconds + ", " +
"requestMessageKey=" + requestMessageKey + ", " +
"expiryTimestampSeconds=" + expiryTimestampSeconds + ", " +
"futureProofed=" + futureProofed + ", " +
"currency=" + currency + ", " +
"transactionStatus=" + transactionStatus + ", " +
"useNoviFormat=" + useNoviFormat + ", " +
"primaryAmount=" + primaryAmount + ", " +
"exchangeAmount=" + exchangeAmount + ']';
}
/**
* The constants of this enumerated type describe the status of a payment described by a
* {@link PaymentInfo}
*/
@ProtobufEnum(name = "PaymentInfo.Status")
public enum Status {
/**
* Unknown status
*/
UNKNOWN_STATUS(0),
/**
* Processing
*/
PROCESSING(1),
/**
* Sent
*/
SENT(2),
/**
* Need to accept
*/
NEED_TO_ACCEPT(3),
/**
* Complete
*/
COMPLETE(4),
/**
* Could not complete
*/
COULD_NOT_COMPLETE(5),
/**
* Refunded
*/
REFUNDED(6),
/**
* Expired
*/
EXPIRED(7),
/**
* Rejected
*/
REJECTED(8),
/**
* Cancelled
*/
CANCELLED(9),
/**
* Waiting for payer
*/
WAITING_FOR_PAYER(10),
/**
* Waiting
*/
WAITING(11);
final int index;
Status(@ProtobufEnumIndex int index) {
this.index = index;
}
}
/**
* The constants of this enumerated type describe the currencies supported for a transaction
* described by a {@link PaymentInfo}
*/
@ProtobufEnum(name = "PaymentInfo.Currency")
public enum Currency {
/**
* Unknown currency
*/
UNKNOWN_CURRENCY(0),
/**
* Indian rupees
*/
INR(1);
final int index;
Currency(@ProtobufEnumIndex int index) {
this.index = index;
}
}
@ProtobufEnum(name = "PaymentInfo.TxnStatus")
public enum TransactionStatus {
UNKNOWN(0),
PENDING_SETUP(1),
PENDING_RECEIVER_SETUP(2),
INIT(3),
SUCCESS(4),
COMPLETED(5),
FAILED(6),
FAILED_RISK(7),
FAILED_PROCESSING(8),
FAILED_RECEIVER_PROCESSING(9),
FAILED_DA(10),
FAILED_DA_FINAL(11),
REFUNDED_TXN(12),
REFUND_FAILED(13),
REFUND_FAILED_PROCESSING(14),
REFUND_FAILED_DA(15),
EXPIRED_TXN(16),
AUTH_CANCELED(17),
AUTH_CANCEL_FAILED_PROCESSING(18),
AUTH_CANCEL_FAILED(19),
COLLECT_INIT(20),
COLLECT_SUCCESS(21),
COLLECT_FAILED(22),
COLLECT_FAILED_RISK(23),
COLLECT_REJECTED(24),
COLLECT_EXPIRED(25),
COLLECT_CANCELED(26),
COLLECT_CANCELLING(27),
IN_REVIEW(28),
REVERSAL_SUCCESS(29),
REVERSAL_PENDING(30),
REFUND_PENDING(31);
final int index;
TransactionStatus(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/ProductListInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.product.ProductListHeaderImage;
import com.github.auties00.cobalt.model.product.ProductSection;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
/**
* A model class that holds the information related to a list of products.
*/
@ProtobufMessage(name = "Message.ListMessage.ProductListInfo")
public final class ProductListInfo implements Info {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final List<ProductSection> productSections;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final ProductListHeaderImage headerImage;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final Jid sellerJid;
ProductListInfo(List<ProductSection> productSections, ProductListHeaderImage headerImage, Jid sellerJid) {
this.productSections = Objects.requireNonNullElse(productSections, List.of());
this.headerImage = headerImage;
this.sellerJid = Objects.requireNonNull(sellerJid, "sellerJid cannot be null");
}
public List<ProductSection> productSections() {
return productSections;
}
public ProductListHeaderImage headerImage() {
return headerImage;
}
public Jid sellerJid() {
return sellerJid;
}
@Override
public boolean equals(Object o) {
return o instanceof ProductListInfo that
&& Objects.equals(productSections, that.productSections)
&& Objects.equals(headerImage, that.headerImage)
&& Objects.equals(sellerJid, that.sellerJid);
}
@Override
public int hashCode() {
return Objects.hash(productSections, headerImage, sellerJid);
}
@Override
public String toString() {
return "ProductListInfo[" +
"productSections=" + productSections +
", headerImage=" + headerImage +
", sellerJid=" + sellerJid +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/info/QuotedMessageInfo.java
================================================
package com.github.auties00.cobalt.model.info;
import com.github.auties00.cobalt.model.chat.Chat;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.model.message.model.MessageContainer;
import com.github.auties00.cobalt.model.message.model.MessageStatus;
import com.github.auties00.cobalt.model.newsletter.Newsletter;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
/**
* An immutable model class that represents a quoted message
*/
@ProtobufMessage
public final class QuotedMessageInfo implements MessageInfo {
/**
* The id of the message
*/
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
/**
* The sender of the message
*/
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
Contact sender;
/**
* The message
*/
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
MessageContainer message;
// FIXME: Add a feature in ModernProtobuf that evaluates sealed types
// and considers if all implementations match the expected type instead of simulating it like this
/**
* The chat of the message
*/
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
Chat parentChat;
/**
* The newsletter of the message
*/
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
Newsletter parentNewsletter;
QuotedMessageInfo(String id, Contact sender, MessageContainer message, Chat chat, Newsletter newsletter) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.sender = sender;
this.message = Objects.requireNonNull(message, "message cannot be null");
if(chat == null && newsletter == null) {
throw new NullPointerException("parent cannot be null");
}
this.parentChat = chat;
this.parentNewsletter = newsletter;
}
QuotedMessageInfo(String id, Contact sender, MessageContainer message, MessageInfoParent parent) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.sender = sender;
this.message = Objects.requireNonNull(message, "message cannot be null");
setParent(parent);
}
/**
* Constructs a quoted message from a context info
*
* @param contextInfo the non-null context info
* @return an optional quoted message
*/
public static Optional<QuotedMessageInfo> of(ContextInfo contextInfo) {
if (!contextInfo.hasQuotedMessage()) {
return Optional.empty();
}
var id = contextInfo.quotedMessageId().orElseThrow();
var parent = contextInfo.quotedMessageParent().orElseThrow();
var sender = contextInfo.quotedMessageSender().orElse(null);
var message = contextInfo.quotedMessage().orElseThrow();
return Optional.of(new QuotedMessageInfo(id, sender, message, parent));
}
@Override
public MessageStatus status() {
return MessageStatus.UNKNOWN;
}
@Override
public void setStatus(MessageStatus status) {
}
@Override
public Jid parentJid() {
if(parentChat != null) {
return parentChat.jid();
}else if(parentNewsletter != null) {
return parentNewsletter.jid();
}else {
throw new InternalError();
}
}
@Override
public Optional<MessageInfoParent> parent() {
if(parentChat != null) {
return Optional.of(parentChat);
} else if(parentNewsletter != null) {
return Optional.of(parentNewsletter);
} else {
return Optional.empty();
}
}
@Override
public void setParent(MessageInfoParent parent) {
switch (Objects.requireNonNull(parent, "parent cannot be null")) {
case Chat chat -> this.parentChat = chat;
case Newsletter newsletter -> this.parentNewsletter = newsletter;
}
}
/**
* Returns the sender's value
*
* @return a value
*/
@Override
public Jid senderJid() {
if(sender != null) {
return sender.jid();
} else if(parentChat != null) {
return parentChat.jid();
}else if(parentNewsletter != null) {
return parentNewsletter.jid();
}else {
throw new InternalError();
}
}
/**
* Returns the sender of this message
*
* @return an optional
*/
public Optional<Contact> sender() {
return Optional.ofNullable(sender);
}
@Override
public void setSender(Contact sender) {
Objects.requireNonNull(sender, "Sender cannot be null");
this.sender = sender;
}
@Override
public String id() {
return id;
}
@Override
public MessageContainer message() {
return message;
}
@Override
public void setMessage(MessageContainer message) {
this.message = message;
}
@Override
public OptionalLong timestampSeconds() {
return OptionalLong.empty();
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/jid/Jid.java
================================================
package com.github.auties00.cobalt.model.jid;
import com.github.auties00.cobalt.exception.MalformedJidException;
import com.github.auties00.libsignal.SignalProtocolAddress;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
import it.auties.protobuf.model.ProtobufString;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A model class that represents a value.
* This class is only a model: this means that changing its values will have no real effect on WhatsappWeb's servers.
*/
public final class Jid implements JidProvider {
private static final char PHONE_CHAR = '+';
private static final char SERVER_CHAR = '@';
private static final char DEVICE_CHAR = ':';
private static final char AGENT_CHAR = '_';
private static final Jid LEGACY_USER = new Jid(null, JidServer.legacyUser(), 0, 0);
private static final Jid GROUP_OR_COMMUNITY = new Jid(null, JidServer.groupOrCommunity(), 0, 0);
private static final Jid BROADCAST = new Jid(null, JidServer.broadcast(), 0, 0);
private static final Jid CALL = new Jid(null, JidServer.call(), 0, 0);
private static final Jid USER = new Jid(null, JidServer.user(), 0, 0);
private static final Jid LID = new Jid(null, JidServer.lid(), 0, 0);
private static final Jid NEWSLETTER = new Jid(null, JidServer.newsletter(), 0, 0);
private static final Jid BOT = new Jid(null, JidServer.bot(), 0, 0);
private static final Jid OFFICIAL_SURVEYS_ACCOUNT = new Jid("16505361212", JidServer.user(), 0, 0);
private static final Jid OFFICIAL_BUSINESS_ACCOUNT = new Jid("16505361212", JidServer.legacyUser(), 0, 0);
private static final Jid STATUS_BROADCAST = new Jid("status", JidServer.broadcast(), 0, 0);
private static final Jid ANNOUNCEMENTS = new Jid("0", JidServer.user(), 0, 0);
private static final ConcurrentMap<JidServer, Jid> unknownServerJidsStore = new ConcurrentHashMap<>();
private final String user;
private final JidServer server;
private final int device;
private final int agent;
private Jid(String user, JidServer server, int device, int agent) {
this.user = user;
this.server = server;
this.device = checkUnsignedByte(device);
this.agent = checkUnsignedByte(agent);
}
private static int checkUnsignedByte(int i) {
if(i < 0 || i > 255) {
throw new MalformedJidException(i + " is not an unsigned byte");
}
return i;
}
public static Jid legacyUserServer() {
return LEGACY_USER;
}
public static Jid groupOrCommunityServer() {
return GROUP_OR_COMMUNITY;
}
public static Jid broadcastServer() {
return BROADCAST;
}
public static Jid callServer() {
return CALL;
}
public static Jid userServer() {
return USER;
}
public static Jid lidServer() {
return LID;
}
public static Jid newsletterServer() {
return NEWSLETTER;
}
public static Jid Server() {
return BOT;
}
public static Jid officialSurveysAccount() {
return OFFICIAL_SURVEYS_ACCOUNT;
}
public static Jid officialBusinessAccount() {
return OFFICIAL_BUSINESS_ACCOUNT;
}
public static Jid statusBroadcastAccount() {
return STATUS_BROADCAST;
}
public static Jid announcementsAccount() {
return ANNOUNCEMENTS;
}
/**
* Creates a new instance of Jid based on the provided input parameters.
* If the user value starts with a '+' character, the character is removed before constructing the Jid.
* Otherwise, the user value is used as is.
*
* @param user the user
* @param server the non-null server
* @param device the device
* @param agent the agent
* @return a non-null contact value
*/
public static Jid of(String user, JidServer server, int device, int agent) {
Objects.requireNonNull(server, "Server cannot be null");
if(user == null) {
return new Jid(null, server, device, agent);
}
var inputLength = user.length();
var offset = !user.isEmpty() && user.charAt(0) == PHONE_CHAR
? 1
: 0;
for(var i = 0; i < inputLength; i++) {
var token = user.charAt(i);
if (token == SERVER_CHAR || token == DEVICE_CHAR || token == AGENT_CHAR) {
return new Jid(user.substring(offset, i), server, device, agent);
}
}
return new Jid(offset == 0 ? user : user.substring(offset), server, device, agent);
}
/**
* Constructs a new ContactId that represents a server
*
* @param server the non-null custom server
* @return a non-null contact value
*/
public static Jid of(JidServer server) {
Objects.requireNonNull(server, "Server cannot be null");
return switch (server.type()) {
case UNKNOWN -> unknownServerJidsStore.computeIfAbsent(server, value -> new Jid(null, value, 0, 0));
case LEGACY_USER -> LEGACY_USER;
case GROUP_OR_COMMUNITY -> GROUP_OR_COMMUNITY;
case BROADCAST -> BROADCAST;
case CALL -> CALL;
case USER -> USER;
case LID -> LID;
case NEWSLETTER -> NEWSLETTER;
case BOT -> BOT;
};
}
/**
* Constructs a new ContactId for a user from a value
*
* @param jid the non-null value of the user
* @return a non-null contact value
*/
public static Jid of(long jid) {
if(jid < 0) {
throw new MalformedJidException("value cannot be negative");
}
return new Jid(String.valueOf(jid), JidServer.user(), 0, 0);
}
/**
* Constructs a new ContactId for a user from a value and a custom server
*
* @param jid the non-null value
* @return a non-null contact value
*/
public static Jid of(String jid) {
if(jid == null) {
return null;
}
var knownServer = JidServer.of(jid, false);
if(knownServer != null) {
return of(knownServer);
}
var serverSeparatorIndex = jid.indexOf(SERVER_CHAR);
var server = serverSeparatorIndex == -1
? JidServer.user()
: JidServer.of(jid, serverSeparatorIndex + 1, jid.length() - serverSeparatorIndex - 1);
return parseJid(jid, serverSeparatorIndex, server);
}
/**
* Constructs a new ContactId for a user from a value and a custom server
*
* @param user the nullable user
* @param server the non-null custom server
* @return a non-null contact value
*/
public static Jid of(String user, JidServer server) {
Objects.requireNonNull(server, "Server cannot be null");
return parseJid(user, user.indexOf(SERVER_CHAR), server);
}
private static Jid parseJid(String jid, int jidLength, JidServer server) {
var length = jidLength == -1 ? jid.length() : jidLength;
if (length == 0) {
return new Jid(null, server, 0, 0);
}
var offset = jid.charAt(0) == PHONE_CHAR ? 1 : 0;
if (offset >= length) {
throw new MalformedJidException("Malformed value '" + jid + "'");
}
enum ParserState {
USER,
DEVICE,
AGENT
}
var state = ParserState.USER;
var userLength = length;
var agent = 0;
var device = 0;
for (var parserPosition = 0; parserPosition < length; parserPosition++) {
var token = jid.charAt(offset + parserPosition);
if (token == SERVER_CHAR) {
if(state == ParserState.USER) {
userLength = parserPosition;
}
server = JidServer.of(jid, offset + parserPosition + 1, length - parserPosition - 1);
break;
}
switch (state) {
case USER -> {
if(token == DEVICE_CHAR) {
userLength = parserPosition;
state = ParserState.DEVICE;
}else if(token == AGENT_CHAR) {
userLength = parserPosition;
state = ParserState.AGENT;
}
}
case DEVICE -> {
if (token == AGENT_CHAR) {
if(agent != 0) {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
state = ParserState.AGENT;
} else if(Character.isDigit(token)) {
device = device * 10 + (token - '0');
}else {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
}
case AGENT -> {
if (token == DEVICE_CHAR) {
if(device != 0) {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
state = ParserState.DEVICE;
} else if(Character.isDigit(token)) {
agent = agent * 10 + (token - '0');
}else {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
}
}
}
var user = jid.substring(offset, offset + userLength);
return new Jid(user, server, device, agent);
}
/**
* Constructs a new ContactId for a user from a value
*
* @param jid the non-null value of the user
* @return a non-null contact value
*/
@ProtobufDeserializer
public static Jid of(ProtobufString jid) {
return switch(jid) {
case ProtobufString.Lazy lazy -> Jid.of(lazy);
case ProtobufString.Value value -> Jid.of(value.toString());
case null -> null;
};
}
/**
* Constructs a new ContactId for a user from a value
*
* @param jid the non-null value of the user
* @return a non-null contact value
*/
public static Jid of(ProtobufString.Lazy jid) {
if(jid == null) {
return null;
}
enum ParserState {
USER,
DEVICE,
AGENT,
SERVER
}
var source = jid.encodedBytes();
var offset = jid.encodedOffset();
var length = jid.encodedLength();
var knownServer = JidServer.of(source, offset, length, false);
if(knownServer != null) {
return of(knownServer);
}
var state = ParserState.USER;
var userLength = length; // Do not allocate a char[], it's slower
var agent = 0;
var device = 0;
var server = JidServer.user();
for (var parserPosition = 0; parserPosition < length; parserPosition++) {
var token = (char) (source[offset + parserPosition] & 0x7F);
if (token == SERVER_CHAR) {
if(state == ParserState.USER) {
userLength = parserPosition;
}
server = JidServer.of(source, offset + parserPosition + 1, length - parserPosition - 1, true);
break;
}
switch (state) {
case USER -> {
if(token == DEVICE_CHAR) {
// device is already 0
userLength = parserPosition;
state = ParserState.DEVICE;
}else if(token == AGENT_CHAR) {
// agent is already 0
userLength = parserPosition;
state = ParserState.AGENT;
}
}
case DEVICE -> {
if (token == AGENT_CHAR) {
if(agent != 0) {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
state = ParserState.AGENT;
} else if(Character.isDigit(token)) {
device = device * 10 + (token - '0');
}else {
var value = new String(source, offset, length, StandardCharsets.US_ASCII);
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + value + "'");
}
}
case AGENT -> {
if (token == DEVICE_CHAR) {
if(device != 0) {
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + jid + "'");
}
state = ParserState.DEVICE;
} else if(Character.isDigit(token)) {
agent = agent * 10 + (token - '0');
}else {
var value = new String(source, offset, length, StandardCharsets.US_ASCII);
throw new MalformedJidException("Encountered unexpected token '" + token + "'" + " while parsing value '" + value + "'");
}
}
}
}
var user = new String(source, offset, userLength, StandardCharsets.UTF_8);
return new Jid(user, server, device, agent);
}
/**
* Returns whether this value ends with the provided server
*
* @param server the server to check against
* @return a boolean
*/
public boolean hasServer(JidServer server) {
return this.server.equals(server);
}
/**
* Returns whether this value is a server value
*
* @param server the server to check against
* @return a boolean
*/
public boolean isServerJid(JidServer server) {
return user == null && this.server.equals(server);
}
/**
* Returns a new value using with a different server
*
* @param server the new server
* @return a non-null value
*/
public Jid withServer(JidServer server) {
return Objects.equals(this.server, server)
? this
: new Jid(user, server, device, agent);
}
/**
* Returns a new value using with a different agent
*
* @param agent the new agent
* @return a non-null value
*/
public Jid withAgent(int agent) {
return this.agent == agent
? this
: new Jid(user, server, device, agent);
}
/**
* Returns a new value using with a different device
*
* @param device the new device
* @return a non-null value
*/
public Jid withDevice(int device) {
return this.device == device
? this
: new Jid(user, server, device, agent);
}
/**
* Converts this value to a user value
*
* @return a non-null value
*/
public Jid withoutData() {
return !hasDevice() && !hasAgent()
? this
: new Jid(user, server, 0, 0);
}
/**
* Converts this value to a non-formatted phone value
*
* @return a non-null String
*/
public Optional<String> toPhoneNumber() {
if(user == null) {
return Optional.empty();
}
var length = user.length();
for(var i = 0; i < length; i++) {
if(!Character.isDigit(user.charAt(i))) {
return Optional.empty();
}
}
return Optional.of(PHONE_CHAR + user);
}
/**
* Converts this value to a String
*
* @return a non-null String
*/
@ProtobufSerializer
@Override
public String toString() {
var hasUser = hasUser();
var hasAgent = hasAgent();
var hasDevice = hasDevice();
if (!hasUser && !hasAgent && !hasDevice) {
return server.toString();
}
var user = hasUser ? this.user : "";
var agent = hasAgent ? "" + AGENT_CHAR + this.agent : "";
var device = hasDevice ? "" + DEVICE_CHAR + this.device : "";
return user + agent + device + SERVER_CHAR + server.toString();
}
/**
* Converts this value to a signal address
*
* @return a non-null {@link SignalProtocolAddress}
*/
public SignalProtocolAddress toSignalAddress() {
return new SignalProtocolAddress(user, device);
}
/**
* Returns this object as a value
*
* @return a non-null value
*/
@Override
public Jid toJid() {
return this;
}
/**
* Returns the user
*
* @return a nullable value
*/
public String user() {
return user;
}
/**
* Returns the server
*
* @return a non-null server
*/
public JidServer server() {
return server;
}
/**
* Returns the device
*
* @return an unsigned int
*/
public int device() {
return device;
}
/**
* Returns the agent
*
* @return an unsigned int
*/
public int agent() {
return agent;
}
/**
* Returns whether this value specifies a user
*
* @return a boolean
*/
public boolean hasUser() {
return user != null;
}
/**
* Returns whether this value specifies a device
*
* @return a boolean
*/
public boolean hasDevice() {
return device != 0;
}
/**
* Returns whether this value specifies an agent
*
* @return a boolean
*/
public boolean hasAgent() {
return agent != 0;
}
@Override
public boolean equals(Object o) {
return o instanceof Jid that
&& Objects.equals(user, that.user)
&& Objects.equals(server, that.server)
&& device == that.device
&& agent == that.agent;
}
@Override
public int hashCode() {
return Objects.hash(user, server, device, agent);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/jid/JidDevice.java
================================================
package com.github.auties00.cobalt.model.jid;
import com.github.auties00.cobalt.client.WhatsAppClientType;
import com.github.auties00.cobalt.model.auth.UserAgent.PlatformType;
import com.github.auties00.cobalt.model.auth.Version;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
@ProtobufMessage
public final class JidDevice {
private static final List<JidDevice> IOS_DEVICES = List.of(
// --- iPhone 7 --- (Supports iOS 10-15)
new JidDevice(
"iPhone 7",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone9,3",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 7",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone9,3",
WhatsAppClientType.MOBILE
),
// --- iPhone 7 Plus --- (Supports iOS 10-15)
new JidDevice(
"iPhone 7 Plus",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone9,4",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 7 Plus",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone9,4",
WhatsAppClientType.MOBILE
),
// --- iPhone 8 --- (Supports iOS 11-16)
new JidDevice(
"iPhone 8",
"Apple",
null,
Version.of("13.7"),
"17H35",
"iPhone10,4",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 8",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone10,4",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 8",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone10,4",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 8",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone10,4",
WhatsAppClientType.MOBILE
),
// --- iPhone 8 Plus --- (Supports iOS 11-16)
new JidDevice(
"iPhone 8 Plus",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone10,5",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 8 Plus",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone10,5",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone 8 Plus",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone10,5",
WhatsAppClientType.MOBILE
),
// --- iPhone X --- (Supports iOS 11-16)
new JidDevice(
"iPhone X",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone10,6",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone X",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone10,6",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone X",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone10,6",
WhatsAppClientType.MOBILE
),
// --- iPhone XR --- (Supports iOS 12-17)
new JidDevice(
"iPhone XR",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone11,8",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XR",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone11,8",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XR",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone11,8",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XR",
"Apple",
null,
Version.of("17.4.1"),
"21E236",
"iPhone11,8",
WhatsAppClientType.MOBILE
),
// --- iPhone XS --- (Supports iOS 12-17)
new JidDevice(
"iPhone XS",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone11,2",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone11,2",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone11,2",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS",
"Apple",
null,
Version.of("17.4.1"),
"21E236",
"iPhone11,2",
WhatsAppClientType.MOBILE
),
// --- iPhone XS Max --- (Supports iOS 12-17)
new JidDevice(
"iPhone XS Max",
"Apple",
null,
Version.of("14.8.1"),
"18H107",
"iPhone11,6",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS Max",
"Apple",
null,
Version.of("15.8.2"),
"19H384",
"iPhone11,6",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS Max",
"Apple",
null,
Version.of("16.7.7"),
"20H330",
"iPhone11,6",
WhatsAppClientType.MOBILE
),
new JidDevice(
"iPhone XS Max",
"Apple",
null,
Version.of("17.4.1"),
"21E236",
"iPhone11,6",
WhatsAppClientType.MOBILE
)
);
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String model;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String manufacturer;
@ProtobufProperty(index = 3, type = ProtobufType.ENUM)
final PlatformType platform;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final Version osVersion;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final String osBuildNumber;
@ProtobufProperty(index = 6, type = ProtobufType.STRING)
final String modelId;
@ProtobufProperty(index = 7, type = ProtobufType.ENUM)
final WhatsAppClientType clientType;
JidDevice(
String model,
String manufacturer,
PlatformType platform,
Version osVersion,
String osBuildNumber,
String modelId,
WhatsAppClientType clientType
) {
this.model = model;
this.modelId = modelId;
this.manufacturer = manufacturer;
this.platform = platform;
this.osVersion = osVersion;
this.osBuildNumber = osBuildNumber;
this.clientType = clientType;
}
public static JidDevice web() {
return new JidDevice(
"Surface Pro 4",
"Microsoft",
PlatformType.MACOS,
Version.of("10.0"),
null,
null,
WhatsAppClientType.WEB
);
}
public static JidDevice ios(boolean business) {
var device = IOS_DEVICES.get(ThreadLocalRandom.current().nextInt(IOS_DEVICES.size()));
return new JidDevice(
device.model,
device.manufacturer,
business ? PlatformType.IOS_BUSINESS : PlatformType.IOS,
device.osVersion,
device.osBuildNumber,
device.modelId,
WhatsAppClientType.MOBILE
);
}
public static JidDevice android(boolean business) {
var model = "Pixel_" + ThreadLocalRandom.current().nextInt(2, 9);
return new JidDevice(
model,
"Google",
business ? PlatformType.ANDROID_BUSINESS : PlatformType.ANDROID,
Version.of(String.valueOf(ThreadLocalRandom.current().nextInt(11, 16))),
null,
model,
WhatsAppClientType.MOBILE
);
}
public String osBuildNumber() {
return Objects.requireNonNullElse(osBuildNumber, osVersion.toString());
}
public String toUserAgent(Version clientVersion) {
if(platform == PlatformType.WINDOWS || platform == PlatformType.MACOS) {
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
}else {
var platformName = switch (platform) {
case ANDROID -> "Android";
case ANDROID_BUSINESS -> "SMBA";
case IOS -> "iOS";
case IOS_BUSINESS -> "SMB iOS";
case MACOS, WINDOWS -> throw new InternalError();
};
var deviceName = switch (platform()) {
case ANDROID, ANDROID_BUSINESS -> manufacturer + "-" + model;
case IOS, IOS_BUSINESS -> model;
case MACOS, WINDOWS -> throw new InternalError();
};
var deviceVersion = osVersion.toString();
return "WhatsApp/%s %s/%s Device/%s".formatted(
clientVersion,
platformName,
deviceVersion,
deviceName
);
}
}
public JidDevice toPersonal() {
if (!platform.isBusiness()) {
return this;
}
return withPlatform(platform.toPersonal());
}
public JidDevice toBusiness() {
if (platform.isBusiness()) {
return this;
}
return withPlatform(platform.toBusiness());
}
public JidDevice withPlatform(PlatformType platform) {
return new JidDevice(
model,
manufacturer,
Objects.requireNonNullElse(platform, this.platform),
osVersion,
osBuildNumber,
modelId,
clientType
);
}
public String model() {
return model;
}
public String modelId() {
return modelId;
}
public String manufacturer() {
return manufacturer;
}
public PlatformType platform() {
return platform;
}
public Version osVersion() {
return osVersion;
}
public WhatsAppClientType clientType() {
return clientType;
}
@Override
public boolean equals(Object o) {
return o instanceof JidDevice that
&& Objects.equals(model, that.model)
&& Objects.equals(manufacturer, that.manufacturer)
&& platform == that.platform
&& Objects.equals(osVersion, that.osVersion)
&& Objects.equals(osBuildNumber, that.osBuildNumber)
&& Objects.equals(modelId, that.modelId)
&& clientType == that.clientType;
}
@Override
public int hashCode() {
return Objects.hash(model, manufacturer, platform, osVersion, osBuildNumber, modelId, clientType);
}
@Override
public String toString() {
return "CompanionDevice[" +
"model='" + model + '\'' +
", manufacturer='" + manufacturer + '\'' +
", platform=" + platform +
", osVersion=" + osVersion +
", osBuildNumber='" + osBuildNumber + '\'' +
", modelId='" + modelId + '\'' +
", clientType=" + clientType +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/jid/JidProvider.java
================================================
package com.github.auties00.cobalt.model.jid;
import com.github.auties00.cobalt.model.contact.Contact;
import com.github.auties00.cobalt.model.info.MessageInfoParent;
/**
* Utility interface to make providing a value easier
*/
public sealed interface JidProvider permits Contact, MessageInfoParent, Jid, JidServer {
/**
* Returns this object as a value
*
* @return a non-null value
*/
Jid toJid();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/jid/JidServer.java
================================================
package com.github.auties00.cobalt.model.jid;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* The constants of this enumerated type describe the various servers that a value might be linked
* to
*/
public final class JidServer implements JidProvider { // String parsing is hard part 2
private static final String LEGACY_USER_ADDRESS = "c.us";
private static final String GROUP_OR_COMMUNITY_ADDRESS = "g.us";
private static final String BROADCAST_ADDRESS = "broadcast";
private static final String CALL_ADDRESS = "call";
private static final String USER_ADDRESS = "s.whatsapp.net";
private static final String LID_ADDRESS = "lid";
private static final String NEWSLETTER_ADDRESS = "newsletter";
private static final String BOT_ADDRESS = "bot";
private static final JidServer LEGACY_USER = new JidServer(LEGACY_USER_ADDRESS, Type.LEGACY_USER);
private static final JidServer GROUP_OR_COMMUNITY = new JidServer(GROUP_OR_COMMUNITY_ADDRESS, Type.GROUP_OR_COMMUNITY);
private static final JidServer BROADCAST = new JidServer(BROADCAST_ADDRESS, Type.BROADCAST);
private static final JidServer CALL = new JidServer(CALL_ADDRESS, Type.CALL);
private static final JidServer USER = new JidServer(USER_ADDRESS, Type.USER);
private static final JidServer LID = new JidServer(LID_ADDRESS, Type.LID);
private static final JidServer NEWSLETTER = new JidServer(NEWSLETTER_ADDRESS, Type.NEWSLETTER);
private static final JidServer BOT = new JidServer(BOT_ADDRESS, Type.BOT);
private static final ConcurrentMap<String, JidServer> unknownServersStore = new ConcurrentHashMap<>();
private final String address;
private final Type type;
private JidServer(String address, Type type) {
this.address = address;
this.type = type;
}
public static JidServer legacyUser() {
return LEGACY_USER;
}
public static JidServer groupOrCommunity() {
return GROUP_OR_COMMUNITY;
}
public static JidServer broadcast() {
return BROADCAST;
}
public static JidServer call() {
return CALL;
}
public static JidServer user() {
return USER;
}
public static JidServer lid() {
return LID;
}
public static JidServer newsletter() {
return NEWSLETTER;
}
public static JidServer bot() {
return BOT;
}
public static JidServer unknown(String address) {
return unknownServersStore.computeIfAbsent(address, value -> new JidServer(value, Type.UNKNOWN));
}
public static JidServer of(String address) {
return of(address, true);
}
static JidServer of(String address, boolean allowUnknown) {
return switch (address) {
case LEGACY_USER_ADDRESS -> LEGACY_USER;
case GROUP_OR_COMMUNITY_ADDRESS -> GROUP_OR_COMMUNITY;
case BROADCAST_ADDRESS -> BROADCAST;
case CALL_ADDRESS -> CALL;
case USER_ADDRESS -> USER;
case LID_ADDRESS -> LID;
case NEWSLETTER_ADDRESS -> NEWSLETTER;
case BOT_ADDRESS -> BOT;
default -> allowUnknown ? unknown(address) : null;
};
}
// Fast path
static JidServer of(String address, int offset, int length) {
if(length == 0) {
return USER;
}
switch (length) {
case 3 -> {
switch (address.charAt(offset)) {
case 'l' -> {
if (address.charAt(offset + 1) == 'i'
&& address.charAt(offset + 2) == 'd') {
return LID;
}
}
case 'b' -> {
if (address.charAt(offset + 1) == 'o'
&& address.charAt(offset + 2) == 't') {
return BOT;
}
}
}
}
case 4 -> {
switch (address.charAt(offset)) {
case 'c' -> {
switch (address.charAt(offset + 1)) {
case '.' -> {
if (address.charAt(offset + 2) == 'u'
&& address.charAt(offset + 3) == 's') {
return LEGACY_USER;
}
}
case 'a' -> {
if (address.charAt(offset + 2) == 'l'
&& address.charAt(offset + 3) == 'l') {
return CALL;
}
}
}
}
case 'g' -> {
if (address.charAt(offset + 1) == '.'
&& address.charAt(offset + 2) == 'u'
&& address.charAt(offset + 3) == 's') {
return GROUP_OR_COMMUNITY;
}
}
}
}
case 9 -> {
if (address.charAt(offset) == 'b'
&& address.charAt(offset + 1) == 'r'
&& address.charAt(offset + 2) == 'o'
&& address.charAt(offset + 3) == 'a'
&& address.charAt(offset + 4) == 'd'
&& address.charAt(offset + 5) == 'c'
&& address.charAt(offset + 6) == 'a'
&& address.charAt(offset + 7) == 's'
&& address.charAt(offset + 8) == 't') {
return BROADCAST;
}
}
case 10 -> {
if (address.charAt(offset) == 'n'
&& address.charAt(offset + 1) == 'e'
&& address.charAt(offset + 2) == 'w'
&& address.charAt(offset + 3) == 's'
&& address.charAt(offset + 4) == 'l'
&& address.charAt(offset + 5) == 'e'
&& address.charAt(offset + 6) == 't'
&& address.charAt(offset + 7) == 't'
&& address.charAt(offset + 8) == 'e'
&& address.charAt(offset + 9) == 'r') {
return NEWSLETTER;
}
}
case 13 -> {
if (address.charAt(offset) == 's'
&& address.charAt(offset + 1) == '.'
&& address.charAt(offset + 2) == 'w'
&& address.charAt(offset + 3) == 'h'
&& address.charAt(offset + 4) == 'a'
&& address.charAt(offset + 5) == 't'
&& address.charAt(offset + 6) == 's'
&& address.charAt(offset + 7) == 'a'
&& address.charAt(offset + 8) == 'p'
&& address.charAt(offset + 9) == 'p'
&& address.charAt(offset + 10) == '.'
&& address.charAt(offset + 11) == 'n'
&& address.charAt(offset + 12) == 'e'
&& address.charAt(offset + 13) == 't') {
return USER;
}
}
}
return unknown(offset == 0 && address.length() == length
? address
: address.substring(offset, offset + length));
}
// Fast path
static JidServer of(byte[] source, int offset, int length, boolean allowUnknown) {
if (length == 0) {
return USER;
}
switch (length) {
case 3 -> {
switch ((char) (source[offset] & 0x7F)) {
case 'l' -> {
if ((char) (source[offset + 1] & 0x7F) == 'i'
&& (char) (source[offset + 2] & 0x7F) == 'd') {
return LID;
}
}
case 'b' -> {
if ((char) (source[offset + 1] & 0x7F) == 'o'
&& (char) (source[offset + 2] & 0x7F) == 't') {
return BOT;
}
}
}
}
case 4 -> {
switch ((char) (source[offset] & 0x7F)) {
case 'c' -> {
switch ((char) (source[offset + 1] & 0x7F)) {
case '.' -> {
if ((char) (source[offset + 2] & 0x7F) == 'u'
&& (char) (source[offset + 3] & 0x7F) == 's') {
return LEGACY_USER;
}
}
case 'a' -> {
if ((char) (source[offset + 2] & 0x7F) == 'l'
&& (char) (source[offset + 3] & 0x7F) == 'l') {
return CALL;
}
}
}
}
case 'g' -> {
if ((char) (source[offset + 1] & 0x7F) == '.'
&& (char) (source[offset + 2] & 0x7F) == 'u'
&& (char) (source[offset + 3] & 0x7F) == 's') {
return GROUP_OR_COMMUNITY;
}
}
}
}
case 9 -> {
if ((char) (source[offset] & 0x7F) == 'b'
&& (char) (source[offset + 1] & 0x7F) == 'r'
&& (char) (source[offset + 2] & 0x7F) == 'o'
&& (char) (source[offset + 3] & 0x7F) == 'a'
&& (char) (source[offset + 4] & 0x7F) == 'd'
&& (char) (source[offset + 5] & 0x7F) == 'c'
&& (char) (source[offset + 6] & 0x7F) == 'a'
&& (char) (source[offset + 7] & 0x7F) == 's'
&& (char) (source[offset + 8] & 0x7F) == 't') {
return BROADCAST;
}
}
case 10 -> {
if ((char) (source[offset] & 0x7F) == 'n'
&& (char) (source[offset + 1] & 0x7F) == 'e'
&& (char) (source[offset + 2] & 0x7F) == 'w'
&& (char) (source[offset + 3] & 0x7F) == 's'
&& (char) (source[offset + 4] & 0x7F) == 'l'
&& (char) (source[offset + 5] & 0x7F) == 'e'
&& (char) (source[offset + 6] & 0x7F) == 't'
&& (char) (source[offset + 7] & 0x7F) == 't'
&& (char) (source[offset + 8] & 0x7F) == 'e'
&& (char) (source[offset + 9] & 0x7F) == 'r') {
return NEWSLETTER;
}
}
case 13 -> {
if ((char) (source[offset] & 0x7F) == 's'
&& (char) (source[offset + 1] & 0x7F) == '.'
&& (char) (source[offset + 2] & 0x7F) == 'w'
&& (char) (source[offset + 3] & 0x7F) == 'h'
&& (char) (source[offset + 4] & 0x7F) == 'a'
&& (char) (source[offset + 5] & 0x7F) == 't'
&& (char) (source[offset + 6] & 0x7F) == 's'
&& (char) (source[offset + 7] & 0x7F) == 'a'
&& (char) (source[offset + 8] & 0x7F) == 'p'
&& (char) (source[offset + 9] & 0x7F) == 'p'
&& (char) (source[offset + 10] & 0x7F) == '.'
&& (char) (source[offset + 11] & 0x7F) == 'n'
&& (char) (source[offset + 12] & 0x7F) == 'e'
&& (char) (source[offset + 13] & 0x7F) == 't') {
return USER;
}
}
}
return allowUnknown
? unknown(new String(source, offset, length, StandardCharsets.US_ASCII))
: null;
}
public String address() {
return address;
}
@Override
public Jid toJid() {
return Jid.of(this);
}
@Override
public String toString() {
return address;
}
@Override
public boolean equals(Object other) {
return this == other
|| other instanceof JidServer that && Objects.equals(address, that.address);
}
@Override
public int hashCode() {
return Objects.hash(address, type);
}
public Type type() {
return type;
}
public enum Type {
UNKNOWN,
LEGACY_USER,
GROUP_OR_COMMUNITY,
BROADCAST,
CALL,
USER,
LID,
NEWSLETTER,
BOT
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/media/MediaData.java
================================================
package com.github.auties00.cobalt.model.media;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
@ProtobufMessage(name = "MediaData")
public final class MediaData {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String localPath;
MediaData(String localPath) {
this.localPath = Objects.requireNonNull(localPath, "localPath cannot be null");
}
public String localPath() {
return this.localPath;
}
@Override
public boolean equals(Object other) {
return other instanceof MediaData that
&& Objects.equals(localPath, that.localPath);
}
@Override
public int hashCode() {
return Objects.hash(localPath);
}
@Override
public String toString() {
return "MediaData[" +
"localPath=" + localPath +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/media/MediaPath.java
================================================
package com.github.auties00.cobalt.model.media;
import it.auties.protobuf.annotation.ProtobufEnum;
import java.util.*;
/**
* The constants of this enumerated type describe the various types of attachments supported by Whatsapp
*/
@ProtobufEnum
public enum MediaPath {
NONE(null, null, false),
AUDIO("mms/audio", "WhatsApp Audio Keys", false),
DOCUMENT("mms/document", "WhatsApp Document Keys", false),
GIF("mms/gif", "WhatsApp Video Keys", false),
IMAGE("mms/image", "WhatsApp Image Keys", false),
PROFILE_PICTURE("pps/photo", null, false),
PRODUCT("mms/image", "WhatsApp Image Keys", false),
VOICE("mms/ptt", "WhatsApp Audio Keys", false),
STICKER("mms/sticker", "WhatsApp Image Keys", false),
THUMBNAIL_DOCUMENT("mms/thumbnail-document", "WhatsApp Document Thumbnail Keys", false),
THUMBNAIL_LINK("mms/thumbnail-link", "WhatsApp Link Thumbnail Keys", false),
THUMBNAIL_IMAGE("mms/thumbnail-image", "WhatsApp Image Thumbnail Keys", false),
THUMBNAIL_VIDEO("mms/thumbnail-video", "WhatsApp Video Thumbnail Keys", false),
VIDEO("mms/video", "WhatsApp Video Keys", false),
PTV("mms/ptv", "WhatsApp Video Keys", false),
APP_STATE("mms/md-app-state", "WhatsApp App State Keys", true),
HISTORY_SYNC("mms/md-msg-hist", "WhatsApp History Keys", true),
PRODUCT_CATALOG_IMAGE("product/image", null, false),
PAYMENT_BG_IMAGE("mms/payment-bg-image", "WhatsApp Payment Background Keys", false),
BUSINESS_COVER_PHOTO("pps/biz-cover-photo", null, false),
NATIVE_AD_IMAGE("mms/ads-image", "ads-image", false),
NATIVE_AD_VIDEO("mms/ads-video", "ads-video", false),
STICKER_PACK("mms/sticker-pack", "WhatsApp Sticker Pack Keys", false),
THUMBNAIL_STICKER_PACK("mms/thumbnail-sticker-pack", "WhatsApp Sticker Pack Thumbnail Keys", false),
MUSIC_ARTWORK("mms/music-artwork", "WhatsApp Music Artwork Keys", false),
GROUP_HISTORY("mms/group-history", "Group History", false),
NEWSLETTER_AUDIO("newsletter/newsletter-audio", null, false),
NEWSLETTER_IMAGE("newsletter/newsletter-image", null, false),
NEWSLETTER_DOCUMENT("newsletter/newsletter-document", null, false),
NEWSLETTER_GIF("newsletter/newsletter-gif", null, false),
NEWSLETTER_VOICE("newsletter/newsletter-ptt", null, false),
NEWSLETTER_PTV("newsletter/newsletter-ptv", null, false),
NEWSLETTER_STICKER("newsletter/newsletter-sticker", null, false),
NEWSLETTER_STICKER_PACK("newsletter/newsletter-sticker-pack", null, false),
NEWSLETTER_THUMBNAIL_LINK("newsletter/newsletter-thumbnail-link", null, false),
NEWSLETTER_VIDEO("newsletter/newsletter-video", null, false),
NEWSLETTER_MUSIC_ARTWORK("mms/newsletter-music-artwork", null, false);
private final String path;
private final String keyName;
private final boolean inflatable;
private static final Set<MediaPath> KNOWN;
private static final Map<String, MediaPath> BY_ID;
static {
var known = new HashSet<MediaPath>();
for(var value : values()) {
if(value != NONE) {
known.add(value);
}
}
KNOWN = Collections.unmodifiableSet(known);
Map<String, MediaPath> byId = HashMap.newHashMap(known.size());
for(var value : known) {
var path = value.path;
var separator = path.indexOf('/');
var id = separator == -1 ? path : path.substring(separator + 1);
byId.put(id, value);
}
BY_ID = Collections.unmodifiableMap(byId);
}
public static Set<MediaPath> known() {
return KNOWN;
}
public static Optional<MediaPath> ofId(String id) {
return Optional.ofNullable(BY_ID.get(id));
}
MediaPath(String path, String keyName, boolean inflatable) {
this.path = path;
this.keyName = keyName;
this.inflatable = inflatable;
}
public Optional<String> path() {
return Optional.ofNullable(path);
}
public Optional<String> keyName() {
return Optional.ofNullable(keyName);
}
public boolean inflatable() {
return this.inflatable;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/media/MediaProvider.java
================================================
package com.github.auties00.cobalt.model.media;
import com.github.auties00.cobalt.model.action.StickerAction;
import com.github.auties00.cobalt.model.message.model.MediaMessage;
import com.github.auties00.cobalt.model.preferences.Sticker;
import com.github.auties00.cobalt.model.sync.ExternalBlobReference;
import com.github.auties00.cobalt.model.sync.HistorySyncNotification;
import java.util.Optional;
import java.util.OptionalLong;
/**
* A sealed interface that represents a class that can provide data about a media
*/
public sealed interface MediaProvider
permits StickerAction, MediaMessage, Sticker, ExternalBlobReference, HistorySyncNotification {
/**
* Returns the url to the media
*
* @return a nullable String
*/
Optional<String> mediaUrl();
/**
* Sets the media url of this provider
*
*/
void setMediaUrl(String mediaUrl);
/**
* Returns the direct path to the media
*
* @return a nullable String
*/
Optional<String> mediaDirectPath();
/**
* Sets the direct path of this provider
*
*/
void setMediaDirectPath(String mediaDirectPath);
/**
* Returns the key of this media
*
* @return a non-null array of bytes
*/
Optional<byte[]> mediaKey();
/**
* Sets the media key of this provider
*
*/
void setMediaKey(byte[] bytes);
/**
* Sets the timestamp of the media key
*
*/
void setMediaKeyTimestamp(Long timestamp);
/**
* Returns the sha256 of this media
*
* @return a non-null array of bytes
*/
Optional<byte[]> mediaSha256();
/**
* Sets the sha256 of the media in this provider
*
*/
void setMediaSha256(byte[] bytes);
/**
* Returns the sha256 of this encrypted media
*
* @return a non-null array of bytes
*/
Optional<byte[]> mediaEncryptedSha256();
/**
* Sets the sha256 of the encrypted media in this provider
*
*/
void setMediaEncryptedSha256(byte[] bytes);
/**
* Returns the size of this media
*
* @return a long
*/
OptionalLong mediaSize();
/**
* Sets the size of this media
*
*/
void setMediaSize(long mediaSize);
/**
* Returns the type of this attachment
*
* @return a non-null attachment
*/
MediaPath mediaPath();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/media/MediaVisibility.java
================================================
package com.github.auties00.cobalt.model.media;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* The constants of this enumerated type describe the various types of media visibility that can be
* set for a chat
*/
@ProtobufEnum(name = "MediaVisibility")
public enum MediaVisibility {
/**
* Default
*/
DEFAULT(0),
/**
* Off
*/
OFF(1),
/**
* On
*/
ON(2);
final int index;
MediaVisibility(@ProtobufEnumIndex int index) {
this.index = index;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ButtonsMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.Button;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Optional;
/**
* A model class that represents a message that contains buttons inside
*/
@ProtobufMessage(name = "Message.ButtonsMessage")
public final class ButtonsMessage implements ButtonMessage, ContextualMessage {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final ButtonsMessageHeaderText headerText;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final DocumentMessage headerDocument;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final ImageMessage headerImage;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final VideoOrGifMessage headerVideo;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final LocationMessage headerLocation;
@ProtobufProperty(index = 6, type = ProtobufType.STRING)
final String body;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String footer;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
@ProtobufProperty(index = 9, type = ProtobufType.MESSAGE)
final List<Button> buttons;
@ProtobufProperty(index = 10, type = ProtobufType.ENUM)
final ButtonsMessageHeader.Type headerType;
ButtonsMessage(ButtonsMessageHeaderText headerText, DocumentMessage headerDocument, ImageMessage headerImage, VideoOrGifMessage headerVideo, LocationMessage headerLocation, String body, String footer, ContextInfo contextInfo, List<Button> buttons, ButtonsMessageHeader.Type headerType) {
this.headerText = headerText;
this.headerDocument = headerDocument;
this.headerImage = headerImage;
this.headerVideo = headerVideo;
this.headerLocation = headerLocation;
this.body = body;
this.footer = footer;
this.contextInfo = contextInfo;
this.buttons = buttons;
this.headerType = headerType;
}
@ProtobufBuilder(className = "ButtonsMessageSimpleBuilder")
static ButtonsMessage customBuilder(ButtonsMessageHeader header, String body, String footer, ContextInfo contextInfo, List<Button> buttons) {
var builder = new ButtonsMessageBuilder()
.body(body)
.footer(footer)
.contextInfo(contextInfo)
.buttons(buttons);
switch (header) {
case ButtonsMessageHeaderText textMessage -> builder.headerText(textMessage)
.headerType(ButtonsMessageHeader.Type.TEXT);
case DocumentMessage documentMessage -> builder.headerDocument(documentMessage)
.headerType(ButtonsMessageHeader.Type.DOCUMENT);
case ImageMessage imageMessage -> builder.headerImage(imageMessage)
.headerType(ButtonsMessageHeader.Type.IMAGE);
case VideoOrGifMessage videoMessage -> builder.headerVideo(videoMessage)
.headerType(ButtonsMessageHeader.Type.VIDEO);
case LocationMessage locationMessage -> builder.headerLocation(locationMessage)
.headerType(ButtonsMessageHeader.Type.LOCATION);
case null -> builder.headerType(ButtonsMessageHeader.Type.UNKNOWN);
}
return builder.build();
}
/**
* Returns the type of this message
*
* @return a non-null type
*/
@Override
public Type type() {
return Type.BUTTONS;
}
/**
* Returns the header of this message
*
* @return an optional
*/
public Optional<? extends ButtonsMessageHeader> header() {
if (headerText != null) {
return Optional.of(headerText);
}else if (headerDocument != null) {
return Optional.of(headerDocument);
}else if (headerImage != null) {
return Optional.of(headerImage);
}else if (headerVideo != null) {
return Optional.of(headerVideo);
}else if(headerLocation != null){
return Optional.of(headerLocation);
}else {
return Optional.empty();
}
}
public Optional<ButtonsMessageHeaderText> headerText() {
return Optional.ofNullable(headerText);
}
public Optional<DocumentMessage> headerDocument() {
return Optional.ofNullable(headerDocument);
}
public Optional<ImageMessage> headerImage() {
return Optional.ofNullable(headerImage);
}
public Optional<VideoOrGifMessage> headerVideo() {
return Optional.ofNullable(headerVideo);
}
public Optional<LocationMessage> headerLocation() {
return Optional.ofNullable(headerLocation);
}
public Optional<String> body() {
return Optional.ofNullable(body);
}
public Optional<String> footer() {
return Optional.ofNullable(footer);
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
public List<Button> buttons() {
return buttons;
}
public ButtonsMessageHeader.Type headerType() {
return headerType;
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
@Override
public String toString() {
return "ButtonsMessage[" +
"headerText=" + headerText + ", " +
"headerDocument=" + headerDocument + ", " +
"headerImage=" + headerImage + ", " +
"headerVideo=" + headerVideo + ", " +
"headerLocation=" + headerLocation + ", " +
"body=" + body + ", " +
"footer=" + footer + ", " +
"contextInfo=" + contextInfo + ", " +
"buttons=" + buttons + ", " +
"headerType=" + headerType + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ButtonsMessageHeader.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.message.standard.DocumentMessage;
import com.github.auties00.cobalt.model.message.standard.ImageMessage;
import com.github.auties00.cobalt.model.message.standard.LocationMessage;
import com.github.auties00.cobalt.model.message.standard.VideoOrGifMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model that represents the header of a {@link ButtonsMessage}
*/
public sealed interface ButtonsMessageHeader permits ButtonsMessageHeaderText, DocumentMessage, ImageMessage, LocationMessage, VideoOrGifMessage {
Type buttonHeaderType();
/**
* The constants of this enumerated type describe the various types of headers that a {@link ButtonsMessage} can have
*/
@ProtobufEnum
enum Type {
/**
* Unknown
*/
UNKNOWN(0),
/**
* Empty
*/
EMPTY(1),
/**
* Text message
*/
TEXT(2),
/**
* Document message
*/
DOCUMENT(3),
/**
* Image message
*/
IMAGE(4),
/**
* Video message
*/
VIDEO(5),
/**
* Location message
*/
LOCATION(6);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
public boolean hasMedia() {
return this == DOCUMENT
|| this == IMAGE
|| this == VIDEO;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ButtonsMessageHeaderText.java
================================================
package com.github.auties00.cobalt.model.message.button;
import it.auties.protobuf.annotation.ProtobufDeserializer;
import it.auties.protobuf.annotation.ProtobufSerializer;
public record ButtonsMessageHeaderText(String text) implements ButtonsMessageHeader {
@ProtobufDeserializer
public static ButtonsMessageHeaderText of(String text) {
return new ButtonsMessageHeaderText(text);
}
@ProtobufSerializer
public String text() {
return text;
}
@Override
public Type buttonHeaderType() {
return Type.TEXT;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ButtonsResponseMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.Button;
import com.github.auties00.cobalt.model.button.base.ButtonBody;
import com.github.auties00.cobalt.model.button.base.ButtonText;
import com.github.auties00.cobalt.model.info.ChatMessageInfo;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonReplyMessage;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
/**
* A model class that represents a message that contains a newsletters to a previous
* {@link ButtonsMessage}
*/
@ProtobufMessage(name = "Message.ButtonsResponseMessage")
public final class ButtonsResponseMessage implements ButtonReplyMessage<ButtonsResponseMessage> {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String buttonId;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String buttonText;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
final ResponseType responseType;
ButtonsResponseMessage(String buttonId, String buttonText, ContextInfo contextInfo, ResponseType responseType) {
this.buttonId = buttonId;
this.buttonText = buttonText;
this.contextInfo = contextInfo;
this.responseType = responseType;
}
public static ButtonsResponseMessage of(ChatMessageInfo quoted, Button button) {
return new ButtonsResponseMessageBuilder()
.buttonId(button.id())
.buttonText(button.bodyText().map(ButtonText::content).orElse(null))
.contextInfo(ContextInfo.of(quoted))
.responseType(button.bodyType() == ButtonBody.Type.TEXT ? ResponseType.SELECTED_DISPLAY_TEXT : ResponseType.UNKNOWN)
.build();
}
@Override
public Type type() {
return Type.BUTTONS_RESPONSE;
}
public String buttonId() {
return buttonId;
}
public Optional<String> buttonText() {
return Optional.ofNullable(buttonText);
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
public ResponseType responseType() {
return responseType;
}
@Override
public String toString() {
return "ButtonsResponseMessage[" +
"buttonId=" + buttonId + ", " +
"buttonText=" + buttonText + ", " +
"contextInfo=" + contextInfo + ", " +
"responseType=" + responseType + ']';
}
@ProtobufEnum
public enum ResponseType {
UNKNOWN(0),
SELECTED_DISPLAY_TEXT(1);
final int index;
ResponseType(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/InteractiveMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.TemplateFormatter;
import com.github.auties00.cobalt.model.button.interactive.*;
import com.github.auties00.cobalt.model.button.interactive.*;
import com.github.auties00.cobalt.model.button.interactive.*;
import com.github.auties00.cobalt.model.button.interactive.*;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import com.github.auties00.cobalt.model.message.model.Message;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
/**
* A model class that represents a message holding an interactive message inside. Not really clear
* how this could be used, contributions are welcomed.
*/
@ProtobufMessage(name = "Message.InteractiveMessage")
public final class InteractiveMessage implements ContextualMessage, ButtonMessage, TemplateFormatter {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final InteractiveHeader header;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final InteractiveBody body;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final InteractiveFooter footer;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final InteractiveShop contentShop;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final InteractiveCollection contentCollection;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final InteractiveNativeFlow contentNativeFlow;
@ProtobufProperty(index = 15, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
public InteractiveMessage(InteractiveHeader header, InteractiveBody body, InteractiveFooter footer, InteractiveShop contentShop, InteractiveCollection contentCollection, InteractiveNativeFlow contentNativeFlow, ContextInfo contextInfo) {
this.header = header;
this.body = body;
this.footer = footer;
this.contentShop = contentShop;
this.contentCollection = contentCollection;
this.contentNativeFlow = contentNativeFlow;
this.contextInfo = contextInfo;
}
@ProtobufBuilder(className = "InteractiveMessageSimpleBuilder")
static InteractiveMessage simpleBuilder(InteractiveHeader header, String body, String footer, InteractiveMessageContent content, ContextInfo contextInfo) {
var interactiveBody = body == null ? null : new InteractiveBodyBuilder()
.content(body)
.build();
var interactiveFooter = footer == null ? null : new InteractiveFooterBuilder()
.content(footer)
.build();
var builder = new InteractiveMessageBuilder()
.header(header)
.body(interactiveBody)
.footer(interactiveFooter)
.contextInfo(contextInfo);
switch (content) {
case InteractiveShop interactiveShop -> builder.contentShop(interactiveShop);
case InteractiveCollection interactiveCollection -> builder.contentCollection(interactiveCollection);
case InteractiveNativeFlow interactiveNativeFlow -> builder.contentNativeFlow(interactiveNativeFlow);
case null -> {}
}
return builder.build();
}
/**
* Returns the type of content that this message wraps
*
* @return a non-null content type
*/
public InteractiveMessageContent.Type contentType() {
return content()
.map(InteractiveMessageContent::contentType)
.orElse(InteractiveMessageContent.Type.NONE);
}
/**
* Returns the content of this message if it's there
*
* @return a non-null content type
*/
public Optional<? extends InteractiveMessageContent> content() {
if (contentShop != null) {
return Optional.of(contentShop);
}else if (contentCollection != null) {
return Optional.of(contentCollection);
}else if(contentNativeFlow != null){
return Optional.of(contentNativeFlow);
}else {
return Optional.empty();
}
}
@Override
public TemplateFormatter.Type templateType() {
return TemplateFormatter.Type.INTERACTIVE;
}
@Override
public Message.Type type() {
return Message.Type.INTERACTIVE;
}
@Override
public Category category() {
return Category.STANDARD;
}
public Optional<InteractiveHeader> header() {
return Optional.ofNullable(header);
}
public Optional<InteractiveBody> body() {
return Optional.ofNullable(body);
}
public Optional<InteractiveFooter> footer() {
return Optional.ofNullable(footer);
}
public Optional<InteractiveShop> contentShop() {
return Optional.ofNullable(contentShop);
}
public Optional<InteractiveCollection> contentCollection() {
return Optional.ofNullable(contentCollection);
}
public Optional<InteractiveNativeFlow> contentNativeFlow() {
return Optional.ofNullable(contentNativeFlow);
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
@Override
public String toString() {
return "InteractiveMessage[" +
"header=" + header + ", " +
"body=" + body + ", " +
"footer=" + footer + ", " +
"contentShop=" + contentShop + ", " +
"contentCollection=" + contentCollection + ", " +
"contentNativeFlow=" + contentNativeFlow + ", " +
"contextInfo=" + contextInfo + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/InteractiveMessageContent.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.interactive.InteractiveCollection;
import com.github.auties00.cobalt.model.button.interactive.InteractiveNativeFlow;
import com.github.auties00.cobalt.model.button.interactive.InteractiveShop;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
/**
* A model class that represents a message that can be used as the content of a {@link InteractiveMessage}
*/
public sealed interface InteractiveMessageContent permits InteractiveShop, InteractiveCollection, InteractiveNativeFlow {
/**
* Returns the type of this content
*
* @return a non-null type
*/
Type contentType();
/**
* The constants of this enumerated type describe the various types of content that an interactive
* message can wrap
*/
@ProtobufEnum
enum Type {
/**
* No content
*/
NONE(0),
/**
* Shop
*/
SHOP(1),
/**
* Collection
*/
COLLECTION(2),
/**
* Native flow
*/
NATIVE_FLOW(3);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/InteractiveResponseMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.interactive.InteractiveBody;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
@ProtobufMessage(name = "Message.InteractiveResponseMessage")
public final class InteractiveResponseMessage implements ContextualMessage {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final InteractiveBody body;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final NativeFlowResponseMessage nativeFlowResponseMessage;
@ProtobufProperty(index = 15, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
InteractiveResponseMessage(InteractiveBody body, NativeFlowResponseMessage nativeFlowResponseMessage, ContextInfo contextInfo) {
this.body = body;
this.nativeFlowResponseMessage = nativeFlowResponseMessage;
this.contextInfo = contextInfo;
}
@Override
public Type type() {
return Type.INTERACTIVE_RESPONSE;
}
@Override
public Category category() {
return Category.BUTTON;
}
public InteractiveMessageContent.Type interactiveResponseMessageType() {
return InteractiveMessageContent.Type.COLLECTION;
}
public InteractiveBody body() {
return body;
}
public NativeFlowResponseMessage nativeFlowResponseMessage() {
return nativeFlowResponseMessage;
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
@Override
public String toString() {
return "InteractiveResponseMessage[" +
"body=" + body + ", " +
"nativeFlowResponseMessage=" + nativeFlowResponseMessage + ", " +
"contextInfo=" + contextInfo + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ListMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.ButtonSection;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.info.ProductListInfo;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import com.github.auties00.cobalt.model.message.model.Message;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.List;
import java.util.Optional;
/**
* A model class that represents a message that contains a list of buttons or a list of products
*/
@ProtobufMessage(name = "Message.ListMessage")
public final class ListMessage implements ContextualMessage, ButtonMessage {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String description;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String button;
@ProtobufProperty(index = 4, type = ProtobufType.ENUM)
final Type listType;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final List<ButtonSection> sections;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final ProductListInfo productListInfo;
@ProtobufProperty(index = 7, type = ProtobufType.STRING)
final String footer;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
ListMessage(String title, String description, String button, Type listType, List<ButtonSection> sections, ProductListInfo productListInfo, String footer, ContextInfo contextInfo) {
this.title = title;
this.description = description;
this.button = button;
this.listType = listType;
this.sections = sections;
this.productListInfo = productListInfo;
this.footer = footer;
this.contextInfo = contextInfo;
}
@Override
public Message.Type type() {
return Message.Type.LIST;
}
public String title() {
return title;
}
public Optional<String> description() {
return Optional.ofNullable(description);
}
public String button() {
return button;
}
public Type listType() {
return listType;
}
public List<ButtonSection> sections() {
return sections;
}
public Optional<ProductListInfo> productListInfo() {
return Optional.ofNullable(productListInfo);
}
public Optional<String> footer() {
return Optional.ofNullable(footer);
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
@Override
public String toString() {
return "ListMessage[" +
"title=" + title + ", " +
"description=" + description + ", " +
"button=" + button + ", " +
"listType=" + listType + ", " +
"sections=" + sections + ", " +
"productListInfo=" + productListInfo + ", " +
"footer=" + footer + ", " +
"contextInfo=" + contextInfo + ']';
}
/**
* The constants of this enumerated type describe the various types of {@link ListMessage}
*/
@ProtobufEnum(name = "Message.ListMessage.Type")
public enum Type {
/**
* Unknown
*/
UNKNOWN(0),
/**
* Only one option can be selected
*/
SINGLE_SELECT(1),
/**
* A list of products
*/
PRODUCT_LIST(2);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/ListResponseMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.SingleSelectReplyButton;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonReplyMessage;
import com.github.auties00.cobalt.model.message.model.Message;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
/**
* A model class that represents a message that contains a newsletters to a previous
* {@link ListMessage}
*/
@ProtobufMessage(name = "Message.ListResponseMessage")
public final class ListResponseMessage implements ButtonReplyMessage<ListResponseMessage> {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String title;
@ProtobufProperty(index = 2, type = ProtobufType.ENUM)
final Type listType;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final SingleSelectReplyButton reply;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
@ProtobufProperty(index = 5, type = ProtobufType.STRING)
final String description;
ListResponseMessage(String title, Type listType, SingleSelectReplyButton reply, ContextInfo contextInfo, String description) {
this.title = title;
this.listType = listType;
this.reply = reply;
this.contextInfo = contextInfo;
this.description = description;
}
@Override
public Message.Type type() {
return Message.Type.LIST_RESPONSE;
}
public String title() {
return title;
}
public SingleSelectReplyButton reply() {
return reply;
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
public Optional<String> description() {
return Optional.ofNullable(description);
}
public Type listType() {
return listType;
}
@Override
public String toString() {
return "ListResponseMessage[" +
"title=" + title + ", " +
"reply=" + reply + ", " +
"contextInfo=" + contextInfo + ", " +
"description=" + description + ", " +
"listType=" + listType + ']';
}
/**
* The constants of this enumerated type describe the various types of {@link ListMessage}
*/
@ProtobufEnum(name = "Message.ListResponseMessage.Type")
public enum Type {
/**
* Unknown
*/
UNKNOWN(0),
/**
* Only one option can be selected
*/
SINGLE_SELECT(1),
/**
* A list of products
*/
PRODUCT_LIST(2);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/NativeFlowResponseMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
@ProtobufMessage(name = "Message.InteractiveResponseMessage.NativeFlowResponseMessage")
public final class NativeFlowResponseMessage implements ButtonMessage {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String name;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String paramsJson;
@ProtobufProperty(index = 3, type = ProtobufType.INT32)
final int version;
NativeFlowResponseMessage(String name, String paramsJson, int version) {
this.name = Objects.requireNonNull(name, "name cannot be null");
this.paramsJson = Objects.requireNonNull(paramsJson, "paramsJson cannot be null");
this.version = version;
}
public String name() {
return name;
}
public String paramsJson() {
return paramsJson;
}
public int version() {
return version;
}
@Override
public Type type() {
return Type.NATIVE_FLOW_RESPONSE;
}
@Override
public boolean equals(Object o) {
return o instanceof NativeFlowResponseMessage that
&& Objects.equals(name, that.name)
&& Objects.equals(paramsJson, that.paramsJson)
&& version == that.version;
}
@Override
public int hashCode() {
return Objects.hash(name, paramsJson, version);
}
@Override
public String toString() {
return "NativeFlowResponseMessage[" +
"name=" + name + ", " +
"paramsJson=" + paramsJson + ", " +
"version=" + version + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/TemplateMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.base.TemplateFormatter;
import com.github.auties00.cobalt.model.button.template.highlyStructured.HighlyStructuredFourRowTemplate;
import com.github.auties00.cobalt.model.button.template.hydrated.HydratedFourRowTemplate;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonMessage;
import com.github.auties00.cobalt.model.message.model.ContextualMessage;
import com.github.auties00.cobalt.util.SecureBytes;
import it.auties.protobuf.annotation.ProtobufBuilder;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.HexFormat;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents a message sent in a WhatsappBusiness chat that provides a list of
* buttons to choose from.
*/
@ProtobufMessage(name = "Message.TemplateMessage")
public final class TemplateMessage implements ContextualMessage, ButtonMessage {
@ProtobufProperty(index = 9, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final HydratedFourRowTemplate content;
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplateFormat;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final HydratedFourRowTemplate hydratedFourRowTemplateFormat;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final InteractiveMessage interactiveMessageFormat;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
public TemplateMessage(String id, HydratedFourRowTemplate content, HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplateFormat, HydratedFourRowTemplate hydratedFourRowTemplateFormat, InteractiveMessage interactiveMessageFormat, ContextInfo contextInfo) {
this.id = id;
this.content = content;
this.highlyStructuredFourRowTemplateFormat = highlyStructuredFourRowTemplateFormat;
this.hydratedFourRowTemplateFormat = hydratedFourRowTemplateFormat;
this.interactiveMessageFormat = interactiveMessageFormat;
this.contextInfo = contextInfo;
}
@ProtobufBuilder(className = "TemplateMessageSimpleBuilder")
static TemplateMessage customBuilder(String id, HydratedFourRowTemplate content, TemplateFormatter format, ContextInfo contextInfo) {
var builder = new TemplateMessageBuilder()
.id(Objects.requireNonNullElseGet(id, () -> HexFormat.of().formatHex(SecureBytes.random(6))))
.content(content)
.contextInfo(contextInfo);
switch (format) {
case HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate ->
builder.highlyStructuredFourRowTemplateFormat(highlyStructuredFourRowTemplate);
case HydratedFourRowTemplate hydratedFourRowTemplate ->
builder.hydratedFourRowTemplateFormat(hydratedFourRowTemplate);
case InteractiveMessage interactiveMessage -> builder.interactiveMessageFormat(interactiveMessage);
case null -> {}
}
return builder.build();
}
/**
* Returns the type of format of this message
*
* @return a non-null {@link TemplateFormatter.Type}
*/
public TemplateFormatter.Type formatType() {
return format().map(TemplateFormatter::templateType)
.orElse(TemplateFormatter.Type.NONE);
}
/**
* Returns the formatter of this message
*
* @return an optional
*/
public Optional<? extends TemplateFormatter> format() {
if (highlyStructuredFourRowTemplateFormat != null) {
return Optional.of(highlyStructuredFourRowTemplateFormat);
}else if (hydratedFourRowTemplateFormat != null) {
return Optional.of(hydratedFourRowTemplateFormat);
}else if(interactiveMessageFormat != null){
return Optional.of(interactiveMessageFormat);
}else {
return Optional.empty();
}
}
@Override
public Type type() {
return Type.TEMPLATE;
}
public String id() {
return id;
}
public HydratedFourRowTemplate content() {
return content;
}
public Optional<HighlyStructuredFourRowTemplate> highlyStructuredFourRowTemplateFormat() {
return Optional.ofNullable(highlyStructuredFourRowTemplateFormat);
}
public Optional<HydratedFourRowTemplate> hydratedFourRowTemplateFormat() {
return Optional.ofNullable(hydratedFourRowTemplateFormat);
}
public Optional<InteractiveMessage> interactiveMessageFormat() {
return Optional.ofNullable(interactiveMessageFormat);
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
@Override
public String toString() {
return "TemplateMessage[" +
"id=" + id + ", " +
"content=" + content + ", " +
"highlyStructuredFourRowTemplateFormat=" + highlyStructuredFourRowTemplateFormat + ", " +
"hydratedFourRowTemplateFormat=" + hydratedFourRowTemplateFormat + ", " +
"interactiveMessageFormat=" + interactiveMessageFormat + ", " +
"contextInfo=" + contextInfo + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/button/TemplateReplyMessage.java
================================================
package com.github.auties00.cobalt.model.message.button;
import com.github.auties00.cobalt.model.button.template.highlyStructured.HighlyStructuredMessage;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.model.ButtonReplyMessage;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Optional;
/**
* A model class that represents a message that contains a newsletters to a previous
* {@link HighlyStructuredMessage}
*/
@ProtobufMessage(name = "Message.TemplateButtonReplyMessage")
public final class TemplateReplyMessage implements ButtonReplyMessage<TemplateReplyMessage> {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 2, type = ProtobufType.STRING)
final String buttonText;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
ContextInfo contextInfo;
@ProtobufProperty(index = 4, type = ProtobufType.UINT32)
final int index;
TemplateReplyMessage(String id, String buttonText, ContextInfo contextInfo, int index) {
this.id = id;
this.buttonText = buttonText;
this.contextInfo = contextInfo;
this.index = index;
}
@Override
public Type type() {
return Type.TEMPLATE_REPLY;
}
public String id() {
return id;
}
public String buttonText() {
return buttonText;
}
@Override
public Optional<ContextInfo> contextInfo() {
return Optional.ofNullable(contextInfo);
}
@Override
public void setContextInfo(ContextInfo contextInfo) {
this.contextInfo = contextInfo;
}
public int index() {
return index;
}
@Override
public String toString() {
return "TemplateReplyMessage[" +
"id=" + id + ", " +
"buttonText=" + buttonText + ", " +
"contextInfo=" + contextInfo + ", " +
"index=" + index + ']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/ButtonMessage.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.button.template.highlyStructured.HighlyStructuredMessage;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.standard.ProductMessage;
/**
* A model interface that represents a button message
*/
public sealed interface ButtonMessage extends Message permits ButtonsMessage, HighlyStructuredMessage, ListMessage, NativeFlowResponseMessage, TemplateMessage, ButtonReplyMessage, InteractiveMessage, ProductMessage {
@Override
default Category category() {
return Category.BUTTON;
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/ButtonReplyMessage.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.message.button.ButtonsResponseMessage;
import com.github.auties00.cobalt.model.message.button.ListResponseMessage;
import com.github.auties00.cobalt.model.message.button.TemplateReplyMessage;
/**
* A model interface that represents a reply to a button message
*/
public sealed interface ButtonReplyMessage<T extends ButtonReplyMessage<T>> extends ContextualMessage, ButtonMessage permits ListResponseMessage, TemplateReplyMessage, ButtonsResponseMessage {
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/ChatMessageKey.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.client.WhatsAppClientType;
import com.github.auties00.cobalt.model.info.ChatMessageInfo;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.util.SecureBytes;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
/**
* A container for unique identifiers and metadata linked to a {@link Message} and contained in
* {@link ChatMessageInfo}.
*/
@ProtobufMessage(name = "MessageKey")
public final class ChatMessageKey {
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
Jid chatJid;
@ProtobufProperty(index = 2, type = ProtobufType.BOOL)
final boolean fromMe;
@ProtobufProperty(index = 3, type = ProtobufType.STRING)
final String id;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
Jid senderJid;
public ChatMessageKey(Jid chatJid, boolean fromMe, String id, Jid senderJid) {
this.chatJid = chatJid;
this.fromMe = fromMe;
this.id = Objects.requireNonNullElseGet(id, () -> UUID.randomUUID().toString());
this.senderJid = senderJid;
}
/**
* Generates a random message id
*
* @return a non-null String
*/
public static String randomId(WhatsAppClientType clientType) {
return switch (clientType) {
case WhatsAppClientType.WEB -> "3EB0" + SecureBytes.randomHex(13);
case WhatsAppClientType.MOBILE -> SecureBytes.randomHex(16);
};
}
public Jid chatJid() {
return chatJid;
}
public void setChatJid(Jid chatJid) {
this.chatJid = chatJid;
}
public boolean fromMe() {
return fromMe;
}
public String id() {
return id;
}
public Optional<Jid> senderJid() {
return Optional.ofNullable(senderJid);
}
public void setSenderJid(Jid senderJid) {
this.senderJid = senderJid;
}
@Override
public boolean equals(Object o) {
return o instanceof ChatMessageKey that && fromMe == that.fromMe && Objects.equals(chatJid, that.chatJid) && Objects.equals(id, that.id) && Objects.equals(senderJid, that.senderJid);
}
@Override
public int hashCode() {
return Objects.hash(chatJid, fromMe, id, senderJid);
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/ContextualMessage.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.info.ContextInfo;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.payment.PaymentOrderMessage;
import com.github.auties00.cobalt.model.message.standard.*;
import java.util.Optional;
/**
* A model interface that represents a message sent by a contact that provides a context. Classes
* that implement this interface must provide an accessor named contextInfo to access said
* property.
*/
public sealed interface ContextualMessage extends Message
permits ButtonsMessage, InteractiveMessage, InteractiveResponseMessage, ListMessage,
TemplateMessage, ButtonReplyMessage, MediaMessage, PaymentOrderMessage, ContactMessage, ContactsMessage,
GroupInviteMessage, LiveLocationMessage, LocationMessage, PollCreationMessage, ProductMessage, RequestPhoneNumberMessage, TextMessage {
Optional<ContextInfo> contextInfo();
void setContextInfo(ContextInfo contextInfo);
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/EncryptedMessage.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.message.standard.EncryptedReactionMessage;
import com.github.auties00.cobalt.model.message.standard.PollUpdateMessage;
public sealed interface EncryptedMessage permits EncryptedReactionMessage, PollUpdateMessage {
String secretName();
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/FutureMessageContainer.java
================================================
package com.github.auties00.cobalt.model.message.model;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
/**
* A container for a future message
*/
@ProtobufMessage(name = "Message.FutureProofMessage")
public final class FutureMessageContainer {
@ProtobufProperty(index = 1, type = ProtobufType.MESSAGE)
final MessageContainer value;
FutureMessageContainer(MessageContainer value) {
this.value = Objects.requireNonNull(value, "content cannot be null");
}
public MessageContainer value() {
return this.value;
}
@Override
public boolean equals(Object other) {
return other instanceof FutureMessageContainer that
&& Objects.equals(value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return "FutureMessageContainer[" +
"content=" + value +
']';
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/KeepInChat.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.jid.Jid;
import com.github.auties00.cobalt.util.Clock;
import it.auties.protobuf.annotation.ProtobufEnum;
import it.auties.protobuf.annotation.ProtobufEnumIndex;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.Optional;
/**
* A model class that represents an ephemeral message that was saved manually by the user in a chat
*/
@ProtobufMessage(name = "KeepInChat")
public final class KeepInChat {
@ProtobufProperty(index = 1, type = ProtobufType.ENUM)
final Type keepType;
@ProtobufProperty(index = 2, type = ProtobufType.INT64)
final long serverTimestampSeconds;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final ChatMessageKey key;
@ProtobufProperty(index = 4, type = ProtobufType.STRING)
final Jid deviceJid;
@ProtobufProperty(index = 5, type = ProtobufType.INT64)
final long clientTimestampInMilliseconds;
@ProtobufProperty(index = 6, type = ProtobufType.INT64)
final long serverTimestampMilliseconds;
KeepInChat(Type keepType, long serverTimestampSeconds, ChatMessageKey key, Jid deviceJid, long clientTimestampInMilliseconds, long serverTimestampMilliseconds) {
this.keepType = Objects.requireNonNull(keepType, "keepType cannot be null");
this.serverTimestampSeconds = serverTimestampSeconds;
this.key = Objects.requireNonNull(key, "key cannot be null");
this.deviceJid = Objects.requireNonNull(deviceJid, "deviceJid cannot be null");
this.clientTimestampInMilliseconds = clientTimestampInMilliseconds;
this.serverTimestampMilliseconds = serverTimestampMilliseconds;
}
public Type keepType() {
return keepType;
}
public long serverTimestampSeconds() {
return serverTimestampSeconds;
}
public ChatMessageKey key() {
return key;
}
public Jid deviceJid() {
return deviceJid;
}
public long clientTimestampInMilliseconds() {
return clientTimestampInMilliseconds;
}
public long serverTimestampMilliseconds() {
return serverTimestampMilliseconds;
}
public Optional<ZonedDateTime> serverTimestamp() {
return Clock.parseSeconds(serverTimestampSeconds);
}
public Optional<ZonedDateTime> clientTimestamp() {
return Clock.parseMilliseconds(clientTimestampInMilliseconds);
}
@Override
public boolean equals(Object o) {
return o instanceof KeepInChat that
&& Objects.equals(keepType, that.keepType)
&& serverTimestampSeconds == that.serverTimestampSeconds
&& Objects.equals(key, that.key)
&& Objects.equals(deviceJid, that.deviceJid)
&& clientTimestampInMilliseconds == that.clientTimestampInMilliseconds
&& serverTimestampMilliseconds == that.serverTimestampMilliseconds;
}
@Override
public int hashCode() {
return Objects.hash(keepType, serverTimestampSeconds, key, deviceJid, clientTimestampInMilliseconds, serverTimestampMilliseconds);
}
@Override
public String toString() {
return "KeepInChat[" +
"keepType=" + keepType +
", serverTimestampSeconds=" + serverTimestampSeconds +
", key=" + key +
", deviceJid=" + deviceJid +
", clientTimestampInMilliseconds=" + clientTimestampInMilliseconds +
", serverTimestampMilliseconds=" + serverTimestampMilliseconds +
']';
}
@ProtobufEnum(name = "KeepType")
public enum Type {
UNKNOWN(0),
KEEP_FOR_ALL(1),
UNDO_KEEP_FOR_ALL(2);
final int index;
Type(@ProtobufEnumIndex int index) {
this.index = index;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/MediaMessage.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.client.WhatsAppClient;
import com.github.auties00.cobalt.model.media.MediaPath;
import com.github.auties00.cobalt.model.media.MediaProvider;
import com.github.auties00.cobalt.model.message.payment.PaymentInvoiceMessage;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.standard.*;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.OptionalLong;
/**
* A media message
* Read its content using {@link WhatsAppClient#downloadMedia(MediaMessage)}
*/
public sealed interface MediaMessage
extends ContextualMessage, MediaProvider
permits PaymentInvoiceMessage, AudioMessage, DocumentMessage, ImageMessage, StickerMessage, VideoOrGifMessage {
/**
* Returns the timestampSeconds, that is the seconds elapsed since {@link java.time.Instant#EPOCH}, for{@link MediaMessage#mediaKey()}
*
* @return an unsigned long
*/
OptionalLong mediaKeyTimestampSeconds();
/**
* Returns the timestampSeconds for{@link MediaMessage#mediaKey()}
*
* @return a zoned date time
*/
Optional<ZonedDateTime> mediaKeyTimestamp();
/**
* Returns the media type of the media that this object wraps
*
* @return a non-null {@link Type}
*/
Type mediaType();
@Override
default Message.Category category() {
return Message.Category.MEDIA;
}
@Override
default Message.Type type() {
return mediaType().toMessageType();
}
@Override
default MediaPath mediaPath() {
return mediaType().toAttachmentType();
}
/**
* The constants of this enumerated type describe the various types of media type that a
* {@link MediaMessage} can hold
*/
enum Type {
/**
* No media
*/
NONE("", "", Message.Type.EMPTY, MediaPath.NONE),
/**
* The message is an image
*/
IMAGE("jpg", "image/jpeg", Message.Type.IMAGE, MediaPath.IMAGE),
/**
* The message is a document
*/
DOCUMENT("", "application/octet-stream", Message.Type.DOCUMENT, MediaPath.DOCUMENT),
/**
* The message is an audio
*/
AUDIO("mp3", "audio/mpeg", Message.Type.AUDIO, MediaPath.AUDIO),
/**
* The message is a video
*/
VIDEO("mp4", "video/mp4", Message.Type.VIDEO, MediaPath.VIDEO),
/**
* The message is a sticker
*/
STICKER("webp", "image/webp", Message.Type.STICKER, MediaPath.IMAGE);
/**
* The default extension for this enumerated type. Might be right, might be wrong, who knows.
*/
private final String extension;
/**
* The default mime type for this enumerated type. Might be right, might be wrong, who knows.
*/
private final String mimeType;
/**
* The message type for this media
*/
private final Message.Type messageType;
/**
* The attachment type for this media
*/
private final MediaPath mediaPath;
Type(String extension, String mimeType, Message.Type messageType, MediaPath mediaPath) {
this.extension = extension;
this.mimeType = mimeType;
this.messageType = messageType;
this.mediaPath = mediaPath;
}
/**
* The message type for this media
*
* @return a message type
*/
public Message.Type toMessageType() {
return messageType;
}
/**
* The attachment type for this media
*
* @return an attachment type
*/
public MediaPath toAttachmentType() {
return mediaPath;
}
/**
* Returns the extension of this media
*
* @return a value
*/
public String extension() {
return extension;
}
/**
* Returns the mime type of this media
*
* @return a value
*/
public String mimeType() {
return this.mimeType;
}
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/Message.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.standard.*;
/**
* A model interface that represents a message sent by a contact or by Whatsapp.
*/
public sealed interface Message permits ButtonMessage, ContextualMessage, PaymentMessage, ServerMessage, CallMessage, EmptyMessage, KeepInChatMessage, NewsletterAdminInviteMessage, PollUpdateMessage, ReactionMessage {
/**
* Return message type
*
* @return a non-null message type
*/
Type type();
/**
* Return message category
*
* @return a non-null message category
*/
Category category();
/**
* The constants of this enumerated type describe the various types of messages that a
* {@link MessageContainer} can wrap
*/
enum Type {
/**
* Empty
*/
EMPTY,
/**
* Text
*/
TEXT,
/**
* Sender key distribution
*/
SENDER_KEY_DISTRIBUTION,
/**
* Image
*/
IMAGE,
/**
* Contact
*/
CONTACT,
/**
* Location
*/
LOCATION,
/**
* Document
*/
DOCUMENT,
/**
* Audio
*/
AUDIO,
/**
* Video
*/
VIDEO,
/**
* Protocol
*/
PROTOCOL,
/**
* Contact array
*/
CONTACT_ARRAY,
/**
* Highly structured
*/
HIGHLY_STRUCTURED,
/**
* Send payment
*/
SEND_PAYMENT,
/**
* Live location
*/
LIVE_LOCATION,
/**
* Request payment
*/
REQUEST_PAYMENT,
/**
* Decline payment request
*/
DECLINE_PAYMENT_REQUEST,
/**
* Cancel payment request
*/
CANCEL_PAYMENT_REQUEST,
/**
* Template
*/
TEMPLATE,
/**
* Sticker
*/
STICKER,
/**
* Group invite
*/
GROUP_INVITE,
/**
* Template reply
*/
TEMPLATE_REPLY,
/**
* Product
*/
PRODUCT,
/**
* Device sent
*/
DEVICE_SENT,
/**
* Device sync
*/
DEVICE_SYNC,
/**
* List
*/
LIST,
/**
* View once
*/
VIEW_ONCE,
/**
* Order
*/
PAYMENT_ORDER,
/**
* List newsletters
*/
LIST_RESPONSE,
/**
* Ephemeral
*/
EPHEMERAL,
/**
* Payment invoice
*/
PAYMENT_INVOICE,
/**
* Buttons newsletters
*/
BUTTONS,
/**
* Buttons newsletters
*/
BUTTONS_RESPONSE,
/**
* Payment invite
*/
PAYMENT_INVITE,
/**
* Interactive
*/
INTERACTIVE,
/**
* Reaction
*/
REACTION,
/**
* Interactive newsletters
*/
INTERACTIVE_RESPONSE,
/**
* Native flow newsletters
*/
NATIVE_FLOW_RESPONSE,
/**
* Keep in chat
*/
KEEP_IN_CHAT,
/**
* Poll creation
*/
POLL_CREATION,
/**
* Poll update
*/
POLL_UPDATE,
/**
* Request phone value
*/
REQUEST_PHONE_NUMBER,
/**
* Encrypted reaction
*/
ENCRYPTED_REACTION,
/**
* A call
*/
CALL,
/**
* Sticker sync
*/
STICKER_SYNC,
/**
* Text edit
*/
EDITED,
/**
* Newsletter admin invite
*/
NEWSLETTER_ADMIN_INVITE
}
/**
* The constants of this enumerated type describe the various categories of messages that a
* {@link MessageContainer} can wrap
*/
enum Category {
/**
* Device message
*/
BUTTON,
/**
* Payment message
*/
PAYMENT,
/**
* Payment message
*/
MEDIA,
/**
* Server message
*/
SERVER,
/**
* Device message
*/
DEVICE,
/**
* Standard message
*/
STANDARD
}
}
================================================
FILE: src/main/java/com/github/auties00/cobalt/model/message/model/MessageContainer.java
================================================
package com.github.auties00.cobalt.model.message.model;
import com.alibaba.fastjson2.JSONObject;
import com.github.auties00.cobalt.model.button.template.highlyStructured.HighlyStructuredMessage;
import com.github.auties00.cobalt.model.info.DeviceContextInfo;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.payment.*;
import com.github.auties00.cobalt.model.message.server.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.payment.*;
import com.github.auties00.cobalt.model.message.server.*;
import com.github.auties00.cobalt.model.message.standard.*;
import com.github.auties00.cobalt.model.message.button.*;
import com.github.auties00.cobalt.model.message.payment.*;
import com.github.auties00.cobalt.model.message.server.*;
import com.github.auties00.cobalt.model.message.standard.*;
import it.auties.protobuf.annotation.ProtobufMessage;
import it.auties.protobuf.annotation.ProtobufProperty;
import it.auties.protobuf.model.ProtobufType;
import java.util.Objects;
import java.util.Optional;
/**
* A container for all types of messages known currently to WhatsappWeb.
* <p>
* Only one of these properties is populated usually, but it is possible to have multiple after a message retry for example
* <p>
* There are several categories of messages:
* <ul>
* <li>Server messages</li>
* <li>Button messages</li>
* <li>Product messages</li>
* <li>Payment messages</li>
* <li>Standard messages</li>
* </ul>
*/
@ProtobufMessage(name = "Message")
public final class MessageContainer {
private static final EmptyMessage EMPTY_MESSAGE = new EmptyMessage();
@ProtobufProperty(index = 1, type = ProtobufType.STRING)
final String textWithNoContextMessage;
@ProtobufProperty(index = 2, type = ProtobufType.MESSAGE)
final SenderKeyDistributionMessage senderKeyDistributionMessage;
@ProtobufProperty(index = 3, type = ProtobufType.MESSAGE)
final ImageMessage imageMessage;
@ProtobufProperty(index = 4, type = ProtobufType.MESSAGE)
final ContactMessage contactMessage;
@ProtobufProperty(index = 5, type = ProtobufType.MESSAGE)
final LocationMessage locationMessage;
@ProtobufProperty(index = 6, type = ProtobufType.MESSAGE)
final TextMessage textMessage;
@ProtobufProperty(index = 7, type = ProtobufType.MESSAGE)
final DocumentMessage documentMessage;
@ProtobufProperty(index = 8, type = ProtobufType.MESSAGE)
final AudioMessage audioMessage;
@ProtobufProperty(index = 9, type = ProtobufType.MESSAGE)
final VideoOrGifMessage videoMessage;
@ProtobufProperty(index = 10, type = ProtobufType.MESSAGE)
final CallMessage callMessage;
@ProtobufProperty(index = 12, type = ProtobufType.MESSAGE)
final ProtocolMessage protocolMessage;
@ProtobufProperty(index = 13, type = ProtobufType.MESSAGE)
final ContactsMessage contactsArrayMessage;
@ProtobufProperty(index = 14, type = ProtobufType.MESSAGE)
final HighlyStructuredMessage highlyStructuredMessage;
@ProtobufProperty(index = 16, type = ProtobufType.MESSAGE)
final SendPaymentMessage sendPaymentMessage;
@ProtobufProperty(index = 18, type = ProtobufType.MESSAGE)
final LiveLocationMessage liveLocationMessage;
@ProtobufProperty(index = 22, type = ProtobufType.MESSAGE)
final RequestPaymentMessage requestPaymentMessage;
@ProtobufProperty(index = 23, type = ProtobufType.MESSAGE)
final DeclinePaymentRequestMessage declinePaymentRequestMessage;
@ProtobufProperty(index = 24, type = ProtobufType.MESSAGE)
final CancelPaymentRequestMessage cancelPaymentRequestMessage;
@ProtobufProperty(index = 25, type = ProtobufType.MESSAGE)
final TemplateMessage templateMessage;
@ProtobufProperty(index = 26, type = ProtobufType.MESSAGE)
final StickerMessage stickerMessage;
@ProtobufProperty(index = 28, type = ProtobufType.MESSAGE)
final GroupInviteMessage groupInviteMessage;
@ProtobufProperty(index = 29, type = ProtobufType.MESSAGE)
final TemplateReplyMessage templateReplyMessage;
@ProtobufProperty(index = 30, type = ProtobufType.MESSAGE)
final ProductMessage productMessage;
@ProtobufProperty(index = 31, type = ProtobufType.MESSAGE)
final DeviceSentMessage deviceSentMessage;
@ProtobufProperty(index = 32, type = ProtobufType.MESSAGE)
final DeviceSyncMessage deviceSyncMessage;
@ProtobufProperty(index = 36, type = ProtobufType.MESSAGE)
final ListMessage listMessage;
@ProtobufProperty(index = 37, type = ProtobufType.MESSAGE)
final FutureMessageContainer viewOnceMessage;
@ProtobufProperty(index = 38, type = ProtobufType.MESSAGE)
final PaymentOrderMessage orderMessage;
@ProtobufProperty(index = 39, type = ProtobufType.MESSAGE)
final ListResponseMessage listResponseMessage;
@ProtobufProperty(index = 40, type = ProtobufType.MESSAGE)
final FutureMessageContainer ephemeralMessage;
@ProtobufProperty(index = 41, type = ProtobufType.MESSAGE)
final PaymentInvoiceMessage invoiceMessage;
@ProtobufProperty(index = 42, type = ProtobufType.MESSAGE)
final ButtonsMessage buttonsMessage;
@ProtobufProperty(index = 43, type = ProtobufType.MESSAGE)
final ButtonsResponseMessage buttonsResponseMessage;
@ProtobufProperty(index = 44, type = ProtobufType.MESSAGE)
final PaymentInviteMessage paymentInviteMessage;
@ProtobufProperty(index = 45, type = ProtobufType.MESSAGE)
final InteractiveMessage interactiveMessage;
@ProtobufProperty(index = 46, type = ProtobufType.MESSAGE)
final ReactionMessage reactionMessage;
@ProtobufProperty(index = 47, type = ProtobufType.MESSAGE)
final StickerSyncRMRMessage stickerSyncMessage;
@ProtobufProperty(index = 48, type = ProtobufType.MESSAGE)
final InteractiveResponseMessage interactiveResponseMessage;
@ProtobufProperty(index = 49, type = ProtobufType.MESSAGE)
final PollCreationMessage pollCreationMessage;
@ProtobufProperty(index = 50, type = ProtobufType.MESSAGE)
final PollUpdateMessage pollUpdateMessage;
@ProtobufProperty(index = 51, type = ProtobufType.MESSAGE)
final KeepInChatMessage keepInChatMessage;
@ProtobufProperty(index = 53, type = ProtobufType.MESSAGE)
final FutureMessageContainer documentWithCaptionMessage;
@ProtobufProperty(index = 54, type = ProtobufType.MESSAGE)
final RequestPhoneNumberMessage requestPhoneNumberMessage;
@ProtobufProperty(index = 55, type = ProtobufType.MESSAGE)
final FutureMessageContainer viewOnceV2Message;
@ProtobufProperty(index = 56, type = ProtobufType.MESSAGE)
final EncryptedReactionMessage encryptedReactionMessage;
@ProtobufProperty(index = 58, type = ProtobufType.MESSAGE)
final FutureMessageContainer editedMessage;
@ProtobufProperty(index = 59, type = ProtobufType.MESSAGE)
final FutureMessageContainer viewOnceV2ExtensionMessage;
@ProtobufProperty(index = 78, type = ProtobufType.MESSAGE)
final NewsletterAdminInviteMessage newsletterAdminInviteMessage;
@ProtobufProperty(index = 35, type = ProtobufType.MESSAGE)
final DeviceContextInfo deviceInfo;
MessageContainer(String textWithNoContextMessage, SenderKeyDistributionMessage senderKeyDistributionMessage, ImageMessage imageMessage, ContactMessage contactMessage, LocationMessage locationMessage, TextMessage textMessage, DocumentMessage documentMessage, AudioMessage audioMessage, VideoOrGifMessage videoMessage, CallMessage callMessage, ProtocolMessage protocolMessage, ContactsMessage contactsArrayMessage, HighlyStructuredMessage highlyStructuredMessage, SendPaymentMessage sendPaymentMessage, LiveLocationMessage liveLocationMessage, RequestPaymentMessage requestPaymentMessage, DeclinePaymentRequestMessage declinePaymentRequestMessage, CancelPaymentRequestMessage cancelPaymentRequestMessage, TemplateMessage templateMessage, StickerMessage stickerMessage, GroupInviteMessage groupInviteMessage, TemplateReplyMessage templateReplyMessage, ProductMessage productMessage, DeviceSentMessage deviceSentMessage, DeviceSyncMessage deviceSyncMessage, ListMessage listMessage, FutureMessageContainer viewOnceMessage, PaymentOrderMessage orderMessage, ListResponseMessage listResponseMessage, FutureMessageContainer ephemeralMessage, PaymentInvoiceMessage invoiceMessage, ButtonsMessage buttonsMessage, ButtonsResponseMessage buttonsResponseMessage, PaymentInviteMessage paymentInviteMessage, InteractiveMessage interactiveMessage, ReactionMessage reactionMessage, StickerSyncRMRMessage stickerSyncMessage, InteractiveResponseMessage interactiveResponseMessage, PollCreationMessage pollCreationMessage, PollUpdateMessage pollUpdateMessage, KeepInChatMessage keepInChatMessage, FutureMessageContainer documentWithCaptionMessage, RequestPhoneNumberMessage requestPhoneNumberMessage, FutureMessageContainer viewOnceV2Message, EncryptedReactionMessage encryptedReactionMessage, FutureMessageContainer editedMessage, FutureMessageContainer viewOnceV2ExtensionMessage, NewsletterAdminInviteMessage newsletterAdminInviteMessage, DeviceContextInfo deviceInfo) {
this.textWithNoContextMessage = textWithNoContextMessage;
this.senderKeyDistributionMessage = senderKeyDistributionMessage;
this.imageMessage = imageMessage;
this.contactMessage = contactMessage;
this.locationMessage = locationMessage;
this.textMessage = textMessage;
this.documentMessage = documentMessage;
this.audioMessage = audioMessage;
this.videoMessage = videoMessage;
this.callMessage = callMessage;
this.protocolMessage = protocolMessage;
this.contactsArrayMessage = contactsArrayMessage;
this.highlyStructuredMessage = highlyStructuredMessage;
this.sendPaymentMessage = sendPaymentMessage;
this.liveLocationMessage = liveLocationMessage;
this.requestPaymentMessage = requestPaymentMessage;
this.declinePaymentRequestMessage = declinePaymentRequestMessage;
this.cancelPaymentRequestMessage = cancelPaymentRequestMessage;
this.templateMessage = templateMessage;
this.stickerMessage = stickerMessage;
this.groupInviteMessage = groupInviteMessage;
this.templateReplyMessage = templateReplyMessage;
this.productMessage = productMessage;
this.deviceSentMessage = deviceSentMessage;
this.deviceSyncMessage = deviceSyncMessage;
this.listMessage = listMessage;
this.viewOnceMessage = viewOnceMessage;
this.orderMessage = orderMessage;
this.listResponseMessage = listResponseMessage;
this.ephemeralMessage = ephemeralMessage;
this.invoiceMessage = invoiceMessage;
this.buttonsMessage = buttonsMessage;
this.buttonsResponseMessage = buttonsResponseMessage;
this.paymentInviteMessage = paymentInviteMessage;
this.interactiveMessage = interactiveMessage;
this.reactionMessage = reactionMessage;
this.stickerSyncMessage = stickerSyncMessage;
this.interactiveResponseMessage = interactiveResponseMessage;
this.pollCreationMessage = pollCreationMessage;
this.pollUpdateMessage = pollUpdateMessage;
this.keepInChatMessage = keepInChatMessage;
this.documentWithCaptionMessage = documentWithCaptionMessage;
this.requestPhoneNumberMessage = requestPhoneNumberMessage;
this.viewOnceV2Message = viewOnceV2Message;
this.encryptedReactionMessage = encryptedReactionMessage;
this.editedMessage = editedMessage;
this.viewOnceV2ExtensionMessage = viewOnceV2ExtensionMessage;
this.newsletterAdminInviteMessage = newsletterAdminInviteMessage;
this.deviceInfo = deviceInfo;
}
@Override
public boolean equals(Object o) {
return o instanceof MessageContainer that
&& Objects.equals(textWithNoContextMessage, that.textWithNoContextMessage)
&& Objects.equals(senderKeyDistributionMessage, that.senderKeyDistributionMessage)
&& Objects.equals(imageMessage, that.imageMessage)
&& Objects.equals(contactMessage, that.contactMessage)
&& Objects.equals(locationMessage, that.locationMessage)
&& Objects.equals(textMessage, that.textMessage)
&& Objects.equals(documentMessage, that.documentMessage)
&& Objects.equals(audioMessage, that.audioMessage)
&& Objects.equals(videoMessage, that.videoMessage)
&& Objects.equals(callMessage, that.callMessage)
&& Objects.equals(protocolMessage, that.protocolMessage)
&& Objects.equals(contactsArrayMessage, that.contactsArrayMessage)
&& Objects.equals(highlyStructuredMessage, that.highlyStructuredMessage)
&& Objects.equals(sendPaymentMessage, that.sendPaymentMessage)
&& Objects.equals(liveLocationMessage, that.liveLocationMessage)
&& Objects.equals(requestPaymentMessage, that.requestPaymentMessage)
&& Objects.equals(declinePaymentRequestMessage, that.declinePaymentRequestMessage)
&& Objects.equals(cancelPaymentRequestMessage, that.cancelPaymentRequestMessage)
&& Objects.equals(templateMessage, that.templateMessage)
&& Objects.equals(stickerMessage, that.stickerMessage)
&& Objects.equals(groupInviteMessage, that.groupInviteMessage)
&& Objects.equals(templateReplyMessage, that.templateReplyMessage)
&& Objects.equals(productMessage, that.productMessage)
&& Objects.equals(deviceSentMessage, that.deviceSentMessage)
&& Objects.equals(deviceSyncMessage, that.deviceSyncMessage)
&& Objects.equals(listMessage, that.listMessage)
&& Objects.equals(viewOnceMessage, that.viewOnceMessage)
&& Objects.equals(orderMessage, that.orderMessage)
&& Objects.equals(listResponseMessage, that.listResponseMessage)
&& Objects.equals(ephemeralMessage, that.ephemeralMessage)
&& Objects.equals(invoiceMessage, that.invoiceMessage)
&& Objects.equals(buttonsMessage, that.buttonsMessage)
&& Objects.equals(buttonsResponseMessage, that.buttonsResponseMessage)
&& Objects.equals(paymentInviteMessage, that.paymentInviteMessage)
&& Objects.equals(interactiveMessage, that.interactiveMessage)
&& Objects.equals(reactionMessage, that.reactionMessage)
&& Objects.equals(stickerSyncMessage, that.stickerSyncMessage)
&& Objects.equals(interactiveResponseMessage, that.interactiveResponseMessage)
&& Objects.equals(pollCreationMessage, that.pollCreationMessage)
&& Objects.equals(pollUpdateMessage, that.pollUpdateMessage)
&& Objects.equals(keepInChatMessage, that.keepInChatMessage)
&& Objects.equals(documentWithCaptionMessage, that.documentWithCaptionMessage)
&& Objects.equals(requestPhoneNumberMessage, that.requestPhoneNumberMessage)
&& Objects.equals(viewOnceV2Message, that.viewOnceV2Message)
&& Objects.equals(encryptedReactionMessage, that.encryptedReactionMessage)
&& Objects.equals(editedMessage, that.editedMessage)
&& Objects.equals(viewOnceV2ExtensionMessage, that.viewOnceV2ExtensionMessage)
&& Objects.equals(newsletterAdminInviteMessage, that.newsletterAdminInviteMessage)
&& Objects.equals(deviceInfo, that.deviceInfo);
}
@Override
public int hashCode() {
return Objects.hash(
textWithNoContextMessage,
senderKeyDistributionMessage,
imageMessage,
contactMessage,
locationMessage,
textMessage,
documentMessage,
audioMessage,
videoMessage,
callMessage,
protocolMessage,
contactsArrayMessage,
highlyStructuredMessage,
sendPaymentMessage,
liveLocationMessage,
requestPaymentMessage,
declinePaymentRequestMessage,
cancelPaymentRequestMessage,
templateMessage,
stickerMessage,
groupInviteMessage,
templateReplyMessage,
productMessage,
deviceSentMessage,
deviceSyncMessage,
listMessage,
viewOnceMessage,
orderMessage,
listResponseMessage,
ephemeralMessage,
invoiceMessage,
buttonsMessage,
buttonsResponseMessage,
paymentInviteMessage,
interactiveMessage,
reactionMessage,
stickerSyncMessage,
interactiveResponseMessage,
pollCreationMessage,
pollUpdateMessage,
keepInChatMessage,
documentWithCaptionMessage,
requestPhoneNumberMessage,
viewOnceV2Message,
encryptedReactionMessage,
editedMessage,
viewOnceV2ExtensionMessage,
newsletterAdminInviteMessage,
deviceInfo
);
}
/**
* Returns an empty message container
*
* @return a non-null container
*/
public static MessageContainer empty() {
return new MessageContainerBuilder().build();
}
public static Optional<MessageContainer> ofJson(JSONObject jsonObject) {
// TODO: Implement me
return Optional.empty();
}
/**
* Constructs a new MessageContainer from a message of any type
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
* @return a non-null container
*/
public static <T extends Message> MessageContainer of(T message) {
return ofBuilder(message).build();
}
/**
* Constructs a new MessageContainerBuilder from a message of any type
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
* @return a non-null builder
*/
public static <T extends Message> MessageContainerBuilder ofBuilder(T message) {
var builder = new MessageContainerBuilder();
switch (message) {
case SenderKeyDistributionMessage senderKeyDistribution ->
builder.senderKeyDistributionMessage(senderKeyDistribution);
case ImageMessage image -> builder.imageMessage(image);
case ContactMessage contact -> builder.contactMessage(contact);
case LocationMessage location -> builder.locationMessage(location);
case TextMessage text -> builder.textMessage(text);
case DocumentMessage document -> builder.documentMessage(document);
case AudioMessage audio -> builder.audioMessage(audio);
case VideoOrGifMessage video -> builder.videoMessage(video);
case ProtocolMessage protocol -> builder.protocolMessage(protocol);
case ContactsMessage contactsArray -> builder.contactsArrayMessage(contactsArray);
case HighlyStructuredMessage highlyStructured ->
builder.highlyStructuredMessage(highlyStructured);
case SendPaymentMessage sendPayment -> builder.sendPaymentMessage(sendPayment);
case LiveLocationMessage liveLocation -> builder.liveLocationMessage(liveLocation);
case RequestPaymentMessage requestPayment -> builder.requestPaymentMessage(requestPayment);
case DeclinePaymentRequestMessage declinePaymentRequest ->
builder.declinePaymentRequestMessage(declinePaymentRequest);
case CancelPaymentRequestMessage cancelPaymentRequest ->
builder.cancelPaymentRequestMessage(cancelPaymentRequest);
case TemplateMessage template -> builder.templateMessage(template);
case StickerMessage sticker -> builder.stickerMessage(sticker);
case GroupInviteMessage groupInvite -> builder.groupInviteMessage(groupInvite);
case TemplateReplyMessage templateButtonReply ->
builder.templateReplyMessage(templateButtonReply);
case ProductMessage product -> builder.productMessage(product);
case DeviceSyncMessage deviceSync -> builder.deviceSyncMessage(deviceSync);
case ListMessage buttonsList -> builder.listMessage(buttonsList);
case PaymentOrderMessage order -> builder.orderMessage(order);
case ListResponseMessage listResponse -> builder.listResponseMessage(listResponse);
case PaymentInvoiceMessage invoice -> builder.invoiceMessage(invoice);
case ButtonsMessage buttons -> builder.buttonsMessage(buttons);
case ButtonsResponseMessage buttonsResponse -> builder.buttonsResponseMessage(buttonsResponse);
case PaymentInviteMessage paymentInvite -> builder.paymentInviteMessage(paymentInvite);
case InteractiveMessage interactive -> builder.interactiveMessage(interactive);
case ReactionMessage reaction -> builder.reactionMessage(reaction);
case StickerSyncRMRMessage stickerSync -> builder.stickerSyncMessage(stickerSync);
case DeviceSentMessage deviceSent -> builder.deviceSentMessage(deviceSent);
case InteractiveResponseMessage interactiveResponseMessage ->
builder.interactiveResponseMessage(interactiveResponseMessage);
case PollCreationMessage pollCreationMessage ->
builder.pollCreationMessage(pollCreationMessage);
case PollUpdateMessage pollUpdateMessage -> builder.pollUpdateMessage(pollUpdateMessage);
case KeepInChatMessage keepInChatMessage -> builder.keepInChatMessage(keepInChatMessage);
case RequestPhoneNumberMessage requestPhoneNumberMessage ->
builder.requestPhoneNumberMessage(requestPhoneNumberMessage);
case EncryptedReactionMessage encReactionMessage ->
builder.encryptedReactionMessage(encReactionMessage);
case CallMessage callMessage -> builder.callMessage(callMessage);
case NewsletterAdminInviteMessage newsletterAdminInviteMessage ->
builder.newsletterAdminInviteMessage(newsletterAdminInviteMessage);
default -> {
}
}
return builder;
}
/**
* Constructs a new MessageContainer from a text message
*
* @param message the text message with no context
*/
public static MessageContainer of(String message) {
return new MessageContainerBuilder()
.textMessage(TextMessage.of(message))
.build();
}
/**
* Constructs a new MessageContainer from a message of any type that can only be seen once
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
*/
public static <T extends Message> MessageContainer ofViewOnce(T message) {
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(message))
.build();
return new MessageContainerBuilder()
.viewOnceMessage(futureMessageContainer)
.build();
}
/**
* Constructs a new MessageContainer from a message of any type that can only be seen once(version
* v2)
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
*/
public static <T extends Message> MessageContainer ofViewOnceV2(T message) {
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(message))
.build();
return new MessageContainerBuilder()
.viewOnceV2Message(futureMessageContainer)
.build();
}
/**
* Constructs a new MessageContainer from a message of any type marking it as ephemeral
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
*/
public static <T extends Message> MessageContainer ofEphemeral(T message) {
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(message))
.build();
return new MessageContainerBuilder()
.ephemeralMessage(futureMessageContainer)
.build();
}
/**
* Constructs a new MessageContainer from an edited message
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
*/
public static <T extends Message> MessageContainer ofEditedMessage(T message) {
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(message))
.build();
return new MessageContainerBuilder()
.editedMessage(futureMessageContainer)
.build();
}
/**
* Constructs a new MessageContainer from a document with caption message
*
* @param message the message that the new container should wrap
* @param <T> the type of the message
*/
public static <T extends Message> MessageContainer ofDocumentWithCaption(T message) {
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(message))
.build();
return new MessageContainerBuilder()
.documentWithCaptionMessage(futureMessageContainer)
.build();
}
/**
* Returns the first populated message inside this container. If no message is found,
* {@link EmptyMessage} is returned
*
* @return a non-null message
*/
public Message content() {
if (this.textWithNoContextMessage != null) {
return TextMessage.of(textWithNoContextMessage);
}
if (this.imageMessage != null) {
return imageMessage;
}
if (this.contactMessage != null) {
return contactMessage;
}
if (this.locationMessage != null) {
return locationMessage;
}
if (this.textMessage != null) {
return textMessage;
}
if (this.documentMessage != null) {
return documentMessage;
}
if (this.audioMessage != null) {
return audioMessage;
}
if (this.videoMessage != null) {
return videoMessage;
}
if (this.protocolMessage != null) {
return protocolMessage;
}
if (this.contactsArrayMessage != null) {
return contactsArrayMessage;
}
if (this.highlyStructuredMessage != null) {
return highlyStructuredMessage;
}
if (this.sendPaymentMessage != null) {
return sendPaymentMessage;
}
if (this.liveLocationMessage != null) {
return liveLocationMessage;
}
if (this.requestPaymentMessage != null) {
return requestPaymentMessage;
}
if (this.declinePaymentRequestMessage != null) {
return declinePaymentRequestMessage;
}
if (this.cancelPaymentRequestMessage != null) {
return cancelPaymentRequestMessage;
}
if (this.templateMessage != null) {
return templateMessage;
}
if (this.stickerMessage != null) {
return stickerMessage;
}
if (this.groupInviteMessage != null) {
return groupInviteMessage;
}
if (this.templateReplyMessage != null) {
return templateReplyMessage;
}
if (this.productMessage != null) {
return productMessage;
}
if (this.deviceSentMessage != null) {
return deviceSentMessage.message().content();
}
if (this.deviceSyncMessage != null) {
return deviceSyncMessage;
}
if (this.listMessage != null) {
return listMessage;
}
if (this.viewOnceMessage != null) {
return viewOnceMessage.value().content();
}
if (this.orderMessage != null) {
return orderMessage;
}
if (this.listResponseMessage != null) {
return listResponseMessage;
}
if (this.ephemeralMessage != null) {
return ephemeralMessage.value().content();
}
if (this.invoiceMessage != null) {
return invoiceMessage;
}
if (this.buttonsMessage != null) {
return buttonsMessage;
}
if (this.buttonsResponseMessage != null) {
return buttonsResponseMessage;
}
if (this.paymentInviteMessage != null) {
return paymentInviteMessage;
}
if (interactiveMessage != null) {
return interactiveMessage;
}
if (reactionMessage != null) {
return reactionMessage;
}
if (stickerSyncMessage != null) {
return stickerSyncMessage;
}
if (interactiveResponseMessage != null) {
return interactiveResponseMessage;
}
if (pollCreationMessage != null) {
return pollCreationMessage;
}
if (pollUpdateMessage != null) {
return pollUpdateMessage;
}
if (keepInChatMessage != null) {
return keepInChatMessage;
}
if (documentWithCaptionMessage != null) {
return documentWithCaptionMessage.value().content();
}
if (requestPhoneNumberMessage != null) {
return requestPhoneNumberMessage;
}
if (viewOnceV2Message != null) {
return viewOnceV2Message.value.content();
}
if (encryptedReactionMessage != null) {
return encryptedReactionMessage;
}
if (editedMessage != null) {
return editedMessage.value().content();
}
if (viewOnceV2ExtensionMessage != null) {
return viewOnceV2ExtensionMessage.value().content();
}
if (callMessage != null) {
return callMessage;
}
if (newsletterAdminInviteMessage != null) {
return newsletterAdminInviteMessage;
}
// This needs to be last
if (this.senderKeyDistributionMessage != null) {
return senderKeyDistributionMessage;
}
return EMPTY_MESSAGE;
}
/**
* Returns the first populated contextual message inside this container
*
* @return a non-null Optional ContextualMessage
*/
public Optional<ContextualMessage> contentWithContext() {
return Optional.of(content())
.filter(entry -> entry instanceof ContextualMessage)
.map(entry -> (ContextualMessage) entry);
}
/**
* Checks whether the message that this container wraps matches the provided type
*
* @param type the non-null type to check against
* @return a boolean
*/
public boolean hasType(Message.Type type) {
return content().type() == type;
}
/**
* Checks whether the message that this container wraps matches the provided category
*
* @param category the non-null category to check against
* @return a boolean
*/
public boolean hasCategory(Message.Category category) {
return content().category() == category;
}
/**
* Returns the type of the message
*
* @return a non-null type
*/
public Message.Type type() {
if (textWithNoContextMessage != null) {
return Message.Type.TEXT;
}
if (ephemeralMessage != null) {
return Message.Type.EPHEMERAL;
}
if (viewOnceMessage != null || viewOnceV2Message != null || viewOnceV2ExtensionMessage != null) {
return Message.Type.VIEW_ONCE;
}
if (editedMessage != null) {
return Message.Type.EDITED;
}
return content().type();
}
/**
* Returns the deep type of the message unwrapping ephemeral and view once messages
*
* @return a non-null type
*/
public Message.Type deepType() {
return content().type();
}
/**
* Returns the category of the message
*
* @return a non-null category
*/
public Message.Category category() {
return content().category();
}
/**
* Converts this message to an ephemeral message
*
* @return a non-null message container
*/
public MessageContainer toEphemeral() {
if (type() == Message.Type.EPHEMERAL) {
return this;
}
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(content()))
.build();
return new MessageContainerBuilder()
.ephemeralMessage(futureMessageContainer)
.deviceInfo(deviceInfo)
.build();
}
/**
* Converts this message to a view once message
*
* @return a non-null message container
*/
public MessageContainer toViewOnce() {
if (type() == Message.Type.VIEW_ONCE) {
return this;
}
var futureMessageContainer = new FutureMessageContainerBuilder()
.value(MessageContainer.of(content()))
.build();
return new MessageContainerBuilder()
.viewOnceMessage(futureMessageContainer)
.deviceInfo(deviceInfo)
.build();
}
/**
* Returns an unboxed message where are all future-proof messages(i.e. ephemeral and view once)
* have been unboxed
*
* @return a non-null message container
*/
public MessageContainer unbox() {
if (deviceSentMessage != null) {
return deviceSentMessage.message();
}
if (viewOnceMessage != null) {
return viewOnceMessage.value();
}
if (ephemeralMessage != null) {
return ephemeralMessage.value();
}
if (documentWithCaptionMessage != null) {
return documentWithCaptionMessage.value();
}
if (viewOnceV2Message != null) {
return viewOnceV2Message.value();
}
if (editedMessage != null) {
return editedMessage.value();
}
if (viewOnceV2ExtensionMessage != null) {
return viewOnceV2ExtensionMessage.value();
}
return this;
}
/**
* Returns a copy of this container with a different device info
*
* @return a non-null message container
*/
public MessageContainer withDeviceInfo(DeviceContextInfo deviceInfo) {
if (deviceSentMessage != null) {
return ofBuilder(deviceSentMessage)
.deviceInfo(deviceInfo)
.build();
}
if (viewOnceMessage != null) {
return new MessageContainerBuilder()
.viewOnceMessage(viewOnceMessage)
.deviceInfo(deviceInfo)
.build();
}
if (ephemeralMessage != null) {
return new MessageContainerBuilder()
.ephemeralMessage(ephemeralMessage)
.deviceInfo(deviceInfo)
.build();
}
if (documentWithCaptionMessage != null) {
return new MessageContainerBuilder()
.documentWithCaptionMessage(documentWithCap
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment