From 9ffcf816eab090a3f3a50383640cd1fc3bd88a24 Mon Sep 17 00:00:00 2001 From: marcos Date: Wed, 5 Aug 2026 15:11:11 +0000 Subject: [PATCH] feat: add Fetcher seam with an identifying user agent --- .../dev/marcospaulo/canalhandia/Fetcher.java | 14 +++++ .../marcospaulo/canalhandia/HttpFetcher.java | 59 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/main/java/dev/marcospaulo/canalhandia/Fetcher.java create mode 100644 src/main/java/dev/marcospaulo/canalhandia/HttpFetcher.java diff --git a/src/main/java/dev/marcospaulo/canalhandia/Fetcher.java b/src/main/java/dev/marcospaulo/canalhandia/Fetcher.java new file mode 100644 index 0000000..49d6334 --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/Fetcher.java @@ -0,0 +1,14 @@ +package dev.marcospaulo.canalhandia; + +import java.io.IOException; + +/** The one place the plugin talks to the network, so tests can replace it. */ +interface Fetcher { + + /** GET a URL, returning the body. */ + String get(String url) throws IOException, InterruptedException; + + /** POST JSON with a bearer token, returning the body. */ + String postJson(String url, String json, String bearer) + throws IOException, InterruptedException; +} diff --git a/src/main/java/dev/marcospaulo/canalhandia/HttpFetcher.java b/src/main/java/dev/marcospaulo/canalhandia/HttpFetcher.java new file mode 100644 index 0000000..0ad6834 --- /dev/null +++ b/src/main/java/dev/marcospaulo/canalhandia/HttpFetcher.java @@ -0,0 +1,59 @@ +package dev.marcospaulo.canalhandia; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +final class HttpFetcher implements Fetcher { + + /** + * pt.minecraft.wiki answers 403 to a default user agent — MediaWiki policy + * requires callers to identify themselves. + */ + private static final String AGENT = + "Canalhandia-Minecraft-Bot/1.0 (https://marcospaulo.dev.br)"; + + private final HttpClient http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + private final int timeoutSeconds; + + HttpFetcher(int timeoutSeconds) { + this.timeoutSeconds = timeoutSeconds; + } + + @Override + public String get(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .timeout(Duration.ofSeconds(timeoutSeconds)) + .header("User-Agent", AGENT) + .GET() + .build(); + return body(http.send(request, HttpResponse.BodyHandlers.ofString())); + } + + @Override + public String postJson(String url, String json, String bearer) + throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder(URI.create(url)) + .timeout(Duration.ofSeconds(timeoutSeconds)) + .header("Content-Type", "application/json") + .header("User-Agent", AGENT) + .header("Authorization", "Bearer " + bearer) + .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) + .build(); + return body(http.send(request, HttpResponse.BodyHandlers.ofString())); + } + + private String body(HttpResponse response) throws IOException { + if (response.statusCode() / 100 != 2) { + throw new IOException("HTTP " + response.statusCode() + ": " + + AiText.forLog(response.body())); + } + return response.body(); + } +}