All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.auties.whatsapp.util.RegistrationHelper Maven / Gradle / Ivy

There is a newer version: 2.7.2
Show newest version
package it.auties.whatsapp.util;

import it.auties.whatsapp.controller.Keys;
import it.auties.whatsapp.controller.Store;
import it.auties.whatsapp.exception.RegistrationException;
import it.auties.whatsapp.model.mobile.VerificationCodeMethod;
import it.auties.whatsapp.model.mobile.VerificationCodeResponse;
import it.auties.whatsapp.model.request.Attributes;
import it.auties.whatsapp.model.signal.auth.UserAgent.UserAgentPlatform;
import it.auties.whatsapp.util.Spec.Whatsapp;
import lombok.experimental.UtilityClass;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.security.Security;
import java.util.Base64;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@UtilityClass
public class RegistrationHelper {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public CompletableFuture registerPhoneNumber(Store store, Keys keys, Supplier> handler, VerificationCodeMethod method) {
        if (method == VerificationCodeMethod.NONE) {
            return sendVerificationCode(store, keys, handler);
        }

        return requestVerificationCode(store, keys, method)
                .thenComposeAsync(ignored -> sendVerificationCode(store, keys, handler));
    }

    public CompletableFuture requestVerificationCode(Store store, Keys keys, VerificationCodeMethod method) {
        if(method == VerificationCodeMethod.NONE){
            return CompletableFuture.completedFuture(null);
        }

        return requestVerificationCodeOptions(store, keys, method)
                .thenComposeAsync(attrs -> sendRegistrationRequest(store,"/code", attrs))
                .thenAcceptAsync(RegistrationHelper::checkResponse)
                .thenRunAsync(() -> saveRegistrationStatus(store, keys, false));
    }

    private static CompletableFuture> requestVerificationCodeOptions(Store store, Keys keys, VerificationCodeMethod method) {
        return getRegistrationOptions(store, keys,
                Map.entry("mcc", store.phoneNumber().countryCode().mcc()),
                Map.entry("mnc", store.phoneNumber().countryCode().mnc()),
                Map.entry("sim_mcc", "000"),
                Map.entry("sim_mnc", "000"),
                Map.entry("method", method.type()),
                Map.entry("reason", ""),
                Map.entry("hasav", "1"));
    }

    public CompletableFuture sendVerificationCode(Store store, Keys keys, Supplier> handler) {
        return handler.get()
                .thenComposeAsync(result -> sendVerificationCode(store, keys, result))
                .thenRunAsync(() -> saveRegistrationStatus(store, keys, true));
    }

    private void saveRegistrationStatus(Store store, Keys keys, boolean registered) {
        keys.registered(registered);
        if(registered){
            store.jid(store.phoneNumber().toJid());
            store.addLinkedDevice(store.jid(), 0);
        }
        keys.serialize(true);
        store.serialize(true);
    }

    private CompletableFuture sendVerificationCode(Store store, Keys keys, String code) {
        return getRegistrationOptions(store, keys, Map.entry("code", code.replaceAll("-", "")))
                .thenComposeAsync(attrs -> sendRegistrationRequest(store, "/register", attrs))
                .thenAcceptAsync(RegistrationHelper::checkResponse);
    }

    private void checkResponse(HttpResponse result) {
        Validate.isTrue(result.statusCode() == HttpURLConnection.HTTP_OK,
                "Invalid status code: %s", RegistrationException.class, result.statusCode(), result.body());
        var response = Json.readValue(result.body(), VerificationCodeResponse.class);
        if(response.status().isSuccessful()){
            return;
        }

        throw new RegistrationException("Invalid response: %s".formatted(result.body()));
    }

    private CompletableFuture> sendRegistrationRequest(Store store, String path, Map params) {
        return getUserAgent(store).thenComposeAsync(userAgent -> {
            var client = createClient(store);
            var request = HttpRequest.newBuilder()
                    .uri(URI.create("%s%s?%s".formatted(Whatsapp.MOBILE_REGISTRATION_ENDPOINT, path, toFormParams(params))))
                    .GET()
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("User-Agent", userAgent)
                    .build();
            return client.sendAsync(request, BodyHandlers.ofString());
        });
    }

    private CompletableFuture getUserAgent(Store store) {
        return store.version()
                .thenApplyAsync(version -> "WhatsApp/%s %s/%s Device/%s-%s".formatted(version, getMobileOsName(store.osType()), store.osVersion(), store.manufacturer(), store.model()));
    }

    private Object getMobileOsName(UserAgentPlatform platform) {
        return switch (platform) {
            case ANDROID -> "Android";
            case IOS -> "iOS";
            default -> throw new IllegalStateException("Unsupported mobile os: " + platform);
        };
    }

    private HttpClient createClient(Store store) {
        var clientBuilder = HttpClient.newBuilder();
        store.proxy().ifPresent(proxy -> {
            clientBuilder.proxy(ProxySelector.of(new InetSocketAddress(proxy.getHost(), proxy.getPort())));
            clientBuilder.authenticator(new ProxyAuthenticator());
        });
        return clientBuilder.build();
    }

    @SafeVarargs
    private CompletableFuture> getRegistrationOptions(Store store, Keys keys, Entry... attributes) {
        return MetadataHelper.getToken(store.phoneNumber().numberWithoutPrefix(), store.osType())
                .thenApplyAsync(token -> getRegistrationOptions(store, keys, token, attributes));
    }

    private Map getRegistrationOptions(Store store, Keys keys, String token, Entry[] attributes) {
        return Attributes.of(attributes)
                .put("cc", store.phoneNumber().countryCode().prefix())
                .put("in", store.phoneNumber().numberWithoutPrefix())
                .put("rc", store.releaseChannel().index())
                .put("lg", "en")
                .put("lc", "GB")
                .put("mistyped", "6")
                .put("authkey", Base64.getUrlEncoder().encodeToString(keys.noiseKeyPair().publicKey()))
                .put("e_regid", Base64.getUrlEncoder().encodeToString(keys.encodedRegistrationId()))
                .put("e_keytype", "BQ")
                .put("e_ident", Base64.getUrlEncoder().encodeToString(keys.identityKeyPair().publicKey()))
                .put("e_skey_id", "AAAA")
                .put("e_skey_val", Base64.getUrlEncoder().encodeToString(keys.signedKeyPair().publicKey()))
                .put("e_skey_sig", Base64.getUrlEncoder().encodeToString(keys.signedKeyPair().signature()))
                .put("fdid", keys.phoneId())
                .put("network_ratio_type", "1")
                .put("expid", keys.deviceId())
                .put("simnum", "1")
                .put("hasinrc", "1")
                .put("pid", ProcessHandle.current().pid())
                .put("id", keys.identityId())
                .put("token", token)
                .toMap();
    }

    private String toFormParams(Map values) {
        return values.entrySet()
                .stream()
                .map(entry -> "%s=%s".formatted(entry.getKey(), entry.getValue()))
                .collect(Collectors.joining("&"));
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy