Add logging configuration endpoints for guilds

Introduced `/guilds/config/{guildId}`, `/guilds/debug/{guildId}`, `/guilds/logging/{guildId}`, and `/guilds/log-channel/{guildId}` endpoints to manage guild-level logging settings via API. Updated `GuildService` with methods for handling debug, logging mode, and log channel configuration. Integrated new routes into `GuildRoutes` and updated OpenAPI schemas with corresponding DTOs.
This commit is contained in:
anthorob committed 2026-09-30 12:43:45 -04:00
1 parent 0f96051fd0
commit 88298380f8
6 files changed
+208 -5

No files matched your search

@@ -83,6 +83,31 @@ public class GuildService {
this.oGuild = guildRepository.save(current); this.oGuild = guildRepository.save(current);
} }
public boolean isLoggingEnabled() {
return oGuild.getLogging();
}
public void updateLoggingMode(boolean logging) {
OGuild current = findOrCreateGuild(guildId);
current.setLogging(logging);
this.oGuild = guildRepository.save(current);
}
public String getLogChannel() {
return oGuild.getLogChannel();
}
/**
* Sets (or clears, when {@code logChannel} is {@code null}/blank) the guild's log channel id.
*
* @param logChannel the Discord channel id to log to, or {@code null}/blank to clear it
*/
public void updateLogChannel(String logChannel) {
OGuild current = findOrCreateGuild(guildId);
current.setLogChannel(logChannel == null || logChannel.isBlank() ? null : logChannel);
this.oGuild = guildRepository.save(current);
}
/** /**
* Mirrors the guild's name/icon into the database so they survive JDA cache eviction. Only * Mirrors the guild's name/icon into the database so they survive JDA cache eviction. Only
* writes when a value actually changed, to avoid needless updates on every event. * writes when a value actually changed, to avoid needless updates on every event.
@@ -7,20 +7,29 @@ package me.bobert.netneutrality.http.server;
*/ */
import io.javalin.config.RoutesConfig; import io.javalin.config.RoutesConfig;
import io.javalin.http.BadRequestResponse;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.NotFoundResponse; import io.javalin.http.NotFoundResponse;
import io.javalin.http.ServiceUnavailableResponse; import io.javalin.http.ServiceUnavailableResponse;
import io.javalin.openapi.HttpMethod; import io.javalin.openapi.HttpMethod;
import io.javalin.openapi.OpenApi; import io.javalin.openapi.OpenApi;
import io.javalin.openapi.OpenApiContent; import io.javalin.openapi.OpenApiContent;
import io.javalin.openapi.OpenApiParam;
import io.javalin.openapi.OpenApiRequestBody;
import io.javalin.openapi.OpenApiResponse; import io.javalin.openapi.OpenApiResponse;
import me.bobert.netneutrality.db.service.GuildService;
import me.bobert.netneutrality.db.service.GuildServiceProvider;
import me.bobert.netneutrality.http.server.dto.ChannelSummaryResponse; import me.bobert.netneutrality.http.server.dto.ChannelSummaryResponse;
import me.bobert.netneutrality.http.server.dto.ErrorResponse; import me.bobert.netneutrality.http.server.dto.ErrorResponse;
import me.bobert.netneutrality.http.server.dto.GuildBooleanUpdateRequest;
import me.bobert.netneutrality.http.server.dto.GuildConfigResponse;
import me.bobert.netneutrality.http.server.dto.GuildLogChannelUpdateRequest;
import me.bobert.netneutrality.http.server.dto.GuildOwnerEntryResponse; import me.bobert.netneutrality.http.server.dto.GuildOwnerEntryResponse;
import me.bobert.netneutrality.http.server.dto.GuildSummaryResponse; import me.bobert.netneutrality.http.server.dto.GuildSummaryResponse;
import me.bobert.netneutrality.http.server.dto.MemberRolesResponse; import me.bobert.netneutrality.http.server.dto.MemberRolesResponse;
import me.bobert.netneutrality.http.server.dto.MemberSummaryResponse; import me.bobert.netneutrality.http.server.dto.MemberSummaryResponse;
import me.bobert.netneutrality.http.server.dto.OwnerResponse; import me.bobert.netneutrality.http.server.dto.OwnerResponse;
import me.bobert.netneutrality.http.server.dto.RoleSummaryResponse;
import me.bobert.netneutrality.http.server.dto.VoiceChannelSummaryResponse; import me.bobert.netneutrality.http.server.dto.VoiceChannelSummaryResponse;
import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Guild;
@@ -34,14 +43,15 @@ import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
* Registers the read-only {@code /guilds/*} endpoints on a {@link RoutesConfig} instance. * Registers the read-only {@code /guilds/*} endpoints, as well as the debug/logging/log-channel
* configuration endpoints, on a {@link RoutesConfig} instance.
*/ */
public final class GuildRoutes { public final class GuildRoutes {
private GuildRoutes() { private GuildRoutes() {
} }
public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier) { public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices) {
routes.get("/guilds", ctx -> guilds(jdaSupplier, ctx)); routes.get("/guilds", ctx -> guilds(jdaSupplier, ctx));
routes.get("/guilds/owner", ctx -> owner(jdaSupplier, ctx)); routes.get("/guilds/owner", ctx -> owner(jdaSupplier, ctx));
routes.get("/guilds/owner/{guildId}", ctx -> ownerOf(jdaSupplier, ctx)); routes.get("/guilds/owner/{guildId}", ctx -> ownerOf(jdaSupplier, ctx));
@@ -51,6 +61,10 @@ public final class GuildRoutes {
routes.get("/guilds/members/{guildId}/{userId}/roles", ctx -> memberRoles(jdaSupplier, ctx)); routes.get("/guilds/members/{guildId}/{userId}/roles", ctx -> memberRoles(jdaSupplier, ctx));
routes.get("/guilds/voice-channels/{guildId}", ctx -> voiceChannels(jdaSupplier, ctx)); routes.get("/guilds/voice-channels/{guildId}", ctx -> voiceChannels(jdaSupplier, ctx));
routes.get("/guilds/voice-channels/{guildId}/{channelId}", ctx -> voiceChannel(jdaSupplier, ctx)); routes.get("/guilds/voice-channels/{guildId}/{channelId}", ctx -> voiceChannel(jdaSupplier, ctx));
routes.get("/guilds/config/{guildId}", ctx -> config(jdaSupplier, guildServices, ctx));
routes.put("/guilds/debug/{guildId}", ctx -> updateDebug(jdaSupplier, guildServices, ctx));
routes.put("/guilds/logging/{guildId}", ctx -> updateLogging(jdaSupplier, guildServices, ctx));
routes.put("/guilds/log-channel/{guildId}", ctx -> updateLogChannel(jdaSupplier, guildServices, ctx));
} }
@OpenApi( @OpenApi(
@@ -156,14 +170,15 @@ public final class GuildRoutes {
tags = {"Guilds"}, tags = {"Guilds"},
pathParams = @io.javalin.openapi.OpenApiParam(name = "guildId", description = "The Discord guild id"), pathParams = @io.javalin.openapi.OpenApiParam(name = "guildId", description = "The Discord guild id"),
responses = { responses = {
@OpenApiResponse(status = "200", content = @OpenApiContent(from = ChannelSummaryResponse[].class)), @OpenApiResponse(status = "200", content = @OpenApiContent(from = RoleSummaryResponse[].class)),
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
} }
) )
private static void roles(Supplier<JDA> jdaSupplier, Context ctx) { private static void roles(Supplier<JDA> jdaSupplier, Context ctx) {
ctx.json( ctx.json(
guild(jdaSupplier, ctx).getRoles().stream() guild(jdaSupplier, ctx).getRoles().stream()
.map(role -> ordered("channel_id", role.getId(), "channel_name", role.getName())) .filter(role -> !role.isManaged())
.map(role -> ordered("role_id", role.getId(), "role_name", role.getName()))
.toList() .toList()
); );
} }
@@ -330,6 +345,119 @@ public final class GuildRoutes {
return map; return map;
} }
@OpenApi(
path = "/guilds/config/{guildId}",
methods = HttpMethod.GET,
summary = "Get a guild's debug/logging configuration",
tags = {"Guilds"},
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
responses = {
@OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildConfigResponse.class)),
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
}
)
private static void config(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
Guild guild = guild(jdaSupplier, ctx);
ctx.json(guildConfig(guild, guildServices));
}
@OpenApi(
path = "/guilds/debug/{guildId}",
methods = HttpMethod.PUT,
summary = "Enable or disable debug/error logging for a guild",
tags = {"Guilds"},
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = GuildBooleanUpdateRequest.class)),
responses = {
@OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildConfigResponse.class)),
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)),
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
}
)
private static void updateDebug(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
Guild guild = guild(jdaSupplier, ctx);
boolean value = booleanValue(ctx);
GuildService guildService = guildServices.get(guild.getId());
guildService.updateDebugMode(value);
ctx.json(guildConfig(guild, guildServices));
}
@OpenApi(
path = "/guilds/logging/{guildId}",
methods = HttpMethod.PUT,
summary = "Enable or disable logging for a guild",
tags = {"Guilds"},
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = GuildBooleanUpdateRequest.class)),
responses = {
@OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildConfigResponse.class)),
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)),
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
}
)
private static void updateLogging(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
Guild guild = guild(jdaSupplier, ctx);
boolean value = booleanValue(ctx);
GuildService guildService = guildServices.get(guild.getId());
guildService.updateLoggingMode(value);
ctx.json(guildConfig(guild, guildServices));
}
@OpenApi(
path = "/guilds/log-channel/{guildId}",
methods = HttpMethod.PUT,
summary = "Set (or clear) the log channel for a guild",
tags = {"Guilds"},
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = GuildLogChannelUpdateRequest.class)),
responses = {
@OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildConfigResponse.class)),
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)),
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
}
)
private static void updateLogChannel(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
Guild guild = guild(jdaSupplier, ctx);
String channelId = ctx.bodyAsClass(Map.class).get("value") instanceof String s ? s : null;
if (channelId != null && !channelId.isBlank() && guild.getTextChannelById(channelId) == null) {
throw new BadRequestResponse("Unknown text channel: " + channelId);
}
GuildService guildService = guildServices.get(guild.getId());
guildService.updateLogChannel(channelId);
ctx.json(guildConfig(guild, guildServices));
}
/**
* Resolves the {@code value} field of a JSON request body as a boolean, or fails with 400.
*/
private static boolean booleanValue(Context ctx) {
Object value = ctx.bodyAsClass(Map.class).get("value");
if (value instanceof Boolean b) {
return b;
}
throw new BadRequestResponse("value must be a boolean");
}
/**
* Builds the {@code /guilds/config/{guildId}} response for a guild's current configuration.
*/
private static GuildConfigResponse guildConfig(Guild guild, GuildServiceProvider guildServices) {
GuildService guildService = guildServices.get(guild.getId());
return new GuildConfigResponse(
guild.getId(),
guildService.isDebugEnabled(),
guildService.isLoggingEnabled(),
guildService.getLogChannel()
);
}
/** /**
* Resolves the JDA client or fails with 503 while it is not ready yet. * Resolves the JDA client or fails with 503 while it is not ready yet.
*/ */
@@ -77,7 +77,7 @@ public class WebApi implements Lifecycle {
config.routes.before(new ApiKeyFilter(settings)); config.routes.before(new ApiKeyFilter(settings));
config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML)); config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML));
GuildRoutes.register(config.routes, jdaSupplier); GuildRoutes.register(config.routes, jdaSupplier, guildServices);
ActionRoutes.register(config.routes, jdaSupplier, shutdownHook); ActionRoutes.register(config.routes, jdaSupplier, shutdownHook);
SettingsRoutes.register(config.routes, jdaSupplier, guildServices); SettingsRoutes.register(config.routes, jdaSupplier, guildServices);
StatsRoutes.register(config.routes, jdaSupplier, guildServices); StatsRoutes.register(config.routes, jdaSupplier, guildServices);
@@ -0,0 +1,16 @@
package me.bobert.netneutrality.http.server.dto;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
/**
* OpenAPI schema for the {@code /guilds/debug/{guildId}} and {@code /guilds/logging/{guildId}}
* request bodies.
*/
public record GuildBooleanUpdateRequest(
boolean value
) {
}
@@ -0,0 +1,19 @@
package me.bobert.netneutrality.http.server.dto;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
/**
* OpenAPI schema for the {@code /guilds/config/{guildId}} response, as well as the
* debug/logging/log-channel update responses.
*/
public record GuildConfigResponse(
String guild_id,
boolean debug,
boolean logging,
String log_channel
) {
}
@@ -0,0 +1,15 @@
package me.bobert.netneutrality.http.server.dto;
/*
* Copyright (c) anthorob
*
* https://discord.gg/qUa6zNFZsd
*/
/**
* OpenAPI schema for the {@code /guilds/log-channel/{guildId}} request body.
*/
public record GuildLogChannelUpdateRequest(
String value
) {
}