367 lines
12 KiB
Java
367 lines
12 KiB
Java
package me.bobert.netneutrality.http;
|
|
|
|
/*
|
|
* Copyright (c) anthorob
|
|
*
|
|
* https://discord.gg/qUa6zNFZsd
|
|
*/
|
|
|
|
import com.google.gson.Gson;
|
|
import io.sentry.Breadcrumb;
|
|
import io.sentry.Sentry;
|
|
import me.bobert.netneutrality.utils.Settings;
|
|
import okhttp3.Headers;
|
|
import okhttp3.MediaType;
|
|
import okhttp3.OkHttpClient;
|
|
import okhttp3.Request;
|
|
import okhttp3.RequestBody;
|
|
import okhttp3.Response;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class WebModule {
|
|
private enum Method {
|
|
PING, GET, POST
|
|
}
|
|
|
|
private static final String USER_AGENT = "Net Neutrality/%s (Discord bot, discord.gg/trAqhQ5Wsr)";
|
|
private static final Logger log = LoggerFactory.getLogger(WebModule.class);
|
|
private static final Map<String, CachedResponse> cache = Collections.synchronizedMap(
|
|
new LinkedHashMap<>(100, 0.75f, true) {
|
|
@Override
|
|
protected boolean removeEldestEntry(Map.Entry<String, CachedResponse> eldest) {
|
|
return size() > 50; // Limit cache to 50 entries
|
|
}
|
|
}
|
|
);
|
|
|
|
private final Method method;
|
|
private final String url;
|
|
private final long cacheExpirationMillis;
|
|
private final OkHttpClient client;
|
|
private final Map<String, String> headers;
|
|
private final Request request;
|
|
private CachedResponse cachedResponse;
|
|
|
|
private WebModule(Builder builder) {
|
|
this.method = builder.method;
|
|
this.url = builder.url;
|
|
this.client = builder.client;
|
|
this.headers = builder.headers;
|
|
this.cacheExpirationMillis = builder.timeUnit.toMillis(builder.timeout);
|
|
this.request = builder.request;
|
|
}
|
|
|
|
/**
|
|
* Invalidates a cached response for the specified URL.
|
|
* <p>
|
|
* This method removes any cached responses matching the given URL.
|
|
*
|
|
* @param url the URL of the cached response to invalidate
|
|
*/
|
|
public static void invalidateCache(String url) {
|
|
cache.keySet().removeIf(s -> s.contains(url));
|
|
}
|
|
|
|
/**
|
|
* Sends the HTTP request based on the configuration and retrieves the response.
|
|
* <p>
|
|
* If the request is a GET request and caching is enabled, the response will be cached.
|
|
* Cached responses are reused until they expire.
|
|
* <p>
|
|
* POST requests do not support caching.
|
|
*
|
|
* @return the {@code WebModule} instance, allowing further interaction with the response
|
|
* @throws RuntimeException if the request fails
|
|
*/
|
|
public WebModule request() {
|
|
String cacheKey = createCacheKey(method, url, headers);
|
|
|
|
if (method == Method.GET && cacheKey != null) {
|
|
CachedResponse cachedResponse = cache.get(cacheKey);
|
|
|
|
if (cachedResponse != null && !cachedResponse.isExpired()) {
|
|
log.debug("Retrieving cached response: {}", cacheKey);
|
|
this.cachedResponse = cachedResponse;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
try (Response response = client.newCall(request).execute()) {
|
|
long elapsedTime = System.currentTimeMillis() - startTime;
|
|
Sentry.addBreadcrumb(Breadcrumb.http(url, method.name(), response.code()));
|
|
String body = response.body() != null ? response.body().string() : null;
|
|
|
|
this.cachedResponse = new CachedResponse(
|
|
response.code(),
|
|
response.headers(),
|
|
elapsedTime,
|
|
body,
|
|
System.currentTimeMillis() + cacheExpirationMillis,
|
|
null
|
|
);
|
|
} catch (IOException e) {
|
|
long elapsedTime = System.currentTimeMillis() - startTime;
|
|
log.error("Request failed: {}", e.getMessage(), e);
|
|
Sentry.captureException(e);
|
|
|
|
this.cachedResponse = new CachedResponse(
|
|
0, // Status code 0 indicates a failure
|
|
new Headers.Builder().build(),
|
|
elapsedTime,
|
|
null,
|
|
System.currentTimeMillis() + cacheExpirationMillis,
|
|
e.getMessage()
|
|
);
|
|
}
|
|
|
|
if (method == Method.GET && cacheKey != null) {
|
|
cache.put(cacheKey, this.cachedResponse);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private String createCacheKey(Method method, String url, Map<String, String> headers) {
|
|
if (method == Method.POST) {
|
|
return null; // no cache for post requests
|
|
}
|
|
|
|
StringBuilder keyBuilder = new StringBuilder(method.name()).append(":").append(url);
|
|
if (headers != null) {
|
|
headers.entrySet().stream()
|
|
.sorted(Map.Entry.comparingByKey())
|
|
.forEach(entry -> keyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append(";"));
|
|
}
|
|
return keyBuilder.toString();
|
|
}
|
|
|
|
public JSONObject getAsObject() {
|
|
return cachedResponse.getBody()
|
|
.map(JSONObject::new)
|
|
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
|
|
}
|
|
|
|
public JSONArray getAsArray() {
|
|
return cachedResponse.getBody()
|
|
.map(JSONArray::new)
|
|
.orElseThrow(() -> new IllegalStateException("No body available for this response"));
|
|
}
|
|
|
|
public String getRawData() {
|
|
return cachedResponse.getBody().orElse(null);
|
|
}
|
|
|
|
public boolean isSuccess() {
|
|
return cachedResponse != null && cachedResponse.getStatusCode() >= 200 && cachedResponse.getStatusCode() < 400;
|
|
}
|
|
|
|
public static class Builder {
|
|
private Request request;
|
|
private final String url;
|
|
private Method method = Method.GET;
|
|
private long timeout = 5;
|
|
private TimeUnit timeUnit = TimeUnit.MINUTES;
|
|
private OkHttpClient client;
|
|
private long connectTimeout;
|
|
private long readTimeout;
|
|
private long writeTimeout;
|
|
private final Map<String, String> headers = new HashMap<>();
|
|
private final Map<String, String> params = new HashMap<>();
|
|
|
|
/**
|
|
* Creates a new {@code Builder} instance for the specified URL.
|
|
*
|
|
* @param url the URL for the HTTP request
|
|
*/
|
|
public Builder(String url) {
|
|
this.url = url;
|
|
}
|
|
|
|
public Builder ping() {
|
|
this.method = Method.PING;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Configures the request to use the HTTP GET method.
|
|
*
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder get() {
|
|
this.method = Method.GET;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Configures the request to use the HTTP POST method and sets the request parameters.
|
|
*
|
|
* @param params a map of key-value pairs to include in the POST request body
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder post(Map<String, String> params) {
|
|
this.params.putAll(params);
|
|
this.method = Method.POST;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Adds a custom header to the request.
|
|
*
|
|
* @param key the name of the header
|
|
* @param value the value of the header
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder addHeader(String key, String value) {
|
|
this.headers.put(key, value);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Configures caching for the request.
|
|
*
|
|
* @param timeout the cache expiration duration
|
|
* @param timeUnit the unit of time for the cache expiration
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder cache(long timeout, TimeUnit timeUnit) {
|
|
this.timeout = timeout;
|
|
this.timeUnit = timeUnit;
|
|
return this;
|
|
}
|
|
|
|
public Builder timeout(long timeout, TimeUnit timeUnit) {
|
|
this.connectTimeout = timeUnit.toMillis(timeout);
|
|
this.readTimeout = timeUnit.toMillis(timeout);
|
|
this.writeTimeout = timeUnit.toMillis(timeout);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the connection timeout for the HTTP client.
|
|
*
|
|
* @param timeout the timeout duration
|
|
* @param timeUnit the unit of time for the timeout
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder connectTimeout(long timeout, TimeUnit timeUnit) {
|
|
this.connectTimeout = timeUnit.toMillis(timeout);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the read timeout for the HTTP client.
|
|
*
|
|
* @param timeout the timeout duration
|
|
* @param timeUnit the unit of time for the timeout
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder readTimeout(long timeout, TimeUnit timeUnit) {
|
|
this.readTimeout = timeUnit.toMillis(timeout);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the write timeout for the HTTP client.
|
|
*
|
|
* @param timeout the timeout duration
|
|
* @param timeUnit the unit of time for the timeout
|
|
* @return the current {@code Builder} instance for chaining
|
|
*/
|
|
public Builder writeTimeout(long timeout, TimeUnit timeUnit) {
|
|
this.writeTimeout = timeUnit.toMillis(timeout);
|
|
return this;
|
|
}
|
|
|
|
private Request createRequest() {
|
|
Request.Builder builder = new Request.Builder().url(url);
|
|
builder.addHeader("User-Agent", String.format(USER_AGENT, Settings.getVersion()));
|
|
headers.forEach(builder::addHeader);
|
|
|
|
if (method == Method.PING) {
|
|
builder.head();
|
|
}
|
|
|
|
if (method == Method.GET) {
|
|
builder.get();
|
|
}
|
|
|
|
if (method == Method.POST && !params.isEmpty()) {
|
|
RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8"));
|
|
builder.post(body);
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
/**
|
|
* Builds and returns a configured {@link WebModule} instance.
|
|
*
|
|
* @return a new {@code WebModule} instance
|
|
*/
|
|
public WebModule build() {
|
|
this.client = new OkHttpClient.Builder()
|
|
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
|
|
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
|
|
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
|
|
.build();
|
|
this.request = createRequest();
|
|
|
|
return new WebModule(this);
|
|
}
|
|
}
|
|
|
|
private static class CachedResponse {
|
|
private final int statusCode;
|
|
private final Headers headers;
|
|
private final long elapsedTime;
|
|
private final String body;
|
|
private final long expirationTime;
|
|
private final String errorMessage;
|
|
|
|
public CachedResponse(int statusCode, Headers headers, long elapsedTime, String body, long expirationTime, String errorMessage) {
|
|
this.statusCode = statusCode;
|
|
this.headers = headers;
|
|
this.elapsedTime = elapsedTime;
|
|
this.body = body;
|
|
this.expirationTime = expirationTime;
|
|
this.errorMessage = errorMessage;
|
|
}
|
|
|
|
public boolean isExpired() {
|
|
return System.currentTimeMillis() > expirationTime;
|
|
}
|
|
|
|
public int getStatusCode() {
|
|
return statusCode;
|
|
}
|
|
|
|
public Headers getHeaders() {
|
|
return headers;
|
|
}
|
|
|
|
public long getElapsedTime() {
|
|
return elapsedTime;
|
|
}
|
|
|
|
public Optional<String> getBody() {
|
|
return Optional.ofNullable(body);
|
|
}
|
|
|
|
public Optional<String> getErrorMessage() {
|
|
return Optional.ofNullable(errorMessage);
|
|
}
|
|
}
|
|
}
|