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

com.github.princesslana.eriscasper.rest.Routes Maven / Gradle / Ivy

The newest version!
package com.github.princesslana.eriscasper.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.princesslana.eriscasper.BotToken;
import com.github.princesslana.eriscasper.rx.Maybes;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.ratelimiter.operator.RateLimiterOperator;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.Callable;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Routes {

  private static final Logger LOG = LoggerFactory.getLogger(Routes.class);

  private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");

  private final RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.ofDefaults();

  private final BotToken token;

  private final OkHttpClient client;
  private final ObjectMapper jackson;

  public Routes(BotToken token, OkHttpClient client, ObjectMapper jackson) {
    this.token = token;
    this.client = client;
    this.jackson = jackson;
  }

  public  Single execute(Route route) {
    return execute(route, null);
  }

  public  Single execute(Route route, I data) {
    Function, Request> buildRequest =
        body ->
            new Request.Builder()
                .method(route.getMethod().get(), body.orElse(null))
                .url(route.getUrl())
                .header("Authorization", "Bot " + token.unwrap())
                .build();

    return Maybes.fromNullable(data)
        .map(d -> RequestBody.create(MEDIA_TYPE_JSON, jackson.writeValueAsString(data)))
        .map(Optional::of)
        .toSingle(Optional.empty())
        .map(buildRequest)
        .flatMap(rq -> executeRequest(route, rq))
        .lift(RateLimiterOperator.of(getRateLimiter(route)));
  }

  private  Single executeRequest(Route route, Request rq) {
    Callable execute =
        () -> {
          LOG.debug("Executing: {}...", route);
          return client.newCall(rq).execute();
        };

    Consumer close =
        r -> {
          r.close();
          LOG.debug("Closed: {}.", r);
        };

    Consumer updateRateLimit =
        r -> {
          String remainingHeader = r.header("X-RateLimit-Remaining");
          String resetHeader = r.header("X-RateLimit-Reset");

          try {
            int remaining = Integer.parseInt(remainingHeader, 10);
            Instant until = Instant.ofEpochSecond(Long.parseLong(resetHeader));

            RateLimiter rl = getRateLimiter(route);

            rl.changeLimitForPeriod(remaining);
            rl.changeTimeoutDuration(Duration.between(Instant.now(), until));
          } catch (IllegalArgumentException e) {
            // we use a little EAFP here (https://docs.python.org/3/glossary.html#term-eafp)
            // If the headers are absent or not numeric or the values passed into the rate limiter
            // are invalid we may end up here
            LOG.debug(
                "Could not update rate limit to {}/{} ({})",
                remainingHeader,
                resetHeader,
                e.getMessage());
          }
        };

    // Single#using does not work here, as it performs the close operation immediately upon emitting
    // the response, because once it's received the response it knows it can receive no more.
    //
    // Observable#using closes the response at a more appropriate time. My feeling is that this is
    // wrong - we're tricking rxjava into thinking there is something coming next so as to delay the
    // close.
    return Observable.using(execute, Observable::just, close)
        .subscribeOn(Schedulers.io())
        .doOnNext(r -> LOG.debug("Done: {} -> {}.", route, r))
        .doOnNext(updateRateLimit)
        .flatMapSingle(
            r ->
                r.isSuccessful()
                    ? Single.just(r)
                    : Single.error(new IllegalStateException("Unexpected response: ")))
        .doOnError(e -> LOG.warn("Error: {} - {}.", route, e))
        .map(rs -> jackson.readValue(rs.body().byteStream(), route.getResponseClass()))
        .firstOrError();
  }

  private RateLimiter getRateLimiter(Route r) {
    return rateLimiterRegistry.rateLimiter(r.getUrl());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy