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.SocketHandler Maven / Gradle / Ivy
package it.auties.whatsapp.implementation;
import it.auties.whatsapp.api.*;
import it.auties.whatsapp.api.ErrorHandler.Location;
import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.controller.Store;
import it.auties.whatsapp.crypto.AesGcm;
import it.auties.whatsapp.io.BinaryDecoder;
import it.auties.whatsapp.io.BinaryEncoder;
import it.auties.whatsapp.listener.Listener;
import it.auties.whatsapp.model.action.Action;
import it.auties.whatsapp.model.business.BusinessCategory;
import it.auties.whatsapp.model.call.Call;
import it.auties.whatsapp.model.chat.*;
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.MessageIndexInfo;
import it.auties.whatsapp.model.info.MessageInfo;
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.message.model.ChatMessageKey;
import it.auties.whatsapp.model.message.model.ChatMessageKeyBuilder;
import it.auties.whatsapp.model.message.model.MessageContainer;
import it.auties.whatsapp.model.message.model.MessageStatus;
import it.auties.whatsapp.model.message.server.ProtocolMessage;
import it.auties.whatsapp.model.mobile.CountryLocale;
import it.auties.whatsapp.model.mobile.PhoneNumber;
import it.auties.whatsapp.model.newsletter.Newsletter;
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.request.CommunityLinkedGroupsRequest;
import it.auties.whatsapp.model.request.CommunityLinkedGroupsRequest.Input;
import it.auties.whatsapp.model.request.CommunityLinkedGroupsRequest.Variable;
import it.auties.whatsapp.model.request.MessageSendRequest;
import it.auties.whatsapp.model.response.CommunityLinkedGroupsResponse;
import it.auties.whatsapp.model.response.ContactAboutResponse;
import it.auties.whatsapp.model.setting.Setting;
import it.auties.whatsapp.model.signal.auth.ClientHelloBuilder;
import it.auties.whatsapp.model.signal.auth.HandshakeMessageBuilder;
import it.auties.whatsapp.model.signal.auth.HandshakeMessageSpec;
import it.auties.whatsapp.model.sync.PatchRequest;
import it.auties.whatsapp.model.sync.PatchType;
import it.auties.whatsapp.model.sync.PrimaryFeature;
import it.auties.whatsapp.util.Bytes;
import it.auties.whatsapp.util.Clock;
import it.auties.whatsapp.util.Exceptions;
import it.auties.whatsapp.util.Json;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.SocketException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Function;
import static it.auties.whatsapp.api.ErrorHandler.Location.*;
@SuppressWarnings("unused")
public class SocketHandler implements SocketListener {
private static final Set connectedUuids = ConcurrentHashMap.newKeySet();
private static final Set connectedPhoneNumbers = ConcurrentHashMap.newKeySet();
private static final Set connectedAlias = ConcurrentHashMap.newKeySet();
public static boolean isConnected(UUID uuid) {
return connectedUuids.contains(uuid);
}
public static boolean isConnected(long phoneNumber) {
return connectedPhoneNumbers.contains(phoneNumber);
}
public static boolean isConnected(String id) {
return connectedAlias.contains(id);
}
private SocketSession session;
private final Whatsapp whatsapp;
private final AuthHandler authHandler;
private final StreamHandler streamHandler;
private final MessageHandler messageHandler;
private final AppStateHandler appStateHandler;
private final ErrorHandler errorHandler;
private final AtomicLong requestsCounter;
private final ScheduledExecutorService scheduler;
private final List> scheduledTasks;
private final ConcurrentMap> pastParticipants;
private final Semaphore writeSemaphore;
private volatile SocketState state;
private volatile CompletableFuture loginFuture;
private Keys keys;
private Store store;
private Thread shutdownHook;
public SocketHandler(Whatsapp whatsapp, Store store, Keys keys, ErrorHandler errorHandler, WebVerificationHandler webVerificationHandler) {
this.whatsapp = whatsapp;
this.store = store;
this.keys = keys;
this.state = SocketState.WAITING;
this.authHandler = new AuthHandler(this);
this.streamHandler = new StreamHandler(this, webVerificationHandler);
this.messageHandler = new MessageHandler(this);
this.appStateHandler = new AppStateHandler(this);
this.errorHandler = Objects.requireNonNullElse(errorHandler, ErrorHandler.toTerminal());
this.requestsCounter = new AtomicLong();
this.scheduler = Executors.newScheduledThreadPool(0, Thread.ofVirtual().factory());
this.scheduledTasks = new CopyOnWriteArrayList<>();
this.writeSemaphore = new Semaphore(1, true);
this.pastParticipants = new ConcurrentHashMap<>();
}
private void onShutdown(boolean reconnect) {
if (state != SocketState.LOGGED_OUT && state != SocketState.RESTORE && state != SocketState.BANNED) {
keys.dispose();
store.dispose();
}
killScheduledTasks();
if (!reconnect) {
dispose();
}
}
private void killScheduledTasks() {
for(var scheduledTask : scheduledTasks) {
scheduledTask.cancel(true);
}
scheduledTasks.clear();
}
protected void onSocketEvent(SocketEvent event) {
callListenersAsync(listener -> {
listener.onSocketEvent(whatsapp, event);
listener.onSocketEvent(event);
});
}
private void callListenersAsync(Consumer consumer) {
store.listeners()
.forEach(listener -> Thread.startVirtualThread(() -> invokeListenerSafe(consumer, listener)));
}
@Override
public void onOpen(SocketSession session) {
this.session = session;
if (state == SocketState.CONNECTED) {
return;
}
if (shutdownHook == null) {
this.shutdownHook = new Thread(() -> onShutdown(false));
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
addToKnownConnections();
this.state = SocketState.WAITING;
onSocketEvent(SocketEvent.OPEN);
var clientHello = new ClientHelloBuilder()
.ephemeral(keys.ephemeralKeyPair().publicKey())
.build();
var handshakeMessage = new HandshakeMessageBuilder()
.clientHello(clientHello)
.build();
sendBinaryWithNoResponse(HandshakeMessageSpec.encode(handshakeMessage), true)
.exceptionallyAsync(throwable -> handleFailure(LOGIN, throwable));
}
protected void addToKnownConnections() {
connectedUuids.add(store.uuid());
store.phoneNumber()
.map(PhoneNumber::number)
.ifPresent(connectedPhoneNumbers::add);
connectedAlias.addAll(store.alias());
}
@Override
public void onMessage(byte[] message, int length) {
if (state == SocketState.WAITING || state == SocketState.RECONNECTING || state == SocketState.PAUSED) {
handshake(message.length != length ? Arrays.copyOfRange(message, 0, length) : message); // for now copy array
return;
}
if(state == SocketState.HANDSHAKE) {
setState(SocketState.CONNECTED);
}
var readKey = keys.readKey();
if (readKey.isEmpty()) {
return;
}
var iv = keys.nextReadCounter(true);
var decipheredMessage = AesGcm.decrypt(iv, message, 0, length, readKey.get());
try(var decoder = new BinaryDecoder(decipheredMessage)) {
var node = decoder.decode();
onNodeReceived(node);
store.resolvePendingRequest(node, false);
streamHandler.digest(node);
} catch (Throwable throwable) {
handleFailure(STREAM, throwable);
}
}
private void handshake(byte[] message) {
authHandler.login(message).whenCompleteAsync((result, throwable) -> {
if (throwable == null || state == SocketState.RECONNECTING) {
setState(SocketState.HANDSHAKE);
return;
}
handleFailure(LOGIN, throwable);
});
}
private void onNodeReceived(Node node) {
var caller = store.findPendingRequest(node.id());
if(caller.isPresent() && caller.get().body() instanceof Node callerNode && callerNode.hasNode("ping")) {
return;
}
callListenersAsync(listener -> {
listener.onNodeReceived(whatsapp, node);
listener.onNodeReceived(node);
});
}
@Override
public void onClose() {
if (state == SocketState.CONNECTED) {
disconnect(DisconnectReason.RECONNECTING);
return;
}
onDisconnected(state.toReason());
onShutdown(state == SocketState.RECONNECTING);
}
@Override
public void onError(Throwable throwable) {
if(!(throwable instanceof SocketException socketException)) {
onSocketEvent(SocketEvent.ERROR);
handleFailure(UNKNOWN, throwable);
return;
}
if(state() == SocketState.RECONNECTING || state() == SocketState.DISCONNECTED) {
return;
}
handleFailure(STREAM, socketException);
}
public CompletableFuture sendNode(Node node) {
return sendNode(node, null);
}
public CompletableFuture sendNode(Node node, Function filter) {
if (node.id() == null) {
node.attributes().put("id", HexFormat.of().formatHex(Bytes.random(6)));
}
return sendRequest(SocketRequest.of(node, filter), false, true);
}
public CompletableFuture sendNodeWithNoResponse(Node node) {
return sendRequest(SocketRequest.of(node, null), false, false)
.thenRun(() -> {});
}
public CompletableFuture sendBinaryWithNoResponse(byte[] binary, boolean prologue) {
return sendRequest(SocketRequest.of(binary), prologue, false)
.thenRun(() -> {});
}
private CompletableFuture sendRequest(SocketRequest request, boolean prologue, boolean response) {
if (state() == SocketState.RESTORE) {
return CompletableFuture.completedFuture(Node.of("error", Map.of("closed", true)));
}
var scheduledRelease = false;
try {
writeSemaphore.acquire();
var ciphered = encryptRequest(request);
var message = Bytes.concat(
prologue ? SocketHandshake.getPrologue(store.clientType()) : null,
Bytes.intToBytes(ciphered.length >> 16, 4),
Bytes.intToBytes(65535 & ciphered.length, 2),
ciphered
);
var future = session.sendBinary(message);
scheduledRelease = true;
future.whenCompleteAsync((result, error) -> {
writeSemaphore.release();
if(request.body() instanceof Node body) {
onNodeSent(body);
}
if(error != null) {
request.future().completeExceptionally(error);
return;
}
if (!response) {
request.future().complete(null);
return;
}
store.addRequest(request);
});
return request.future();
}catch (Throwable throwable) {
if(!scheduledRelease) {
writeSemaphore.release();
}
return CompletableFuture.failedFuture(throwable);
}
}
private byte[] encryptRequest(SocketRequest request) {
var body = request.toBytes();
var writeKey = keys.writeKey();
if(writeKey.isEmpty()) {
return body;
}
var iv = keys.nextWriteCounter(true);
return AesGcm.encrypt(iv, body, writeKey.get());
}
private byte[] getBody(Object encodedBody) {
return switch (encodedBody) {
case byte[] bytes -> bytes;
case Node node -> {
try(var encoder = new BinaryEncoder()) {
yield encoder.encode(node);
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
case null, default ->
throw new IllegalArgumentException("Cannot create request, illegal body: %s".formatted(encodedBody));
};
}
public CompletableFuture connect() {
if (state == SocketState.CONNECTED) {
return CompletableFuture.completedFuture(null);
}
if(loginFuture == null || loginFuture.isDone()) {
this.loginFuture = new CompletableFuture<>();
}
this.session = SocketSession.of(store.proxy().orElse(null), store.clientType() == ClientType.WEB);
return session.connect(this).exceptionallyAsync(throwable -> {
if(state == SocketState.CONNECTED || state == SocketState.RECONNECTING || state == SocketState.PAUSED) {
setState(SocketState.PAUSED);
onSocketEvent(SocketEvent.PAUSED);
handleFailure(Location.RECONNECT, throwable);
return null;
}
if(loginFuture != null && !loginFuture.isDone()) {
loginFuture.completeExceptionally(throwable);
}
Exceptions.rethrow(throwable);
return null;
});
}
public CompletableFuture disconnect(DisconnectReason reason) {
var newState = SocketState.of(reason);
if (state == newState) {
return CompletableFuture.completedFuture(null);
}
setState(newState);
keys.clearReadWriteKey();
return switch (reason) {
case DISCONNECTED -> handleDisconnection();
case RECONNECTING -> handleReconnection();
case LOGGED_OUT, BANNED -> handleLoggedOut();
case RESTORE -> handleRestore();
};
}
private CompletableFuture handleRestore() {
store.deleteSession();
store.resolveAllPendingRequests();
var oldListeners = new ArrayList<>(store.listeners());
if (session != null) {
session.disconnect();
}
var uuid = UUID.randomUUID();
var number = store.phoneNumber()
.map(PhoneNumber::number)
.orElse(null);
var result = store.serializer()
.newStoreKeysPair(uuid, number, store.alias(), store.clientType());
this.keys = result.keys();
this.store = result.store();
store.addListeners(oldListeners);
return connect();
}
private CompletableFuture handleLoggedOut() {
store.deleteSession();
store.resolveAllPendingRequests();
return handleDisconnection();
}
private CompletableFuture handleReconnection() {
store.resolveAllPendingRequests();
if (session != null) {
session.disconnect();
}
return connect();
}
private CompletableFuture handleDisconnection() {
store.resolveAllPendingRequests();
if (session != null) {
session.disconnect();
}
return CompletableFuture.completedFuture(null);
}
public CompletableFuture pushPatch(PatchRequest request) {
var jid = store.jid().orElseThrow(() -> new IllegalStateException("The session isn't connected"));
return appStateHandler.push(jid, List.of(request));
}
public CompletableFuture pushPatches(Jid jid, List requests) {
return appStateHandler.push(jid, requests);
}
public void pullPatch(PatchType... patchTypes) {
appStateHandler.pull(patchTypes);
}
protected CompletableFuture pullInitialPatches() {
return appStateHandler.pullInitial();
}
public void decodeMessage(Node node, JidProvider chatOverride, boolean notify) {
messageHandler.decode(node, chatOverride, notify);
}
public CompletableFuture sendPeerMessage(Jid companion, ProtocolMessage message) {
if (message == null) {
return CompletableFuture.completedFuture(null);
}
var jid = store.jid()
.orElseThrow(() -> new IllegalStateException("The session isn't connected"));
var key = new ChatMessageKeyBuilder()
.id(ChatMessageKey.randomIdV2(jid, store.clientType()))
.chatJid(companion)
.fromMe(true)
.senderJid(jid)
.build();
var info = new ChatMessageInfoBuilder()
.status(MessageStatus.PENDING)
.senderJid(jid)
.key(key)
.message(MessageContainer.of(message))
.timestampSeconds(Clock.nowSeconds())
.build();
var request = new MessageSendRequest.Chat(info, null, false, true, null);
return sendMessage(request);
}
public CompletableFuture sendMessage(MessageSendRequest request) {
return messageHandler.encode(request);
}
@SuppressWarnings("UnusedReturnValue")
public CompletableFuture sendQueryWithNoResponse(String method, String category, Node... body) {
return sendQueryWithNoResponse(null, JidServer.WHATSAPP.toJid(), method, category, null, body);
}
public CompletableFuture sendQueryWithNoResponse(String id, Jid to, String method, String category, Map metadata, Node... body) {
var attributes = Attributes.ofNullable(metadata)
.put("id", id, Objects::nonNull)
.put("type", method)
.put("to", to)
.put("xmlns", category, Objects::nonNull)
.toMap();
return sendNodeWithNoResponse(Node.of("iq", attributes, body));
}
private SocketRequest createRequest(Node node, Function filter, boolean response) {
if (response && node.id() == null) {
node.attributes().put("id", store.initializationTimeStamp() + "-" + requestsCounter.incrementAndGet());
}
return SocketRequest.of(node, filter);
}
private void onNodeSent(Node node) {
if (node.hasNode("ping")) {
return;
}
callListenersAsync(listener -> {
listener.onNodeSent(whatsapp, node);
listener.onNodeSent(node);
});
}
public CompletableFuture> queryAbout(JidProvider chat) {
var query = Node.of("status");
var body = Node.of("user", Map.of("jid", chat.toJid()));
return sendInteractiveQuery(List.of(query), List.of(body), List.of())
.thenApplyAsync(this::parseAbout);
}
public CompletableFuture> sendInteractiveQuery(Collection queries, Collection listData, Collection sideListData) {
var query = Node.of("query", queries);
var list = Node.of("list", listData);
var sideList = Node.of("side_list", sideListData);
var sync = Node.of(
"usync",
Map.of("sid", randomSid(), "mode", "query", "last", "true", "index", "0", "context", "interactive"),
query,
list,
sideList
);
return sendQuery("get", "usync", sync)
.thenApplyAsync(this::parseQueryResult);
}
public static String randomSid() {
return Clock.nowSeconds() + "-" + ThreadLocalRandom.current().nextLong(1_000_000_000, 9_999_999_999L) + "-" + ThreadLocalRandom.current().nextInt(0, 1000);
}
private Optional parseAbout(List responses) {
return responses.stream()
.map(entry -> entry.findChild("status"))
.flatMap(Optional::stream)
.findFirst()
.map(ContactAboutResponse::ofNode);
}
public CompletableFuture sendQuery(String method, String category, Node... body) {
return sendQuery(null, JidServer.WHATSAPP.toJid(), method, category, null, body);
}
private List parseQueryResult(Node result) {
return result == null ? List.of() : result.listChildren("usync")
.stream()
.map(node -> node.findChild("list"))
.flatMap(Optional::stream)
.map(node -> node.listChildren("user"))
.flatMap(Collection::stream)
.toList();
}
public CompletableFuture sendQuery(String id, Jid to, String method, String category, Map metadata, Node... body) {
var attributes = Attributes.ofNullable(metadata)
.put("xmlns", category, Objects::nonNull)
.put("id", id, Objects::nonNull)
.put("to", to)
.put("type", method)
.toMap();
return sendNode(Node.of("iq", attributes, body));
}
public CompletableFuture> queryPicture(JidProvider chat) {
var body = Node.of("picture", Map.of("query", "url", "type", "image"));
if (chat.toJid().hasServer(JidServer.GROUP_OR_COMMUNITY)) {
return queryGroupMetadata(chat.toJid())
.thenComposeAsync(result -> sendQuery("get", "w:profile:picture", Map.of(result.isCommunity() ? "parent_group_jid" : "target", chat.toJid()), body))
.thenApplyAsync(this::parseChatPicture);
}
return sendQuery("get", "w:profile:picture", Map.of("target", chat.toJid()), body)
.thenApplyAsync(this::parseChatPicture);
}
public CompletableFuture sendQuery(String method, String category, Map metadata, Node... body) {
return sendQuery(null, JidServer.WHATSAPP.toJid(), method, category, metadata, body);
}
private Optional parseChatPicture(Node result) {
return result.findChild("picture")
.flatMap(picture -> picture.attributes().getOptionalString("url"))
.map(URI::create);
}
public CompletableFuture> queryBlockList() {
return sendQuery("get", "blocklist", (Node) null)
.thenApplyAsync(this::parseBlockList);
}
private List parseBlockList(Node result) {
return result.findChild("list")
.stream()
.flatMap(node -> node.listChildren("item").stream())
.map(item -> item.attributes().getOptionalJid("jid"))
.flatMap(Optional::stream)
.toList();
}
public CompletableFuture subscribeToPresence(JidProvider jid) {
var node = Node.of("presence", Map.of("to", jid.toJid(), "type", "subscribe"));
return sendNodeWithNoResponse(node);
}
public CompletableFuture subscribeToNewsletterReactions(JidProvider channel) {
return sendQuery(channel.toJid(), "set", "newsletter", Node.of("live_updates"))
.thenApplyAsync(this::parseNewsletterSubscription);
}
private OptionalLong parseNewsletterSubscription(Node result) {
return result.findChild("live_updates")
.stream()
.map(node -> node.attributes().getOptionalLong("duration"))
.flatMapToLong(OptionalLong::stream)
.findFirst();
}
public CompletableFuture queryNewsletterMessages(JidProvider newsletterJid, int count) {
var newsletter = store.findNewsletterByJid(newsletterJid)
.orElseThrow(() -> new NoSuchElementException("Missing newsletter"));
var newsletterInvite = newsletter.metadata()
.invite()
.orElseThrow(() -> new NoSuchElementException("Missing newsletter key"));
return sendQuery("get", "newsletter", Node.of("messages", Map.of("count", count, "type", "invite", "key", newsletterInvite)))
.thenAcceptAsync(result -> onNewsletterMessages(newsletter, result));
}
private void onNewsletterMessages(Newsletter newsletter, Node result) {
result.findChild("messages")
.stream()
.map(messages -> messages.listChildren("message"))
.flatMap(Collection::stream)
.forEach(messages -> decodeMessage(messages, newsletter, false));
}
public CompletableFuture queryGroupMetadata(JidProvider group) {
var body = Node.of("query", Map.of("request", "interactive"));
return sendQuery(group.toJid(), "get", "w:g2", body)
.thenComposeAsync(this::handleGroupMetadata);
}
public CompletableFuture handleGroupMetadata(Node response) {
var metadataNode = Optional.of(response)
.filter(entry -> entry.hasDescription("group"))
.or(() -> response.findChild("group"))
.orElseThrow(() -> new NoSuchElementException("Erroneous response: %s".formatted(response)));
return parseGroupMetadata(metadataNode).thenApplyAsync(metadata -> {
var chat = store.findChatByJid(metadata.jid())
.orElseGet(() -> store().addNewChat(metadata.jid()));
chat.setName(metadata.subject());
return metadata;
});
}
private CompletableFuture parseGroupMetadata(Node node) {
var groupId = node.attributes()
.getOptionalString("id")
.map(id -> Jid.of(id, JidServer.GROUP_OR_COMMUNITY))
.orElseThrow(() -> new NoSuchElementException("Missing group jid"));
var subject = node.attributes().getString("subject");
var subjectAuthor = node.attributes().getOptionalJid("s_o");
var subjectTimestampSeconds = node.attributes()
.getOptionalLong("s_t")
.orElse(0L);
var foundationTimestampSeconds = node.attributes()
.getOptionalLong("creation")
.orElse(0L);
var founder = node.attributes()
.getOptionalJid("creator");
var description = node.findChild("description")
.flatMap(parent -> parent.findChild("body"))
.flatMap(Node::contentAsString);
var descriptionId = node.findChild("description")
.map(Node::attributes)
.flatMap(attributes -> attributes.getOptionalString("id"));
var parentCommunityJid = node.findChild("linked_parent")
.flatMap(entry -> entry.attributes().getOptionalJid("jid"));
var ephemeral = node.findChild("ephemeral")
.map(Node::attributes)
.map(attributes -> attributes.getLong("expiration"))
.flatMap(Clock::parseSeconds);
var communityNode = node.findChild("parent")
.orElse(null);
var policies = new HashMap();
var pastParticipants = Objects.requireNonNullElseGet(this.pastParticipants.get(groupId), List::of);
if (communityNode == null) {
policies.put(GroupSetting.EDIT_GROUP_INFO, ChatSettingPolicy.of(node.hasNode("announce")));
policies.put(GroupSetting.SEND_MESSAGES, ChatSettingPolicy.of(node.hasNode("restrict")));
var addParticipantsMode = node.findChild("member_add_mode")
.flatMap(Node::contentAsString)
.orElse(null);
policies.put(GroupSetting.ADD_PARTICIPANTS, ChatSettingPolicy.of(Objects.equals(addParticipantsMode, "admin_add")));
var groupJoin = node.findChild("membership_approval_mode")
.flatMap(entry -> entry.findChild("group_join"))
.map(entry -> entry.attributes().hasValue("state", "on"))
.orElse(false);
policies.put(GroupSetting.APPROVE_PARTICIPANTS, ChatSettingPolicy.of(groupJoin));
var participants = node.listChildren("participant")
.stream()
.map(this::parseGroupParticipant)
.flatMap(Optional::stream)
.toList();
return CompletableFuture.completedFuture(new ChatMetadata(
groupId,
subject,
subjectAuthor,
Clock.parseSeconds(subjectTimestampSeconds),
Clock.parseSeconds(foundationTimestampSeconds),
founder,
description,
descriptionId,
Collections.unmodifiableMap(policies),
participants,
pastParticipants,
ephemeral,
parentCommunityJid,
false,
List.of()
));
}
policies.put(CommunitySetting.MODIFY_GROUPS, ChatSettingPolicy.of(communityNode.hasNode("allow_non_admin_sub_group_creation")));
var addParticipantsMode = node.findChild("member_add_mode")
.flatMap(Node::contentAsString)
.orElse(null);
policies.put(CommunitySetting.ADD_PARTICIPANTS, ChatSettingPolicy.of(Objects.equals(addParticipantsMode, "admin_add")));
var mexBody = Json.writeValueAsBytes(new CommunityLinkedGroupsRequest(new Variable(new Input(groupId, "INTERACTIVE"))));
return sendQuery(groupId, "get", "w:g2", Node.of("linked_groups_participants")).thenComposeAsync(participantsNode -> {
var participants = participantsNode.findChild("linked_groups_participants")
.stream()
.flatMap(participantsNodeBody -> participantsNodeBody.streamChildren("participant"))
.flatMap(participantNode -> participantNode.attributes().getOptionalJid("jid").stream())
.map(participantJid -> (ChatParticipant) new CommunityParticipant(participantJid))
.toList();
return sendQuery("get", "w:mex", Node.of("query", Map.of("query_id", "7353258338095347"), mexBody)).thenApplyAsync(communityResponse -> {
var linkedGroups = communityResponse.findChild("result")
.flatMap(Node::contentAsBytes)
.flatMap(CommunityLinkedGroupsResponse::ofJson)
.map(CommunityLinkedGroupsResponse::linkedGroups)
.orElse(List.of());
return new ChatMetadata(
groupId,
subject,
subjectAuthor,
Clock.parseSeconds(subjectTimestampSeconds),
Clock.parseSeconds(foundationTimestampSeconds),
founder,
description,
descriptionId,
Collections.unmodifiableMap(policies),
participants,
pastParticipants,
ephemeral,
parentCommunityJid,
true,
linkedGroups
);
});
});
}
private Optional parseGroupParticipant(Node node) {
if(node.attributes().hasKey("error")) {
return Optional.empty();
}
var id = node.attributes().getRequiredJid("jid");
var role = GroupRole.of(node.attributes().getString("type", null));
return Optional.of(new GroupParticipant(id, role));
}
public CompletableFuture sendQuery(Jid to, String method, String category, Node... body) {
return sendQuery(null, to, method, category, null, body);
}
public CompletableFuture sendRetryReceipt(long nodeTimestamp, Jid chatJid, Jid participantJid, String messageId, int retryCount) {
var retryAttributes = Attributes.of()
.put("count", 1)
.put("id", messageId)
.put("t", nodeTimestamp)
.put("v", 1)
.toMap();
var retryNode = Node.of("retry", retryAttributes);
var registrationNode = Node.of("registration", keys.encodedRegistrationId());
var receiptAttributes = Attributes.of()
.put("id", messageId)
.put("type", "retry")
.put("to", chatJid.withAgent(null))
.put("participant", participantJid == null ? null : participantJid.withAgent(null), participantJid != null)
.toMap();
var receipt = Node.of("receipt", receiptAttributes, retryNode, registrationNode);
return sendNodeWithNoResponse(receipt);
}
public CompletableFuture sendReceipt(Jid jid, Jid participant, List messages, String type) {
if (messages.isEmpty()) {
return CompletableFuture.completedFuture(null);
}
var attributes = Attributes.of()
.put("id", messages.getFirst())
.put("t", Clock.nowMilliseconds(), () -> Objects.equals(type, "read") || Objects.equals(type, "read-self"))
.put("to", jid.withAgent(null))
.put("type", type, Objects::nonNull);
if (Objects.equals(type, "sender") && jid.hasServer(JidServer.WHATSAPP)) {
attributes.put("recipient", jid.withAgent(null));
attributes.put("to", participant.withAgent(null));
}
var receipt = Node.of("receipt", attributes.toMap(), toMessagesNode(messages));
return sendNodeWithNoResponse(receipt);
}
private List toMessagesNode(List messages) {
if (messages.size() <= 1) {
return null;
}
return messages.subList(1, messages.size())
.stream()
.map(id -> Node.of("item", Map.of("id", id)))
.toList();
}
protected CompletableFuture sendMessageAck(Jid from, Node node) {
var attrs = node.attributes();
var type = attrs.getOptionalString("type")
.filter(entry -> !Objects.equals(entry, "message"))
.orElse(null);
var participant = attrs.getNullableString("participant");
var recipient = attrs.getNullableString("recipient");
var attributes = Attributes.of()
.put("id", node.id())
.put("to", from)
.put("class", node.description())
.put("participant", Jid.of(participant).withAgent(null), Objects.nonNull(participant))
.put("recipient", Jid.of(recipient).withAgent(null), Objects.nonNull(recipient))
.put("type", type, Objects::nonNull)
.toMap();
return sendNodeWithNoResponse(Node.of("ack", attributes));
}
protected void onRegistrationCode(long code) {
callListenersAsync(listener -> {
listener.onRegistrationCode(whatsapp, code);
listener.onRegistrationCode(code);
});
}
protected void onMetadata(Map properties) {
callListenersAsync(listener -> {
listener.onMetadata(whatsapp, properties);
listener.onMetadata(properties);
});
}
protected void onMessageStatus(MessageInfo> message) {
callListenersAsync(listener -> {
listener.onMessageStatus(whatsapp, message);
listener.onMessageStatus(message);
});
}
protected void onUpdateChatPresence(ContactStatus status, Jid jid, Chat chat) {
var contact = store.findContactByJid(jid);
if (contact.isPresent()) {
contact.get().setLastKnownPresence(status);
contact.get().setLastSeen(ZonedDateTime.now());
}
var provider = contact.isPresent() ? contact.get() : jid;
chat.presences().put(jid, status);
callListenersAsync(listener -> {
listener.onContactPresence(whatsapp, chat, provider);
listener.onContactPresence(chat, provider);
});
}
protected void onNewMessage(MessageInfo> info) {
callListenersAsync(listener -> {
listener.onNewMessage(whatsapp, info);
listener.onNewMessage(info);
});
}
protected void onNewStatus(ChatMessageInfo info) {
callListenersAsync(listener -> {
listener.onNewStatus(whatsapp, info);
listener.onNewStatus(info);
});
}
protected void onChatRecentMessages(Chat chat, boolean last) {
callListenersAsync(listener -> {
listener.onChatMessagesSync(whatsapp, chat, last);
listener.onChatMessagesSync(chat, last);
});
}
protected void onFeatures(PrimaryFeature features) {
callListenersAsync(listener -> {
listener.onFeatures(whatsapp, features.flags());
listener.onFeatures(features.flags());
});
}
protected void onSetting(Setting setting) {
callListenersAsync(listener -> {
listener.onSetting(whatsapp, setting);
listener.onSetting(setting);
});
}
protected void onMessageDeleted(MessageInfo> message, boolean everyone) {
callListenersAsync(listener -> {
listener.onMessageDeleted(whatsapp, message, everyone);
listener.onMessageDeleted(message, everyone);
});
}
protected void onAction(Action action, MessageIndexInfo indexInfo) {
callListenersAsync(listener -> {
listener.onAction(whatsapp, action, indexInfo);
listener.onAction(action, indexInfo);
});
}
protected void onDisconnected(DisconnectReason reason) {
if(state == SocketState.WAITING || state == SocketState.HANDSHAKE) {
handleFailure(LOGIN, new RuntimeException("Cannot login: no response from Whatsapp"));
return;
}
if (reason != DisconnectReason.RECONNECTING) {
connectedUuids.remove(store.uuid());
store.phoneNumber()
.map(PhoneNumber::number)
.ifPresent(connectedPhoneNumbers::remove);
if (shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
confirmConnection();
}
callListenersSync(listener -> {
listener.onDisconnected(whatsapp, reason);
listener.onDisconnected(reason);
});
}
protected void onLoggedIn() {
callListenersAsync(listener -> {
listener.onLoggedIn(whatsapp);
listener.onLoggedIn();
});
}
public void callListenersSync(Consumer consumer) {
for(var listener : store.listeners()) {
invokeListenerSafe(consumer, listener);
}
}
private void invokeListenerSafe(Consumer consumer, Listener listener) {
try {
consumer.accept(listener);
} catch (Throwable throwable) {
handleFailure(UNKNOWN, throwable);
}
}
protected void onChats() {
callListenersAsync(listener -> {
listener.onChats(whatsapp, store().chats());
listener.onChats(store().chats());
});
}
protected void onNewsletters() {
callListenersAsync(listener -> {
listener.onNewsletters(whatsapp, store().newsletters());
listener.onNewsletters(store().newsletters());
});
}
protected void onStatus() {
callListenersAsync(listener -> {
listener.onStatus(whatsapp, store().status());
listener.onStatus(store().status());
});
}
protected void onContacts() {
callListenersAsync(listener -> {
listener.onContacts(whatsapp, store().contacts());
listener.onContacts(store().contacts());
});
}
protected void onHistorySyncProgress(Integer progress, boolean recent) {
callListenersAsync(listener -> {
listener.onHistorySyncProgress(whatsapp, progress, recent);
listener.onHistorySyncProgress(progress, recent);
});
}
protected void onReply(ChatMessageInfo info) {
var quoted = info.quotedMessage().orElse(null);
if (quoted == null) {
return;
}
store.resolvePendingReply(info);
callListenersAsync(listener -> {
listener.onMessageReply(whatsapp, info, quoted);
listener.onMessageReply(info, quoted);
});
}
protected void onGroupPictureChanged(Chat fromChat) {
callListenersAsync(listener -> {
listener.onGroupPictureChanged(whatsapp, fromChat);
listener.onGroupPictureChanged(fromChat);
});
}
protected void onContactPictureChanged(Contact fromContact) {
callListenersAsync(listener -> {
listener.onProfilePictureChanged(whatsapp, fromContact);
listener.onProfilePictureChanged(fromContact);
});
}
protected void onUserAboutChanged(String newAbout, String oldAbout) {
callListenersAsync(listener -> {
listener.onAboutChanged(whatsapp, oldAbout, newAbout);
listener.onAboutChanged(oldAbout, newAbout);
});
}
public void onUserPictureChanged(URI newPicture, URI oldPicture) {
callListenersAsync(listener -> store().jid()
.flatMap(store()::findContactByJid)
.ifPresent(selfJid -> {
listener.onProfilePictureChanged(whatsapp, selfJid);
listener.onProfilePictureChanged(selfJid);
}));
}
public void onUserChanged(String newName, String oldName) {
if (oldName != null && !Objects.equals(newName, oldName)) {
onUserNameChanged(newName, oldName);
}
var self = store.jid()
.orElseThrow(() -> new IllegalStateException("The session isn't connected"))
.toSimpleJid();
store().findContactByJid(self)
.orElseGet(() -> store().addContact(self))
.setChosenName(newName);
store().setName(newName);
}
private void onUserNameChanged(String newName, String oldName) {
callListenersAsync(listener -> {
listener.onNameChanged(whatsapp, oldName, newName);
listener.onNameChanged(oldName, newName);
});
}
public void updateLocale(CountryLocale newLocale, CountryLocale oldLocale) {
if (!Objects.equals(newLocale, oldLocale)) {
return;
}
if (oldLocale != null) {
onUserLocaleChanged(newLocale, oldLocale);
}
store().setLocale(newLocale);
}
private void onUserLocaleChanged(CountryLocale newLocale, CountryLocale oldLocale) {
callListenersAsync(listener -> {
listener.onLocaleChanged(whatsapp, oldLocale, newLocale);
listener.onLocaleChanged(oldLocale, newLocale);
});
}
protected void onContactBlocked(Contact contact) {
callListenersAsync(listener -> {
listener.onContactBlocked(whatsapp, contact);
listener.onContactBlocked(contact);
});
}
protected void onNewContact(Contact contact) {
callListenersAsync(listener -> {
listener.onNewContact(whatsapp, contact);
listener.onNewContact(contact);
});
}
protected void onDevices(LinkedHashMap devices) {
callListenersAsync(listener -> {
listener.onLinkedDevices(whatsapp, devices.keySet());
listener.onLinkedDevices(devices.keySet());
});
}
public void onCall(Call call) {
callListenersAsync(listener -> {
listener.onCall(whatsapp, call);
listener.onCall(call);
});
}
public void onPrivacySettingChanged(PrivacySettingEntry oldEntry, PrivacySettingEntry newEntry) {
callListenersAsync(listener -> {
listener.onPrivacySettingChanged(whatsapp, oldEntry, newEntry);
listener.onPrivacySettingChanged(oldEntry, newEntry);
});
}
protected CompletableFuture querySessionsForcefully(Jid jid) {
return messageHandler.querySessions(List.of(jid), true);
}
private void dispose() {
onSocketEvent(SocketEvent.CLOSE);
streamHandler.dispose();
messageHandler.dispose();
appStateHandler.dispose();
scheduler.shutdownNow();
confirmConnection();
}
protected T handleFailure(Location location, Throwable throwable) {
if (state() == SocketState.RESTORE || state() == SocketState.LOGGED_OUT || state() == SocketState.BANNED) {
return null;
}
var result = errorHandler.handleError(whatsapp, location, throwable);
switch (result) {
case RESTORE -> disconnect(DisconnectReason.RESTORE);
case LOG_OUT -> disconnect(DisconnectReason.LOGGED_OUT);
case DISCONNECT -> disconnect(DisconnectReason.DISCONNECTED);
case RECONNECT -> disconnect(DisconnectReason.RECONNECTING);
}
return null;
}
public CompletableFuture> querySessions(List jid) {
return messageHandler.querySessions(jid, true)
.thenComposeAsync(values -> messageHandler.queryDevices(jid, false));
}
public void parseSessions(Node result) {
messageHandler.parseSessions(result);
}
public CompletableFuture> queryBusinessCategories() {
return sendQuery("get", "fb:thrift_iq", Node.of("request", Map.of("op", "profile_typeahead", "type", "catkit", "v", "1"), Node.of("query", List.of())))
.thenApplyAsync(this::parseBusinessCategories);
}
private List parseBusinessCategories(Node result) {
return result.findChild("response")
.flatMap(entry -> entry.findChild("categories"))
.stream()
.map(entry -> entry.listChildren("category"))
.flatMap(Collection::stream)
.map(BusinessCategory::of)
.toList();
}
public SocketState state() {
return this.state;
}
public Keys keys() {
return this.keys;
}
public Store store() {
return this.store;
}
protected void setState(SocketState state) {
this.state = state;
}
public CompletableFuture changeAbout(String newAbout) {
return sendQuery("set", "status", Node.of("status", newAbout.getBytes(StandardCharsets.UTF_8)))
.thenRun(() -> store().setAbout(newAbout));
}
void confirmConnection() {
if (loginFuture == null || loginFuture.isDone()) {
return;
}
loginFuture.complete(null);
}
@SuppressWarnings("SameParameterValue")
protected void scheduleAtFixedInterval(Runnable command, long initialDelay, long period) {
var result = scheduler.scheduleAtFixedRate(command, initialDelay, period, TimeUnit.SECONDS);
scheduledTasks.add(result);
}
protected ScheduledFuture> scheduleDelayed(Runnable command, long delay) {
var result = scheduler.schedule(command, delay, TimeUnit.SECONDS);
scheduledTasks.add(result);
return result;
}
protected void sendPing() {
sendQuery("get", "w:p", Node.of("ping"))
.thenRunAsync(() -> onSocketEvent(SocketEvent.PING))
.exceptionallyAsync(throwable -> {
disconnect(DisconnectReason.RECONNECTING);
return null;
});
}
public CompletableFuture updateBusinessCertificate(String newName) {
return streamHandler.updateBusinessCertificate(newName);
}
public ConcurrentMap> pastParticipants() {
return pastParticipants;
}
public void addPastParticipant(Jid jid, ChatPastParticipant pastParticipant) {
var pastParticipants = pastParticipants().get(jid);
if(pastParticipants != null) {
pastParticipants.add(pastParticipant);
pastParticipants().put(jid, pastParticipants);
}else {
pastParticipants().put(jid, List.of(pastParticipant));
}
}
}