it.auties.whatsapp.socket.SocketHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of whatsappweb4j Show documentation
Show all versions of whatsappweb4j Show documentation
Standalone fully-featured Whatsapp Web API for Java and Kotlin
The newest version!
package it.auties.whatsapp.socket;
import it.auties.whatsapp.api.*;
import it.auties.whatsapp.api.ErrorHandler.Location;
import it.auties.whatsapp.binary.BinaryDecoder;
import it.auties.whatsapp.binary.BinaryPatchType;
import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.controller.Store;
import it.auties.whatsapp.crypto.AesGmc;
import it.auties.whatsapp.listener.Listener;
import it.auties.whatsapp.model.action.Action;
import it.auties.whatsapp.model.business.BusinessCategory;
import it.auties.whatsapp.model.chat.Chat;
import it.auties.whatsapp.model.chat.GroupMetadata;
import it.auties.whatsapp.model.contact.Contact;
import it.auties.whatsapp.model.contact.ContactJid;
import it.auties.whatsapp.model.contact.ContactJid.Server;
import it.auties.whatsapp.model.contact.ContactJidProvider;
import it.auties.whatsapp.model.contact.ContactStatus;
import it.auties.whatsapp.model.info.MessageIndexInfo;
import it.auties.whatsapp.model.info.MessageInfo;
import it.auties.whatsapp.model.message.model.MessageContainer;
import it.auties.whatsapp.model.message.model.MessageKey;
import it.auties.whatsapp.model.message.model.MessageStatus;
import it.auties.whatsapp.model.message.server.ProtocolMessage;
import it.auties.whatsapp.model.mobile.PhoneNumber;
import it.auties.whatsapp.model.privacy.PrivacySettingEntry;
import it.auties.whatsapp.model.request.Attributes;
import it.auties.whatsapp.model.request.MessageSendRequest;
import it.auties.whatsapp.model.request.Node;
import it.auties.whatsapp.model.request.Request;
import it.auties.whatsapp.model.response.ContactStatusResponse;
import it.auties.whatsapp.model.setting.Setting;
import it.auties.whatsapp.model.signal.auth.ClientHello;
import it.auties.whatsapp.model.signal.auth.HandshakeMessage;
import it.auties.whatsapp.model.sync.ActionValueSync;
import it.auties.whatsapp.model.sync.PatchRequest;
import it.auties.whatsapp.util.Clock;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.function.Function;
import static it.auties.whatsapp.api.ErrorHandler.Location.*;
@Accessors(fluent = true)
@SuppressWarnings("unused")
public class SocketHandler implements SocketListener {
private static final Executor DEFAULT_EXECUTOR = ForkJoinPool.getCommonPoolParallelism() > 1 ? ForkJoinPool.commonPool() : runnable -> new Thread(runnable).start();
private static final Set connectedUuids = ConcurrentHashMap.newKeySet();
private static final Set connectedPhoneNumbers = ConcurrentHashMap.newKeySet();
private static final Set connectedAlias = ConcurrentHashMap.newKeySet();
private SocketSession session;
@NonNull
@Getter
private final Whatsapp whatsapp;
@NonNull
private final AuthHandler authHandler;
@NonNull
private final StreamHandler streamHandler;
@NonNull
private final MessageHandler messageHandler;
@NonNull
private final AppStateHandler appStateHandler;
@NonNull
private final ErrorHandler errorHandler;
@NonNull
private final Executor socketExecutor;
@NonNull
@Getter
@Setter(AccessLevel.PROTECTED)
private SocketState state;
@Getter
@NonNull
private Keys keys;
@Getter
@NonNull
private Store store;
private Thread shutdownHook;
private CompletableFuture loginFuture;
private CompletableFuture logoutFuture;
private ExecutorService listenersService;
private Node lastNode;
public static boolean isConnected(@NonNull UUID uuid){
return connectedUuids.contains(uuid);
}
public static boolean isConnected(long phoneNumber){
return connectedPhoneNumbers.contains(phoneNumber);
}
public static boolean isConnected(@NonNull String id){
return connectedAlias.contains(id);
}
public SocketHandler(@NonNull Whatsapp whatsapp, @NonNull Store store, @NonNull Keys keys, ErrorHandler errorHandler, WebVerificationSupport webVerificationSupport, Executor socketExecutor) {
this.whatsapp = whatsapp;
this.store = store;
this.keys = keys;
this.state = SocketState.WAITING;
this.authHandler = new AuthHandler(this);
this.streamHandler = new StreamHandler(this, webVerificationSupport);
this.messageHandler = new MessageHandler(this);
this.appStateHandler = new AppStateHandler(this);
this.errorHandler = Objects.requireNonNullElse(errorHandler, ErrorHandler.toTerminal());
this.socketExecutor = Objects.requireNonNullElse(socketExecutor, DEFAULT_EXECUTOR);
}
private void onShutdown(boolean reconnect) {
if (state != SocketState.LOGGED_OUT && state != SocketState.RESTORE) {
keys.dispose();
store.dispose();
}
if (!reconnect) {
dispose();
}
}
protected void onSocketEvent(SocketEvent event) {
callListenersAsync(listener -> {
listener.onSocketEvent(whatsapp, event);
listener.onSocketEvent(event);
});
}
private void callListenersAsync(Consumer consumer) {
var service = getOrCreateListenersService();
store.listeners().forEach(listener -> service.execute(() -> 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);
}
markConnected();
this.state = SocketState.WAITING;
onSocketEvent(SocketEvent.OPEN);
var clientHello = new ClientHello(keys.ephemeralKeyPair().publicKey());
var handshakeMessage = new HandshakeMessage(clientHello);
Request.of(handshakeMessage)
.sendWithPrologue(session, keys, store)
.exceptionallyAsync(throwable -> handleFailure(LOGIN, throwable));
}
protected void markConnected() {
connectedUuids.add(store.uuid());
store.phoneNumber()
.map(PhoneNumber::number)
.ifPresent(connectedPhoneNumbers::add);
connectedAlias.addAll(store.alias());
}
@Override
public void onMessage(byte[] message) {
if (state != SocketState.CONNECTED && state != SocketState.RESTORE) {
authHandler.login(session, message)
.thenApplyAsync(result -> result ? state(SocketState.CONNECTED) : null)
.exceptionallyAsync(throwable -> handleFailure(LOGIN, throwable));
return;
}
if(keys.readKey() == null){
return;
}
var plainText = AesGmc.decrypt(keys.readCounter(true), message, keys.readKey());
var decoder = new BinaryDecoder();
var node = decoder.decode(plainText);
if(!node.hasNode("bad-mac")) {
this.lastNode = node;
}
onNodeReceived(node);
store.resolvePendingRequest(node, false);
streamHandler.digest(node);
}
private void onNodeReceived(Node deciphered) {
callListenersAsync(listener -> {
listener.onNodeReceived(whatsapp, deciphered);
listener.onNodeReceived(deciphered);
});
}
@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) {
onSocketEvent(SocketEvent.ERROR);
handleFailure(UNKNOWN, throwable);
}
public synchronized CompletableFuture connect() {
if(state == SocketState.CONNECTED){
return CompletableFuture.completedFuture(null);
}
if (loginFuture == null || loginFuture.isDone()) {
this.loginFuture = new CompletableFuture<>();
}
if (logoutFuture == null || logoutFuture.isDone()) {
this.logoutFuture = new CompletableFuture<>();
}
this.session = new SocketSession(store.proxy().orElse(null), socketExecutor);
return session.connect(this)
.thenCompose(ignored -> loginFuture);
}
public CompletableFuture loginFuture(){
return loginFuture;
}
public CompletableFuture logoutFuture(){
return logoutFuture;
}
public CompletableFuture disconnect(DisconnectReason reason) {
state(SocketState.of(reason));
keys.clearReadWriteKey();
return switch (reason) {
case DISCONNECTED -> {
if(session != null) {
session.close();
}
yield CompletableFuture.completedFuture(null);
}
case RECONNECTING -> {
if(session != null) {
session.close();
}
yield connect();
}
case LOGGED_OUT -> {
store.deleteSession();
store.resolveAllPendingRequests();
if(session != null) {
session.close();
}
yield CompletableFuture.completedFuture(null);
}
case RESTORE -> {
store.deleteSession();
store.resolveAllPendingRequests();
var oldListeners = new ArrayList<>(store.listeners());
if(session != null) {
session.close();
}
var uuid = UUID.randomUUID();
var number = store.phoneNumber()
.map(PhoneNumber::number)
.orElse(null);
this.keys = Keys.random(uuid, number, store.clientType(), store.serializer());
this.store = Store.random(uuid, number, store.clientType(), store.serializer());
store.addListeners(oldListeners);
yield connect();
}
};
}
public CompletableFuture pushPatch(PatchRequest request) {
return appStateHandler.push(store.jid(), List.of(request));
}
public CompletableFuture pushPatches(ContactJid jid, List requests) {
return appStateHandler.push(jid, requests);
}
public void pullPatch(BinaryPatchType... patchTypes) {
appStateHandler.pull(patchTypes);
}
protected CompletableFuture pullInitialPatches() {
return appStateHandler.pullInitial();
}
public void decodeMessage(Node node) {
messageHandler.decode(node);
}
public CompletableFuture sendPeerMessage(ContactJid companion, ProtocolMessage message) {
if(message == null){
return CompletableFuture.completedFuture(null);
}
var key = MessageKey.builder()
.chatJid(companion)
.fromMe(true)
.senderJid(store().jid())
.build();
var info = MessageInfo.builder()
.senderJid(store().jid())
.key(key)
.message(MessageContainer.of(message))
.timestampSeconds(Clock.nowSeconds())
.build();
var request = MessageSendRequest.builder()
.info(info)
.peer(true)
.build();
return sendMessage(request);
}
public CompletableFuture sendMessage(MessageSendRequest request) {
store.attribute(request.info());
return messageHandler.encode(request);
}
@SuppressWarnings("UnusedReturnValue")
public CompletableFuture sendQueryWithNoResponse(String method, String category, Node... body) {
return sendQueryWithNoResponse(null, Server.WHATSAPP.toJid(), method, category, null, body);
}
public CompletableFuture sendQueryWithNoResponse(String id, ContactJid 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 sendWithNoResponse(Node.of("iq", attributes, body));
}
public CompletableFuture sendWithNoResponse(Node node) {
if (state() == SocketState.RESTORE) {
return CompletableFuture.completedFuture(null);
}
return node.toRequest(null, false)
.sendWithNoResponse(session, keys, store)
.exceptionallyAsync(throwable -> handleFailure(STREAM, throwable))
.thenRunAsync(() -> onNodeSent(node));
}
private void onNodeSent(Node node) {
callListenersAsync(listener -> {
listener.onNodeSent(whatsapp, node);
listener.onNodeSent(node);
});
}
public CompletableFuture> queryAbout(@NonNull ContactJidProvider chat) {
var query = Node.of("status");
var body = Node.of("user", Map.of("jid", chat.toJid()));
return sendInteractiveQuery(query, body).thenApplyAsync(this::parseStatus);
}
public CompletableFuture> sendInteractiveQuery(Node queryNode, Node... queryBody) {
var query = Node.of("query", queryNode);
var list = Node.of("list", queryBody);
var sync = Node.of("usync",
Map.of("sid", UUID.randomUUID().toString(), "mode", "query", "last", "true", "index", "0", "context", "interactive"),
query, list);
return sendQuery("get", "usync", sync).thenApplyAsync(this::parseQueryResult);
}
private Optional parseStatus(List responses) {
return responses.stream()
.map(entry -> entry.findNode("status"))
.flatMap(Optional::stream)
.findFirst()
.map(ContactStatusResponse::new);
}
public CompletableFuture sendQuery(String method, String category, Node... body) {
return sendQuery(null, Server.WHATSAPP.toJid(), method, category, null, body);
}
private List parseQueryResult(Node result) {
return result.findNodes("usync")
.stream()
.map(node -> node.findNode("list"))
.flatMap(Optional::stream)
.map(node -> node.findNodes("user"))
.flatMap(Collection::stream)
.toList();
}
public CompletableFuture sendQuery(String id, ContactJid 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 send(Node.of("iq", attributes, body));
}
public CompletableFuture send(Node node) {
return send(node, null);
}
public CompletableFuture send(Node node, Function filter) {
if (state() == SocketState.RESTORE) {
return CompletableFuture.completedFuture(node);
}
var request = node.toRequest(filter, true);
var result = request.send(session, keys, store);
onNodeSent(node);
return result;
}
public CompletableFuture> queryPicture(@NonNull ContactJidProvider chat) {
var body = Node.of("picture", Map.of("query", "url", "type", "image"));
if (chat.toJid().hasServer(Server.GROUP)) {
return queryGroupMetadata(chat.toJid())
.thenComposeAsync(result -> sendQuery("get", "w:profile:picture", Map.of(result.community() ? "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, Server.WHATSAPP.toJid(), method, category, metadata, body);
}
private Optional parseChatPicture(Node result) {
return result.findNode("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.findNode("list")
.orElseThrow(() -> new NoSuchElementException("Missing block list in response"))
.findNodes("item")
.stream()
.map(item -> item.attributes().getJid("jid"))
.flatMap(Optional::stream)
.toList();
}
public CompletableFuture subscribeToPresence(ContactJidProvider jid) {
var node = Node.of("presence", Map.of("to", jid.toJid(), "type", "subscribe"));
return sendWithNoResponse(node);
}
public CompletableFuture queryGroupMetadata(ContactJidProvider group) {
var body = Node.of("query", Map.of("request", "interactive"));
return sendQuery(group.toJid(), "get", "w:g2", body)
.thenApplyAsync(response -> handleGroupMetadata(group, response));
}
private GroupMetadata handleGroupMetadata(ContactJidProvider group, Node response) {
var metadata = response.findNode("group")
.map(GroupMetadata::of)
.orElseThrow(() -> new NoSuchElementException("Erroneous response: %s".formatted(response)));
var chat = group instanceof Chat entry ? entry : store.findChatByJid(group).orElse(null);
if(chat != null) {
metadata.founder().ifPresent(chat::founder);
chat.foundationTimestampSeconds(metadata.foundationTimestamp().toEpochSecond());
metadata.description().ifPresent(chat::description);
chat.addParticipants(metadata.participants());
}
return metadata;
}
public CompletableFuture sendQuery(ContactJid to, String method, String category, Node... body) {
return sendQuery(null, to, method, category, null, body);
}
public void sendReceipt(ContactJid jid, ContactJid participant, List messages, String type) {
if (messages.isEmpty()) {
return;
}
var attributes = Attributes.of()
.put("id", messages.get(0))
.put("t", Clock.nowMilliseconds(), () -> Objects.equals(type, "read") || Objects.equals(type, "read-self"))
.put("to", jid)
.put("type", type, Objects::nonNull);
if(Objects.equals(type, "sender") && jid.hasServer(Server.WHATSAPP)){
attributes.put("recipient", jid);
attributes.put("to", participant);
}else {
attributes.put("to", jid);
attributes.put("participant", participant, Objects::nonNull);
}
var receipt = Node.of("receipt", attributes.toMap(), toMessagesNode(messages));
sendWithNoResponse(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 void sendMessageAck(Node node) {
var attrs = node.attributes();
var type = attrs.getOptionalString("type")
.filter(entry -> !Objects.equals(entry, "message"))
.orElse(null);
var attributes = Attributes.of()
.put("id", node.id())
.put("to", node.attributes().getRequiredString("from"))
.put("class", node.description())
.put("participant", attrs.getNullableString("participant"), Objects::nonNull)
.put("recipient", attrs.getNullableString("recipient"), Objects::nonNull)
.put("type", type, Objects::nonNull)
.toMap();
sendWithNoResponse(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(MessageStatus status, Contact participant, MessageInfo message, Chat chat) {
callListenersAsync(listener -> {
if (participant == null) {
listener.onConversationMessageStatus(whatsapp, message, status);
listener.onConversationMessageStatus(message, status);
}
listener.onAnyMessageStatus(whatsapp, chat, participant, message, status);
listener.onAnyMessageStatus(chat, participant, message, status);
});
}
protected void onUpdateChatPresence(ContactStatus status, ContactJid contactJid, Chat chat) {
var contact = store.findContactByJid(contactJid);
if(contact.isPresent()) {
contact.get().lastKnownPresence(status);
if (status == contact.get().lastKnownPresence()) {
return;
}
contact.get().lastSeen(ZonedDateTime.now());
}
chat.presences().put(contactJid, status);
callListenersAsync(listener -> {
listener.onContactPresence(whatsapp, chat, contactJid, status);
listener.onContactPresence(chat, contactJid, status);
});
}
protected void onNewMessage(MessageInfo info, boolean offline) {
callListenersAsync(listener -> {
listener.onNewMessage(whatsapp, info);
listener.onNewMessage(info);
listener.onNewMessage(whatsapp, info, offline);
listener.onNewMessage(info, offline);
});
}
protected void onNewStatus(MessageInfo 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(ActionValueSync.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 loggedOut) {
if(loggedOut != DisconnectReason.RECONNECTING) {
connectedUuids.remove(store.uuid());
store.phoneNumber()
.map(PhoneNumber::number)
.ifPresent(connectedPhoneNumbers::remove);
if(shutdownHook != null){
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
if(loginFuture != null && !loginFuture.isDone()){
loginFuture.complete(null);
}
if(logoutFuture != null && !logoutFuture.isDone()) {
logoutFuture.complete(null);
}
}
callListenersSync(listener -> {
listener.onDisconnected(whatsapp, loggedOut);
listener.onDisconnected(loggedOut);
});
}
protected void onLoggedIn() {
if(!loginFuture.isDone()) {
loginFuture.complete(null);
}
callListenersAsync(listener -> {
listener.onLoggedIn(whatsapp);
listener.onLoggedIn();
});
}
public void callListenersSync(Consumer consumer) {
var service = getOrCreateListenersService();
var futures = store.listeners()
.stream()
.map(listener -> CompletableFuture.runAsync(() -> invokeListenerSafe(consumer, listener), service))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
}
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 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(MessageInfo 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 onGroupPictureChange(Chat fromChat) {
callListenersAsync(listener -> {
listener.onGroupPictureChange(whatsapp, fromChat);
listener.onGroupPictureChange(fromChat);
});
}
protected void onContactPictureChange(Contact fromContact) {
callListenersAsync(listener -> {
listener.onContactPictureChange(whatsapp, fromContact);
listener.onContactPictureChange(fromContact);
});
}
protected void onUserAboutChange(String newAbout, String oldAbout) {
callListenersAsync(listener -> {
listener.onUserAboutChange(whatsapp, oldAbout, newAbout);
listener.onUserAboutChange(oldAbout, newAbout);
});
}
public void onUserPictureChange(URI newPicture, URI oldPicture) {
callListenersAsync(listener -> {
listener.onUserPictureChange(whatsapp, oldPicture, newPicture);
listener.onUserPictureChange(oldPicture, newPicture);
});
}
public void updateUserName(String newName, String oldName) {
if (oldName != null && !Objects.equals(newName, oldName)) {
sendWithNoResponse(Node.of("presence", Map.of("name", oldName, "type", "unavailable")));
sendWithNoResponse(Node.of("presence", Map.of("name", newName, "type", "available")));
onUserNameChange(newName, oldName);
}
var self = store().jid().toWhatsappJid();
store().findContactByJid(self).orElseGet(() -> store().addContact(self)).chosenName(newName);
store().name(newName);
}
private void onUserNameChange(String newName, String oldName) {
callListenersAsync(listener -> {
listener.onUserNameChange(whatsapp, oldName, newName);
listener.onUserNameChange(oldName, newName);
});
}
public void updateLocale(String newLocale, String oldLocale) {
if (!Objects.equals(newLocale, oldLocale)) {
return;
}
if (oldLocale != null) {
onUserLocaleChange(newLocale, oldLocale);
}
store().locale(newLocale);
}
private void onUserLocaleChange(String newLocale, String oldLocale) {
callListenersAsync(listener -> {
listener.onUserLocaleChange(whatsapp, oldLocale, newLocale);
listener.onUserLocaleChange(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 onPrivacySettingChanged(PrivacySettingEntry oldEntry, PrivacySettingEntry newEntry) {
callListenersAsync(listener -> {
listener.onPrivacySettingChanged(whatsapp, oldEntry, newEntry);
listener.onPrivacySettingChanged(oldEntry, newEntry);
});
}
protected void querySessionsForcefully(ContactJid contactJid) {
messageHandler.querySessions(List.of(contactJid), true);
}
private void dispose() {
onSocketEvent(SocketEvent.CLOSE);
streamHandler.dispose();
messageHandler.dispose();
appStateHandler.dispose();
if(listenersService != null){
listenersService.shutdownNow();
}
}
private synchronized ExecutorService getOrCreateListenersService(){
if(listenersService == null || listenersService.isShutdown()){
listenersService = Executors.newCachedThreadPool();
}
return listenersService;
}
protected T handleFailure(Location location, Throwable throwable) {
if (state() == SocketState.RESTORE || state() == SocketState.LOGGED_OUT) {
return null;
}
var result = errorHandler.handleError(store.clientType(), 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 queryCompanionDevices() {
return messageHandler.getDevices(List.of(store.jid().toWhatsappJid()), true)
.thenCompose(values -> messageHandler.querySessions(values, 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.findNode("response")
.flatMap(entry -> entry.findNode("categories"))
.stream()
.map(entry -> entry.findNodes("category"))
.flatMap(Collection::stream)
.map(BusinessCategory::of)
.toList();
}
Node lastNode() {
return lastNode;
}
}