FriendlyCaptchaV2Client.java

package org.drjekyll.friendlycaptcha;

import static org.drjekyll.friendlycaptcha.StringUtil.isEmpty;

import java.io.InputStream;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpRequest;
import java.nio.charset.StandardCharsets;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.NonNull;

/** Verifies a Friendly Captcha solution using the v2 API. */
@RequiredArgsConstructor
@Slf4j
class FriendlyCaptchaV2Client implements FriendlyCaptchaClient {

  private static final URI DEFAULT_ENDPOINT =
      URI.create("https://global.frcapi.com/api/v2/captcha/siteverify");

  private final FriendlyCaptchaParams friendlyCaptchaParams;

  private final VerificationResponseReader verificationResponseReader;

  @Override
  public URI getDefaultEndpoint() {
    return DEFAULT_ENDPOINT;
  }

  @Override
  public String buildRequestBody(@NonNull String solution) {
    String entity = "response=" + URLEncoder.encode(solution, StandardCharsets.UTF_8);
    if (!isEmpty(friendlyCaptchaParams.getSitekey())) {
      entity +=
          "&sitekey="
              + URLEncoder.encode(friendlyCaptchaParams.getSitekey(), StandardCharsets.UTF_8);
    }
    return entity;
  }

  @Override
  public void addVersionSpecificHeaders(HttpRequest.Builder requestBuilder) {
    requestBuilder.header("X-API-Key", friendlyCaptchaParams.getApiKey());
  }

  @Override
  public boolean processResponse(int statusCode, InputStream inputStream) {
    VerificationResponseV2 response =
        verificationResponseReader.readResponse(inputStream, VerificationResponseV2.class);
    if (statusCode == 200) {
      return response.isSuccess();
    }

    log.warn("Received error response: {}", response);

    ErrorDetails error = response.getError();
    if (error == null || error.getErrorCode() == null) {
      throw new FriendlyCaptchaException("Verification API returned error status", statusCode);
    }
    throw new FriendlyCaptchaException(
        error.getErrorCode().getDescription(), statusCode, error.getErrorCode());
  }
}