diff --git a/src/main/java/me/bobert/netneutrality/App.java b/src/main/java/me/bobert/netneutrality/App.java index a8cc55f..e93f39e 100644 --- a/src/main/java/me/bobert/netneutrality/App.java +++ b/src/main/java/me/bobert/netneutrality/App.java @@ -161,7 +161,7 @@ public class App { }, () -> { })); // Web API (priority 70) — Lifecycle with graceful stop. - context.register(new WebApi(settings, guildServices, this::getJDA, this::shutdown)); + context.register(new WebApi(settings, guildServices, music, this::getJDA, this::shutdown)); } private static Lifecycle lifecycle(int priority, ThrowingRunnable start, Runnable stop) { diff --git a/src/main/java/me/bobert/netneutrality/http/server/ActionRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/ActionRoutes.java index bdd27fa..875a8e0 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/ActionRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/ActionRoutes.java @@ -13,6 +13,7 @@ import io.javalin.openapi.HttpMethod; import io.javalin.openapi.OpenApi; import io.javalin.openapi.OpenApiContent; import io.javalin.openapi.OpenApiResponse; +import me.bobert.netneutrality.http.server.dto.StatusResponse; import net.dv8tion.jda.api.JDA; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -47,7 +48,7 @@ public final class ActionRoutes { methods = HttpMethod.GET, summary = "Report the current Discord gateway health", tags = {"Actions"}, - responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)) + responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = StatusResponse.class)) ) private static void status(Supplier jdaSupplier, Context ctx) { JDA jda = GuildRoutes.jda(jdaSupplier); 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 92108c6..876df37 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/GuildRoutes.java @@ -14,6 +14,14 @@ import io.javalin.openapi.HttpMethod; import io.javalin.openapi.OpenApi; import io.javalin.openapi.OpenApiContent; import io.javalin.openapi.OpenApiResponse; +import me.bobert.netneutrality.http.server.dto.ChannelSummaryResponse; +import me.bobert.netneutrality.http.server.dto.ErrorResponse; +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.VoiceChannelSummaryResponse; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; @@ -50,7 +58,7 @@ public final class GuildRoutes { methods = HttpMethod.GET, summary = "List every guild the bot is currently in", tags = {"Guilds"}, - responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map[].class)) + responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildSummaryResponse[].class)) ) private static void guilds(Supplier jdaSupplier, Context ctx) { ctx.json( @@ -90,7 +98,7 @@ public final class GuildRoutes { methods = HttpMethod.GET, summary = "List every bot guild with its owner id", tags = {"Guilds"}, - responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map[].class)) + responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildOwnerEntryResponse[].class)) ) private static void owner(Supplier jdaSupplier, Context ctx) { ctx.json( @@ -107,8 +115,8 @@ 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 = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = OwnerResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void ownerOf(Supplier jdaSupplier, Context ctx) { @@ -129,8 +137,8 @@ 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 = Map[].class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = ChannelSummaryResponse[].class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void channels(Supplier jdaSupplier, Context ctx) { @@ -148,8 +156,8 @@ 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 = Map[].class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = ChannelSummaryResponse[].class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void roles(Supplier jdaSupplier, Context ctx) { @@ -170,8 +178,8 @@ public final class GuildRoutes { @io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id") }, responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = MemberSummaryResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void member(Supplier jdaSupplier, Context ctx) { @@ -196,8 +204,8 @@ public final class GuildRoutes { @io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id") }, responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = MemberRolesResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void memberRoles(Supplier jdaSupplier, Context ctx) { @@ -245,8 +253,8 @@ 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 = Map[].class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = VoiceChannelSummaryResponse[].class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void voiceChannels(Supplier jdaSupplier, Context ctx) { @@ -267,8 +275,8 @@ public final class GuildRoutes { @io.javalin.openapi.OpenApiParam(name = "channelId", description = "The Discord voice channel id") }, responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = VoiceChannelSummaryResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void voiceChannel(Supplier jdaSupplier, Context ctx) { diff --git a/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java new file mode 100644 index 0000000..c90a9c8 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/MusicRoutes.java @@ -0,0 +1,250 @@ +package me.bobert.netneutrality.http.server; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import dev.arbjerg.lavalink.client.player.Track; +import io.javalin.config.RoutesConfig; +import io.javalin.http.BadRequestResponse; +import io.javalin.http.Context; +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.core.command.CommandExecutionResult; +import me.bobert.netneutrality.http.server.dto.CommandResultResponse; +import me.bobert.netneutrality.http.server.dto.ErrorResponse; +import me.bobert.netneutrality.http.server.dto.MusicStatusResponse; +import me.bobert.netneutrality.http.server.dto.QueuedTrackResponse; +import me.bobert.netneutrality.http.server.dto.VolumeRequest; +import me.bobert.netneutrality.http.server.dto.VolumeResponse; +import me.bobert.netneutrality.music.MusicManager; +import me.bobert.netneutrality.music.MusicService; +import me.bobert.netneutrality.music.TrackInfo; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * Registers the {@code /music/*} endpoints on a {@link RoutesConfig} instance. + * + *

These expose the guild music player over HTTP, mirroring the slash commands. Vote-skipping + * in particular has no Lavalink API equivalent, so it is only reachable through + * {@link MusicManager#voteSkip(Member)}. + */ +public final class MusicRoutes { + + private MusicRoutes() { + } + + public static void register(RoutesConfig routes, Supplier jdaSupplier, MusicService musicService) { + routes.get("/music/{guildId}/status", ctx -> status(jdaSupplier, musicService, ctx)); + routes.get("/music/{guildId}/queue", ctx -> queue(jdaSupplier, musicService, ctx)); + routes.post("/music/{guildId}/skip", ctx -> skip(jdaSupplier, musicService, ctx)); + routes.post("/music/{guildId}/stop", ctx -> stop(jdaSupplier, musicService, ctx)); + routes.put("/music/{guildId}/volume", ctx -> volume(jdaSupplier, musicService, ctx)); + routes.post("/music/{guildId}/vote-skip/{userId}", ctx -> voteSkip(jdaSupplier, musicService, ctx)); + } + + @OpenApi( + path = "/music/{guildId}/status", + methods = HttpMethod.GET, + summary = "Get the current player status of a guild", + tags = {"Music"}, + pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = MusicStatusResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void status(Supplier jdaSupplier, MusicService musicService, Context ctx) { + MusicManager manager = manager(jdaSupplier, musicService, ctx); + AudioChannel channel = manager.getConnectedChannel(); + + Map body = new LinkedHashMap<>(); + body.put("connected", manager.isConnected()); + body.put("channel_id", channel == null ? null : channel.getId()); + body.put("channel_name", channel == null ? null : channel.getName()); + body.put("playing", manager.isCurrentlyPlaying()); + body.put("current_track", trackSummary(manager.getCurrentlyPlaying())); + body.put("position_ms", manager.getCurrentPosition()); + body.put("queue_size", manager.getQueueSize()); + + ctx.json(body); + } + + @OpenApi( + path = "/music/{guildId}/queue", + methods = HttpMethod.GET, + summary = "List the queued tracks of a guild", + tags = {"Music"}, + pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = QueuedTrackResponse[].class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void queue(Supplier jdaSupplier, MusicService musicService, Context ctx) { + MusicManager manager = manager(jdaSupplier, musicService, ctx); + + ctx.json( + manager.getQueueManager().getQueue().stream() + .map(MusicRoutes::trackInfoSummary) + .toList() + ); + } + + @OpenApi( + path = "/music/{guildId}/skip", + methods = HttpMethod.POST, + summary = "Force-skip the currently playing track", + tags = {"Music"}, + pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = CommandResultResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void skip(Supplier jdaSupplier, MusicService musicService, Context ctx) { + MusicManager manager = manager(jdaSupplier, musicService, ctx); + ctx.json(resultBody(manager.getQueueManager().skip())); + } + + @OpenApi( + path = "/music/{guildId}/stop", + methods = HttpMethod.POST, + summary = "Stop the currently playing track, keeping the queue", + tags = {"Music"}, + pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = CommandResultResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void stop(Supplier jdaSupplier, MusicService musicService, Context ctx) { + MusicManager manager = manager(jdaSupplier, musicService, ctx); + manager.stopTrack(); + ctx.json(Map.of("status", "ok", "message", "Stopping current track")); + } + + @OpenApi( + path = "/music/{guildId}/volume", + methods = HttpMethod.PUT, + summary = "Set the player volume of a guild", + tags = {"Music"}, + pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), + requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = VolumeRequest.class)), + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = VolumeResponse.class)), + @OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void volume(Supplier jdaSupplier, MusicService musicService, Context ctx) { + MusicManager manager = manager(jdaSupplier, musicService, ctx); + int newVolume = volume(ctx); + + manager.setVolume(newVolume); + ctx.json(Map.of("status", "ok", "volume", newVolume)); + } + + /** + * Reads and validates the {@code volume} field of the request body, failing with 400 when + * missing or out of the {@code [0, 100]} range. + */ + private static int volume(Context ctx) { + Object rawVolume = ctx.bodyAsClass(Map.class).get("volume"); + int volume; + try { + volume = rawVolume instanceof Number number ? number.intValue() : Integer.parseInt(String.valueOf(rawVolume)); + } catch (NumberFormatException e) { + throw new BadRequestResponse("volume must be an integer between 0 and 100"); + } + + if (volume < 0 || volume > 100) { + throw new BadRequestResponse("volume must be an integer between 0 and 100"); + } + + return volume; + } + + @OpenApi( + path = "/music/{guildId}/vote-skip/{userId}", + methods = HttpMethod.POST, + summary = "Cast a vote to skip the currently playing track on behalf of a guild member", + tags = {"Music"}, + pathParams = { + @OpenApiParam(name = "guildId", description = "The Discord guild id"), + @OpenApiParam(name = "userId", description = "The Discord user id casting the vote") + }, + responses = { + @OpenApiResponse(status = "200", content = @OpenApiContent(from = CommandResultResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) + } + ) + private static void voteSkip(Supplier jdaSupplier, MusicService musicService, Context ctx) { + Guild guild = GuildRoutes.guild(jdaSupplier, ctx); + MusicManager manager = musicService.get(guild); + String userId = ctx.pathParam("userId"); + + CacheOrFetch.resolve( + ctx, + () -> guild.getMemberById(userId), + () -> guild.retrieveMemberById(userId), + member -> resultBody(manager.voteSkip(member)) + ); + } + + /** + * Resolves the {@link MusicManager} for the guild path parameter, creating it on first use. + */ + private static MusicManager manager(Supplier jdaSupplier, MusicService musicService, Context ctx) { + return musicService.get(GuildRoutes.guild(jdaSupplier, ctx)); + } + + /** + * Builds a JSON-friendly summary for a {@link CommandExecutionResult}. + */ + private static Map resultBody(CommandExecutionResult result) { + Map body = new LinkedHashMap<>(); + body.put("status", result.getResult().name()); + body.put("message", result.getMessage()); + return body; + } + + /** + * Builds a JSON-friendly summary for the currently playing track, or {@code null} when idle. + */ + private static Map trackSummary(Track track) { + if (track == null) { + return null; + } + + Map summary = new LinkedHashMap<>(); + summary.put("title", track.getInfo().getTitle()); + summary.put("author", track.getInfo().getAuthor()); + summary.put("uri", track.getInfo().getUri()); + summary.put("duration_ms", track.getInfo().getLength()); + return summary; + } + + /** + * Builds a JSON-friendly summary for a queued {@link TrackInfo}. + */ + private static Map trackInfoSummary(TrackInfo info) { + Map summary = trackSummary(info.getTrack()); + summary.put("requester_id", info.getRequester().getId()); + summary.put("skip_count", info.getSkipCount()); + return summary; + } +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/SettingsRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/SettingsRoutes.java index 549d1c7..24d7b92 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/SettingsRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/SettingsRoutes.java @@ -19,6 +19,12 @@ import me.bobert.netneutrality.db.GSettings; import me.bobert.netneutrality.db.SettingType; import me.bobert.netneutrality.db.service.GuildServiceProvider; import me.bobert.netneutrality.db.service.GuildSettingService; +import me.bobert.netneutrality.http.server.dto.ErrorResponse; +import me.bobert.netneutrality.http.server.dto.SettingCatalogEntryResponse; +import me.bobert.netneutrality.http.server.dto.SettingsCurrentResponse; +import me.bobert.netneutrality.http.server.dto.SettingsReloadResponse; +import me.bobert.netneutrality.http.server.dto.SettingsUpdateRequest; +import me.bobert.netneutrality.http.server.dto.SettingsUpdateResponse; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; @@ -50,7 +56,7 @@ public final class SettingsRoutes { methods = HttpMethod.GET, summary = "List every known per-guild setting with its default value, description, and type", tags = {"Settings"}, - responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map[].class)) + responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingCatalogEntryResponse[].class)) ) private static void settingsCatalog(Context ctx) { ctx.json( @@ -77,8 +83,8 @@ public final class SettingsRoutes { tags = {"Settings"}, pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsCurrentResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void settingsCurrent(Supplier jdaSupplier, GuildServiceProvider guildServices, Context ctx) { @@ -97,11 +103,11 @@ public final class SettingsRoutes { @OpenApiParam(name = "guildId", description = "The Discord guild id"), @OpenApiParam(name = "name", description = "The setting name, as returned by /settings/catalog") }, - requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = Map.class)), + requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = SettingsUpdateRequest.class)), responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "400", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsUpdateResponse.class)), + @OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void settingsUpdate(Supplier jdaSupplier, GuildServiceProvider guildServices, Context ctx) { @@ -185,8 +191,8 @@ public final class SettingsRoutes { tags = {"Settings"}, pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsReloadResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void settingsReload(Supplier jdaSupplier, GuildServiceProvider guildServices, Context ctx) { diff --git a/src/main/java/me/bobert/netneutrality/http/server/StatsRoutes.java b/src/main/java/me/bobert/netneutrality/http/server/StatsRoutes.java index 971d9a7..7bdadc7 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/StatsRoutes.java +++ b/src/main/java/me/bobert/netneutrality/http/server/StatsRoutes.java @@ -15,6 +15,9 @@ import io.javalin.openapi.OpenApiParam; 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.BotStatsResponse; +import me.bobert.netneutrality.http.server.dto.ErrorResponse; +import me.bobert.netneutrality.http.server.dto.GuildStatsResponse; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; @@ -39,7 +42,7 @@ public final class StatsRoutes { methods = HttpMethod.GET, summary = "Report bot-wide stats (guild count, gateway health, uptime)", tags = {"Stats"}, - responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)) + responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = BotStatsResponse.class)) ) private static void bot(Supplier jdaSupplier, Context ctx) { JDA jda = GuildRoutes.jda(jdaSupplier); @@ -59,8 +62,8 @@ public final class StatsRoutes { tags = {"Stats"}, pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"), responses = { - @OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)), - @OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class)) + @OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildStatsResponse.class)), + @OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class)) } ) private static void guild(Supplier jdaSupplier, GuildServiceProvider guildServices, Context ctx) { 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 dca3c9c..ad1d567 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java +++ b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java @@ -12,6 +12,7 @@ import io.javalin.openapi.plugin.OpenApiPlugin; import io.javalin.openapi.plugin.swagger.SwaggerPlugin; import me.bobert.netneutrality.bootstrap.Lifecycle; import me.bobert.netneutrality.db.service.GuildServiceProvider; +import me.bobert.netneutrality.music.MusicService; import me.bobert.netneutrality.utils.SettingsProvider; import net.dv8tion.jda.api.JDA; import org.apache.logging.log4j.LogManager; @@ -30,6 +31,7 @@ public class WebApi implements Lifecycle { private final SettingsProvider settings; private final GuildServiceProvider guildServices; + private final MusicService musicService; private final Supplier jdaSupplier; private final Runnable shutdownHook; private Javalin app; @@ -37,11 +39,13 @@ public class WebApi implements Lifecycle { public WebApi( SettingsProvider settings, GuildServiceProvider guildServices, + MusicService musicService, Supplier jdaSupplier, Runnable shutdownHook ) { this.settings = settings; this.guildServices = guildServices; + this.musicService = musicService; this.jdaSupplier = jdaSupplier; this.shutdownHook = shutdownHook; } @@ -77,6 +81,7 @@ public class WebApi implements Lifecycle { ActionRoutes.register(config.routes, jdaSupplier, shutdownHook); SettingsRoutes.register(config.routes, jdaSupplier, guildServices); StatsRoutes.register(config.routes, jdaSupplier, guildServices); + MusicRoutes.register(config.routes, jdaSupplier, musicService); config.routes.exception(HttpResponseException.class, (e, ctx) -> ctx.status(e.getStatus()).json(Map.of("status", "error", "message", e.getMessage())) diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/BotStatsResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/BotStatsResponse.java new file mode 100644 index 0000000..9b20cc0 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/BotStatsResponse.java @@ -0,0 +1,18 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /stats/bot} response. + */ +public record BotStatsResponse( + int guild_count, + long gateway_ping, + String status, + long uptime_seconds +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/ChannelSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/ChannelSummaryResponse.java new file mode 100644 index 0000000..194c0b3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/ChannelSummaryResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a {@code /guilds/channels/{guildId}} and {@code /guilds/roles/{guildId}} + * entry. + */ +public record ChannelSummaryResponse( + String channel_id, + String channel_name +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/CommandResultResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/CommandResultResponse.java new file mode 100644 index 0000000..557cf9d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/CommandResultResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /music/{guildId}/skip}, {@code /music/{guildId}/stop}, and + * {@code /music/{guildId}/vote-skip/{userId}} responses. + */ +public record CommandResultResponse( + String status, + String message +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/ErrorResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/ErrorResponse.java new file mode 100644 index 0000000..96723f3 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/ErrorResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * Shared OpenAPI schema for the error body written by the {@code HttpResponseException} handler + * in {@link me.bobert.netneutrality.http.server.WebApi}. + */ +public record ErrorResponse( + String status, + String message +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildOwnerEntryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildOwnerEntryResponse.java new file mode 100644 index 0000000..96f2762 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildOwnerEntryResponse.java @@ -0,0 +1,16 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a {@code /guilds/owner} entry. + */ +public record GuildOwnerEntryResponse( + String guild_id, + String owner_id +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildStatsResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildStatsResponse.java new file mode 100644 index 0000000..b1fc518 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildStatsResponse.java @@ -0,0 +1,20 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /stats/guilds/{guildId}} response. + */ +public record GuildStatsResponse( + String guild_id, + int member_count, + int channel_count, + int role_count, + boolean debug_enabled, + int blacklist_size +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/GuildSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildSummaryResponse.java new file mode 100644 index 0000000..4b6907d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/GuildSummaryResponse.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a {@code /guilds} entry. + */ +public record GuildSummaryResponse( + String guild_id, + String name, + String icon_url, + int member_count, + String owner_id +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/MemberRolesResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/MemberRolesResponse.java new file mode 100644 index 0000000..a6f62a4 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/MemberRolesResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /guilds/members/{guildId}/{userId}/roles} response. + */ +public record MemberRolesResponse( + String guild_id, + String user_id, + RoleSummaryResponse[] roles +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/MemberSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/MemberSummaryResponse.java new file mode 100644 index 0000000..ba97b0e --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/MemberSummaryResponse.java @@ -0,0 +1,18 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /guilds/members/{guildId}/{userId}} response. + */ +public record MemberSummaryResponse( + String user_id, + String username, + String nickname, + String[] role_ids +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/MusicStatusResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/MusicStatusResponse.java new file mode 100644 index 0000000..99d81d9 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/MusicStatusResponse.java @@ -0,0 +1,21 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /music/{guildId}/status} response. + */ +public record MusicStatusResponse( + boolean connected, + String channel_id, + String channel_name, + boolean playing, + TrackSummaryResponse current_track, + long position_ms, + int queue_size +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/OwnerResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/OwnerResponse.java new file mode 100644 index 0000000..591f6ba --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/OwnerResponse.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/owner/{guildId}} response. + */ +public record OwnerResponse( + String guild_id, + String owner_id +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/QueuedTrackResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/QueuedTrackResponse.java new file mode 100644 index 0000000..2ac49ec --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/QueuedTrackResponse.java @@ -0,0 +1,20 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a {@code /music/{guildId}/queue} entry. + */ +public record QueuedTrackResponse( + String title, + String author, + String uri, + long duration_ms, + String requester_id, + int skip_count +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/RoleSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/RoleSummaryResponse.java new file mode 100644 index 0000000..4f5ae0b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/RoleSummaryResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a role entry of the {@code /guilds/members/{guildId}/{userId}/roles} + * response. + */ +public record RoleSummaryResponse( + String role_id, + String role_name +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/SettingCatalogEntryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingCatalogEntryResponse.java new file mode 100644 index 0000000..e2e3372 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingCatalogEntryResponse.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a {@code /settings/catalog} entry. + */ +public record SettingCatalogEntryResponse( + String name, + String default_value, + String description, + String type, + String[] allowed_values +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsCurrentResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsCurrentResponse.java new file mode 100644 index 0000000..dba07a6 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsCurrentResponse.java @@ -0,0 +1,18 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import java.util.Map; + +/** + * OpenAPI schema for the {@code /settings/current/{guildId}} response. + */ +public record SettingsCurrentResponse( + String status, + Map settings +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsReloadResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsReloadResponse.java new file mode 100644 index 0000000..c45bb99 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsReloadResponse.java @@ -0,0 +1,15 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /settings/reload/{guildId}} response. + */ +public record SettingsReloadResponse( + boolean status +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateRequest.java b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateRequest.java new file mode 100644 index 0000000..e24d28c --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateRequest.java @@ -0,0 +1,15 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /settings/current/{guildId}/{name}} request body. + */ +public record SettingsUpdateRequest( + String value +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateResponse.java new file mode 100644 index 0000000..17c9b05 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/SettingsUpdateResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /settings/current/{guildId}/{name}} response. + */ +public record SettingsUpdateResponse( + String status, + String name, + String value +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/StatusResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/StatusResponse.java new file mode 100644 index 0000000..20452c6 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/StatusResponse.java @@ -0,0 +1,16 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /actions/status} response. + */ +public record StatusResponse( + String health, + String ping +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/TrackSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/TrackSummaryResponse.java new file mode 100644 index 0000000..3527a8d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/TrackSummaryResponse.java @@ -0,0 +1,19 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the currently playing track summary embedded in + * {@code /music/{guildId}/status}. + */ +public record TrackSummaryResponse( + String title, + String author, + String uri, + long duration_ms +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceChannelSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceChannelSummaryResponse.java new file mode 100644 index 0000000..cf61973 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceChannelSummaryResponse.java @@ -0,0 +1,20 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /guilds/voice-channels/{guildId}} and + * {@code /guilds/voice-channels/{guildId}/{channelId}} responses. + */ +public record VoiceChannelSummaryResponse( + String channel_id, + String channel_name, + int user_limit, + int member_count, + VoiceMemberSummaryResponse[] members +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceMemberSummaryResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceMemberSummaryResponse.java new file mode 100644 index 0000000..784dc39 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/VoiceMemberSummaryResponse.java @@ -0,0 +1,17 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for a member entry connected to a voice channel. + */ +public record VoiceMemberSummaryResponse( + String user_id, + String username, + String nickname +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeRequest.java b/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeRequest.java new file mode 100644 index 0000000..9e4943b --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeRequest.java @@ -0,0 +1,15 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /music/{guildId}/volume} request body. + */ +public record VolumeRequest( + int volume +) { +} diff --git a/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeResponse.java b/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeResponse.java new file mode 100644 index 0000000..9194703 --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/http/server/dto/VolumeResponse.java @@ -0,0 +1,16 @@ +package me.bobert.netneutrality.http.server.dto; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +/** + * OpenAPI schema for the {@code /music/{guildId}/volume} response. + */ +public record VolumeResponse( + String status, + int volume +) { +} diff --git a/src/main/java/me/bobert/netneutrality/music/MusicManager.java b/src/main/java/me/bobert/netneutrality/music/MusicManager.java index 05a90c1..d3b90dd 100644 --- a/src/main/java/me/bobert/netneutrality/music/MusicManager.java +++ b/src/main/java/me/bobert/netneutrality/music/MusicManager.java @@ -20,6 +20,7 @@ import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.utils.MusicUtils; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.GuildVoiceState; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -75,6 +76,18 @@ public class MusicManager implements MusicManagerCallback { return scheduler.getQueueManager(); } + /** + * Returns the vote-skip service for this guild. + * + *

Exposed separately since vote-skipping is application logic that has no equivalent on + * the Lavalink API. + * + * @return the vote-skip service + */ + public VoteSkipService getVoteSkipService() { + return voteSkipService; + } + public Guild getGuild() { return guild; } @@ -213,7 +226,7 @@ public class MusicManager implements MusicManagerCallback { } public synchronized void stopTrack() { - updatePlayer("stop track", builder -> builder.stopTrack()); + updatePlayer("stop track", PlayerUpdateBuilder::stopTrack); } public synchronized CommandExecutionResult resume() { @@ -221,8 +234,16 @@ public class MusicManager implements MusicManagerCallback { } public synchronized CommandExecutionResult voteSkip(CommandContext context) { + return voteSkip(context.getGuildUser()); + } + + /** + * Same as {@link #voteSkip(CommandContext)} but driven directly by a {@link Member}, so it + * can be invoked from non-command callers such as the REST API. + */ + public synchronized CommandExecutionResult voteSkip(Member member) { int threshold = parseVoteSkipThreshold(guildService); - return voteSkipService.voteSkip(context, getQueueManager(), getConnectedChannel(), isCurrentlyPlaying(), threshold); + return voteSkipService.voteSkip(member, guild.getId(), getQueueManager(), getConnectedChannel(), isCurrentlyPlaying(), threshold); } private static int parseVoteSkipThreshold(GuildService guildService) { diff --git a/src/main/java/me/bobert/netneutrality/music/VoteSkipService.java b/src/main/java/me/bobert/netneutrality/music/VoteSkipService.java index f363883..ac4ba60 100644 --- a/src/main/java/me/bobert/netneutrality/music/VoteSkipService.java +++ b/src/main/java/me/bobert/netneutrality/music/VoteSkipService.java @@ -8,6 +8,7 @@ package me.bobert.netneutrality.music; import me.bobert.netneutrality.core.command.CommandExecutionResult; import me.bobert.netneutrality.core.context.CommandContext; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -37,6 +38,15 @@ public class VoteSkipService { * configurable vote threshold percentage of connected listeners. */ public CommandExecutionResult voteSkip(CommandContext context, QueueManager queueManager, AudioChannel audioChannel, boolean isCurrentlyPlaying, int thresholdPercent) { + return voteSkip(context.getGuildUser(), context.getGuild().getId(), queueManager, audioChannel, isCurrentlyPlaying, thresholdPercent); + } + + /** + * Same as {@link #voteSkip(CommandContext, QueueManager, AudioChannel, boolean, int)} but + * driven directly by a {@link Member}, so it can be invoked from non-command callers such as + * the REST API, which has no {@link CommandContext} to work with. + */ + public CommandExecutionResult voteSkip(Member voter, String guildId, QueueManager queueManager, AudioChannel audioChannel, boolean isCurrentlyPlaying, int thresholdPercent) { if (audioChannel == null) { return CommandExecutionResult.warn("Bot is not connected to a voice channel"); } @@ -60,8 +70,8 @@ public class VoteSkipService { double needed = connected * (thresholdPercent / 100.0); - info.addSkip(context.getGuildUser()); - logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", context.getGuild().getId(), connected, needed, info.getSkipCount(), context.getGuildUser().getId()); + info.addSkip(voter); + logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", guildId, connected, needed, info.getSkipCount(), voter.getId()); if (info.getSkipCount() > needed) { return queueManager.skip(); diff --git a/src/test/java/me/bobert/netneutrality/http/server/WebApiTest.java b/src/test/java/me/bobert/netneutrality/http/server/WebApiTest.java index 403e391..8237cdd 100644 --- a/src/test/java/me/bobert/netneutrality/http/server/WebApiTest.java +++ b/src/test/java/me/bobert/netneutrality/http/server/WebApiTest.java @@ -10,6 +10,7 @@ import me.bobert.netneutrality.db.service.BlacklistService; import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.db.service.GuildServiceProvider; import me.bobert.netneutrality.db.service.GuildSettingService; +import me.bobert.netneutrality.music.MusicService; import me.bobert.netneutrality.utils.SettingsProvider; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.entities.Guild; @@ -82,7 +83,9 @@ class WebApiTest { when(settings.get("NETNEUTRALITY_API_PORT", "9000")).thenReturn(String.valueOf(port)); when(settings.get("NETNEUTRALITY_API_KEY")).thenReturn(API_KEY); - webApi = new WebApi(settings, provider, () -> jda, () -> { }); + MusicService musicService = mock(MusicService.class); + + webApi = new WebApi(settings, provider, musicService, () -> jda, () -> { }); webApi.start(); }