Replace GitLab integration with Gitea in issue reporting

Migrated issue reporting functionality from GitLab to Gitea. Updated environment variables, dependencies, and configuration references to support Gitea. Introduced `GiteaClient` for communication with the Gitea API and refactored related code and tests to align with the new integration.
This commit is contained in:
anthorob committed 2026-10-13 13:16:54 -04:00
1 parent a23e3843ed
commit abdd2d1656
11 files changed
+194 -89

No files matched your search

@@ -10,11 +10,10 @@ import me.bobert.netneutrality.core.command.CommandExecutionResult;
import me.bobert.netneutrality.core.command.CommandOptionsBuilder;
import me.bobert.netneutrality.core.command.impl.AbstractCommand;
import me.bobert.netneutrality.core.context.CommandContext;
import me.bobert.netneutrality.http.WebModule;
import me.bobert.netneutrality.utils.GiteaClient;
import me.bobert.netneutrality.tasks.git.ReportIssueTask;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import me.bobert.netneutrality.core.tasks.TaskManager;
import me.bobert.netneutrality.utils.SettingsProvider;
@@ -72,15 +71,16 @@ public class ReportIssueCommand extends AbstractCommand {
@Override
public CommandExecutionResult execute(CommandContext context) {
String gitlabHost = settings.get("GITLAB_HOST");
String gitlabToken = settings.get("GITLAB_TOKEN");
String projectId = settings.get("GITLAB_PROJECT_ID");
String host = settings.get("GITEA_HOST");
String token = settings.get("GITEA_TOKEN");
String owner = settings.get("GITEA_OWNER");
String repo = settings.get("GITEA_REPO");
if (gitlabHost == null || gitlabToken == null || projectId == null) {
if (host == null || token == null || owner == null || repo == null) {
return CommandExecutionResult.error("Command is not configured, failed to create issue.");
}
if (new WebModule.Builder(gitlabHost).ping().timeout(2, TimeUnit.SECONDS).build().request().isSuccess()) {
if (new GiteaClient(host, token, owner, repo).ping()) {
return CommandExecutionResult.task(new ReportIssueTask(taskManager, context, settings));
}
@@ -21,9 +21,9 @@ import net.dv8tion.jda.api.interactions.modals.ModalMapping;
import net.dv8tion.jda.api.modals.Modal;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.gitlab4j.api.GitLabApi;
import org.gitlab4j.api.GitLabApiException;
import org.gitlab4j.api.models.Issue;
import me.bobert.netneutrality.utils.GiteaClient;
import java.io.IOException;
public class ReportIssueTask extends ModalTask {
private static final Logger logger = LogManager.getLogger(ReportIssueTask.class);
@@ -72,14 +72,16 @@ public class ReportIssueTask extends ModalTask {
public Lifecycle execute(TaskContext context) {
context.ephemeral();
String gitlabHost = settings.get("GITLAB_HOST");
String gitlabToken = settings.get("GITLAB_TOKEN");
String projectId = settings.get("GITLAB_PROJECT_ID");
String host = settings.get("GITEA_HOST");
String token = settings.get("GITEA_TOKEN");
String owner = settings.get("GITEA_OWNER");
String repo = settings.get("GITEA_REPO");
if (gitlabHost == null || gitlabHost.isBlank()
|| gitlabToken == null || gitlabToken.isBlank()
|| projectId == null || projectId.isBlank()) {
logger.error("Gitlab is not configured in the bot settings, can't execute task");
if (host == null || host.isBlank()
|| token == null || token.isBlank()
|| owner == null || owner.isBlank()
|| repo == null || repo.isBlank()) {
logger.error("Gitea is not configured in the bot settings, can't execute task");
context.reply("Command is not configured, failed to create issue.");
return null;
}
@@ -93,8 +95,7 @@ public class ReportIssueTask extends ModalTask {
}
User user = context.getDiscordUser();
try (GitLabApi gitLabApi = new GitLabApi(gitlabHost, gitlabToken)) {
gitLabApi.setRequestTimeout(1000, 5000);
try {
String title = String.format("Bug report: %s", modalTitle.getAsString());
String description = "Issue reported from user of bot\n\n" +
@@ -102,11 +103,11 @@ public class ReportIssueTask extends ModalTask {
"Last Sentry ID: " + Sentry.getLastEventId() + "\n\n" +
"Description from user: \n\n" + modalDescription.getAsString();
Issue issue = gitLabApi.getIssuesApi().createIssue(projectId, title, description);
String url = new GiteaClient(host, token, owner, repo).createIssue(title, description);
context.reply(String.format("Issue was successfully created: %s", issue.getWebUrl()));
context.reply(String.format("Issue was successfully created: %s", url));
} catch (GitLabApiException e) {
} catch (IOException e) {
context.reply("Failed to create issue.");
LogHandler.log(e);
}
@@ -0,0 +1,88 @@
package me.bobert.netneutrality.utils;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import java.io.IOException;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public class GiteaClient {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private static final ObjectMapper MAPPER = new ObjectMapper();
private final String host;
private final String token;
private final String owner;
private final String repo;
private final OkHttpClient client;
public GiteaClient(String host, String token, String owner, String repo) {
this(host, token, owner, repo, new OkHttpClient.Builder()
.connectTimeout(Duration.ofSeconds(1))
.readTimeout(Duration.ofSeconds(5))
.build());
}
public GiteaClient(String host, String token, String owner, String repo, OkHttpClient client) {
Objects.requireNonNull(host, "host");
this.host = host.endsWith("/") ? host.replaceAll("/+$", "") : host;
this.token = Objects.requireNonNull(token, "token");
this.owner = Objects.requireNonNull(owner, "owner");
this.repo = Objects.requireNonNull(repo, "repo");
this.client = Objects.requireNonNull(client, "client");
}
public boolean ping() {
Request request = new Request.Builder().url(host + "/api/v1/version").get().build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
} catch (IOException e) {
return false;
}
}
/**
* Creates an issue and returns its html url.
*
* @throws IOException on network failure, non-201 response or malformed response
*/
public String createIssue(String title, String body) throws IOException {
Map<String, String> payload = new LinkedHashMap<>();
payload.put("title", title);
payload.put("body", body);
Request request = new Request.Builder()
.url(host + "/api/v1/repos/" + owner + "/" + repo + "/issues")
.header("Authorization", "token " + token)
.header("Accept", "application/json")
.post(RequestBody.create(MAPPER.writeValueAsString(payload), JSON))
.build();
try (Response response = client.newCall(request).execute()) {
if (response.code() != 201) {
throw new IOException("Gitea returned HTTP " + response.code() + " when creating issue");
}
if (response.body() == null) {
throw new IOException("Gitea returned an empty response");
}
JsonNode url = MAPPER.readTree(response.body().string()).get("html_url");
if (url == null || !url.isTextual()) {
throw new IOException("Gitea response did not contain html_url");
}
return url.asText();
}
}
}
@@ -6,57 +6,85 @@ package me.bobert.netneutrality.command.git;
* https://discord.gg/qUa6zNFZsd
*/
import org.gitlab4j.api.GitLabApi;
import org.gitlab4j.api.IssuesApi;
import org.gitlab4j.api.models.Issue;
import com.sun.net.httpserver.HttpServer;
import me.bobert.netneutrality.utils.GiteaClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ReportIssueCommandTest {
private GitLabApi mockGitLabApi;
private IssuesApi mockIssuesApi;
private HttpServer server;
private final AtomicReference<String> lastAuth = new AtomicReference<>();
private final AtomicReference<String> lastBody = new AtomicReference<>();
private volatile int status = 201;
private volatile String response = "{\"html_url\":\"http://x/o/r/issues/1\"}";
@BeforeEach
void setup() {
mockGitLabApi = mock(GitLabApi.class);
mockIssuesApi = mock(IssuesApi.class);
void setup() throws IOException {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/api/v1/version", ex -> {
ex.sendResponseHeaders(200, -1);
ex.close();
});
server.createContext("/api/v1/repos/o/r/issues", ex -> {
lastAuth.set(ex.getRequestHeaders().getFirst("Authorization"));
lastBody.set(new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
ex.sendResponseHeaders(status, bytes.length);
ex.getResponseBody().write(bytes);
ex.close();
});
server.start();
}
when(mockGitLabApi.getIssuesApi()).thenReturn(mockIssuesApi);
@AfterEach
void teardown() {
server.stop(0);
}
private GiteaClient client() {
return new GiteaClient("http://127.0.0.1:" + server.getAddress().getPort() + "/", "tok", "o", "r");
}
@Test
void testCreateIssueMocked() throws Exception {
int projectId = 123456;
String modalTitle = "Bug report: mocked bug report";
String modalDescription = "This is a mocked description";
String userName = "TestUser";
String userId = UUID.randomUUID().toString();
String sentryId = UUID.randomUUID().toString();
String expectedTitle = String.format("Bug report: %s", modalTitle);
String expectedDescription = "Issue reported from user of bot\n\n" +
"User: " + userName + String.format("[%s]", userId) + "\n\n" +
"Last Sentry ID: " + sentryId + "\n\n" +
"Description from user: \n\n" + modalDescription;
Issue mockedIssue = new Issue();
mockedIssue.setTitle(expectedTitle);
mockedIssue.setDescription(expectedDescription);
when(mockIssuesApi.createIssue(eq(projectId), eq(expectedTitle), eq(expectedDescription))).thenReturn(mockedIssue);
Issue created = mockGitLabApi.getIssuesApi().createIssue(projectId, expectedTitle, expectedDescription);
assertNotNull(created);
assertEquals(expectedTitle, created.getTitle());
assertEquals(expectedDescription, created.getDescription());
verify(mockIssuesApi).createIssue(projectId, expectedTitle, expectedDescription);
void createIssueReturnsHtmlUrl() throws IOException {
assertEquals("http://x/o/r/issues/1", client().createIssue("t", "b"));
assertEquals("token tok", lastAuth.get());
assertTrue(lastBody.get().contains("\"title\":\"t\""));
}
}
@Test
void nonCreatedStatusThrows() {
status = 401;
assertThrows(IOException.class, () -> client().createIssue("t", "b"));
}
@Test
void malformedJsonThrows() {
response = "not json";
assertThrows(IOException.class, () -> client().createIssue("t", "b"));
}
@Test
void missingHtmlUrlThrows() {
response = "{}";
assertThrows(IOException.class, () -> client().createIssue("t", "b"));
}
@Test
void pingWorksAndFailsWhenDown() {
assertTrue(client().ping());
server.stop(0);
assertFalse(client().ping());
}
}