Please wait. This can take some minutes ...
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.
it.auties.whatsapp.implementation.MessageHandler Maven / Gradle / Ivy
package it.auties.whatsapp.implementation;
import it.auties.linkpreview.LinkPreview;
import it.auties.linkpreview.LinkPreviewMatch;
import it.auties.linkpreview.LinkPreviewMedia;
import it.auties.whatsapp.api.TextPreviewSetting;
import it.auties.whatsapp.crypto.*;
import it.auties.whatsapp.model.action.ContactAction;
import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec;
import it.auties.whatsapp.model.button.template.hsm.HighlyStructuredFourRowTemplate;
import it.auties.whatsapp.model.button.template.hydrated.HydratedFourRowTemplate;
import it.auties.whatsapp.model.chat.Chat;
import it.auties.whatsapp.model.chat.ChatEphemeralTimer;
import it.auties.whatsapp.model.chat.ChatMetadata;
import it.auties.whatsapp.model.chat.ChatParticipant;
import it.auties.whatsapp.model.contact.Contact;
import it.auties.whatsapp.model.contact.ContactStatus;
import it.auties.whatsapp.model.info.*;
import it.auties.whatsapp.model.jid.Jid;
import it.auties.whatsapp.model.jid.JidProvider;
import it.auties.whatsapp.model.jid.JidServer;
import it.auties.whatsapp.model.jid.JidType;
import it.auties.whatsapp.model.media.AttachmentType;
import it.auties.whatsapp.model.media.MediaFile;
import it.auties.whatsapp.model.media.MutableAttachmentProvider;
import it.auties.whatsapp.model.message.button.*;
import it.auties.whatsapp.model.message.model.*;
import it.auties.whatsapp.model.message.payment.PaymentOrderMessage;
import it.auties.whatsapp.model.message.server.DeviceSentMessage;
import it.auties.whatsapp.model.message.server.ProtocolMessage;
import it.auties.whatsapp.model.message.server.SenderKeyDistributionMessage;
import it.auties.whatsapp.model.message.standard.*;
import it.auties.whatsapp.model.newsletter.NewsletterReaction;
import it.auties.whatsapp.model.node.Attributes;
import it.auties.whatsapp.model.node.Node;
import it.auties.whatsapp.model.poll.*;
import it.auties.whatsapp.model.request.MessageSendRequest;
import it.auties.whatsapp.model.setting.EphemeralSettings;
import it.auties.whatsapp.model.signal.auth.SignedDeviceIdentitySpec;
import it.auties.whatsapp.model.signal.keypair.ISignalKeyPair;
import it.auties.whatsapp.model.signal.keypair.SignalSignedKeyPair;
import it.auties.whatsapp.model.signal.message.SignalDistributionMessage;
import it.auties.whatsapp.model.signal.message.SignalMessage;
import it.auties.whatsapp.model.signal.message.SignalPreKeyMessage;
import it.auties.whatsapp.model.signal.sender.SenderKeyName;
import it.auties.whatsapp.model.sync.HistorySync;
import it.auties.whatsapp.model.sync.HistorySync.Type;
import it.auties.whatsapp.model.sync.HistorySyncNotification;
import it.auties.whatsapp.model.sync.HistorySyncSpec;
import it.auties.whatsapp.model.sync.PushName;
import it.auties.whatsapp.util.Bytes;
import it.auties.whatsapp.util.Clock;
import it.auties.whatsapp.util.Medias;
import it.auties.whatsapp.util.Validate;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static it.auties.whatsapp.api.ErrorHandler.Location.*;
import static it.auties.whatsapp.util.SignalConstants.*;
class MessageHandler {
private static final int HISTORY_SYNC_TIMEOUT = 25;
private static final int MAX_MESSAGE_RETRIES = 5;
private static final Set BASE_HISTORY_SYNC_TYPES = Set.of(Type.INITIAL_STATUS_V3, Type.PUSH_NAME, Type.INITIAL_BOOTSTRAP, Type.NON_BLOCKING_DATA);
private final SocketHandler socketHandler;
private final Map> devicesCache;
private final Map retries;
private final Set historyCache;
private final EnumSet historySyncTypes;
private final ReentrantLock lock;
private final AtomicBoolean startedInitialAppSync;
private final HistorySyncProgressTracker recentHistorySyncTracker;
private final HistorySyncProgressTracker fullHistorySyncTracker;
private ScheduledFuture> historySyncTask;
protected MessageHandler(SocketHandler socketHandler) {
this.socketHandler = socketHandler;
this.devicesCache = new ConcurrentHashMap<>();
this.historyCache = ConcurrentHashMap.newKeySet();
this.historySyncTypes = EnumSet.noneOf(Type.class);
this.lock = new ReentrantLock(true);
this.startedInitialAppSync = new AtomicBoolean(false);
this.retries = new ConcurrentHashMap<>();
this.recentHistorySyncTracker = new HistorySyncProgressTracker();
this.fullHistorySyncTracker = new HistorySyncProgressTracker();
}
protected CompletableFuture encode(MessageSendRequest request) {
return switch (request) {
case MessageSendRequest.Chat chatRequest -> encodeChatMessage(chatRequest);
case MessageSendRequest.Newsletter newsletterRequest -> encodeNewsletterMessage(newsletterRequest);
};
}
private CompletableFuture encodeChatMessage(MessageSendRequest.Chat request) {
return prepareOutgoingChatMessage(request.info())
.thenComposeAsync(ignored -> {
try {
lock.lock();
if (request.peer() || isConversation(request.info())) {
return encodeConversation(request);
}else {
return encodeGroup(request);
}
} finally {
lock.unlock();
}
})
.thenRunAsync(() -> {
if (request.peer()) {
return;
}
saveMessage(request.info(), false);
attributeMessageReceipt(request.info());
})
.exceptionallyAsync(throwable -> {
request.info().setStatus(MessageStatus.ERROR);
saveMessage(request.info(), false);
return socketHandler.handleFailure(MESSAGE, throwable);
});
}
private CompletableFuture prepareOutgoingChatMessage(MessageInfo> messageInfo) {
var result = switch (messageInfo.message().content()) {
case MediaMessage> mediaMessage -> attributeMediaMessage(messageInfo.parentJid(), mediaMessage);
case ButtonMessage buttonMessage -> attributeButtonMessage(messageInfo.parentJid(), buttonMessage);
case TextMessage textMessage -> attributeTextMessage(textMessage);
case PollCreationMessage pollCreationMessage when messageInfo instanceof ChatMessageInfo pollCreationInfo -> // I guess they will be supported some day in newsletters
attributePollCreationMessage(pollCreationInfo, pollCreationMessage);
case PollUpdateMessage pollUpdateMessage when messageInfo instanceof ChatMessageInfo pollUpdateInfo // I guess they will be supported some day in newsletters
-> attributePollUpdateMessage(pollUpdateInfo, pollUpdateMessage);
default -> CompletableFuture.completedFuture(null);
};
if(messageInfo instanceof ChatMessageInfo chatMessageInfo) {
attributeChatMessage(chatMessageInfo);
fixEphemeralMessage(chatMessageInfo);
}
return result;
}
private void fixEphemeralMessage(ChatMessageInfo info) {
if (info.message().hasCategory(MessageCategory.SERVER)) {
return;
}
var chat = info.chat().orElse(null);
if (chat != null && chat.isEphemeral()) {
info.message()
.contentWithContext()
.flatMap(ContextualMessage::contextInfo)
.ifPresent(contextInfo -> createEphemeralContext(chat, contextInfo));
info.setMessage(info.message().toEphemeral());
return;
}
if (info.message().type() != MessageType.EPHEMERAL) {
return;
}
info.setMessage(info.message().unbox());
}
private void createEphemeralContext(Chat chat, ContextInfo contextInfo) {
var period = chat.ephemeralMessageDuration()
.period()
.toSeconds();
contextInfo.setEphemeralExpiration((int) period);
}
private CompletableFuture attributeTextMessage(TextMessage textMessage) {
if (socketHandler.store().textPreviewSetting() == TextPreviewSetting.DISABLED) {
return CompletableFuture.completedFuture(null);
}
return LinkPreview.createPreviewAsync(textMessage.text())
.thenComposeAsync(result -> attributeTextMessage(textMessage, result.orElse(null)))
.exceptionallyAsync(error -> null);
}
private CompletableFuture attributeTextMessage(TextMessage textMessage, LinkPreviewMatch match) {
if (match == null) {
return CompletableFuture.completedFuture(null);
}
var uri = match.result().uri().toString();
if (socketHandler.store().textPreviewSetting() == TextPreviewSetting.ENABLED_WITH_INFERENCE
&& !Objects.equals(match.text(), uri)) {
textMessage.setText(textMessage.text().replace(match.text(), uri));
}
var imageThumbnail = match.result()
.images()
.stream()
.reduce(this::compareDimensions);
var videoUri = match.result()
.videos()
.stream()
.reduce(this::compareDimensions);
textMessage.setMatchedText(uri);
textMessage.setCanonicalUrl(videoUri.map(LinkPreviewMedia::uri).orElse(match.result().uri()).toString());
textMessage.setThumbnailWidth(imageThumbnail.map(LinkPreviewMedia::width).orElse(null));
textMessage.setThumbnailHeight(imageThumbnail.map(LinkPreviewMedia::height).orElse(null));
textMessage.setDescription(match.result().siteDescription());
textMessage.setTitle(match.result().title());
textMessage.setPreviewType(videoUri.isPresent() ? TextMessage.PreviewType.VIDEO : TextMessage.PreviewType.NONE);
return imageThumbnail.map(data -> Medias.downloadAsync(data.uri()).thenAccept(textMessage::setThumbnail))
.orElseGet(() -> CompletableFuture.completedFuture(null));
}
private LinkPreviewMedia compareDimensions(LinkPreviewMedia first, LinkPreviewMedia second) {
return first.width() * first.height() > second.width() * second.height() ? first : second;
}
private CompletableFuture attributeMediaMessage(Jid chatJid, MediaMessage> mediaMessage) {
var media = mediaMessage.decodedMedia()
.orElseThrow(() -> new IllegalArgumentException("Missing media to upload"));
var attachmentType = getAttachmentType(chatJid, mediaMessage);
var mediaConnection = socketHandler.store().mediaConnection();
var userAgent = socketHandler.store()
.device()
.toUserAgent(socketHandler.store().version());
var proxy = socketHandler.store()
.proxy()
.filter(ignored -> socketHandler.store().mediaProxySetting().allowsUploads())
.orElse(null);
return Medias.upload(media, attachmentType, mediaConnection, userAgent, proxy)
.thenAccept(upload -> attributeMediaMessage(mediaMessage, upload));
}
private AttachmentType getAttachmentType(Jid chatJid, MediaMessage> mediaMessage) {
if (!chatJid.hasServer(JidServer.NEWSLETTER)) {
return mediaMessage.attachmentType();
}
return switch (mediaMessage.mediaType()) {
case IMAGE -> AttachmentType.NEWSLETTER_IMAGE;
case DOCUMENT -> AttachmentType.NEWSLETTER_DOCUMENT;
case AUDIO -> AttachmentType.NEWSLETTER_AUDIO;
case VIDEO -> AttachmentType.NEWSLETTER_VIDEO;
case STICKER -> AttachmentType.NEWSLETTER_STICKER;
case NONE -> throw new IllegalArgumentException("Unexpected empty message");
};
}
private void attributeMediaMessage(MutableAttachmentProvider> attachmentProvider, MediaFile upload) {
if (attachmentProvider instanceof MediaMessage> mediaMessage) {
mediaMessage.setHandle(upload.handle());
}
attachmentProvider
.setMediaSha256(upload.fileSha256())
.setMediaEncryptedSha256(upload.fileEncSha256())
.setMediaKey(upload.mediaKey())
.setMediaUrl(upload.url())
.setMediaKeyTimestamp(upload.timestamp())
.setMediaDirectPath(upload.directPath())
.setMediaSize(upload.fileLength());
}
private CompletableFuture attributePollCreationMessage(ChatMessageInfo info, PollCreationMessage pollCreationMessage) {
var pollEncryptionKey = pollCreationMessage.encryptionKey()
.orElseGet(() -> Bytes.random(32));
pollCreationMessage.setEncryptionKey(pollEncryptionKey);
info.setMessageSecret(pollEncryptionKey);
var metadata = new PollAdditionalMetadata(false);
info.setPollAdditionalMetadata(metadata);
info.message().deviceInfo().ifPresentOrElse(deviceInfo -> deviceInfo.setMessageSecret(pollEncryptionKey), () -> {
var deviceInfo = new DeviceContextInfoBuilder()
.messageSecret(pollEncryptionKey)
.build();
var message = info.message().withDeviceInfo(deviceInfo);
info.setMessage(message);
});
return CompletableFuture.completedFuture(null);
}
private CompletableFuture attributePollUpdateMessage(ChatMessageInfo info, PollUpdateMessage pollUpdateMessage) {
if (pollUpdateMessage.encryptedMetadata().isPresent()) {
return CompletableFuture.completedFuture(null);
}
var me = socketHandler.store().jid();
if (me.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
var additionalData = "%s\0%s".formatted(pollUpdateMessage.pollCreationMessageKey().id(), me.get().toSimpleJid());
var encryptedOptions = pollUpdateMessage.votes().stream().map(entry -> Sha256.calculate(entry.name())).toList();
var pollUpdateEncryptedOptions = PollUpdateEncryptedOptionsSpec.encode(new PollUpdateEncryptedOptions(encryptedOptions));
var originalPollInfo = socketHandler.store()
.findMessageByKey(pollUpdateMessage.pollCreationMessageKey())
.orElseThrow(() -> new NoSuchElementException("Missing original poll message"));
var originalPollMessage = (PollCreationMessage) originalPollInfo.message().content();
var originalPollSender = originalPollInfo.senderJid().toSimpleJid().toString().getBytes(StandardCharsets.UTF_8);
var modificationSenderJid = info.senderJid().toSimpleJid();
pollUpdateMessage.setVoter(modificationSenderJid);
var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8);
var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8);
var useSecretPayload = Bytes.concat(
pollUpdateMessage.pollCreationMessageKey().id().getBytes(StandardCharsets.UTF_8),
originalPollSender,
modificationSender,
secretName
);
var encryptionKey = originalPollMessage.encryptionKey()
.orElseThrow(() -> new NoSuchElementException("Missing encryption key"));
var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32);
var iv = Bytes.random(12);
var pollUpdateEncryptedPayload = AesGcm.encrypt(iv, pollUpdateEncryptedOptions, useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8));
var pollUpdateEncryptedMetadata = new PollUpdateEncryptedMetadata(pollUpdateEncryptedPayload, iv);
pollUpdateMessage.setEncryptedMetadata(pollUpdateEncryptedMetadata);
return CompletableFuture.completedFuture(null);
}
private CompletableFuture attributeButtonMessage(Jid chatJid, ButtonMessage buttonMessage) {
return switch (buttonMessage) {
case ButtonsMessage buttonsMessage when buttonsMessage.header().isPresent()
&& buttonsMessage.header().get() instanceof MediaMessage> mediaMessage ->
attributeMediaMessage(chatJid, mediaMessage);
case TemplateMessage templateMessage when templateMessage.format().isPresent() -> {
var templateFormatter = templateMessage.format().get();
yield switch (templateFormatter) {
case HighlyStructuredFourRowTemplate highlyStructuredFourRowTemplate
when highlyStructuredFourRowTemplate.title().isPresent() && highlyStructuredFourRowTemplate.title().get() instanceof MediaMessage> fourRowMedia ->
attributeMediaMessage(chatJid, fourRowMedia);
case HydratedFourRowTemplate hydratedFourRowTemplate when hydratedFourRowTemplate.title().isPresent() && hydratedFourRowTemplate.title().get() instanceof MediaMessage> hydratedFourRowMedia ->
attributeMediaMessage(chatJid, hydratedFourRowMedia);
default -> CompletableFuture.completedFuture(null);
};
}
case InteractiveMessage interactiveMessage
when interactiveMessage.header().isPresent()
&& interactiveMessage.header().get().attachment().isPresent()
&& interactiveMessage.header().get().attachment().get() instanceof MediaMessage> interactiveMedia ->
attributeMediaMessage(chatJid, interactiveMedia);
default -> CompletableFuture.completedFuture(null);
};
}
private CompletableFuture encodeNewsletterMessage(MessageSendRequest.Newsletter request) {
return prepareOutgoingChatMessage(request.info()).thenComposeAsync(ignored -> {
var message = request.info().message();
var messageNode = getPlainMessageNode(message);
var type = request.info().message().content() instanceof MediaMessage> ? "media" : "text";
var attributes = Attributes.ofNullable(request.additionalAttributes())
.put("id", request.info().id())
.put("to", request.info().parentJid())
.put("type", type)
.put("media_id", getPlainMessageHandle(request), Objects::nonNull)
.toMap();
return socketHandler.sendNode(Node.of("message", attributes, messageNode))
.thenRunAsync(() -> {
var newsletter = request.info().newsletter();
newsletter.addMessage(request.info());
})
.exceptionallyAsync(throwable -> {
request.info().setStatus(MessageStatus.ERROR);
return socketHandler.handleFailure(MESSAGE, throwable);
});
});
}
private String getPlainMessageHandle(MessageSendRequest.Newsletter request) {
var message = request.info()
.message()
.content();
if (!(message instanceof MediaMessage> extendedMediaMessage)) {
return null;
}
return extendedMediaMessage.handle()
.orElse(null);
}
private Node getPlainMessageNode(MessageContainer message) {
if(message.content() instanceof ReactionMessage reactionMessage) {
return Node.of("reaction", Map.of("code", reactionMessage.content()));
}
if(message.content() instanceof TextMessage textMessage && textMessage.thumbnail().isEmpty()) {
var byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(10);
var encoded = textMessage.text().getBytes(StandardCharsets.UTF_8);
byteArrayOutputStream.writeBytes(Bytes.intToVarInt(encoded.length));
byteArrayOutputStream.writeBytes(encoded);
return Node.of("plaintext", byteArrayOutputStream.toByteArray());
}
var messageAttributes = Attributes.of()
.put("mediatype", getMediaType(message), Objects::nonNull)
.toMap();
return Node.of("plaintext", messageAttributes, message.isEmpty() ? null : MessageContainerSpec.encode(message));
}
private CompletableFuture encodeGroup(MessageSendRequest.Chat request) {
var encodedMessage = Bytes.messageToBytes(request.info().message());
var sender = socketHandler.store()
.jid()
.orElse(null);
if (sender == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Cannot create message: user is not signed in"));
}
var senderName = new SenderKeyName(request.info().chatJid().toString(), sender.toSignalAddress());
var groupBuilder = new GroupBuilder(socketHandler.keys());
var signalMessage = groupBuilder.createOutgoing(senderName);
var groupCipher = new GroupCipher(senderName, socketHandler.keys());
var groupMessage = groupCipher.encrypt(encodedMessage);
var messageNode = createMessageNode(request, groupMessage);
if (request.hasRecipientOverride()) {
return queryDevices(request.recipients(), false)
.thenComposeAsync(allDevices -> createGroupNodes(request, signalMessage, allDevices, request.force()))
.thenApplyAsync(preKeys -> createEncodedMessageNode(request, preKeys, messageNode))
.thenComposeAsync(socketHandler::sendNode);
}
if(request.info().chatJid().type() == JidType.STATUS) {
var recipients = socketHandler.store()
.contacts()
.stream()
.map(Contact::jid)
.toList();
return queryDevices(recipients, false)
.thenComposeAsync(allDevices -> createGroupNodes(request, signalMessage, allDevices, true))
.thenApplyAsync(preKeys -> createEncodedMessageNode(request, preKeys, messageNode))
.thenComposeAsync(socketHandler::sendNode);
}
if (request.force()) {
return socketHandler.queryGroupMetadata(request.info().chatJid())
.thenComposeAsync(this::getGroupDevices)
.thenComposeAsync(allDevices -> createGroupNodes(request, signalMessage, allDevices, true))
.thenApplyAsync(preKeys -> createEncodedMessageNode(request, preKeys, messageNode))
.thenComposeAsync(socketHandler::sendNode);
}
return socketHandler.queryGroupMetadata(request.info().chatJid())
.thenComposeAsync(this::getGroupDevices)
.thenComposeAsync(allDevices -> createGroupNodes(request, signalMessage, allDevices, false))
.thenApplyAsync(preKeys -> createEncodedMessageNode(request, preKeys, messageNode))
.thenComposeAsync(socketHandler::sendNode);
}
private CompletableFuture encodeConversation(MessageSendRequest.Chat request) {
var sender = socketHandler.store()
.jid()
.orElse(null);
if (sender == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Cannot create message: user is not signed in"));
}
var encodedMessage = Bytes.messageToBytes(request.info().message());
if (request.peer()) {
var chatJid = request.info().chatJid();
var peerNode = createMessageNode(request, chatJid, encodedMessage, true);
var encodedMessageNode = createEncodedMessageNode(request, List.of(peerNode), null);
return socketHandler.sendNode(encodedMessageNode);
}
var deviceMessage = new DeviceSentMessage(request.info().chatJid(), request.info().message(), Optional.empty());
var encodedDeviceMessage = Bytes.messageToBytes(deviceMessage);
var recipients = getRecipients(request);
return queryDevices(recipients, !isMe(request.info().chatJid()))
.thenComposeAsync(allDevices -> createConversationNodes(request, allDevices, encodedMessage, encodedDeviceMessage))
.thenApplyAsync(sessions -> createEncodedMessageNode(request, sessions, null))
.thenComposeAsync(socketHandler::sendNode);
}
private Set getRecipients(MessageSendRequest.Chat request) {
if (request.hasRecipientOverride()) {
return request.recipients();
}
if(request.peer()) {
return Set.of(request.info().chatJid());
}
return new HashSet<>(List.of(socketHandler.store().jid().orElseThrow().toSimpleJid(), request.info().chatJid()));
}
private boolean isConversation(ChatMessageInfo info) {
return info.chatJid().hasServer(JidServer.WHATSAPP)
|| info.chatJid().hasServer(JidServer.USER);
}
private Node createEncodedMessageNode(MessageSendRequest.Chat request, List preKeys, Node descriptor) {
var body = new ArrayList();
if (!preKeys.isEmpty()) {
if (request.peer()) {
body.addAll(preKeys);
} else {
body.add(Node.of("participants", preKeys));
}
}
if (descriptor != null) {
body.add(descriptor);
}
if (!request.peer() && hasPreKeyMessage(preKeys)) {
socketHandler.keys().companionIdentity()
.ifPresent(companionIdentity -> body.add(Node.of("device-identity", SignedDeviceIdentitySpec.encode(companionIdentity))));
}
var attributes = Attributes.ofNullable(request.additionalAttributes())
.put("id", request.info().id())
.put("to", request.info().chatJid())
.put("type", request.info().message().content() instanceof MediaMessage> ? "media" : "text")
.put("verified_name", socketHandler.store().verifiedName().orElse(""), socketHandler.store().verifiedName().isPresent() && !request.peer())
.put("category", "peer", request.peer())
.put("duration", "900", request.info().message().type() == MessageType.LIVE_LOCATION)
.put("device_fanout", false, request.info().message().type() == MessageType.BUTTONS)
.put("push_priority", "high", isAppStateKeyShare(request))
.toMap();
return Node.of("message", attributes, body);
}
private boolean isAppStateKeyShare(MessageSendRequest.Chat request) {
return request.peer()
&& request.info().message().content() instanceof ProtocolMessage protocolMessage
&& protocolMessage.protocolType() == ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE;
}
private boolean hasPreKeyMessage(List participants) {
return participants.stream()
.map(Node::children)
.flatMap(Collection::stream)
.map(node -> node.attributes().getOptionalString("type"))
.flatMap(Optional::stream)
.anyMatch(PKMSG::equals);
}
private CompletableFuture> createConversationNodes(MessageSendRequest.Chat request, List contacts, byte[] message, byte[] deviceMessage) {
var jid = socketHandler.store()
.jid()
.orElse(null);
if (jid == null) {
return CompletableFuture.failedFuture(new IllegalStateException("Cannot create message: user is not signed in"));
}
var partitioned = contacts.stream()
.collect(Collectors.partitioningBy(contact -> Objects.equals(contact.user(), jid.user())));
var companions = querySessions(partitioned.get(true), request.force())
.thenApplyAsync(ignored -> createMessageNodes(request, partitioned.get(true), deviceMessage));
var others = querySessions(partitioned.get(false), request.force())
.thenApplyAsync(ignored -> createMessageNodes(request, partitioned.get(false), message));
return companions.thenCombineAsync(others, (first, second) -> toSingleList(first, second));
}
private CompletableFuture> createGroupNodes(MessageSendRequest.Chat request, byte[] distributionMessage, List participants, boolean force) {
var missingParticipants = participants.stream()
.filter(participant -> force || !socketHandler.keys().hasGroupKeys(request.info().chatJid(), participant))
.toList();
if (missingParticipants.isEmpty()) {
return CompletableFuture.completedFuture(List.of());
}
var whatsappMessage = new SenderKeyDistributionMessage(request.info().chatJid().toString(), distributionMessage);
var paddedMessage = Bytes.messageToBytes(whatsappMessage);
return querySessions(missingParticipants, force)
.thenApplyAsync(ignored -> createMessageNodes(request, missingParticipants, paddedMessage))
.thenApplyAsync(results -> {
socketHandler.keys().addRecipientsWithPreKeys(request.info().chatJid(), missingParticipants);
return results;
});
}
protected CompletableFuture querySessions(Collection contacts, boolean force) {
var missingSessions = contacts.stream()
.filter(contact -> force || !socketHandler.keys().hasSession(contact.toSignalAddress()))
.map(contact -> Node.of("user", Map.of("jid", contact)))
.toList();
return missingSessions.isEmpty() ? CompletableFuture.completedFuture(null) : querySession(missingSessions);
}
private CompletableFuture querySession(List children) {
return socketHandler.sendQuery("get", "encrypt", Node.of("key", children))
.thenAcceptAsync(this::parseSessions);
}
private List createMessageNodes(MessageSendRequest.Chat request, List contacts, byte[] message) {
return contacts.stream()
.map(contact -> createMessageNode(request, contact, message, false))
.toList();
}
private Node createMessageNode(MessageSendRequest.Chat request, Jid contact, byte[] message, boolean peer) {
var cipher = new SessionCipher(contact.toSignalAddress(), socketHandler.keys());
var encrypted = cipher.encrypt(message);
var messageNode = createMessageNode(request, encrypted);
return peer ? messageNode : Node.of("to", Map.of("jid", contact), messageNode);
}
private CompletableFuture> getGroupDevices(ChatMetadata metadata) {
var jids = metadata.participants()
.stream()
.map(ChatParticipant::jid)
.toList();
return queryDevices(jids, false);
}
protected CompletableFuture> queryDevices(Collection contacts, boolean excludeSelf) {
var cachedDevices = contacts.stream()
.map(devicesCache::get)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(entry -> !excludeSelf || !isMe(entry))
.toList();
var contactNodes = contacts.stream()
.filter(entry -> !devicesCache.containsKey(entry) && (!excludeSelf || !isMe(entry)))
.map(contact -> Node.of("user", Map.of("jid", contact)))
.toList();
if(contactNodes.isEmpty()) {
return CompletableFuture.completedFuture(cachedDevices);
}
var body = Node.of("usync",
Map.of("context", "message", "index", "0", "last", "true", "mode", "query", "sid", SocketHandler.randomSid()),
Node.of("query", Node.of("devices", Map.of("version", "2"))),
Node.of("list", contactNodes));
return socketHandler.sendQuery("get", "usync", body)
.thenApplyAsync(result -> toSingleList(cachedDevices, parseDevices(result, excludeSelf)));
}
private List parseDevices(Node node, boolean excludeSelf) {
return node.children()
.stream()
.map(child -> child.findChild("list"))
.flatMap(Optional::stream)
.map(Node::children)
.flatMap(Collection::stream)
.map(entry -> parseDevice(entry, excludeSelf))
.flatMap(Collection::stream)
.toList();
}
private List parseDevice(Node wrapper, boolean excludeSelf) {
var jid = wrapper.attributes().getRequiredJid("jid");
var devices = wrapper.findChild("devices")
.orElseThrow(() -> new NoSuchElementException("Missing devices"))
.findChild("device-list")
.orElseThrow(() -> new NoSuchElementException("Missing device list"))
.children();
if(devices.isEmpty()) {
return excludeSelf && isMe(jid) ? List.of() : List.of(jid);
}
return devices.stream()
.map(child -> parseDeviceId(child, jid, excludeSelf))
.flatMap(Optional::stream)
.toList();
}
private Optional parseDeviceId(Node child, Jid jid, boolean excludeSelf) {
var deviceId = child.attributes().getInt("id");
if(!child.description().equals("device")) {
return Optional.empty();
}
if(deviceId != 0 && !child.attributes().hasKey("key-index")) {
return Optional.empty();
}
var result = Jid.ofDevice(jid.user(), deviceId);
cacheDevice(result);
if(excludeSelf && isMe(result)) {
return Optional.empty();
}
return Optional.of(Jid.ofDevice(jid.user(), deviceId));
}
private boolean isMe(Jid jid) {
var self = socketHandler.store().jid().orElse(null);
if (self == null) {
return false;
}
return jid.user().equals(self.user()) && Objects.equals(self.device(), jid.device());
}
private void cacheDevice(Jid jid) {
var cachedDevices = devicesCache.get(jid.toSimpleJid());
if(cachedDevices != null) {
cachedDevices.add(jid);
return;
}
var devices = new CopyOnWriteArrayList();
devices.add(jid);
devicesCache.put(jid.toSimpleJid(), devices);
}
protected void parseSessions(Node node) {
if(node == null) {
return;
}
node.findChild("list")
.orElseThrow(() -> new IllegalArgumentException("Cannot parse sessions: " + node))
.listChildren("user")
.forEach(this::parseSession);
}
private void parseSession(Node node) {
Validate.isTrue(!node.hasNode("error"), "Erroneous session node", SecurityException.class);
var jid = node.attributes()
.getRequiredJid("jid");
var registrationId = node.findChild("registration")
.map(id -> Bytes.bytesToInt(id.contentAsBytes().orElseThrow(), 4))
.orElseThrow(() -> new NoSuchElementException("Missing id"));
var identity = node.findChild("identity")
.flatMap(Node::contentAsBytes)
.map(ISignalKeyPair::toSignalKey)
.orElseThrow(() -> new NoSuchElementException("Missing identity"));
var signedKey = node.findChild("skey")
.flatMap(SignalSignedKeyPair::of)
.orElseThrow(() -> new NoSuchElementException("Missing signed key"));
var key = node.findChild("key")
.flatMap(SignalSignedKeyPair::of)
.orElse(null);
var builder = new SessionBuilder(jid.toSignalAddress(), socketHandler.keys());
builder.createOutgoing(registrationId, identity, signedKey, key);
}
public void decode(Node node, JidProvider chatOverride, boolean notify) {
try {
var businessName = getBusinessName(node);
if (node.hasNode("unavailable")) {
decodeChatMessage(node, null, businessName, notify);
return;
}
var encrypted = node.listChildren("enc");
if (!encrypted.isEmpty()) {
encrypted.forEach(message -> decodeChatMessage(node, message, businessName, notify));
return;
}
var plainText = node.findChild("plaintext");
if (plainText.isPresent()) {
decodeNewsletterMessage(node, plainText.get(), chatOverride, notify);
return;
}
var reaction = node.findChild("reaction");
if (reaction.isPresent()) {
decodeNewsletterReaction(node, reaction.get(), chatOverride, notify);
return;
}
decodeChatMessage(node, null, businessName, notify);
} catch (Throwable throwable) {
socketHandler.handleFailure(MESSAGE, throwable);
}
}
private String getBusinessName(Node node) {
return node.attributes()
.getOptionalString("verified_name")
.or(() -> getBusinessNameFromNode(node))
.orElse(null);
}
private static Optional getBusinessNameFromNode(Node node) {
return node.findChild("verified_name")
.flatMap(Node::contentAsBytes)
.map(BusinessVerifiedNameCertificateSpec::decode)
.map(certificate -> certificate.details().name());
}
private Node createMessageNode(MessageSendRequest.Chat request, CipheredMessageResult groupMessage) {
var mediaType = getMediaType(request.info().message());
var attributes = Attributes.of()
.put("v", "2")
.put("type", groupMessage.type())
.put("mediatype", mediaType, Objects::nonNull);
return Node.of("enc", attributes, groupMessage.message());
}
private String getMediaType(MessageContainer container) {
var content = container.content();
return switch (content) {
case ImageMessage ignored -> "image";
case VideoOrGifMessage videoMessage -> videoMessage.gifPlayback() ? "gif" : "video";
case AudioMessage audioMessage -> audioMessage.voiceMessage() ? "ptt" : "audio";
case ContactMessage ignored -> "vcard";
case DocumentMessage ignored -> "document";
case ContactsMessage ignored -> "contact_array";
case LiveLocationMessage ignored -> "livelocation";
case StickerMessage ignored -> "sticker";
case ListMessage ignored -> "list";
case ListResponseMessage ignored -> "list_response";
case ButtonsResponseMessage ignored -> "buttons_response";
case PaymentOrderMessage ignored -> "order";
case ProductMessage ignored -> "product";
case NativeFlowResponseMessage ignored -> "native_flow_response";
case ButtonsMessage buttonsMessage ->
buttonsMessage.headerType().hasMedia() ? buttonsMessage.headerType().name().toLowerCase() : null;
case null, default -> null;
};
}
private void decodeNewsletterMessage(Node messageNode, Node plainTextNode, JidProvider chatOverride, boolean notify) {
try {
var newsletterJid = messageNode.attributes()
.getOptionalJid("from")
.or(() -> Optional.ofNullable(chatOverride).map(JidProvider::toJid))
.orElseThrow(() -> new NoSuchElementException("Missing from"));
var messageId = messageNode.attributes()
.getRequiredString("id");
if (notify) {
socketHandler.sendMessageAck(newsletterJid, messageNode);
if(socketHandler.store().automaticMessageReceipts()) {
var receiptType = getReceiptType("newsletter", false);
socketHandler.sendReceipt(newsletterJid, null, List.of(messageId), receiptType);
}
}
var newsletter = socketHandler.store()
.findNewsletterByJid(newsletterJid);
if(newsletter.isEmpty()) {
return;
}
var serverId = messageNode.attributes()
.getRequiredInt("server_id");
var timestamp = messageNode.attributes()
.getNullableLong("t");
var views = messageNode.findChild("views_count")
.map(value -> value.attributes().getNullableLong("count"))
.orElse(null);
var reactions = messageNode.findChild("reactions")
.stream()
.map(node -> node.listChildren("reaction"))
.flatMap(Collection::stream)
.collect(Collectors.toConcurrentMap(
entry -> entry.attributes().getRequiredString("code"),
entry -> new NewsletterReaction(
entry.attributes().getRequiredString("code"),
entry.attributes().getLong("count"),
entry.attributes().getBoolean("is_sender")
)
));
var result = plainTextNode.contentAsBytes()
.map(MessageContainerSpec::decode)
.map(messageContainer -> {
var readStatus = notify ? MessageStatus.DELIVERED : MessageStatus.READ;
var message = new NewsletterMessageInfo(
messageId,
serverId,
timestamp,
views,
reactions,
messageContainer,
readStatus
);
message.setNewsletter(newsletter.get());
return message;
});
if (result.isEmpty()) {
return;
}
newsletter.get().addMessage(result.get());
if(notify) {
socketHandler.onNewMessage(result.get());
}
} catch (Throwable throwable) {
socketHandler.handleFailure(MESSAGE, throwable);
}
}
private void decodeNewsletterReaction(Node messageNode, Node reactionNode, JidProvider chatOverride, boolean notify) {
try {
var messageId = messageNode.attributes()
.getRequiredString("id");
var newsletterJid = messageNode.attributes()
.getOptionalJid("from")
.or(() -> Optional.ofNullable(chatOverride).map(JidProvider::toJid))
.orElseThrow(() -> new NoSuchElementException("Missing from"));
var isSender = messageNode.attributes()
.getBoolean("is_sender");
if(notify) {
socketHandler.sendMessageAck(newsletterJid, messageNode);
if(socketHandler.store().automaticMessageReceipts()) {
var receiptType = getReceiptType("newsletter", false);
socketHandler.sendReceipt(newsletterJid, null, List.of(messageId), receiptType);
}
}
var newsletter = socketHandler.store()
.findNewsletterByJid(newsletterJid);
if(newsletter.isEmpty()) {
return;
}
var message = socketHandler.store()
.findMessageById(newsletter.get(), messageId);
if(message.isEmpty()) {
return;
}
var myReaction = isSender ? message.get()
.reactions()
.stream()
.filter(NewsletterReaction::fromMe)
.findFirst()
.orElse(null) : null;
if(myReaction != null) {
message.get().decrementReaction(myReaction.content());
}
var code = reactionNode.attributes()
.getOptionalString("code");
if(code.isEmpty()) {
return;
}
message.get().incrementReaction(code.get(), isSender);
} catch (Throwable throwable) {
socketHandler.handleFailure(MESSAGE, throwable);
}
}
private void decodeChatMessage(Node infoNode, Node messageNode, String businessName, boolean notify) {
try {
var pushName = infoNode.attributes().getNullableString("notify");
var timestamp = infoNode.attributes().getLong("t");
var id = infoNode.attributes().getRequiredString("id");
var from = infoNode.attributes()
.getRequiredJid("from");
var recipient = infoNode.attributes()
.getOptionalJid("recipient")
.orElse(from);
var participant = infoNode.attributes()
.getOptionalJid("participant")
.orElse(null);
var messageBuilder = new ChatMessageInfoBuilder()
.status(MessageStatus.PENDING);
var keyBuilder = new ChatMessageKeyBuilder()
.id(SocketHandler.randomSid());
var receiver = socketHandler.store()
.jid()
.map(Jid::toSimpleJid)
.orElse(null);
if (receiver == null) {
return;
}
if (from.hasServer(JidServer.WHATSAPP) || from.hasServer(JidServer.USER)) {
keyBuilder.chatJid(recipient);
keyBuilder.senderJid(from);
keyBuilder.fromMe(Objects.equals(from.toSimpleJid(), receiver));
messageBuilder.senderJid(from);
} else {
keyBuilder.chatJid(from);
keyBuilder.senderJid(Objects.requireNonNull(participant, "Missing participant in group message"));
keyBuilder.fromMe(Objects.equals(participant.toSimpleJid(), receiver));
messageBuilder.senderJid(Objects.requireNonNull(participant, "Missing participant in group message"));
}
var key = keyBuilder.id(id).build();
if (Objects.equals(key.senderJid().orElse(null), socketHandler.store().jid().orElse(null))) {
sendEncMessageSuccessReceipt(infoNode, id, key.chatJid(), key.senderJid().orElse(null), key.fromMe());
return;
}
var messageContainer = decodeChatMessageContainer(messageNode, from, participant);
if(messageContainer.hasError()) {
sendEncMessageRetryReceipt(infoNode, key, participant, timestamp, messageContainer.error());
return;
}
var info = messageBuilder.key(key)
.broadcast(key.chatJid().hasServer(JidServer.BROADCAST))
.pushName(pushName)
.status(MessageStatus.DELIVERED)
.businessVerifiedName(businessName)
.timestampSeconds(timestamp)
.message(messageContainer.message())
.build();
attributeMessageReceipt(info);
attributeChatMessage(info);
saveMessage(info, notify);
socketHandler.onReply(info);
sendEncMessageSuccessReceipt(infoNode, id, key.chatJid(), key.senderJid().orElse(null), key.fromMe());
} catch (Throwable throwable) {
socketHandler.handleFailure(MESSAGE, throwable);
}
}
private void sendEncMessageRetryReceipt(Node infoNode, ChatMessageKey key, Jid participant, long timestamp, Throwable error) {
socketHandler.sendMessageAck(key.chatJid(), infoNode).thenComposeAsync(ignored -> {
var counter = retries.getOrDefault(key.id(), 0);
if(counter >= MAX_MESSAGE_RETRIES) {
socketHandler.handleFailure(MESSAGE, error);
return CompletableFuture.completedFuture(null);
}
retries.put(key.id(), counter + 1);
return socketHandler.sendRetryReceipt(timestamp, key.chatJid(), participant, key.id(), 1);
});
}
private MessageDecodeResult decodeChatMessageContainer(Node messageNode, Jid from, Jid participant) {
if (messageNode == null) {
return new MessageDecodeResult(null, new RuntimeException("Message is not available"));
}
var type = messageNode.attributes().getRequiredString("type");
var encodedMessage = messageNode.contentAsBytes().orElse(null);
return decodeMessageBytes(type, encodedMessage, from, participant);
}
private void sendEncMessageSuccessReceipt(Node infoNode, String id, Jid chatJid, Jid senderJid, boolean fromMe) {
socketHandler.sendMessageAck(chatJid, infoNode).thenComposeAsync(ignored -> {
if(!socketHandler.store().automaticMessageReceipts()) {
return CompletableFuture.completedFuture(null);
}
var participant = fromMe && senderJid == null ? chatJid : senderJid;
var category = infoNode.attributes().getString("category");
var receiptType = getReceiptType(category, fromMe);
return socketHandler.sendReceipt(chatJid, participant, List.of(id), receiptType);
});
}
private String getReceiptType(String category, boolean fromMe) {
if (Objects.equals(category, "peer")) {
return "peer_msg";
}
if (fromMe) {
return "sender";
}
if (!socketHandler.store().online()) {
return "inactive";
}
return null;
}
private MessageDecodeResult decodeMessageBytes(String type, byte[] encodedMessage, Jid from, Jid participant) {
try {
lock.lock();
if (encodedMessage == null) {
return new MessageDecodeResult(null, new IllegalArgumentException("Missing encoded message"));
}
var result = switch (type) {
case SKMSG -> {
Objects.requireNonNull(participant, "Cannot decipher skmsg without participant");
var senderName = new SenderKeyName(from.toString(), participant.toSignalAddress());
var signalGroup = new GroupCipher(senderName, socketHandler.keys());
yield signalGroup.decrypt(encodedMessage);
}
case PKMSG -> {
var user = from.hasServer(JidServer.WHATSAPP) ? from : participant;
Objects.requireNonNull(user, "Cannot decipher pkmsg without user");
var session = new SessionCipher(user.toSignalAddress(), socketHandler.keys());
var preKey = SignalPreKeyMessage.ofSerialized(encodedMessage);
yield session.decrypt(preKey);
}
case MSG -> {
var user = from.hasServer(JidServer.WHATSAPP) ? from : participant;
Objects.requireNonNull(user, "Cannot decipher msg without user");
var session = new SessionCipher(user.toSignalAddress(), socketHandler.keys());
var signalMessage = SignalMessage.ofSerialized(encodedMessage);
yield session.decrypt(signalMessage);
}
default -> throw new IllegalArgumentException("Unsupported encoded message type: %s".formatted(type));
};
var message = Bytes.bytesToMessage(result)
.unbox();
return new MessageDecodeResult(message, null);
} catch (Throwable throwable) {
return new MessageDecodeResult(null, throwable);
}finally {
lock.unlock();
}
}
private void attributeMessageReceipt(ChatMessageInfo info) {
var self = socketHandler.store()
.jid()
.map(Jid::toSimpleJid)
.orElse(null);
if (!info.fromMe() || (self != null && !info.chatJid().equals(self))) {
return;
}
info.receipt().readTimestampSeconds(info.timestampSeconds().orElse(0L));
info.receipt().deliveredJids().add(self);
info.receipt().readJids().add(self);
info.setStatus(MessageStatus.READ);
}
private void saveMessage(ChatMessageInfo info, boolean notify) {
info.message()
.senderKeyDistributionMessage()
.ifPresent(keyDistributionMessage -> handleDistributionMessage(keyDistributionMessage, info.senderJid()));
if (info.chatJid().type() == JidType.STATUS) {
socketHandler.store().addStatus(info);
socketHandler.onNewStatus(info);
return;
}
if (info.message().hasCategory(MessageCategory.SERVER)) {
if (info.message().content() instanceof ProtocolMessage protocolMessage) {
handleProtocolMessage(info, protocolMessage);
}
return;
}
var chat = info.chat()
.orElseGet(() -> socketHandler.store().addNewChat(info.chatJid()));
var result = chat.addNewMessage(info);
if (!result || info.timestampSeconds().orElse(0L) <= socketHandler.store().initializationTimeStamp()) {
return;
}
if (chat.archived() && socketHandler.store().unarchiveChats()) {
chat.setArchived(false);
}
info.sender()
.filter(this::isTyping)
.ifPresent(sender -> socketHandler.onUpdateChatPresence(ContactStatus.AVAILABLE, sender.jid(), chat));
if (!info.ignore() && !info.fromMe()) {
chat.setUnreadMessagesCount(chat.unreadMessagesCount() + 1);
}
if (notify) {
socketHandler.onNewMessage(info);
}
}
private void handleDistributionMessage(SenderKeyDistributionMessage distributionMessage, Jid from) {
var groupName = new SenderKeyName(distributionMessage.groupId(), from.toSignalAddress());
var builder = new GroupBuilder(socketHandler.keys());
var message = SignalDistributionMessage.ofSerialized(distributionMessage.data());
builder.createIncoming(groupName, message);
}
private void handleProtocolMessage(ChatMessageInfo info, ProtocolMessage protocolMessage) {
switch (protocolMessage.protocolType()) {
case HISTORY_SYNC_NOTIFICATION -> onHistorySyncNotification(info, protocolMessage);
case APP_STATE_SYNC_KEY_SHARE -> onAppStateSyncKeyShare(protocolMessage);
case REVOKE -> onMessageRevoked(info, protocolMessage);
case EPHEMERAL_SETTING -> onEphemeralSettings(info, protocolMessage);
case null, default -> {}
}
}
private void onEphemeralSettings(ChatMessageInfo info, ProtocolMessage protocolMessage) {
var chat = info.chat().orElse(null);
var timestampSeconds = info.timestampSeconds().orElse(0L);
if (chat != null) {
chat.setEphemeralMessagesToggleTimeSeconds(timestampSeconds);
chat.setEphemeralMessageDuration(ChatEphemeralTimer.of((int) protocolMessage.ephemeralExpiration()));
}
var setting = new EphemeralSettings((int) protocolMessage.ephemeralExpiration(), timestampSeconds);
socketHandler.onSetting(setting);
}
private void onMessageRevoked(ChatMessageInfo info, ProtocolMessage protocolMessage) {
var id = protocolMessage.key().orElseThrow().id();
info.chat()
.flatMap(chat -> socketHandler.store().findMessageById(chat, id))
.ifPresent(message -> onMessageDeleted(info, message));
}
private void onAppStateSyncKeyShare(ProtocolMessage protocolMessage) {
var data = protocolMessage.appStateSyncKeyShare()
.orElseThrow(() -> new NoSuchElementException("Missing app state keys"));
var self = socketHandler.store()
.jid()
.orElseThrow(() -> new IllegalStateException("The session isn't connected"));
socketHandler.keys()
.addAppKeys(self, data.keys());
}
private void onHistorySyncNotification(ChatMessageInfo info, ProtocolMessage protocolMessage) {
if(isBaseHistorySyncComplete()) {
if(!socketHandler.keys().initialAppSync() && !startedInitialAppSync.get()) {
startedInitialAppSync.set(true);
socketHandler.pullInitialPatches()
.exceptionallyAsync(throwable -> socketHandler.handleFailure(INITIAL_APP_STATE_SYNC, throwable));
}
if(socketHandler.store().historyLength().isZero()) {
return;
}
}
downloadHistorySync(protocolMessage)
.thenAcceptAsync(this::onHistoryNotification)
.exceptionallyAsync(throwable -> socketHandler.handleFailure(HISTORY_SYNC, throwable))
.thenRunAsync(() -> socketHandler.sendReceipt(info.chatJid(), null, List.of(info.id()), "hist_sync"));
}
private boolean isBaseHistorySyncComplete() {
return historySyncTypes.containsAll(BASE_HISTORY_SYNC_TYPES);
}
private boolean isTyping(Contact sender) {
return sender.lastKnownPresence() == ContactStatus.COMPOSING
|| sender.lastKnownPresence() == ContactStatus.RECORDING;
}
private CompletableFuture downloadHistorySync(ProtocolMessage protocolMessage) {
return protocolMessage.historySyncNotification()
.map(this::downloadHistorySyncNotification)
.orElseGet(() -> CompletableFuture.completedFuture(null));
}
private CompletableFuture downloadHistorySyncNotification(HistorySyncNotification notification) {
return notification.initialHistBootstrapInlinePayload()
.map(result -> CompletableFuture.completedFuture(HistorySyncSpec.decode(Bytes.decompress(result))))
.orElseGet(() -> Medias.downloadAsync(notification)
.thenApplyAsync(entry -> entry.orElseThrow(() -> new NoSuchElementException("Cannot download history sync")))
.thenApplyAsync(result -> HistorySyncSpec.decode(Bytes.decompress(result))));
}
private void onHistoryNotification(HistorySync history) {
handleHistorySync(history);
if (history.progress() == null) {
return;
}
var recent = history.syncType() == Type.RECENT;
if(recent) {
recentHistorySyncTracker.commit(history.chunkOrder(), history.progress() == 100);
if(recentHistorySyncTracker.isDone()) {
socketHandler.onHistorySyncProgress(history.progress(), true);
}
}else {
fullHistorySyncTracker.commit(history.chunkOrder(), history.progress() == 100);
if(fullHistorySyncTracker.isDone()) {
socketHandler.onHistorySyncProgress(history.progress(), false);
}
}
}
private void onMessageDeleted(ChatMessageInfo info, ChatMessageInfo message) {
info.chat().ifPresent(chat -> chat.removeMessage(message));
message.setRevokeTimestampSeconds(Clock.nowSeconds());
socketHandler.onMessageDeleted(message, true);
}
private void handleHistorySync(HistorySync history) {
try {
switch (history.syncType()) {
case INITIAL_STATUS_V3 -> handleInitialStatus(history);
case PUSH_NAME -> handlePushNames(history);
case INITIAL_BOOTSTRAP -> handleInitialBootstrap(history);
case FULL -> handleChatsSync(history, false);
case RECENT -> handleChatsSync(history, true);
case NON_BLOCKING_DATA -> handleNonBlockingData(history);
}
} finally {
historySyncTypes.add(history.syncType());
}
}
private void handleInitialStatus(HistorySync history) {
for (var messageInfo : history.statusV3Messages()) {
socketHandler.store().addStatus(messageInfo);
}
socketHandler.onStatus();
}
private void handlePushNames(HistorySync history) {
for (var pushName : history.pushNames()) {
handNewPushName(pushName);
}
socketHandler.onContacts();
}
private void handNewPushName(PushName pushName) {
var jid = Jid.of(pushName.id());
var contact = socketHandler.store()
.findContactByJid(jid)
.orElseGet(() -> createNewContact(jid));
pushName.name().ifPresent(contact::setChosenName);
var action = new ContactAction(pushName.name(), Optional.empty(), Optional.empty());
socketHandler.onAction(action, MessageIndexInfo.of("contact", jid, null, true));
}
private Contact createNewContact(Jid jid) {
var contact = socketHandler.store().addContact(jid);
socketHandler.onNewContact(contact);
return contact;
}
private void handleInitialBootstrap(HistorySync history) {
if (!socketHandler.store().historyLength().isZero()) {
var jids = history.conversations()
.stream()
.map(Chat::jid)
.toList();
historyCache.addAll(jids);
}
handleConversations(history);
socketHandler.onChats();
}
private void handleChatsSync(HistorySync history, boolean recent) {
if (socketHandler.store().historyLength().isZero()) {
return;
}
handleConversations(history);
handleConversationsNotifications(history, recent);
scheduleHistorySyncTimeout();
}
private void handleConversationsNotifications(HistorySync history, boolean recent) {
var toRemove = new HashSet();
for (var cachedJid : historyCache) {
var chat = socketHandler.store()
.findChatByJid(cachedJid)
.orElse(null);
if (chat == null) {
continue;
}
var done = !recent && !history.conversations().contains(chat);
if (done) {
chat.setEndOfHistoryTransfer(true);
chat.setEndOfHistoryTransferType(Chat.EndOfHistoryTransferType.COMPLETE_AND_NO_MORE_MESSAGE_REMAIN_ON_PRIMARY);
toRemove.add(cachedJid);
}
socketHandler.onChatRecentMessages(chat, done);
}
historyCache.removeAll(toRemove);
}
private void scheduleHistorySyncTimeout() {
if (historySyncTask != null && !historySyncTask.isDone()) {
historySyncTask.cancel(true);
}
this.historySyncTask = socketHandler.scheduleDelayed(this::onForcedHistorySyncCompletion, HISTORY_SYNC_TIMEOUT);
}
private void onForcedHistorySyncCompletion() {
for (var cachedJid : historyCache) {
var chat = socketHandler.store()
.findChatByJid(cachedJid)
.orElse(null);
if (chat == null) {
continue;
}
socketHandler.onChatRecentMessages(chat, true);
}
historyCache.clear();
}
private void handleConversations(HistorySync history) {
for (var chat : history.conversations()) {
for (var message : chat.messages()) {
attributeChatMessage(message.messageInfo());
}
socketHandler.store().addChat(chat);
}
}
private void handleNonBlockingData(HistorySync history) {
for (var pastParticipants : history.pastParticipants()) {
socketHandler.pastParticipants().put(pastParticipants.groupJid(), pastParticipants.pastParticipants());
}
}
@SafeVarargs
private List toSingleList(List... all) {
return switch (all.length) {
case 0 -> List.of();
case 1 -> all[0];
default -> Stream.of(all)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.toList();
};
}
private void attributeSender(ChatMessageInfo info, Jid senderJid) {
if(senderJid.server() != JidServer.WHATSAPP && senderJid.server() != JidServer.USER) {
return;
}
var contact = socketHandler.store().findContactByJid(senderJid)
.orElseGet(() -> socketHandler.store().addContact(new Contact(senderJid)));
info.setSender(contact);
}
private void attributeContext(ContextInfo contextInfo) {
contextInfo.quotedMessageSenderJid().ifPresent(senderJid -> attributeContextSender(contextInfo, senderJid));
contextInfo.quotedMessageChatJid().ifPresent(chatJid -> attributeContextChat(contextInfo, chatJid));
}
private void attributeContextChat(ContextInfo contextInfo, Jid chatJid) {
var chat = socketHandler.store().findChatByJid(chatJid)
.orElseGet(() -> socketHandler.store().addNewChat(chatJid));
contextInfo.setQuotedMessageChat(chat);
}
private void attributeContextSender(ContextInfo contextInfo, Jid senderJid) {
var contact = socketHandler.store().findContactByJid(senderJid)
.orElseGet(() -> socketHandler.store().addContact(new Contact(senderJid)));
contextInfo.setQuotedMessageSender(contact);
}
protected void attributeChatMessage(ChatMessageInfo info) {
var chat = socketHandler.store().findChatByJid(info.chatJid())
.orElseGet(() -> socketHandler.store().addNewChat(info.chatJid()));
info.setChat(chat);
var me = socketHandler.store().jid().orElse(null);
if (info.fromMe() && me != null) {
info.key().setSenderJid(me.toSimpleJid());
}
attributeSender(info, info.senderJid());
info.message()
.contentWithContext()
.flatMap(ContextualMessage::contextInfo)
.ifPresent(this::attributeContext);
processMessageWithSecret(info);
}
private void processMessageWithSecret(ChatMessageInfo info) {
switch (info.message().content()) {
case PollCreationMessage pollCreationMessage -> handlePollCreation(info, pollCreationMessage);
case PollUpdateMessage pollUpdateMessage -> handlePollUpdate(info, pollUpdateMessage);
case ReactionMessage reactionMessage -> handleReactionMessage(info, reactionMessage);
default -> {}
}
}
private void handlePollCreation(ChatMessageInfo info, PollCreationMessage pollCreationMessage) {
if (pollCreationMessage.encryptionKey().isPresent()) {
return;
}
info.message()
.deviceInfo()
.flatMap(DeviceContextInfo::messageSecret)
.or(info::messageSecret)
.ifPresent(pollCreationMessage::setEncryptionKey);
}
private void handlePollUpdate(ChatMessageInfo info, PollUpdateMessage pollUpdateMessage) {
var originalPollInfo = socketHandler.store().findMessageByKey(pollUpdateMessage.pollCreationMessageKey());
if(originalPollInfo.isEmpty()) {
return;
}
if(!(originalPollInfo.get().message().content() instanceof PollCreationMessage originalPollMessage)) {
return;
}
pollUpdateMessage.setPollCreationMessage(originalPollMessage);
var originalPollSender = originalPollInfo.get()
.senderJid()
.toSimpleJid()
.toString()
.getBytes(StandardCharsets.UTF_8);
var modificationSenderJid = info.senderJid().toSimpleJid();
pollUpdateMessage.setVoter(modificationSenderJid);
var modificationSender = modificationSenderJid.toString().getBytes(StandardCharsets.UTF_8);
var secretName = pollUpdateMessage.secretName().getBytes(StandardCharsets.UTF_8);
var useSecretPayload = Bytes.concat(
originalPollInfo.get().id().getBytes(StandardCharsets.UTF_8),
originalPollSender,
modificationSender,
secretName
);
var encryptionKey = originalPollMessage.encryptionKey()
.orElseThrow(() -> new NoSuchElementException("Missing encryption key"));
var useCaseSecret = Hkdf.extractAndExpand(encryptionKey, useSecretPayload, 32);
var additionalData = "%s\0%s".formatted(
originalPollInfo.get().id(),
modificationSenderJid
);
var metadata = pollUpdateMessage.encryptedMetadata()
.orElseThrow(() -> new NoSuchElementException("Missing encrypted metadata"));
var decrypted = AesGcm.decrypt(metadata.iv(), metadata.payload(), useCaseSecret, additionalData.getBytes(StandardCharsets.UTF_8));
var pollVoteMessage = PollUpdateEncryptedOptionsSpec.decode(decrypted);
var selectedOptions = pollVoteMessage.selectedOptions()
.stream()
.map(sha256 -> originalPollMessage.getSelectableOption(HexFormat.of().formatHex(sha256)))
.flatMap(Optional::stream)
.toList();
originalPollMessage.addSelectedOptions(modificationSenderJid, selectedOptions);
pollUpdateMessage.setVotes(selectedOptions);
var update = new PollUpdate(info.key(), pollVoteMessage, Clock.nowMilliseconds());
info.pollUpdates().add(update);
}
private void handleReactionMessage(ChatMessageInfo info, ReactionMessage reactionMessage) {
info.setIgnore(true);
socketHandler.store().findMessageByKey(reactionMessage.key())
.ifPresent(message -> message.reactions().add(reactionMessage));
}
protected void dispose() {
historyCache.clear();
historySyncTask = null;
historySyncTypes.clear();
startedInitialAppSync.set(false);
recentHistorySyncTracker.clear();
fullHistorySyncTracker.clear();
}
private record MessageDecodeResult(MessageContainer message, Throwable error) {
public boolean hasError() {
return error != null;
}
}
private static class HistorySyncProgressTracker {
private final BitSet chunksMarker;
private final AtomicInteger chunkEnd;
private HistorySyncProgressTracker() {
this.chunksMarker = new BitSet();
this.chunkEnd = new AtomicInteger(0);
}
private boolean isDone() {
var chunkEnd = this.chunkEnd.get();
return chunkEnd > 0 && IntStream.range(0, chunkEnd)
.allMatch(chunksMarker::get);
}
private void commit(int chunk, boolean finished) {
if(finished) {
chunkEnd.set(chunk);
}
chunksMarker.set(chunk);
}
private void clear() {
chunksMarker.clear();
chunkEnd.set(0);
}
}
}