diff --git a/src/main/java/me/bobert/netneutrality/db/service/GuildService.java b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java index d7338f3..a3f0692 100644 --- a/src/main/java/me/bobert/netneutrality/db/service/GuildService.java +++ b/src/main/java/me/bobert/netneutrality/db/service/GuildService.java @@ -83,6 +83,31 @@ public class GuildService { 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 * writes when a value actually changed, to avoid needless updates on every event. diff --git a/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java index 876df37..3edc56a 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java @@ -7,20 +7,29 @@ package me.bobert.netneutrality.http.server; */ import io.javalin.config.RoutesConfig; +import io.javalin.http.BadRequestResponse; import io.javalin.http.Context; import io.javalin.http.NotFoundResponse; import io.javalin.http.ServiceUnavailableResponse; import io.javalin.openapi.HttpMethod; import io.javalin.openapi.OpenApi; import io.javalin.openapi.OpenApiContent; +import io.javalin.openapi.OpenApiParam; +import io.javalin.openapi.OpenApiRequestBody; 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.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.GuildSummaryResponse; import me.bobert.netneutrality.http.server.dto.MemberRolesResponse; import me.bobert.netneutrality.http.server.dto.MemberSummaryResponse; 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 net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; @@ -34,14 +43,15 @@ import java.util.Map; 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 { private GuildRoutes() { } - public static void register(RoutesConfig routes, Supplier jdaSupplier) { + public static void register(RoutesConfig routes, Supplier jdaSupplier, GuildServiceProvider guildServices) { routes.get("/guilds", ctx -> guilds(jdaSupplier, ctx)); routes.get("/guilds/owner", ctx -> owner(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/voice-channels/{guildId}", ctx -> voiceChannels(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( @@ -156,14 +170,15 @@ public final class GuildRoutes { tags = {"Guilds"}, pathParams = @io.javalin.openapi.OpenApiParam(name = "guildId", description = "The Discord guild id"), 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)) } ) private static void roles(Supplier jdaSupplier, Context ctx) { ctx.json( 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() ); } @@ -330,6 +345,119 @@ public final class GuildRoutes { 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 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 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 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 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. */ diff --git a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java index ad1d567..61b4808 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java +++ b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java @@ -77,7 +77,7 @@ public class WebApi implements Lifecycle { config.routes.before(new ApiKeyFilter(settings)); 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); SettingsRoutes.register(config.routes, jdaSupplier, guildServices); StatsRoutes.register(config.routes, jdaSupplier, guildServices); diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildBooleanUpdateRequest.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildBooleanUpdateRequest.java new file mode 100644 index 0000000..9de00f4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildBooleanUpdateRequest.java @@ -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 +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildConfigResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildConfigResponse.java new file mode 100644 index 0000000..3843af0 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildConfigResponse.java @@ -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 +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildLogChannelUpdateRequest.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildLogChannelUpdateRequest.java new file mode 100644 index 0000000..5e3774e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildLogChannelUpdateRequest.java @@ -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 +) { +}