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.StreamHandler Maven / Gradle / Ivy
package it.auties.whatsapp.implementation;
import com.fasterxml.jackson.databind.ObjectMapper;
import it.auties.curve25519.Curve25519;
import it.auties.whatsapp.api.*;
import it.auties.whatsapp.crypto.AesGcm;
import it.auties.whatsapp.crypto.Hkdf;
import it.auties.whatsapp.crypto.Hmac;
import it.auties.whatsapp.exception.HmacValidationException;
import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateBuilder;
import it.auties.whatsapp.model.business.BusinessVerifiedNameCertificateSpec;
import it.auties.whatsapp.model.business.BusinessVerifiedNameDetailsBuilder;
import it.auties.whatsapp.model.business.BusinessVerifiedNameDetailsSpec;
import it.auties.whatsapp.model.call.Call;
import it.auties.whatsapp.model.call.CallStatus;
import it.auties.whatsapp.model.chat.Chat;
import it.auties.whatsapp.model.chat.ChatEphemeralTimer;
import it.auties.whatsapp.model.chat.ChatPastParticipant;
import it.auties.whatsapp.model.contact.Contact;
import it.auties.whatsapp.model.contact.ContactStatus;
import it.auties.whatsapp.model.info.ChatMessageInfo;
import it.auties.whatsapp.model.info.ChatMessageInfoBuilder;
import it.auties.whatsapp.model.info.NewsletterMessageInfo;
import it.auties.whatsapp.model.jid.Jid;
import it.auties.whatsapp.model.jid.JidServer;
import it.auties.whatsapp.model.media.MediaConnection;
import it.auties.whatsapp.model.message.model.ChatMessageKey;
import it.auties.whatsapp.model.message.model.ChatMessageKeyBuilder;
import it.auties.whatsapp.model.message.model.MessageStatus;
import it.auties.whatsapp.model.mobile.PhoneNumber;
import it.auties.whatsapp.model.newsletter.NewsletterMetadata;
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.privacy.PrivacySettingEntry;
import it.auties.whatsapp.model.privacy.PrivacySettingType;
import it.auties.whatsapp.model.privacy.PrivacySettingValue;
import it.auties.whatsapp.model.request.MessageSendRequest;
import it.auties.whatsapp.model.request.SubscribedNewslettersRequest;
import it.auties.whatsapp.model.response.*;
import it.auties.whatsapp.model.signal.auth.*;
import it.auties.whatsapp.model.signal.keypair.SignalKeyPair;
import it.auties.whatsapp.model.signal.keypair.SignalPreKeyPair;
import it.auties.whatsapp.model.sync.PatchType;
import it.auties.whatsapp.util.Bytes;
import it.auties.whatsapp.util.Clock;
import it.auties.whatsapp.util.Json;
import it.auties.whatsapp.util.Validate;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
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.KEY_BUNDLE_TYPE;
class StreamHandler {
private static final byte[] DEVICE_WEB_SIGNATURE_HEADER = {6, 1};
private static final int PRE_KEYS_UPLOAD_CHUNK = 10;
private static final int PING_INTERVAL = 20;
private static final int MAX_MESSAGE_RETRIES = 5;
private static final int DEFAULT_NEWSLETTER_MESSAGES = 100;
private static final byte[][] CALL_RELAY = new byte[][]{
new byte[]{-105, 99, -47, -29, 13, -106},
new byte[]{-99, -16, -53, 62, 13, -106},
new byte[]{-99, -16, -25, 62, 13, -106},
new byte[]{-99, -16, -5, 62, 13, -106},
new byte[]{-71, 60, -37, 62, 13, -106}
};
private static final byte[] ACCOUNT_SIGNATURE_HEADER = {6, 0};
private final SocketHandler socketHandler;
private final WebVerificationHandler webVerificationHandler;
private final Map retries;
private final AtomicReference lastLinkCodeKey;
private final AtomicBoolean retryConnection;
protected StreamHandler(SocketHandler socketHandler, WebVerificationHandler webVerificationHandler) {
this.socketHandler = socketHandler;
this.webVerificationHandler = webVerificationHandler;
this.retries = new ConcurrentHashMap<>();
this.lastLinkCodeKey = new AtomicReference<>();
this.retryConnection = new AtomicBoolean(false);
}
protected void digest(Node node) {
switch (node.description()) {
case "ack" -> digestAck(node);
case "call" -> digestCall(node);
case "failure" -> digestFailure(node);
case "ib" -> digestIb(node);
case "iq" -> digestIq(node);
case "receipt" -> digestReceipt(node);
case "stream:error" -> digestError(node);
case "success" -> digestSuccess(node);
case "message" -> socketHandler.decodeMessage(node, null, true);
case "notification" -> digestNotification(node);
case "presence", "chatstate" -> digestChatState(node);
case "xmlstreamend" -> digestStreamEnd();
}
}
private void digestStreamEnd() {
if(socketHandler.state() == SocketState.CONNECTED) {
socketHandler.disconnect(DisconnectReason.RECONNECTING);
}
}
private void digestFailure(Node node) {
var reason = node.attributes().getInt("reason");
switch (reason) {
case 503, 403 -> socketHandler.disconnect(DisconnectReason.BANNED);
case 401, 405 -> socketHandler.disconnect(DisconnectReason.LOGGED_OUT);
default -> socketHandler.disconnect(DisconnectReason.RECONNECTING);
}
}
private void digestChatState(Node node) {
var chatJid = node.attributes()
.getRequiredJid("from");
var participantJid = node.attributes()
.getOptionalJid("participant")
.orElse(chatJid);
updateContactPresence(chatJid, getUpdateType(node), participantJid);
}
private ContactStatus getUpdateType(Node node) {
var metadata = node.findChild();
var recording = metadata.map(entry -> entry.attributes().getString("media"))
.filter(entry -> entry.equals("audio"))
.isPresent();
if (recording) {
return ContactStatus.RECORDING;
}
return node.attributes()
.getOptionalString("type")
.or(() -> metadata.map(Node::description))
.flatMap(ContactStatus::of)
.orElse(ContactStatus.AVAILABLE);
}
private void updateContactPresence(Jid chatJid, ContactStatus status, Jid contact) {
socketHandler.store()
.findChatByJid(chatJid)
.ifPresent(chat -> socketHandler.onUpdateChatPresence(status, contact, chat));
}
private void digestReceipt(Node node) {
var senderJid = node.attributes()
.getRequiredJid("from");
for (var messageId : getReceiptsMessageIds(node)) {
var message = socketHandler.store().findMessageById(senderJid, messageId);
if (message.isEmpty()) {
continue;
}
switch (message.get()) {
case ChatMessageInfo chatMessageInfo -> onChatReceipt(node, senderJid, chatMessageInfo);
case NewsletterMessageInfo newsletterMessageInfo -> onNewsletterReceipt(node, newsletterMessageInfo);
default -> throw new IllegalStateException("Unexpected value: " + message.get());
}
}
socketHandler.sendMessageAck(senderJid, node);
}
private void onNewsletterReceipt(Node node, NewsletterMessageInfo message) {
var messageStatus = node.attributes()
.getOptionalString("type")
.flatMap(MessageStatus::of);
if (messageStatus.isEmpty()) {
return;
}
message.setStatus(messageStatus.get());
socketHandler.onMessageStatus(message);
}
private void onChatReceipt(Node node, Jid chatJid, ChatMessageInfo message) {
var type = node.attributes().getOptionalString("type");
var status = type.flatMap(MessageStatus::of)
.orElse(MessageStatus.DELIVERED);
socketHandler.store().findChatByJid(chatJid).ifPresent(chat -> {
var newCount = chat.unreadMessagesCount() - 1;
chat.setUnreadMessagesCount(newCount);
var participant = node.attributes()
.getOptionalJid("participant")
.flatMap(socketHandler.store()::findContactByJid)
.orElse(null);
updateReceipt(status, chat, participant, message);
socketHandler.onMessageStatus(message);
});
message.setStatus(status);
if (!Objects.equals(type.orElse(null), "retry")) {
return;
}
acceptMessageRetry(message);
}
private void acceptMessageRetry(ChatMessageInfo message) {
if (!message.fromMe()) {
return;
}
var attempts = retries.getOrDefault(message.id(), 0);
if (attempts > MAX_MESSAGE_RETRIES) {
return;
}
socketHandler.querySessionsForcefully(message.senderJid()).whenCompleteAsync((result, error) -> {
if(error != null) {
return;
}
var all = message.senderJid().device() == 0;
var recipients = all ? null : Set.of(message.senderJid());
var request = new MessageSendRequest.Chat(message, recipients, !all, false, null);
socketHandler.sendMessage(request);
retries.put(message.id(), attempts + 1);
});
}
private void updateReceipt(MessageStatus status, Chat chat, Contact participant, ChatMessageInfo message) {
var container = status == MessageStatus.READ ? message.receipt().readJids() : message.receipt().deliveredJids();
container.add(participant != null ? participant.jid() : message.senderJid());
if(chat == null) {
return;
}
socketHandler.queryGroupMetadata(chat.jid()).thenAcceptAsync(metadata -> {
if (participant != null && metadata.participants().size() != container.size()) {
return;
}
switch (status) {
case READ -> message.receipt().readTimestampSeconds(Clock.nowSeconds());
case PLAYED -> message.receipt().playedTimestampSeconds(Clock.nowSeconds());
}
});
}
private List getReceiptsMessageIds(Node node) {
var messageIds = Stream.ofNullable(node.findChild("list"))
.flatMap(Optional::stream)
.map(list -> list.listChildren("item"))
.flatMap(Collection::stream)
.map(item -> item.attributes().getOptionalString("id"))
.flatMap(Optional::stream)
.collect(Collectors.toList());
messageIds.add(node.attributes().getRequiredString("id"));
return messageIds;
}
private CallStatus getCallStatus(Node node) {
return switch (node.description()) {
case "terminate" ->
node.attributes().hasValue("reason", "timeout") ? CallStatus.TIMED_OUT : CallStatus.REJECTED;
case "reject" -> CallStatus.REJECTED;
case "accept" -> CallStatus.ACCEPTED;
default -> CallStatus.RINGING;
};
}
private void digestCall(Node node) {
var from = node.attributes()
.getRequiredJid("from");
socketHandler.sendMessageAck(from, node);
var callNode = node.children().peekFirst();
if (callNode == null) {
return;
}
if (!callNode.description().equals("offer")) {
return;
}
var callId = callNode.attributes()
.getString("call-id");
var caller = callNode.attributes()
.getOptionalJid("call-creator")
.orElse(from);
var status = getCallStatus(callNode);
var timestampSeconds = callNode.attributes()
.getOptionalLong("t")
.orElseGet(Clock::nowSeconds);
var isOffline = callNode.attributes().hasKey("offline");
var hasVideo = callNode.hasNode("video");
var call = new Call(from, caller, callId, timestampSeconds, hasVideo, status, isOffline);
socketHandler.store().addCall(call);
socketHandler.onCall(call);
}
private void digestAck(Node node) {
var ackClass = node.attributes().getString("class");
switch (ackClass) {
case "call" -> digestCallAck(node);
case "message" -> digestMessageAck(node);
}
}
private void digestMessageAck(Node node) {
var error = node.attributes().getInt("error");
var messageId = node.id();
var from = node.attributes()
.getRequiredJid("from");
var match = socketHandler.store()
.findMessageById(from, messageId)
.orElse(null);
if (match == null) {
return;
}
if (error != 0) {
match.setStatus(MessageStatus.ERROR);
return;
}
if (match.status().index() >= MessageStatus.SERVER_ACK.index()) {
return;
}
match.setStatus(MessageStatus.SERVER_ACK);
}
private void digestCallAck(Node node) {
var relayNode = node.findChild("relay").orElse(null);
if (relayNode == null) {
return;
}
var callCreator = relayNode.attributes()
.getRequiredJid("call-creator");
var callId = relayNode.attributes()
.getString("call-id");
relayNode.listChildren("participant")
.stream()
.map(entry -> entry.attributes().getOptionalJid("jid"))
.flatMap(Optional::stream)
.forEach(to -> sendRelay(callCreator, callId, to));
}
private void sendRelay(Jid callCreator, String callId, Jid to) {
for (var value : CALL_RELAY) {
var te = Node.of("te", Map.of("latency", 33554440), value);
var relay = Node.of("relaylatency", Map.of("call-creator", callCreator, "call-id", callId), te);
socketHandler.sendNode(Node.of("call", Map.of("to", to), relay));
}
}
private void digestNotification(Node node) {
var from = node.attributes()
.getRequiredJid("from");
try {
var type = node.attributes().getString("type", null);
switch (type) {
case "w:gp2" -> handleGroupNotification(node);
case "server_sync" -> handleServerSyncNotification(node);
case "account_sync" -> handleAccountSyncNotification(node);
case "encrypt" -> handleEncryptNotification(node);
case "picture" -> handlePictureNotification(node);
case "registration" -> handleRegistrationNotification(node);
case "link_code_companion_reg" -> handleCompanionRegistration(node);
case "newsletter" -> handleNewsletter(from, node);
case "mex" -> handleMexNamespace(node);
}
} finally {
socketHandler.sendMessageAck(from, node);
}
}
private void handleNewsletter(Jid newsletterJid, Node node) {
var newsletter = socketHandler.store()
.findNewsletterByJid(newsletterJid);
if (newsletter.isEmpty()) {
return;
}
var liveUpdates = node.findChild("live_updates");
if (liveUpdates.isEmpty()) {
return;
}
var messages = liveUpdates.get().findChild("messages");
if (messages.isEmpty()) {
return;
}
for (var messageNode : messages.get().listChildren("message")) {
var messageId = messageNode.attributes()
.getRequiredString("server_id");
var newsletterMessage = socketHandler.store().findMessageById(newsletter.get(), messageId);
if (newsletterMessage.isEmpty()) {
continue;
}
messageNode.findChild("reactions")
.map(reactions -> reactions.listChildren("reaction"))
.stream()
.flatMap(Collection::stream)
.forEach(reaction -> onNewsletterReaction(reaction, newsletterMessage.get()));
}
}
private void onNewsletterReaction(Node reaction, NewsletterMessageInfo newsletterMessage) {
var reactionCode = reaction.attributes()
.getRequiredString("code");
var reactionCount = reaction.attributes()
.getRequiredInt("count");
var newReaction = new NewsletterReaction(reactionCode, reactionCount, false);
newsletterMessage.addReaction(newReaction)
.ifPresent(oldReaction -> newReaction.setFromMe(oldReaction.fromMe()));
}
private void handleMexNamespace(Node node) {
var update = node.findChild("update")
.orElse(null);
if (update == null) {
return;
}
switch (update.attributes().getString("op_name")) {
case "NotificationNewsletterJoin" -> handleNewsletterJoin(update);
case "NotificationNewsletterMuteChange" -> handleNewsletterMute(update);
case "NotificationNewsletterLeave" -> handleNewsletterLeave(update);
case "NotificationNewsletterUpdate" -> handleNewsletterMetadataUpdate(update);
case "NotificationNewsletterStateChange" -> handleNewsletterStateUpdate(update);
}
}
private void handleNewsletterStateUpdate(Node update) {
var updatePayload = update.contentAsString()
.orElseThrow(() -> new NoSuchElementException("Missing state update payload"));
var updateJson = NewsletterStateResponse.ofJson(updatePayload)
.orElseThrow(() -> new NoSuchElementException("Malformed state update payload"));
var newsletter = socketHandler.store()
.findNewsletterByJid(updateJson.jid())
.orElseThrow(() -> new NoSuchElementException("Missing newsletter"));
newsletter.setState(updateJson.state());
}
private void handleNewsletterMetadataUpdate(Node update) {
var updatePayload = update.contentAsString()
.orElseThrow(() -> new NoSuchElementException("Missing update payload"));
var updateJson = NewsletterResponse.ofJson(updatePayload)
.orElseThrow(() -> new NoSuchElementException("Malformed update payload"));
var newsletter = socketHandler.store()
.findNewsletterByJid(updateJson.newsletter().jid())
.orElseThrow(() -> new NoSuchElementException("Missing newsletter"));
var oldMetadata = newsletter.metadata();
var updatedMetadata = updateJson.newsletter().metadata();
var mergedMetadata = new NewsletterMetadata(
updatedMetadata.name().or(oldMetadata::name),
updatedMetadata.description().or(oldMetadata::description),
updatedMetadata.picture().or(oldMetadata::picture),
updatedMetadata.handle().or(oldMetadata::handle),
updatedMetadata.settings().or(oldMetadata::settings),
updatedMetadata.invite().or(oldMetadata::invite),
updatedMetadata.verification().or(oldMetadata::verification),
updatedMetadata.creationTimestampSeconds().isPresent() ? updatedMetadata.creationTimestampSeconds() : oldMetadata.creationTimestampSeconds()
);
newsletter.setMetadata(mergedMetadata);
}
private void handleNewsletterJoin(Node update) {
var joinPayload = update.contentAsString()
.orElseThrow(() -> new NoSuchElementException("Missing join payload"));
var joinJson = NewsletterResponse.ofJson(joinPayload)
.orElseThrow(() -> new NoSuchElementException("Malformed join payload"));
socketHandler.store().addNewsletter(joinJson.newsletter());
if(!socketHandler.store().historyLength().isZero()) {
socketHandler.queryNewsletterMessages(joinJson.newsletter().jid(), DEFAULT_NEWSLETTER_MESSAGES);
}
}
private void handleNewsletterMute(Node update) {
var mutePayload = update.contentAsString()
.orElseThrow(() -> new NoSuchElementException("Missing mute payload"));
var muteJson = NewsletterMuteResponse.ofJson(mutePayload)
.orElseThrow(() -> new NoSuchElementException("Malformed mute payload"));
var newsletter = socketHandler.store()
.findNewsletterByJid(muteJson.jid())
.orElseThrow(() -> new NoSuchElementException("Missing newsletter"));
newsletter.viewerMetadata()
.ifPresent(viewerMetadata -> viewerMetadata.setMute(muteJson.mute()));
}
private void handleNewsletterLeave(Node update) {
var leavePayload = update.contentAsString()
.orElseThrow(() -> new NoSuchElementException("Missing leave payload"));
var leaveJson = NewsletterLeaveResponse.ofJson(leavePayload)
.orElseThrow(() -> new NoSuchElementException("Malformed leave payload"));
socketHandler.store().removeNewsletter(leaveJson.jid());
}
private void handleCompanionRegistration(Node node) {
var phoneNumber = getPhoneNumberAsJid();
var linkCodeCompanionReg = node.findChild("link_code_companion_reg")
.orElseThrow(() -> new NoSuchElementException("Missing link_code_companion_reg: " + node));
var ref = linkCodeCompanionReg.findChild("link_code_pairing_ref")
.flatMap(Node::contentAsBytes)
.orElseThrow(() -> new IllegalArgumentException("Missing link_code_pairing_ref: " + node));
var primaryIdentityPublicKey = linkCodeCompanionReg.findChild("primary_identity_pub")
.flatMap(Node::contentAsBytes)
.orElseThrow(() -> new IllegalArgumentException("Missing primary_identity_pub: " + node));
var primaryEphemeralPublicKeyWrapped = linkCodeCompanionReg.findChild("link_code_pairing_wrapped_primary_ephemeral_pub")
.flatMap(Node::contentAsBytes)
.orElseThrow(() -> new IllegalArgumentException("Missing link_code_pairing_wrapped_primary_ephemeral_pub: " + node));
var codePairingPublicKey = decipherLinkPublicKey(primaryEphemeralPublicKeyWrapped);
var companionSharedKey = Curve25519.sharedKey(codePairingPublicKey, socketHandler.keys().companionKeyPair().privateKey());
var random = Bytes.random(32);
var linkCodeSalt = Bytes.random(32);
var linkCodePairingExpanded = Hkdf.extractAndExpand(companionSharedKey, linkCodeSalt, "link_code_pairing_key_bundle_encryption_key".getBytes(StandardCharsets.UTF_8), 32);
var encryptPayload = Bytes.concat(socketHandler.keys().identityKeyPair().publicKey(), primaryIdentityPublicKey, random);
var encryptIv = Bytes.random(12);
var encrypted = AesGcm.encrypt(encryptIv, encryptPayload, linkCodePairingExpanded);
var encryptedPayload = Bytes.concat(linkCodeSalt, encryptIv, encrypted);
var identitySharedKey = Curve25519.sharedKey(primaryIdentityPublicKey, socketHandler.keys().identityKeyPair().privateKey());
var identityPayload = Bytes.concat(companionSharedKey, identitySharedKey, random);
var advSecretPublicKey = Hkdf.extractAndExpand(identityPayload, "adv_secret".getBytes(StandardCharsets.UTF_8), 32);
socketHandler.keys().setCompanionKeyPair(new SignalKeyPair(advSecretPublicKey, socketHandler.keys().companionKeyPair().privateKey()));
var confirmation = Node.of(
"link_code_companion_reg",
Map.of("jid", phoneNumber, "stage", "companion_finish"),
Node.of("link_code_pairing_wrapped_key_bundle", encryptedPayload),
Node.of("companion_identity_public", socketHandler.keys().identityKeyPair().publicKey()),
Node.of("link_code_pairing_ref", ref)
);
socketHandler.sendQuery("set", "md", confirmation);
}
private byte[] decipherLinkPublicKey(byte[] primaryEphemeralPublicKeyWrapped) {
try {
var salt = Arrays.copyOfRange(primaryEphemeralPublicKeyWrapped, 0, 32);
var secretKey = getSecretPairingKey(lastLinkCodeKey.get(), salt);
var iv = Arrays.copyOfRange(primaryEphemeralPublicKeyWrapped, 32, 48);
var payload = Arrays.copyOfRange(primaryEphemeralPublicKeyWrapped, 48, 80);
var cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
return cipher.doFinal(payload);
} catch (GeneralSecurityException exception) {
throw new RuntimeException("Cannot decipher link code pairing key", exception);
}
}
private void handleRegistrationNotification(Node node) {
var child = node.findChild("wa_old_registration");
if (child.isEmpty()) {
return;
}
var code = child.get().attributes().getOptionalLong("code");
if (code.isEmpty()) {
return;
}
socketHandler.onRegistrationCode(code.getAsLong());
}
private void handlePictureNotification(Node node) {
var fromJid = node.attributes()
.getRequiredJid("from");
var fromChat = socketHandler.store()
.findChatByJid(fromJid)
.orElseGet(() -> socketHandler.store().addNewChat(fromJid));
var timestamp = node.attributes().getLong("t");
if (fromChat.isGroup()) {
addMessageForGroupStubType(fromChat, ChatMessageInfo.StubType.GROUP_CHANGE_ICON, timestamp, node);
socketHandler.onGroupPictureChanged(fromChat);
return;
}
var fromContact = socketHandler.store().findContactByJid(fromJid).orElseGet(() -> {
var contact = socketHandler.store().addContact(fromJid);
socketHandler.onNewContact(contact);
return contact;
});
socketHandler.onContactPictureChanged(fromContact);
}
private void handleGroupNotification(Node node) {
var child = node.findChild();
if (child.isEmpty()) {
return;
}
var stubType = ChatMessageInfo.StubType.of(child.get().description());
if (stubType.isEmpty()) {
return;
}
// TODO: Handle all cases
if (stubType.get() == ChatMessageInfo.StubType.GROUP_CHANGE_SUBJECT) {
onGroupSubjectChange(node);
}
handleGroupStubNotification(node, stubType.get());
}
private void onGroupSubjectChange(Node node) {
var subject = node.findChild("subject")
.flatMap(subjectNode -> subjectNode.attributes().getOptionalString("subject"))
.orElse(null);
if(subject == null) {
return;
}
var fromJid = node.attributes()
.getRequiredJid("from");
socketHandler.store()
.findChatByJid(fromJid)
.ifPresent(chat -> chat.setName(subject));
}
private void handleGroupStubNotification(Node node, ChatMessageInfo.StubType stubType) {
var timestamp = node.attributes().getLong("t");
var fromJid = node.attributes()
.getRequiredJid("from");
var fromChat = socketHandler.store()
.findChatByJid(fromJid)
.orElseGet(() -> socketHandler.store().addNewChat(fromJid));
addMessageForGroupStubType(fromChat, stubType, timestamp, node);
}
private void addMessageForGroupStubType(Chat chat, ChatMessageInfo.StubType stubType, long timestamp, Node metadata) {
var participantJid = metadata.attributes()
.getOptionalJid("participant")
.orElse(null);
var parameters = getStubTypeParameters(metadata);
var key = new ChatMessageKeyBuilder()
.id(ChatMessageKey.randomIdV2(Objects.requireNonNullElse(participantJid, chat.jid()), socketHandler.store().clientType()))
.chatJid(chat.jid())
.senderJid(participantJid)
.build();
var message = new ChatMessageInfoBuilder()
.status(MessageStatus.PENDING)
.timestampSeconds(timestamp)
.key(key)
.ignore(true)
.stubType(stubType)
.stubParameters(parameters)
.senderJid(participantJid)
.build();
chat.addNewMessage(message);
socketHandler.onNewMessage(message);
if (participantJid == null) {
return;
}
handleGroupStubType(timestamp, chat, stubType, participantJid);
}
private void handleGroupStubType(long timestamp, Chat chat, ChatMessageInfo.StubType stubType, Jid participantJid) {
switch (stubType) {
case GROUP_PARTICIPANT_REMOVE, GROUP_PARTICIPANT_LEAVE -> {
var reason = stubType == ChatMessageInfo.StubType.GROUP_PARTICIPANT_REMOVE ? ChatPastParticipant.Reason.REMOVED : ChatPastParticipant.Reason.LEFT;
var pastParticipant = new ChatPastParticipant(participantJid, reason, timestamp);
socketHandler.addPastParticipant(chat.jid(), pastParticipant);
}
case GROUP_PARTICIPANT_ADD -> {
var pastParticipants = socketHandler.pastParticipants().get(chat.jid());
if(pastParticipants == null) {
return;
}
pastParticipants.removeIf(entry -> Objects.equals(entry.jid(), participantJid));
}
}
}
private List getStubTypeParameters(Node metadata) {
try {
var mapper = new ObjectMapper();
var attributes = new ArrayList();
attributes.add(mapper.writeValueAsString(metadata.attributes().toMap()));
for (var child : metadata.children()) {
var data = child.attributes();
if (data.isEmpty()) {
continue;
}
attributes.add(mapper.writeValueAsString(data.toMap()));
}
return Collections.unmodifiableList(attributes);
} catch (IOException exception) {
throw new UncheckedIOException("Cannot encode stub parameters", exception);
}
}
private void handleEncryptNotification(Node node) {
var chat = node.attributes()
.getRequiredJid("from");
if (!chat.isServerJid(JidServer.WHATSAPP)) {
return;
}
var keysSize = node.findChild("count")
.orElseThrow(() -> new NoSuchElementException("Missing count in notification"))
.attributes()
.getInt("value");
sendPreKeys(keysSize);
}
private void handleAccountSyncNotification(Node node) {
var child = node.findChild();
if (child.isEmpty()) {
return;
}
switch (child.get().description()) {
case "devices" -> handleDevices(child.get());
case "privacy" -> changeUserPrivacySetting(child.get());
case "disappearing_mode" -> updateUserDisappearingMode(child.get());
case "status" -> updateUserAbout(true);
case "picture" -> updateUserPicture(true);
case "blocklist" -> updateBlocklist(child.orElse(null));
}
}
private void handleDevices(Node child) {
var deviceHash = child.attributes().getString("dhash");
socketHandler.store().setDeviceHash(deviceHash);
var devices = child.listChildren("device")
.stream()
.collect(Collectors.toMap(
entry -> entry.attributes().getRequiredJid("jid"),
entry -> entry.attributes().getInt("key-index"),
(first, second) -> second,
LinkedHashMap::new
));
var companionJid = socketHandler.store()
.jid()
.orElseThrow(() -> new IllegalStateException("The session isn't connected"))
.toSimpleJid();
var companionDevice = devices.remove(companionJid);
devices.put(companionJid, companionDevice);
socketHandler.store().setLinkedDevicesKeys(devices);
socketHandler.onDevices(devices);
var keyIndexListNode = child.findChild("key-index-list")
.orElseThrow(() -> new NoSuchElementException("Missing index key node from device sync"));
var signedKeyIndexBytes = keyIndexListNode.contentAsBytes()
.orElseThrow(() -> new NoSuchElementException("Missing index key from device sync"));
socketHandler.keys().setSignedKeyIndex(signedKeyIndexBytes);
var signedKeyIndexTimestamp = keyIndexListNode.attributes().getLong("ts");
socketHandler.keys().setSignedKeyIndexTimestamp(signedKeyIndexTimestamp);
}
private void updateBlocklist(Node child) {
child.listChildren("item").forEach(this::updateBlocklistEntry);
}
private void updateBlocklistEntry(Node entry) {
entry.attributes()
.getOptionalJid("jid")
.flatMap(socketHandler.store()::findContactByJid)
.ifPresent(contact -> {
contact.setBlocked(Objects.equals(entry.attributes().getString("action"), "block"));
socketHandler.onContactBlocked(contact);
});
}
private void changeUserPrivacySetting(Node child) {
var category = child.listChildren("category");
category.forEach(entry -> addPrivacySetting(entry, true));
}
private void updateUserDisappearingMode(Node child) {
var timer = ChatEphemeralTimer.of(child.attributes().getInt("duration"));
socketHandler.store().setNewChatsEphemeralTimer(timer);
}
private CompletableFuture addPrivacySetting(Node node, boolean update) {
var privacySettingName = node.attributes().getString("name");
var privacyType = PrivacySettingType.of(privacySettingName);
if(privacyType.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
var privacyValueName = node.attributes().getString("value");
var privacyValue = PrivacySettingValue.of(privacyValueName);
if(privacyValue.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
if (!update) {
return queryPrivacyExcludedContacts(privacyType.get(), privacyValue.get())
.thenAcceptAsync(response -> socketHandler.store().addPrivacySetting(privacyType.get(), new PrivacySettingEntry(privacyType.get(), privacyValue.get(), response)));
}
var oldEntry = socketHandler.store().findPrivacySetting(privacyType.get());
var newValues = getUpdatedBlockedList(node, oldEntry, privacyValue.get());
var newEntry = new PrivacySettingEntry(privacyType.get(), privacyValue.get(), Collections.unmodifiableList(newValues));
socketHandler.store().addPrivacySetting(privacyType.get(), newEntry);
socketHandler.onPrivacySettingChanged(oldEntry, newEntry);
return CompletableFuture.completedFuture(null);
}
private List getUpdatedBlockedList(Node node, PrivacySettingEntry privacyEntry, PrivacySettingValue privacyValue) {
if (privacyValue != PrivacySettingValue.CONTACTS_EXCEPT) {
return List.of();
}
var newValues = new ArrayList<>(privacyEntry.excluded());
for (var entry : node.listChildren("user")) {
var jid = entry.attributes()
.getRequiredJid("jid");
if (entry.attributes().hasValue("action", "add")) {
newValues.add(jid);
continue;
}
newValues.remove(jid);
}
return newValues;
}
private CompletableFuture> queryPrivacyExcludedContacts(PrivacySettingType type, PrivacySettingValue value) {
if (value != PrivacySettingValue.CONTACTS_EXCEPT) {
return CompletableFuture.completedFuture(List.of());
}
return socketHandler.sendQuery("get", "privacy", Node.of("privacy", Node.of("list", Map.of("name", type.data(), "value", value.data()))))
.thenApplyAsync(this::parsePrivacyExcludedContacts);
}
private List parsePrivacyExcludedContacts(Node result) {
return result.findChild("privacy")
.flatMap(node -> node.findChild("list"))
.map(node -> node.listChildren("user"))
.stream()
.flatMap(Collection::stream)
.map(user -> user.attributes().getOptionalJid("jid"))
.flatMap(Optional::stream)
.toList();
}
private void handleServerSyncNotification(Node node) {
if(!socketHandler.keys().initialAppSync()) {
return;
}
var patches = node.listChildren("collection")
.stream()
.map(entry -> entry.attributes().getRequiredString("name"))
.map(PatchType::of)
.toArray(PatchType[]::new);
socketHandler.pullPatch(patches);
}
private void digestIb(Node node) {
var dirty = node.findChild("dirty");
if (dirty.isEmpty()) {
Validate.isTrue(!node.hasNode("downgrade_webclient"), "Multi device beta is not enabled. Please enable it from Whatsapp");
return;
}
var type = dirty.get().attributes().getString("type");
if (!Objects.equals(type, "account_sync")) {
return;
}
var timestamp = dirty.get().attributes().getString("timestamp");
socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty",
Node.of("clean", Map.of("type", type, "timestamp", timestamp)));
}
private void digestError(Node node) {
if(node.hasNode("conflict")) {
socketHandler.disconnect(DisconnectReason.RECONNECTING);
return;
}
if (node.hasNode("bad-mac")) {
socketHandler.handleFailure(CRYPTOGRAPHY, new RuntimeException("Detected a bad mac. Unresolved nodes: " + getUnresolvedNodes()));
return;
}
var statusCode = node.attributes().getInt("code");
switch (statusCode) {
case 403, 503 -> socketHandler.disconnect(retryConnection.getAndSet(true) ? DisconnectReason.BANNED : DisconnectReason.RECONNECTING);
case 500 -> socketHandler.disconnect(DisconnectReason.LOGGED_OUT);
case 401 -> handleStreamError(node);
default -> node.children().forEach(error -> socketHandler.store().resolvePendingRequest(error, true));
}
}
private String getUnresolvedNodes() {
return socketHandler.store()
.pendingRequests()
.stream()
.map(SocketRequest::body)
.map(String::valueOf)
.collect(Collectors.joining("\n"));
}
private void handleStreamError(Node node) {
var child = node.children().getFirst();
var type = child.attributes().getString("type");
var reason = child.attributes().getString("reason", type);
if (!Objects.equals(reason, "device_removed")) {
socketHandler.handleFailure(STREAM, new RuntimeException(reason));
return;
}
socketHandler.disconnect(DisconnectReason.LOGGED_OUT);
}
private void digestSuccess(Node node) {
socketHandler.confirmConnection();
node.attributes().getOptionalJid("lid")
.ifPresent(socketHandler.store()::setLid);
finishLogin();
}
private CompletableFuture attributeStore() {
return socketHandler.store()
.serializer()
.attributeStore(socketHandler.store())
.exceptionallyAsync(exception -> socketHandler.handleFailure(MESSAGE, exception));
}
private void finishLogin() {
switch (socketHandler.store().clientType()) {
case WEB -> finishWebLogin();
case MOBILE -> finishMobileLogin();
};
}
private void finishWebLogin() {
var loginFuture = CompletableFuture.allOf(
setActiveConnection(),
queryRequiredWebInfo(),
sendInitialPreKeys(),
scheduleMediaConnectionUpdate(),
updateSelfPresence(),
queryInitial2fa(),
queryInitialAboutPrivacy(),
queryInitialPrivacySettings(),
queryInitialDisappearingMode(),
queryInitialBlockList()
)
.thenRunAsync(this::onInitialInfo)
.exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable));
CompletableFuture.allOf(loginFuture, attributeStore())
.thenComposeAsync(result -> loadAdditionalWebData())
.thenRunAsync(this::notifyChatsAndNewsletters);
}
private CompletableFuture setActiveConnection() {
return socketHandler.sendQuery("set", "passive", Node.of("active"));
}
private CompletableFuture> sendInitialPreKeys() {
if (socketHandler.keys().hasPreKeys()) {
return CompletableFuture.completedFuture(null);
}
return sendPreKeys(PRE_KEYS_UPLOAD_CHUNK);
}
private CompletableFuture loadAdditionalWebData() {
if (socketHandler.keys().initialAppSync()) {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(queryGroups(), queryNewsletters());
}
private void finishMobileLogin() {
if (!socketHandler.keys().initialAppSync()) {
initMobileSession();
return;
}
var loginFuture = CompletableFuture.allOf(
setupRescueToken(),
setActiveConnection(),
queryMobileSessionMex(),
acceptDynamicTermsOfService(),
setPushEndpoint(),
updateSelfPresence(),
scheduleMediaConnectionUpdate()
)
.thenRunAsync(this::onInitialInfo);
CompletableFuture.allOf(loginFuture, attributeStore())
.thenRunAsync(this::notifyChatsAndNewsletters)
.exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable));
}
private CompletableFuture acceptDynamicTermsOfService() {
return socketHandler.sendQuery("get", "tos", Node.of("get_user_disclosures", Map.of("t", 0))).thenComposeAsync(result -> {
var notices = result.listChildren("notice")
.stream()
.map(notice -> Node.of("notice", Map.of("id", notice.attributes().getRequiredString("id"))))
.map(notice -> socketHandler.sendQuery("get", "tos", Node.of("request", notice)))
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(notices);
});
}
private CompletableFuture queryMobileSessionMex() {
return CompletableFuture.allOf(
socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7561558900567547"), HexFormat.of().parseHex("7b227661726961626c6573223a7b7d7d"))),
socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7480997188628461"), HexFormat.of().parseHex("7b227661726961626c6573223a7b22696e707574223a22454d41494c227d7d")))
);
}
private void initMobileSession() {
initMobileSessionPresence(false)
.thenComposeAsync(result -> CompletableFuture.allOf(
setPushEndpoint(),
queryProtocolV2(),
queryGroups(),
queryLists(),
updateUserPicture(false),
updateUserAbout(false),
sendInitialPreKeys(),
setActiveConnection(),
setDefaultStatus(),
queryInitial2fa(),
queryInitialAboutPrivacy(),
queryInitialPrivacySettings(),
queryInitialDisappearingMode(),
queryInitialBlockList(),
acceptTermsOfService(),
queryProtocolV1(),
setupGoogleCrypto(),
resetCompanionDevices(),
checkBusinessStatus(),
setupGoogleCrypto(),
cleanGroups(),
initMobileSessionPresence(true),
getInviteSender(),
queryMobileSessionInitMex()
))
.thenComposeAsync(ignored -> {
socketHandler.keys().setInitialAppSync(true);
return socketHandler.disconnect(DisconnectReason.RECONNECTING);
})
.exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable));
}
private CompletableFuture initMobileSessionPresence(boolean done) {
if(!done) {
return socketHandler.sendNodeWithNoResponse(Node.of("presence", Map.of("type", "unavailable")));
}
return socketHandler.sendNodeWithNoResponse(Node.of("presence", Map.of("type", "available", "name", socketHandler.store().name())));
}
private CompletableFuture queryMobileSessionInitMex() {
return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7561558900567547"), HexFormat.of().parseHex("7b227661726961626c6573223a7b7d7d")));
}
private CompletableFuture cleanGroups() {
return socketHandler.sendQuery("set", "urn:xmpp:whatsapp:dirty", Node.of("clean", Map.of("type", "groups")));
}
private CompletableFuture resetCompanionDevices() {
return socketHandler.sendQuery("set", "md", Node.of("remove-companion-device", Map.of("all", true, "reason", "user_initiated")));
}
private CompletableFuture queryProtocolV1() {
return socketHandler.sendQuery("get", "abt", Node.of("props", Map.of("protocol", 1)));
}
private CompletableFuture queryLists() {
return socketHandler.sendQuery("get", "w:b", Node.of("lists"));
}
private CompletableFuture queryProtocolV2() {
return socketHandler.sendQuery("get", "w", Node.of("props", Map.of("protocol", "2", "hash", "")));
}
private CompletableFuture> getInviteSender() {
return socketHandler.sendQuery("get", "w:growth", Node.of("invite", Node.of("get_sender")));
}
private CompletableFuture setDefaultStatus() {
return socketHandler.changeAbout("Hey there! I am using WhatsApp.");
}
private CompletableFuture> setPushEndpoint() {
var configAttributes = Attributes.of()
.put("background_location", 1)
.put("call", "Opening.m4r")
.put("default", "note.m4r")
.put("groups", "node.m4r")
.put("id", HexFormat.of().formatHex(Bytes.random(32)))
.put("lc", "US")
.put("lg", "en")
.put("nse_call", 0)
.put("nse_read", 0)
.put("nse_ver", 2)
.put("pkey", Base64.getUrlEncoder().encodeToString(SignalKeyPair.random().publicKey()))
.put("platform", "apple")
.put("preview", 1)
.put("reg_push", 1)
.put("version", 2)
.put("voip", HexFormat.of().formatHex(Bytes.random(32)))
.put("voip_payload_type", 2)
.toMap();
return socketHandler.sendQuery("set", "urn:xmpp:whatsapp:push", Node.of("config", configAttributes));
}
private CompletableFuture> setupRescueToken() {
return socketHandler.sendQuery("set", "w:auth:token", Node.of("token", HexFormat.of().parseHex("20292dbd11e06094feb1908737ca76e6")));
}
private CompletableFuture> setupGoogleCrypto() {
var firstCrypto = Node.of("crypto", Map.of("action", "create"), Node.of("google", HexFormat.of().parseHex("7d7ce52cde18aa4854bf522bc72899074e06b60b1bf51864de82e8576b759d12")));
var secondCrypto = Node.of("crypto", Map.of("action", "create"), Node.of("google", HexFormat.of().parseHex("2f39184f8feb97d57493a69bf5558507472c6bfb633b1c2d369f3409210401c6")));
return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", firstCrypto)
.thenCompose(ignored -> socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", secondCrypto));
}
private CompletableFuture> acceptTermsOfService() {
var notices = Stream.of("20230901", "20240729", "20230902", "20231027")
.map(id -> Node.of("notice", Map.of("id", id)))
.toList();
return socketHandler.sendQuery("get", "tos", Node.of("request", notices))
.thenComposeAsync(ignored -> socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("accept")));
}
private void notifyChatsAndNewsletters() {
if(socketHandler.store().clientType() != ClientType.WEB || socketHandler.keys().initialAppSync()) {
socketHandler.onChats();
socketHandler.onNewsletters();
}
}
private CompletableFuture queryNewsletters() {
var query = new SubscribedNewslettersRequest(new SubscribedNewslettersRequest.Variable());
return socketHandler.sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "6388546374527196"), Json.writeValueAsBytes(query)))
.thenAcceptAsync(this::parseNewsletters)
.exceptionallyAsync(throwable -> socketHandler.handleFailure(LOGIN, throwable));
}
private void parseNewsletters(Node result) {
var newslettersPayload = result.findChild("result")
.flatMap(Node::contentAsString);
if(newslettersPayload.isEmpty()) {
return;
}
var newslettersJson = SubscribedNewslettersResponse.ofJson(newslettersPayload.get())
.orElseThrow(() -> new NoSuchElementException("Malformed newsletters payload: " + newslettersPayload));
onNewsletters(newslettersJson);
}
private void onNewsletters(SubscribedNewslettersResponse result) {
var noMessages = socketHandler.store().historyLength().isZero();
var data = result.newsletters();
var futures = noMessages ? null : new CompletableFuture>[data.size()];
for (var index = 0; index < data.size(); index++) {
var newsletter = data.get(index);
socketHandler.store().addNewsletter(newsletter);
if(!noMessages) {
futures[index] = socketHandler.queryNewsletterMessages(newsletter, DEFAULT_NEWSLETTER_MESSAGES);
}
}
if(noMessages) {
socketHandler.onNewsletters();
return;
}
CompletableFuture.allOf(futures)
.thenRun(socketHandler::onNewsletters)
.exceptionally(throwable -> socketHandler.handleFailure(MESSAGE, throwable));
}
private CompletableFuture queryGroups() {
return socketHandler.sendQuery(JidServer.GROUP_OR_COMMUNITY.toJid(), "get", "w:g2", Node.of("participating", Node.of("participants"), Node.of("description")))
.thenAcceptAsync(this::onGroupsQuery);
}
private void onGroupsQuery(Node result) {
var groups = result.findChild("groups");
if (groups.isEmpty()) {
return;
}
groups.get()
.listChildren("group")
.forEach(socketHandler::handleGroupMetadata);
}
protected CompletableFuture updateBusinessCertificate(String name) {
var details = new BusinessVerifiedNameDetailsBuilder()
.name(Objects.requireNonNullElse(name, socketHandler.store().name()))
.issuer("smb:wa")
.serial(Math.abs(ThreadLocalRandom.current().nextLong()))
.build();
var encodedDetails = BusinessVerifiedNameDetailsSpec.encode(details);
var certificate = new BusinessVerifiedNameCertificateBuilder()
.encodedDetails(encodedDetails)
.signature(Curve25519.sign(socketHandler.keys().identityKeyPair().privateKey(), encodedDetails, true))
.build();
return socketHandler.sendQuery("set", "w:biz", Node.of("verified_name", Map.of("v", 2), BusinessVerifiedNameCertificateSpec.encode(certificate))).thenAccept(result -> {
var verifiedName = result.findChild("verified_name")
.map(node -> node.attributes().getString("id"))
.orElse("");
socketHandler.store().setVerifiedName(verifiedName);
});
}
private CompletableFuture setBusinessProfile() {
var version = socketHandler.store().properties().getOrDefault("biz_profile_options", "2");
var body = new ArrayList();
socketHandler.store()
.businessAddress()
.ifPresent(value -> body.add(Node.of("address", value)));
socketHandler.store()
.businessLongitude()
.ifPresent(value -> body.add(Node.of("longitude", value)));
socketHandler.store()
.businessLatitude()
.ifPresent(value -> body.add(Node.of("latitude", value)));
socketHandler.store()
.businessDescription()
.ifPresent(value -> body.add(Node.of("description", value)));
socketHandler.store()
.businessWebsite()
.ifPresent(value -> body.add(Node.of("website", value)));
socketHandler.store()
.businessEmail()
.ifPresent(value -> body.add(Node.of("email", value)));
return getBusinessCategoryNode().thenComposeAsync(result -> {
body.add(Node.of("categories", Node.of("category", Map.of("id", result.id()))));
return socketHandler.sendQuery("set", "w:biz", Node.of("business_profile", Map.of("v", version), body));
});
}
private CompletableFuture getBusinessCategoryNode() {
return socketHandler.store()
.businessCategory()
.map(businessCategory -> CompletableFuture.completedFuture(Node.of("category", Map.of("id", businessCategory.id()))))
.orElseGet(() -> socketHandler.queryBusinessCategories()
.thenApplyAsync(entries -> Node.of("category", Map.of("id", entries.getFirst().id()))));
}
private void onInitialInfo() {
if(!socketHandler.keys().registered()) {
socketHandler.keys().setRegistered(true);
socketHandler.store().serialize(true);
socketHandler.keys().serialize(true);
}
if(socketHandler.store().clientType() == ClientType.MOBILE) {
socketHandler.store()
.jid()
.map(Jid::toSimpleJid)
.ifPresent(jid -> {
var me = new Contact(jid, socketHandler.store().name(), null, null, ContactStatus.AVAILABLE, Clock.nowSeconds(), false);
socketHandler.store().addContact(me);
});
}
schedulePing();
retryConnection.set(false);
socketHandler.onLoggedIn();
if (socketHandler.keys().initialAppSync()) {
socketHandler.onContacts();
}
}
private CompletableFuture queryRequiredWebInfo() {
return socketHandler.sendQuery("get", "w", Node.of("props"))
.thenAcceptAsync(this::parseProps)
.exceptionallyAsync(exception -> socketHandler.handleFailure(LOGIN, exception));
}
private CompletableFuture checkBusinessStatus() {
if (!socketHandler.store().device().platform().isBusiness() || socketHandler.keys().businessCertificate()) {
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.allOf(updateBusinessCertificate(null), setBusinessProfile())
.thenRunAsync(() -> socketHandler.keys().setBusinessCertificate(true));
}
private CompletableFuture queryInitial2fa() {
return socketHandler.sendQuery("get", "urn:xmpp:whatsapp:account", Node.of("2fa"))
.thenAcceptAsync(result -> { /* TODO: Handle 2FA */ });
}
private CompletableFuture queryInitialAboutPrivacy() {
return socketHandler.sendQuery("get", "status", Node.of("privacy"))
.thenAcceptAsync(result -> { /* TODO: Handle about privacy */ });
}
private CompletableFuture queryInitialPrivacySettings() {
return socketHandler.sendQuery("get", "privacy", Node.of("privacy"))
.thenComposeAsync(this::parsePrivacySettings);
}
private CompletableFuture queryInitialDisappearingMode() {
return socketHandler.sendQuery("get", "disappearing_mode")
.thenAcceptAsync(result -> { /* TODO: Handle disappearing mode */ });
}
private CompletableFuture queryInitialBlockList() {
return socketHandler.queryBlockList()
.thenAcceptAsync(entry -> entry.forEach(this::markBlocked));
}
private CompletableFuture updateSelfPresence() {
if (!socketHandler.store().automaticPresenceUpdates()) {
if(!socketHandler.store().online()) { // Just to be sure
socketHandler.sendNodeWithNoResponse(Node.of("presence", Map.of("name", socketHandler.store().name(), "type", "unavailable")));
}
return CompletableFuture.completedFuture(null);
}
return socketHandler.sendNodeWithNoResponse(Node.of("presence", Map.of("name", socketHandler.store().name(), "type", "available")))
.thenRun(this::onPresenceUpdated)
.exceptionally(exception -> socketHandler.handleFailure(STREAM, exception));
}
private void onPresenceUpdated() {
socketHandler.store().setOnline(true);
socketHandler.store()
.jid()
.flatMap(socketHandler.store()::findContactByJid)
.ifPresent(entry -> entry.setLastKnownPresence(ContactStatus.AVAILABLE).setLastSeen(ZonedDateTime.now()));
}
private CompletableFuture updateUserAbout(boolean update) {
var jid = socketHandler.store().jid();
if (jid.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
return socketHandler.queryAbout(jid.get().toSimpleJid())
.thenAcceptAsync(result -> parseNewAbout(result.orElse(null), update));
}
private void parseNewAbout(ContactAboutResponse result, boolean update) {
if (result == null) {
return;
}
result.about().ifPresent(about -> {
socketHandler.store().setAbout(about);
if (!update) {
return;
}
var oldStatus = socketHandler.store().about();
socketHandler.onUserAboutChanged(about, oldStatus.orElse(null));
});
}
private CompletableFuture updateUserPicture(boolean update) {
var jid = socketHandler.store().jid();
if (jid.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
return socketHandler.queryPicture(jid.get().toSimpleJid())
.thenAcceptAsync(result -> handleUserPictureChange(result.orElse(null), update));
}
private void handleUserPictureChange(URI newPicture, boolean update) {
var oldStatus = socketHandler.store()
.profilePicture()
.orElse(null);
socketHandler.store().setProfilePicture(newPicture);
if (!update) {
return;
}
socketHandler.onUserPictureChanged(newPicture, oldStatus);
}
private void markBlocked(Jid entry) {
socketHandler.store().findContactByJid(entry).orElseGet(() -> {
var contact = socketHandler.store().addContact(entry);
socketHandler.onNewContact(contact);
return contact;
}).setBlocked(true);
}
private CompletableFuture parsePrivacySettings(Node result) {
var privacy = result.listChildren("privacy")
.stream()
.flatMap(entry -> entry.children().stream())
.map(entry -> addPrivacySetting(entry, false))
.toArray(CompletableFuture[]::new);
return CompletableFuture.allOf(privacy);
}
private void parseProps(Node result) {
var properties = result.findChild("props")
.stream()
.map(entry -> entry.listChildren("prop"))
.flatMap(Collection::stream)
.map(node -> Map.entry(node.attributes().getString("name"), node.attributes().getString("value")))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (first, second) -> second, ConcurrentHashMap::new));
socketHandler.store().addProperties(properties);
socketHandler.onMetadata(properties);
}
private void schedulePing() {
if (socketHandler.state() != SocketState.CONNECTED) {
return;
}
socketHandler.scheduleAtFixedInterval(() -> {
socketHandler.sendPing();
socketHandler.store().serialize(true);
socketHandler.store().serializer().linkMetadata(socketHandler.store());
socketHandler.keys().serialize(true);
}, PING_INTERVAL / 2, PING_INTERVAL);
}
private CompletableFuture scheduleMediaConnectionUpdate() {
if (socketHandler.state() != SocketState.CONNECTED) {
return CompletableFuture.completedFuture(null);
}
return socketHandler.sendQuery("set", "w:m", Node.of("media_conn"))
.thenAcceptAsync(this::onMediaConnection)
.exceptionallyAsync(throwable -> {
socketHandler.store().setMediaConnection(null);
socketHandler.handleFailure(MEDIA_CONNECTION, throwable);
return null;
});
}
private void onMediaConnection(Node node) {
var mediaConnection = node.findChild("media_conn").orElse(node);
var auth = mediaConnection.attributes().getString("auth");
var ttl = mediaConnection.attributes().getInt("ttl");
var maxBuckets = mediaConnection.attributes().getInt("max_buckets");
var timestamp = System.currentTimeMillis();
var hosts = mediaConnection.listChildren("host")
.stream()
.map(Node::attributes)
.map(attributes -> attributes.getString("hostname"))
.toList();
var result = new MediaConnection(auth, ttl, maxBuckets, timestamp, hosts);
socketHandler.store().setMediaConnection(result);
socketHandler.scheduleDelayed(this::scheduleMediaConnectionUpdate, result.ttl());
}
private void digestIq(Node node) {
if (node.attributes().hasValue("xmlns", "urn:xmpp:ping")) {
socketHandler.sendQueryWithNoResponse("result", null);
return;
}
var container = node.findChild().orElse(null);
if (container == null) {
return;
}
switch (container.description()) {
case "pair-device" -> startPairing(node, container);
case "pair-success" -> confirmPairing(node, container);
}
}
private CompletableFuture> sendPreKeys(int size) {
var startId = socketHandler.keys().lastPreKeyId() + 1;
var preKeys = IntStream.range(startId, startId + size)
.mapToObj(SignalPreKeyPair::random)
.peek(socketHandler.keys()::addPreKey)
.map(SignalPreKeyPair::toNode)
.toList();
return socketHandler.sendQuery(
"set",
"encrypt",
Node.of("registration", socketHandler.keys().encodedRegistrationId()),
Node.of("type", KEY_BUNDLE_TYPE),
Node.of("identity", socketHandler.keys().identityKeyPair().publicKey()),
Node.of("list", preKeys),
socketHandler.keys().signedKeyPair().toNode()
);
}
private void startPairing(Node node, Node container) {
switch (webVerificationHandler) {
case QrHandler qrHandler -> {
printQrCode(qrHandler, container);
sendConfirmNode(node, null);
}
case PairingCodeHandler codeHandler -> askPairingCode(codeHandler);
default -> throw new IllegalArgumentException("Cannot verify account: unknown verification method");
}
}
private void askPairingCode(PairingCodeHandler codeHandler) {
var code = Bytes.bytesToCrockford(Bytes.random(5));
var registration = Node.of(
"link_code_companion_reg",
Map.of("jid", getPhoneNumberAsJid(), "stage", "companion_hello", "should_show_push_notification", true),
Node.of("link_code_pairing_wrapped_companion_ephemeral_pub", cipherLinkPublicKey(code)),
Node.of("companion_server_auth_key_pub", socketHandler.keys().noiseKeyPair().publicKey()),
Node.of("companion_platform_id", 49),
Node.of("companion_platform_display", "Chrome (Linux)".getBytes(StandardCharsets.UTF_8)),
Node.of("link_code_pairing_nonce", 0)
);
socketHandler.sendQuery("set", "md", registration).thenAccept(result -> {
lastLinkCodeKey.set(code);
codeHandler.accept(code);
});
}
private byte[] cipherLinkPublicKey(String linkCodeKey) {
try {
var salt = Bytes.random(32);
var randomIv = Bytes.random(16);
var secretKey = getSecretPairingKey(linkCodeKey, salt);
var cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(randomIv));
var ciphered = cipher.doFinal(socketHandler.keys().companionKeyPair().publicKey());
return Bytes.concat(salt, randomIv, ciphered);
} catch (GeneralSecurityException exception) {
throw new RuntimeException("Cannot cipher link code pairing key", exception);
}
}
private Jid getPhoneNumberAsJid() {
return socketHandler.store()
.phoneNumber()
.map(PhoneNumber::toJid)
.orElseThrow(() -> new IllegalArgumentException("Missing phone number while registering via OTP"));
}
private SecretKey getSecretPairingKey(String pairingKey, byte[] salt) {
try {
var factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
var spec = new PBEKeySpec(pairingKey.toCharArray(), salt, 2 << 16, 256);
var tmp = factory.generateSecret(spec);
return new SecretKeySpec(tmp.getEncoded(), "AES");
} catch (GeneralSecurityException exception) {
throw new RuntimeException("Cannot compute pairing key", exception);
}
}
private void printQrCode(QrHandler qrHandler, Node container) {
var ref = container.findChild("ref")
.flatMap(Node::contentAsString)
.orElseThrow(() -> new NoSuchElementException("Missing ref"));
var qr = String.join(
",",
ref,
Base64.getEncoder().encodeToString(socketHandler.keys().noiseKeyPair().publicKey()),
Base64.getEncoder().encodeToString(socketHandler.keys().identityKeyPair().publicKey()),
Base64.getEncoder().encodeToString(socketHandler.keys().companionKeyPair().publicKey()),
"1"
);
qrHandler.accept(qr);
}
private void confirmPairing(Node node, Node container) {
saveCompanion(container);
var deviceIdentity = container.findChild("device-identity")
.orElseThrow(() -> new NoSuchElementException("Missing device identity"));
var advIdentity = SignedDeviceIdentityHMACSpec.decode(deviceIdentity.contentAsBytes().orElseThrow());
var advSign = Hmac.calculateSha256(advIdentity.details(), socketHandler.keys().companionKeyPair().publicKey());
if (!Arrays.equals(advIdentity.hmac(), advSign)) {
socketHandler.handleFailure(LOGIN, new HmacValidationException("adv_sign"));
return;
}
var account = SignedDeviceIdentitySpec.decode(advIdentity.details());
socketHandler.keys().setCompanionIdentity(account);
var message = Bytes.concat(
ACCOUNT_SIGNATURE_HEADER,
account.details(),
socketHandler.keys().identityKeyPair().publicKey()
);
if (!Curve25519.verifySignature(account.accountSignatureKey(), message, account.accountSignature())) {
socketHandler.handleFailure(LOGIN, new HmacValidationException("message_header"));
return;
}
var deviceSignatureMessage = Bytes.concat(
DEVICE_WEB_SIGNATURE_HEADER,
account.details(),
socketHandler.keys().identityKeyPair().publicKey(),
account.accountSignatureKey()
);
var result = new SignedDeviceIdentityBuilder()
.accountSignature(account.accountSignature())
.accountSignatureKey(account.accountSignatureKey())
.details(account.details())
.deviceSignature(Curve25519.sign(socketHandler.keys().identityKeyPair().privateKey(), deviceSignatureMessage, true))
.build();
var keyIndex = DeviceIdentitySpec.decode(result.details()).keyIndex();
var outgoingDeviceIdentity = SignedDeviceIdentitySpec.encode(new SignedDeviceIdentity(result.details(), null, result.accountSignature(), result.deviceSignature()));
var devicePairNode = Node.of(
"pair-device-sign",
Node.of(
"device-identity",
Map.of("key-index", keyIndex),
outgoingDeviceIdentity
)
);
socketHandler.keys().companionIdentity(result);
var device = socketHandler.store().device();
var platform = getWebPlatform(node);
socketHandler.store().setDevice(device.withPlatform(platform));
sendConfirmNode(node, devicePairNode);
}
private UserAgent.PlatformType getWebPlatform(Node node) {
var name = node.findChild("platform")
.flatMap(entry -> entry.attributes().getOptionalString("name"))
.orElse(null);
return switch (name) {
case "smbi" -> UserAgent.PlatformType.IOS_BUSINESS;
case "smba" -> UserAgent.PlatformType.ANDROID_BUSINESS;
case "android" -> UserAgent.PlatformType.ANDROID;
case "ios" -> UserAgent.PlatformType.IOS;
case null, default -> null;
};
}
private void sendConfirmNode(Node node, Node content) {
var attributes = Attributes.of()
.put("id", node.id())
.put("type", "result")
.put("to", JidServer.WHATSAPP.toJid())
.toMap();
var request = Node.of("iq", attributes, content);
socketHandler.sendNodeWithNoResponse(request);
}
private void saveCompanion(Node container) {
var node = container.findChild("device")
.orElseThrow(() -> new NoSuchElementException("Missing device"));
var companion = node.attributes()
.getOptionalJid("jid")
.orElseThrow(() -> new NoSuchElementException("Missing companion"));
socketHandler.store().setJid(companion);
socketHandler.store().setPhoneNumber(PhoneNumber.of(companion.user()));
socketHandler.addToKnownConnections();
var me = new Contact(companion.toSimpleJid(), socketHandler.store().name(), null, null, ContactStatus.AVAILABLE, Clock.nowSeconds(), false);
socketHandler.store().addContact(me);
}
protected void dispose() {
retries.clear();
lastLinkCodeKey.set(null);
}
}