Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
package it.auties.whatsapp.api;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.ChecksumException;
import com.google.zxing.FormatException;
import com.google.zxing.NotFoundException;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import it.auties.curve25519.Curve25519;
import it.auties.linkpreview.LinkPreview;
import it.auties.linkpreview.LinkPreviewMedia;
import it.auties.linkpreview.LinkPreviewResult;
import it.auties.whatsapp.binary.BinaryPatchType;
import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.controller.Store;
import it.auties.whatsapp.crypto.AesGmc;
import it.auties.whatsapp.crypto.Hkdf;
import it.auties.whatsapp.crypto.Hmac;
import it.auties.whatsapp.crypto.Sha256;
import it.auties.whatsapp.listener.*;
import it.auties.whatsapp.model.action.*;
import it.auties.whatsapp.model.business.*;
import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplate;
import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplate;
import it.auties.whatsapp.model.chat.*;
import it.auties.whatsapp.model.chat.PastParticipant.LeaveReason;
import it.auties.whatsapp.model.companion.CompanionLinkResult;
import it.auties.whatsapp.model.contact.Contact;
import it.auties.whatsapp.model.contact.ContactJid;
import it.auties.whatsapp.model.contact.ContactJid.Server;
import it.auties.whatsapp.model.contact.ContactJidProvider;
import it.auties.whatsapp.model.contact.ContactStatus;
import it.auties.whatsapp.model.info.ContextInfo;
import it.auties.whatsapp.model.info.MessageInfo;
import it.auties.whatsapp.model.media.AttachmentProvider;
import it.auties.whatsapp.model.media.AttachmentType;
import it.auties.whatsapp.model.media.MediaFile;
import it.auties.whatsapp.model.message.button.ButtonsMessage;
import it.auties.whatsapp.model.message.button.InteractiveMessage;
import it.auties.whatsapp.model.message.button.TemplateMessage;
import it.auties.whatsapp.model.message.model.*;
import it.auties.whatsapp.model.message.server.ProtocolMessage;
import it.auties.whatsapp.model.message.server.ProtocolMessage.ProtocolMessageType;
import it.auties.whatsapp.model.message.standard.*;
import it.auties.whatsapp.model.message.standard.TextMessage.TextMessagePreviewType;
import it.auties.whatsapp.model.poll.PollAdditionalMetadata;
import it.auties.whatsapp.model.poll.PollUpdateEncryptedMetadata;
import it.auties.whatsapp.model.poll.PollUpdateEncryptedOptions;
import it.auties.whatsapp.model.privacy.GdprAccountReport;
import it.auties.whatsapp.model.privacy.PrivacySettingEntry;
import it.auties.whatsapp.model.privacy.PrivacySettingType;
import it.auties.whatsapp.model.privacy.PrivacySettingValue;
import it.auties.whatsapp.model.request.*;
import it.auties.whatsapp.model.response.ContactStatusResponse;
import it.auties.whatsapp.model.response.HasWhatsappResponse;
import it.auties.whatsapp.model.response.MexQueryResult;
import it.auties.whatsapp.model.setting.LocaleSetting;
import it.auties.whatsapp.model.setting.PushNameSetting;
import it.auties.whatsapp.model.signal.auth.*;
import it.auties.whatsapp.model.signal.auth.UserAgent.UserAgentPlatform;
import it.auties.whatsapp.model.signal.keypair.SignalKeyPair;
import it.auties.whatsapp.model.sync.*;
import it.auties.whatsapp.model.sync.HistorySyncNotification.Type;
import it.auties.whatsapp.model.sync.PatchRequest.PatchEntry;
import it.auties.whatsapp.model.sync.RecordSync.Operation;
import it.auties.whatsapp.socket.SocketHandler;
import it.auties.whatsapp.socket.SocketState;
import it.auties.whatsapp.util.*;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.experimental.Accessors;
import javax.imageio.ImageIO;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* A class used to interface a user to WhatsappWeb's WebSocket
*/
@Data
@Accessors(fluent = true)
@SuppressWarnings({"unused", "UnusedReturnValue"})
public class Whatsapp {
// The instances are added and removed when the client connects/disconnects
// This is to make sure that the instances remain in memory only as long as it's needed
private static final Map instances = new ConcurrentHashMap<>();
private final SocketHandler socketHandler;
/**
* Checks if a connection exists
*
* @param uuid the non-null uuid
* @return a boolean
*/
public static boolean isConnected(@NonNull UUID uuid) {
return SocketHandler.isConnected(uuid);
}
/**
* Checks if a connection exists
*
* @param phoneNumber the non-null phone number
* @return a boolean
*/
public static boolean isConnected(long phoneNumber) {
return SocketHandler.isConnected(phoneNumber);
}
/**
* Checks if a connection exists
*
* @param alias the non-null alias
* @return a boolean
*/
public static boolean isConnected(String alias) {
return SocketHandler.isConnected(alias);
}
/**
* Advanced builder if you need more customization
*/
@Builder(builderMethodName = "customBuilder")
private static Whatsapp builder(@NonNull Store store, @NonNull Keys keys, ErrorHandler errorHandler, WebVerificationSupport webVerificationSupport, Executor socketExecutor){
Validate.isTrue(Objects.equals(store.uuid(), keys.uuid()),
"UUIDs for store and keys don't match: %s != %s", store.uuid(), keys.uuid());
var knownInstance = instances.get(store.uuid());
if(knownInstance != null){
return knownInstance;
}
var checkedSupport = getWebVerificationMethod(store, keys, webVerificationSupport);
var result = new Whatsapp(store, keys, errorHandler, checkedSupport, socketExecutor);
result.addDisconnectedListener(reason -> instances.remove(store.uuid()));
return result;
}
private static WebVerificationSupport getWebVerificationMethod(Store store, Keys keys, WebVerificationSupport webVerificationSupport) {
if(store.clientType() != ClientType.WEB){
return null;
}
if(!keys.registered() && webVerificationSupport == null) {
return QrHandler.toTerminal();
}
return webVerificationSupport;
}
private Whatsapp(@NonNull Store store, @NonNull Keys keys, ErrorHandler errorHandler, WebVerificationSupport webVerificationSupport, Executor socketExecutor) {
this.socketHandler = new SocketHandler(this, store, keys, errorHandler, webVerificationSupport, socketExecutor);
if(store.autodetectListeners()){
return;
}
store().addListeners(ListenerScanner.scan(this));
}
/**
* Creates a new web api
* The web api is based around the WhatsappWeb client
*
* @return a web api builder
*/
public static ConnectionBuilder webBuilder(){
return new ConnectionBuilder<>(ClientType.WEB);
}
/**
* Creates a new mobile api
* The mobile api is based around the Whatsapp App available on IOS and Android
*
* @return a web mobile builder
*/
public static ConnectionBuilder mobileBuilder(){
return new ConnectionBuilder<>(ClientType.MOBILE);
}
/**
* Connects to Whatsapp
*
* @return a future
*/
public synchronized CompletableFuture connect(){
return socketHandler.connect()
.thenRunAsync(() -> instances.put(store().uuid(), this))
.thenApply(ignored -> this);
}
/**
* Connects to Whatsapp
*
* @return a future
*/
public synchronized CompletableFuture connectAwaitingLogout(){
return socketHandler.connect()
.thenRunAsync(() -> instances.put(store().uuid(), this))
.thenCompose(ignored -> socketHandler.logoutFuture());
}
/**
* Returns whether the connection is active or not
*
* @return a boolean
*/
public boolean isConnected(){
return socketHandler.state() == SocketState.CONNECTED;
}
/**
* Returns the keys associated with this session
*
* @return a non-null WhatsappKeys
*/
public Keys keys() {
return socketHandler.keys();
}
/**
* Returns the store associated with this session
*
* @return a non-null WhatsappStore
*/
public Store store() {
return socketHandler.store();
}
/**
* Disconnects from Whatsapp Web's WebSocket if a previous connection exists
*
* @return a future
*/
public synchronized CompletableFuture disconnect() {
return socketHandler.disconnect(DisconnectReason.DISCONNECTED);
}
/**
* Waits for this connection to close
*/
public void awaitDisconnection() {
socketHandler.logoutFuture().join();
}
/**
* Disconnects and reconnects to Whatsapp Web's WebSocket if a previous connection exists
*
* @return a future
*/
public CompletableFuture reconnect() {
return socketHandler.disconnect(DisconnectReason.RECONNECTING);
}
/**
* Disconnects from Whatsapp Web's WebSocket and logs out of WhatsappWeb invalidating the previous
* saved credentials. The next time the API is used, the QR code will need to be scanned again.
*
* @return a future
*/
public CompletableFuture logout() {
if (store().jid() == null) {
return socketHandler.disconnect(DisconnectReason.LOGGED_OUT);
}
var metadata = Map.of("jid", store().jid(), "reason", "user_initiated");
var device = Node.of("remove-companion-device", metadata);
return socketHandler.sendQuery("set", "md", device)
.thenRun(() -> {});
}
/**
* Changes a privacy setting in Whatsapp's settings. If the value is
* {@link PrivacySettingValue#CONTACTS_EXCEPT}, the excluded parameter should also be filled or an
* exception will be thrown, otherwise it will be ignored.
*
* @param type the non-null setting to change
* @param value the non-null value to attribute to the setting
* @param excluded the non-null excluded contacts if value is {@link PrivacySettingValue#CONTACTS_EXCEPT}
* @return the same instance wrapped in a completable future
*/
@SafeVarargs
public final CompletableFuture changePrivacySetting(@NonNull PrivacySettingType type, @NonNull PrivacySettingValue value, @NonNull T @NonNull ... excluded) {
Validate.isTrue(type.isSupported(value),
"Cannot change setting %s to %s: this toggle cannot be used because Whatsapp doesn't support it", value.name(), type.name());
var attributes = Attributes.of()
.put("name", type.data())
.put("value", value.data())
.put("dhash", "none", () -> value == PrivacySettingValue.CONTACTS_EXCEPT)
.toMap();
var excludedJids = Arrays.stream(excluded).map(ContactJidProvider::toJid).toList();
var children = value != PrivacySettingValue.CONTACTS_EXCEPT ? null : excludedJids.stream()
.map(entry -> Node.of("user", Map.of("jid", entry, "action", "add")))
.toList();
return socketHandler.sendQuery("set", "privacy", Node.of("privacy", Node.of("category", attributes, children)))
.thenRunAsync(() -> onPrivacyFeatureChanged(type, value, excludedJids))
.thenApply(ignored -> this);
}
private void onPrivacyFeatureChanged(PrivacySettingType type, PrivacySettingValue value, List excludedJids) {
var newEntry = new PrivacySettingEntry(type, value, excludedJids);
var oldEntry = store().findPrivacySetting(type);
store().addPrivacySetting(type, newEntry);
socketHandler.onPrivacySettingChanged(oldEntry, newEntry);
}
/**
* Changes the default ephemeral timer of new chats.
*
* @param timer the new ephemeral timer
* @return the same instance wrapped in a completable future
*/
public CompletableFuture changeNewChatsEphemeralTimer(@NonNull ChatEphemeralTimer timer) {
return socketHandler.sendQuery("set", "disappearing_mode", Node.of("disappearing_mode", Map.of("duration", timer.period()
.toSeconds())))
.thenRunAsync(() -> store().newChatsEphemeralTimer(timer))
.thenApply(ignored -> this);
}
/**
* Creates a new request to get a document containing all the data that was collected by Whatsapp
* about this user. It takes three business days to receive it. To query the result status, use
* {@link Whatsapp#getGdprAccountInfoStatus()}
*
* @return the same instance wrapped in a completable future
*/
public CompletableFuture createGdprAccountInfo() {
return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("gdpr", Map.of("gdpr", "request")))
.thenApply(ignored -> this);
}
/**
* Queries the document containing all the data that was collected by Whatsapp about this user. To
* create a request for this document, use {@link Whatsapp#createGdprAccountInfo()}
*
* @return the same instance wrapped in a completable future
*/
// TODO: Implement ready and error states
public CompletableFuture getGdprAccountInfoStatus() {
return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("gdpr", Map.of("gdpr", "status")))
.thenApplyAsync(result -> GdprAccountReport.ofPending(result.attributes().getLong("timestamp")));
}
/**
* Changes the name of this user
*
* @param newName the non-null new name
* @return the same instance wrapped in a completable future
*/
public CompletableFuture changeName(@NonNull String newName) {
var oldName = store().name();
return socketHandler.send(Node.of("presence", Map.of("name", newName)))
.thenRunAsync(() -> socketHandler.updateUserName(newName, oldName))
.thenApply(ignored -> this);
}
/**
* Changes the status(i.e. user description) of this user
*
* @param newStatus the non-null new status
* @return the same instance wrapped in a completable future
*/
public CompletableFuture changeStatus(@NonNull String newStatus) {
return socketHandler.sendQuery("set", "status", Node.of("status", newStatus.getBytes(StandardCharsets.UTF_8)))
.thenRunAsync(() -> store().name(newStatus))
.thenApply(ignored -> this);
}
/**
* Sends a request to Whatsapp in order to receive updates when the status of a contact changes.
* These changes include the last known presence and the seconds the contact was last seen.
*
* @param jid the contact whose status the api should receive updates on
* @return a CompletableFuture
*/
public CompletableFuture subscribeToPresence(@NonNull T jid) {
return socketHandler.subscribeToPresence(jid).thenApplyAsync(ignored -> jid);
}
/**
* Remove a reaction from a message
*
* @param message the non-null message
* @return a CompletableFuture
*/
public CompletableFuture removeReaction(@NonNull MessageMetadataProvider message) {
return sendReaction(message, (String) null);
}
/**
* Send a reaction to a message
*
* @param message the non-null message
* @param reaction the reaction to send, null if you want to remove the reaction. If a string that
* isn't an emoji supported by Whatsapp is used, it will not get displayed
* correctly. Use {@link Whatsapp#sendReaction(MessageMetadataProvider, Emoji)} if
* you need a typed emoji enum.
* @return a CompletableFuture
*/
public CompletableFuture sendReaction(@NonNull MessageMetadataProvider message, String reaction) {
var key = MessageKey.builder()
.chatJid(message.chat().jid())
.senderJid(message.senderJid())
.fromMe(Objects.equals(message.senderJid().toWhatsappJid(), store().jid().toWhatsappJid()))
.id(message.id())
.build();
var reactionMessage = ReactionMessage.builder()
.key(key)
.content(reaction)
.timestamp(Instant.now().toEpochMilli())
.build();
return sendMessage(message.chat(), reactionMessage);
}
/**
* Builds and sends a message from a chat and a message
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull Message message) {
return sendMessage(chat, MessageContainer.of(message));
}
/**
* Builds and sends a message from a chat and a message
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull MessageContainer message) {
var key = MessageKey.builder()
.chatJid(chat.toJid())
.fromMe(true)
.senderJid(store().jid())
.build();
var info = MessageInfo.builder()
.senderJid(store().jid())
.key(key)
.message(message)
.timestampSeconds(Clock.nowSeconds())
.broadcast(chat.toJid().hasServer(Server.BROADCAST))
.build();
return sendMessage(info);
}
/**
* Sends a message info to a chat
*
* @param info the info to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull MessageInfo info) {
return sendMessage(MessageSendRequest.of(info));
}
/**
* Sends a message info to a chat
*
* @param request the request to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull MessageSendRequest request) {
store().attribute(request.info());
return attributeMessageMetadata(request.info())
.thenComposeAsync(ignored -> socketHandler.sendMessage(request))
.thenApplyAsync(ignored -> request.info());
}
private CompletableFuture attributeMessageMetadata(MessageInfo info) {
info.key().chatJid(info.chatJid().toWhatsappJid());
info.key().senderJid(info.senderJid() == null ? null : info.senderJid().toWhatsappJid());
fixEphemeralMessage(info);
var content = info.message().content();
if (content instanceof MediaMessage mediaMessage) {
return attributeMediaMessage(mediaMessage);
} else if (content instanceof ButtonMessage buttonMessage) {
return attributeButtonMessage(info, buttonMessage);
} else if (content instanceof TextMessage textMessage) {
attributeTextMessage(textMessage);
} else if (content instanceof PollCreationMessage pollCreationMessage) {
attributePollCreationMessage(info, pollCreationMessage);
} else if (content instanceof PollUpdateMessage pollUpdateMessage) {
attributePollUpdateMessage(info, pollUpdateMessage);
} else if (content instanceof GroupInviteMessage groupInviteMessage) {
attributeGroupInviteMessage(info, groupInviteMessage);
}
return CompletableFuture.completedFuture(null);
}
/**
* Marks a chat as read.
*
* @param chat the target chat
* @return a CompletableFuture
*/
public CompletableFuture markRead(@NonNull T chat) {
return mark(chat, true).thenComposeAsync(ignored -> markAllAsRead(chat)).thenApplyAsync(ignored -> chat);
}
private void fixEphemeralMessage(MessageInfo info) {
if (info.message().hasCategory(MessageCategory.SERVER)) {
return;
}
if (info.chat().isEphemeral()) {
info.message()
.contentWithContext()
.map(ContextualMessage::contextInfo)
.ifPresent(contextInfo -> createEphemeralContext(info.chat(), contextInfo));
info.message(info.message().toEphemeral());
return;
}
if (info.message().type() != MessageType.EPHEMERAL) {
return;
}
info.message(info.message().unbox());
}
private void attributeTextMessage(TextMessage textMessage) {
if (store().textPreviewSetting() == TextPreviewSetting.DISABLED) {
return;
}
var match = LinkPreview.createPreview(textMessage.text()).orElse(null);
if (match == null) {
return;
}
var uri = match.result().uri().toString();
if (store().textPreviewSetting() == TextPreviewSetting.ENABLED_WITH_INFERENCE && !match.text()
.equals(uri)) {
textMessage.text(textMessage.text().replace(match.text(), uri));
}
var imageUri = match.result()
.images()
.stream()
.reduce(this::compareDimensions)
.map(LinkPreviewMedia::uri)
.orElse(null);
var videoUri = match.result()
.videos()
.stream()
.reduce(this::compareDimensions)
.map(LinkPreviewMedia::uri)
.orElse(null);
textMessage.matchedText(uri);
textMessage.canonicalUrl(Objects.requireNonNullElse(videoUri, match.result().uri()).toString());
textMessage.thumbnail(Medias.download(imageUri).orElse(null));
textMessage.description(match.result().siteDescription());
textMessage.title(match.result().title());
textMessage.previewType(videoUri != null ? TextMessagePreviewType.VIDEO : TextMessagePreviewType.NONE);
}
private CompletableFuture attributeMediaMessage(MediaMessage mediaMessage) {
return Medias.upload(mediaMessage.decodedMedia().orElseThrow(), mediaMessage.mediaType().toAttachmentType(), store().mediaConnection())
.thenAccept(upload -> attributeMediaMessage(mediaMessage, upload));
}
private AttachmentProvider attributeMediaMessage(MediaMessage mediaMessage, MediaFile upload) {
return mediaMessage.mediaSha256(upload.fileSha256())
.mediaEncryptedSha256(upload.fileEncSha256())
.mediaKey(upload.mediaKey())
.mediaUrl(upload.url())
.mediaDirectPath(upload.directPath())
.mediaSize(upload.fileLength());
}
private void attributePollCreationMessage(MessageInfo info, PollCreationMessage pollCreationMessage) {
var pollEncryptionKey = Objects.requireNonNullElseGet(pollCreationMessage.encryptionKey(), KeyHelper::senderKey);
pollCreationMessage.encryptionKey(pollEncryptionKey);
info.messageSecret(pollEncryptionKey);
info.message().deviceInfo().messageSecret(pollEncryptionKey);
var metadata = new PollAdditionalMetadata(false);
info.pollAdditionalMetadata(metadata);
}
private void attributePollUpdateMessage(MessageInfo info, PollUpdateMessage pollUpdateMessage) {
if (pollUpdateMessage.encryptedMetadata() != null) {
return;
}
var iv = BytesHelper.random(12);
var additionalData = "%s\0%s".formatted(pollUpdateMessage.pollCreationMessageKey().id(), store().jid().toWhatsappJid());
var encryptedOptions = pollUpdateMessage.votes().stream().map(entry -> Sha256.calculate(entry.name())).toList();
var pollUpdateEncryptedOptions = Protobuf.writeMessage(new PollUpdateEncryptedOptions(encryptedOptions));
var originalPollInfo = store()
.findMessageByKey(pollUpdateMessage.pollCreationMessageKey())
.orElseThrow(() -> new NoSuchElementException("Missing original poll message"));
var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content();
var originalPollSender = originalPollInfo.senderJid().toWhatsappJid().toString().getBytes(StandardCharsets.UTF_8);
var modificationSenderJid = info.senderJid().toWhatsappJid();
pollUpdateMessage.voter(modificationSenderJid);
var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8);
var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8);
var useSecretPayload = BytesHelper.concat(
pollUpdateMessage.pollCreationMessageKey().id().getBytes(StandardCharsets.UTF_8),
originalPollSender,
modificationSender,
secretName
);
var useCaseSecret = Hkdf.extractAndExpand(originalPollMessage.encryptionKey(), useSecretPayload, 32);
var pollUpdateEncryptedPayload = AesGmc.encrypt(iv, pollUpdateEncryptedOptions, useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8));
var pollUpdateEncryptedMetadata = new PollUpdateEncryptedMetadata(pollUpdateEncryptedPayload, iv);
pollUpdateMessage.encryptedMetadata(pollUpdateEncryptedMetadata);
}
private CompletableFuture attributeButtonMessage(MessageInfo info, ButtonMessage buttonMessage) {
if (buttonMessage instanceof ButtonsMessage buttonsMessage
&& buttonsMessage.header().isPresent()
&& buttonsMessage.header().get() instanceof MediaMessage mediaMessage) {
return attributeMediaMessage(mediaMessage);
} else if (buttonMessage instanceof TemplateMessage templateMessage && templateMessage.format().isPresent()) {
var templateFormatter = templateMessage.format().get();
if (templateFormatter instanceof HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate
&& highlyStructuredFourRowTemplate.title().isPresent()
&& highlyStructuredFourRowTemplate.title().get() instanceof MediaMessage mediaMessage) {
return attributeMediaMessage(mediaMessage);
} else if (templateFormatter instanceof HydratedFourRowTemplate hydratedFourRowTemplate
&& hydratedFourRowTemplate.title().isPresent()
&& hydratedFourRowTemplate.title().get() instanceof MediaMessage mediaMessage) {
return attributeMediaMessage(mediaMessage);
}else {
return CompletableFuture.completedFuture(null);
}
} else if (buttonMessage instanceof InteractiveMessage interactiveMessage
&& interactiveMessage.header().isPresent()
&& interactiveMessage.header().get().attachment().isPresent()
&& interactiveMessage.header().get().attachment().get() instanceof MediaMessage mediaMessage) {
return attributeMediaMessage(mediaMessage);
} else {
return CompletableFuture.completedFuture(null);
}
}
// This is not needed probably, but Whatsapp uses a text message by default, so maybe it makes sense
private void attributeGroupInviteMessage(MessageInfo info, GroupInviteMessage groupInviteMessage) {
Validate.isTrue(groupInviteMessage.code() != null, "Invalid message code");
var url = "https://chat.whatsapp.com/%s".formatted(groupInviteMessage.code());
var preview = LinkPreview.createPreview(URI.create(url))
.stream()
.map(LinkPreviewResult::images)
.map(Collection::stream)
.map(Stream::findFirst)
.flatMap(Optional::stream)
.findFirst()
.map(LinkPreviewMedia::uri)
.orElse(null);
var replacement = TextMessage.builder()
.text(groupInviteMessage.caption() != null ? "%s: %s".formatted(groupInviteMessage.caption(), url) : url)
.description("WhatsApp Group Invite")
.title(groupInviteMessage.groupName())
.previewType(TextMessagePreviewType.NONE)
.thumbnail(Medias.download(preview).orElse(null))
.matchedText(url)
.canonicalUrl(url)
.build();
info.message(MessageContainer.of(replacement));
}
private CompletableFuture mark(@NonNull T chat, boolean read) {
if(store().clientType() == ClientType.MOBILE){
// TODO: Send notification to companions
store().findChatByJid(chat.toJid())
.ifPresent(entry -> entry.markedAsUnread(read));
return CompletableFuture.completedFuture(chat);
}
var range = createRange(chat, false);
var markAction = new MarkChatAsReadAction(read, range);
var syncAction = ActionValueSync.of(markAction);
var entry = PatchEntry.of(syncAction, Operation.SET, 3, chat.toJid().toString());
var request = new PatchRequest(BinaryPatchType.REGULAR_HIGH, List.of(entry));
return socketHandler.pushPatch(request).thenApplyAsync(ignored -> chat);
}
private CompletableFuture markAllAsRead(ContactJidProvider chat) {
var all = store()
.findChatByJid(chat.toJid())
.stream()
.map(Chat::unreadMessages)
.flatMap(Collection::stream)
.map(this::markRead)
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(all);
}
private LinkPreviewMedia compareDimensions(LinkPreviewMedia first, LinkPreviewMedia second) {
return first.width() * first.height() > second.width() * second.height() ? first : second;
}
private ActionMessageRangeSync createRange(ContactJidProvider chat, boolean allMessages) {
var known = store().findChatByJid(chat.toJid()).orElseGet(() -> store().addNewChat(chat.toJid()));
return new ActionMessageRangeSync(known, allMessages);
}
/**
* Marks a message as read
*
* @param info the target message
* @return a CompletableFuture
*/
public CompletableFuture markRead(@NonNull MessageInfo info) {
var type = store().findPrivacySetting(PrivacySettingType.READ_RECEIPTS)
.value() == PrivacySettingValue.EVERYONE ? "read" : "read-self";
socketHandler.sendReceipt(info.chatJid(), info.senderJid(), List.of(info.id()), type);
var count = info.chat().unreadMessagesCount();
if (count > 0) {
info.chat().unreadMessagesCount(count - 1);
}
return CompletableFuture.completedFuture(info.status(MessageStatus.READ));
}
private void createEphemeralContext(Chat chat, ContextInfo contextInfo) {
var period = chat.ephemeralMessageDuration().period().toSeconds();
contextInfo.ephemeralExpiration((int) period);
}
/**
* Send a reaction to a message
*
* @param message the non-null message
* @param reaction the reaction to send, null if you want to remove the reaction
* @return a CompletableFuture
*/
public CompletableFuture sendReaction(@NonNull MessageMetadataProvider message, Emoji reaction) {
return sendReaction(message, Objects.toString(reaction));
}
/**
* Builds and sends a message from a chat and a message
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull String message) {
return sendMessage(chat, MessageContainer.of(message));
}
/**
* Builds and sends a message from a chat, a message and a quoted message
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @param quotedMessage the quoted message
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull String message, @NonNull MessageMetadataProvider quotedMessage) {
return sendMessage(chat, TextMessage.of(message), quotedMessage);
}
/**
* Builds and sends a message from a chat, a message and a quoted message
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @param quotedMessage the quoted message
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull ContextualMessage message, @NonNull MessageMetadataProvider quotedMessage) {
Validate.isTrue(!quotedMessage.message().isEmpty(), "Cannot quote an empty message");
Validate.isTrue(!quotedMessage.message().hasCategory(MessageCategory.SERVER), "Cannot quote a server message");
return sendMessage(chat, message, ContextInfo.of(quotedMessage));
}
/**
* Builds and sends a message from a chat, a message and a context
*
* @param chat the chat where the message should be sent
* @param message the message to send
* @param contextInfo the context of the message to send
* @return a CompletableFuture
*/
public CompletableFuture sendMessage(@NonNull ContactJidProvider chat, @NonNull ContextualMessage message, @NonNull ContextInfo contextInfo) {
message.contextInfo(contextInfo);
return sendMessage(chat, message);
}
/**
* Awaits for a single response to a message
*
* @param info the non-null message whose response is pending
* @return a non-null result
*/
public CompletableFuture awaitReply(@NonNull MessageInfo info) {
return awaitReply(info.id());
}
/**
* Awaits for a single response to a message
*
* @param id the non-null id of message whose response is pending
* @return a non-null result
*/
public CompletableFuture awaitReply(@NonNull String id) {
return store().addPendingReply(ReplyHandler.of(id));
}
/**
* Executes a query to determine whether a user has an account on Whatsapp
*
* @param contact the contact to check
* @return a CompletableFuture that wraps a non-null response
*/
public CompletableFuture hasWhatsapp(@NonNull ContactJidProvider contact) {
return hasWhatsapp(new ContactJidProvider[]{contact}).thenApply(result -> result.get(contact.toJid()));
}
/**
* Executes a query to determine whether any number of users have an account on Whatsapp
*
* @param contacts the contacts to check
* @return a CompletableFuture that wraps a non-null map
*/
public CompletableFuture