it.auties.whatsapp.model.request.Request 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
package it.auties.whatsapp.model.request;
import it.auties.bytes.Bytes;
import it.auties.whatsapp.util.Protobuf;
import it.auties.whatsapp.binary.Encoder;
import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.controller.Store;
import it.auties.whatsapp.crypto.AesGmc;
import it.auties.whatsapp.socket.SocketSession;
import it.auties.whatsapp.util.Exceptions;
import lombok.NonNull;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import static java.util.concurrent.CompletableFuture.delayedExecutor;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* An abstract model class that represents a request made from the client to the server.
*/
@SuppressWarnings("UnusedReturnValue")
public record Request(String id, @NonNull Object body, @NonNull CompletableFuture future,
Function filter, Throwable caller) {
/**
* The timeout in seconds before a Request wrapping a Node fails
*/
private static final int TIMEOUT = 60;
/**
* The delayed executor used to cancel futures
*/
private static final Executor EXECUTOR = delayedExecutor(TIMEOUT, SECONDS);
private Request(String id, Function filter, @NonNull Object body) {
this(id, body, new CompletableFuture<>(), filter, trace(body));
EXECUTOR.execute(this::cancelTimedFuture);
}
private static Throwable trace(Object body) {
var message = body instanceof Node node ? "%s node timed out".formatted(node.toString()) : "Binary timed out";
var current = Exceptions.current(message);
var actualStackTrace = Arrays.stream(current.getStackTrace())
.filter(entry -> !entry.getClassName().equals(Request.class.getName()) && !entry.getClassName().equals(Node.class.getName()))
.toArray(StackTraceElement[]::new);
current.setStackTrace(actualStackTrace);
return current;
}
private void cancelTimedFuture() {
if (future.isDone()) {
return;
}
future.completeExceptionally(caller);
}
/**
* Constructs a new request with the provided body expecting a response
*/
public static Request of(@NonNull Node body, Function filter) {
return new Request(body.id(), filter, body);
}
/**
* Constructs a new request with the provided body expecting a response
*/
public static Request of(@NonNull Object body) {
return new Request(null, null, Protobuf.writeMessage(body));
}
/**
* Sends a request to the WebSocket linked to {@code session}.
*
* @param session the WhatsappWeb's WebSocket session
* @param store the store
*/
public CompletableFuture sendWithPrologue(@NonNull SocketSession session, @NonNull Keys keys, @NonNull Store store) {
return send(session, keys, store, true, false);
}
/**
* Sends a request to the WebSocket linked to {@code session}.
*
* @param store the store
* @param session the WhatsappWeb's WebSocket session
* @param prologue whether the prologue should be prepended to the request
* @param response whether the request expects a response
* @return this request
*/
public CompletableFuture send(@NonNull SocketSession session, @NonNull Keys keys, @NonNull Store store, boolean prologue, boolean response) {
var ciphered = encryptMessage(keys);
var buffer = Bytes.of(prologue ? keys.prologue() : new byte[0])
.appendInt(ciphered.length >> 16)
.appendShort(65535 & ciphered.length)
.append(ciphered)
.toByteArray();
session.sendBinary(buffer)
.thenRunAsync(() -> onSendSuccess(store, response))
.exceptionallyAsync(this::onSendError);
return future;
}
private byte[] encryptMessage(Keys keys) {
var encodedBody = body();
var body = switch (encodedBody) {
case byte[] bytes -> bytes;
case Node node -> {
var encoder = new Encoder();
yield encoder.encode(node);
}
default ->
throw new IllegalArgumentException("Cannot create request, illegal body: %s".formatted(encodedBody));
};
if (keys.writeKey() == null) {
return body;
}
return AesGmc.encrypt(keys.writeCounter(true), body, keys.writeKey().toByteArray());
}
private void onSendSuccess(Store store, boolean response) {
store.addRequest(this);
if (response) {
return;
}
future.complete(null);
}
private Void onSendError(Throwable throwable) {
future.completeExceptionally(new IOException("Cannot send %s, an unknown exception occurred".formatted(this), throwable));
return null;
}
/**
* Sends a request to the WebSocket linked to {@code session}.
*
* @param store the store
* @param session the WhatsappWeb's WebSocket session
* @return this request
*/
public CompletableFuture send(@NonNull SocketSession session, @NonNull Keys keys, @NonNull Store store) {
return send(session, keys, store, false, true);
}
/**
* Sends a request to the WebSocket linked to {@code session}.
*
* @param store the store
* @param session the WhatsappWeb's WebSocket session
* @return this request
*/
public CompletableFuture sendWithNoResponse(@NonNull SocketSession session, @NonNull Keys keys, @NonNull Store store) {
return send(session, keys, store, false, false).thenRunAsync(() -> {
});
}
/**
* Completes this request using {@code response}
*
* @param response the response used to complete {@link Request#future}
*/
public boolean complete(Node response, boolean exceptionally) {
if (response == null) {
future.complete(Node.of("xmlstreamend"));
return true;
}
if (exceptionally) {
future.completeExceptionally(new RuntimeException("Cannot process request %s with %s".formatted(this, response), caller));
return true;
}
if (filter != null && !filter.apply(response)) {
return false;
}
future.complete(response);
return true;
}
}