Add music API endpoints and update OpenAPI responses
Introduced `/music/*` routes for managing music player actions via HTTP, including status, queue, skip, stop, volume control, and vote-skip functionality. Refactored settings and guild routes to use new `Response` DTOs for precise OpenAPI schema definitions. Updated `WebApi` to register music routes and ensure proper integration with the existing `MusicService` logic.
This commit is contained in:
1 parent
e9c6f403e5
commit
0f96051fd0
34 files changed
+762
-35
No files matched your search
@@ -161,7 +161,7 @@ public class App {
|
|||||||
}, () -> { }));
|
}, () -> { }));
|
||||||
|
|
||||||
// Web API (priority 70) — Lifecycle with graceful stop.
|
// 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) {
|
private static Lifecycle lifecycle(int priority, ThrowingRunnable start, Runnable stop) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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.OpenApiResponse;
|
import io.javalin.openapi.OpenApiResponse;
|
||||||
|
import me.bobert.netneutrality.http.server.dto.StatusResponse;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -47,7 +48,7 @@ public final class ActionRoutes {
|
|||||||
methods = HttpMethod.GET,
|
methods = HttpMethod.GET,
|
||||||
summary = "Report the current Discord gateway health",
|
summary = "Report the current Discord gateway health",
|
||||||
tags = {"Actions"},
|
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<JDA> jdaSupplier, Context ctx) {
|
private static void status(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
JDA jda = GuildRoutes.jda(jdaSupplier);
|
JDA jda = GuildRoutes.jda(jdaSupplier);
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ 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.OpenApiResponse;
|
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.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
@@ -50,7 +58,7 @@ public final class GuildRoutes {
|
|||||||
methods = HttpMethod.GET,
|
methods = HttpMethod.GET,
|
||||||
summary = "List every guild the bot is currently in",
|
summary = "List every guild the bot is currently in",
|
||||||
tags = {"Guilds"},
|
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<JDA> jdaSupplier, Context ctx) {
|
private static void guilds(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
@@ -90,7 +98,7 @@ public final class GuildRoutes {
|
|||||||
methods = HttpMethod.GET,
|
methods = HttpMethod.GET,
|
||||||
summary = "List every bot guild with its owner id",
|
summary = "List every bot guild with its owner id",
|
||||||
tags = {"Guilds"},
|
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<JDA> jdaSupplier, Context ctx) {
|
private static void owner(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
@@ -107,8 +115,8 @@ 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 = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = OwnerResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void ownerOf(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void ownerOf(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
@@ -129,8 +137,8 @@ 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 = Map[].class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = ChannelSummaryResponse[].class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void channels(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void channels(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
@@ -148,8 +156,8 @@ 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 = Map[].class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = ChannelSummaryResponse[].class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.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) {
|
||||||
@@ -170,8 +178,8 @@ public final class GuildRoutes {
|
|||||||
@io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id")
|
@io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id")
|
||||||
},
|
},
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = MemberSummaryResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void member(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void member(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
@@ -196,8 +204,8 @@ public final class GuildRoutes {
|
|||||||
@io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id")
|
@io.javalin.openapi.OpenApiParam(name = "userId", description = "The Discord user id")
|
||||||
},
|
},
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = MemberRolesResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void memberRoles(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void memberRoles(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
@@ -245,8 +253,8 @@ 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 = Map[].class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = VoiceChannelSummaryResponse[].class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voiceChannels(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void voiceChannels(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
@@ -267,8 +275,8 @@ public final class GuildRoutes {
|
|||||||
@io.javalin.openapi.OpenApiParam(name = "channelId", description = "The Discord voice channel id")
|
@io.javalin.openapi.OpenApiParam(name = "channelId", description = "The Discord voice channel id")
|
||||||
},
|
},
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = VoiceChannelSummaryResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voiceChannel(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void voiceChannel(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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<JDA> 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<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
||||||
|
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
||||||
|
AudioChannel channel = manager.getConnectedChannel();
|
||||||
|
|
||||||
|
Map<String, Object> 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<JDA> 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<JDA> 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<JDA> 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<JDA> 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<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
||||||
|
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
||||||
|
MusicManager manager = musicService.get(guild);
|
||||||
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
|
CacheOrFetch.<Member>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<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
||||||
|
return musicService.get(GuildRoutes.guild(jdaSupplier, ctx));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a JSON-friendly summary for a {@link CommandExecutionResult}.
|
||||||
|
*/
|
||||||
|
private static Map<String, Object> resultBody(CommandExecutionResult result) {
|
||||||
|
Map<String, Object> 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<String, Object> trackSummary(Track track) {
|
||||||
|
if (track == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> 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<String, Object> trackInfoSummary(TrackInfo info) {
|
||||||
|
Map<String, Object> summary = trackSummary(info.getTrack());
|
||||||
|
summary.put("requester_id", info.getRequester().getId());
|
||||||
|
summary.put("skip_count", info.getSkipCount());
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,12 @@ import me.bobert.netneutrality.db.GSettings;
|
|||||||
import me.bobert.netneutrality.db.SettingType;
|
import me.bobert.netneutrality.db.SettingType;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||||
import me.bobert.netneutrality.db.service.GuildSettingService;
|
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.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
@@ -50,7 +56,7 @@ public final class SettingsRoutes {
|
|||||||
methods = HttpMethod.GET,
|
methods = HttpMethod.GET,
|
||||||
summary = "List every known per-guild setting with its default value, description, and type",
|
summary = "List every known per-guild setting with its default value, description, and type",
|
||||||
tags = {"Settings"},
|
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) {
|
private static void settingsCatalog(Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
@@ -77,8 +83,8 @@ public final class SettingsRoutes {
|
|||||||
tags = {"Settings"},
|
tags = {"Settings"},
|
||||||
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsCurrentResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsCurrent(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsCurrent(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
||||||
@@ -97,11 +103,11 @@ public final class SettingsRoutes {
|
|||||||
@OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
@OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
||||||
@OpenApiParam(name = "name", description = "The setting name, as returned by /settings/catalog")
|
@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 = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsUpdateResponse.class)),
|
||||||
@OpenApiResponse(status = "400", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsUpdate(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsUpdate(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
||||||
@@ -185,8 +191,8 @@ public final class SettingsRoutes {
|
|||||||
tags = {"Settings"},
|
tags = {"Settings"},
|
||||||
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = SettingsReloadResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsReload(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsReload(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import io.javalin.openapi.OpenApiParam;
|
|||||||
import io.javalin.openapi.OpenApiResponse;
|
import io.javalin.openapi.OpenApiResponse;
|
||||||
import me.bobert.netneutrality.db.service.GuildService;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
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.JDA;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ public final class StatsRoutes {
|
|||||||
methods = HttpMethod.GET,
|
methods = HttpMethod.GET,
|
||||||
summary = "Report bot-wide stats (guild count, gateway health, uptime)",
|
summary = "Report bot-wide stats (guild count, gateway health, uptime)",
|
||||||
tags = {"Stats"},
|
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<JDA> jdaSupplier, Context ctx) {
|
private static void bot(Supplier<JDA> jdaSupplier, Context ctx) {
|
||||||
JDA jda = GuildRoutes.jda(jdaSupplier);
|
JDA jda = GuildRoutes.jda(jdaSupplier);
|
||||||
@@ -59,8 +62,8 @@ public final class StatsRoutes {
|
|||||||
tags = {"Stats"},
|
tags = {"Stats"},
|
||||||
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
pathParams = @OpenApiParam(name = "guildId", description = "The Discord guild id"),
|
||||||
responses = {
|
responses = {
|
||||||
@OpenApiResponse(status = "200", content = @OpenApiContent(from = Map.class)),
|
@OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildStatsResponse.class)),
|
||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = Map.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void guild(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void guild(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import io.javalin.openapi.plugin.OpenApiPlugin;
|
|||||||
import io.javalin.openapi.plugin.swagger.SwaggerPlugin;
|
import io.javalin.openapi.plugin.swagger.SwaggerPlugin;
|
||||||
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||||
|
import me.bobert.netneutrality.music.MusicService;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -30,6 +31,7 @@ public class WebApi implements Lifecycle {
|
|||||||
|
|
||||||
private final SettingsProvider settings;
|
private final SettingsProvider settings;
|
||||||
private final GuildServiceProvider guildServices;
|
private final GuildServiceProvider guildServices;
|
||||||
|
private final MusicService musicService;
|
||||||
private final Supplier<JDA> jdaSupplier;
|
private final Supplier<JDA> jdaSupplier;
|
||||||
private final Runnable shutdownHook;
|
private final Runnable shutdownHook;
|
||||||
private Javalin app;
|
private Javalin app;
|
||||||
@@ -37,11 +39,13 @@ public class WebApi implements Lifecycle {
|
|||||||
public WebApi(
|
public WebApi(
|
||||||
SettingsProvider settings,
|
SettingsProvider settings,
|
||||||
GuildServiceProvider guildServices,
|
GuildServiceProvider guildServices,
|
||||||
|
MusicService musicService,
|
||||||
Supplier<JDA> jdaSupplier,
|
Supplier<JDA> jdaSupplier,
|
||||||
Runnable shutdownHook
|
Runnable shutdownHook
|
||||||
) {
|
) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.guildServices = guildServices;
|
this.guildServices = guildServices;
|
||||||
|
this.musicService = musicService;
|
||||||
this.jdaSupplier = jdaSupplier;
|
this.jdaSupplier = jdaSupplier;
|
||||||
this.shutdownHook = shutdownHook;
|
this.shutdownHook = shutdownHook;
|
||||||
}
|
}
|
||||||
@@ -77,6 +81,7 @@ public class WebApi implements Lifecycle {
|
|||||||
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);
|
||||||
|
MusicRoutes.register(config.routes, jdaSupplier, musicService);
|
||||||
|
|
||||||
config.routes.exception(HttpResponseException.class, (e, ctx) ->
|
config.routes.exception(HttpResponseException.class, (e, ctx) ->
|
||||||
ctx.status(e.getStatus()).json(Map.of("status", "error", "message", e.getMessage()))
|
ctx.status(e.getStatus()).json(Map.of("status", "error", "message", e.getMessage()))
|
||||||
|
|||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
+19
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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<String, String> settings
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
+20
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import me.bobert.netneutrality.db.service.GuildService;
|
|||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.GuildVoiceState;
|
import net.dv8tion.jda.api.entities.GuildVoiceState;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -75,6 +76,18 @@ public class MusicManager implements MusicManagerCallback {
|
|||||||
return scheduler.getQueueManager();
|
return scheduler.getQueueManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the vote-skip service for this guild.
|
||||||
|
*
|
||||||
|
* <p>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() {
|
public Guild getGuild() {
|
||||||
return guild;
|
return guild;
|
||||||
}
|
}
|
||||||
@@ -213,7 +226,7 @@ public class MusicManager implements MusicManagerCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void stopTrack() {
|
public synchronized void stopTrack() {
|
||||||
updatePlayer("stop track", builder -> builder.stopTrack());
|
updatePlayer("stop track", PlayerUpdateBuilder::stopTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized CommandExecutionResult resume() {
|
public synchronized CommandExecutionResult resume() {
|
||||||
@@ -221,8 +234,16 @@ public class MusicManager implements MusicManagerCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized CommandExecutionResult voteSkip(CommandContext context) {
|
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);
|
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) {
|
private static int parseVoteSkipThreshold(GuildService guildService) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package me.bobert.netneutrality.music;
|
|||||||
|
|
||||||
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
import me.bobert.netneutrality.core.command.CommandExecutionResult;
|
||||||
import me.bobert.netneutrality.core.context.CommandContext;
|
import me.bobert.netneutrality.core.context.CommandContext;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -37,6 +38,15 @@ public class VoteSkipService {
|
|||||||
* configurable vote threshold percentage of connected listeners.
|
* configurable vote threshold percentage of connected listeners.
|
||||||
*/
|
*/
|
||||||
public CommandExecutionResult voteSkip(CommandContext context, QueueManager queueManager, AudioChannel audioChannel, boolean isCurrentlyPlaying, int thresholdPercent) {
|
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) {
|
if (audioChannel == null) {
|
||||||
return CommandExecutionResult.warn("Bot is not connected to a voice channel");
|
return CommandExecutionResult.warn("Bot is not connected to a voice channel");
|
||||||
}
|
}
|
||||||
@@ -60,8 +70,8 @@ public class VoteSkipService {
|
|||||||
|
|
||||||
double needed = connected * (thresholdPercent / 100.0);
|
double needed = connected * (thresholdPercent / 100.0);
|
||||||
|
|
||||||
info.addSkip(context.getGuildUser());
|
info.addSkip(voter);
|
||||||
logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", context.getGuild().getId(), connected, needed, info.getSkipCount(), context.getGuildUser().getId());
|
logger.debug("VoteSkip: guild={}, connected={}, needed={}, skips={}, user={}", guildId, connected, needed, info.getSkipCount(), voter.getId());
|
||||||
|
|
||||||
if (info.getSkipCount() > needed) {
|
if (info.getSkipCount() > needed) {
|
||||||
return queueManager.skip();
|
return queueManager.skip();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import me.bobert.netneutrality.db.service.BlacklistService;
|
|||||||
import me.bobert.netneutrality.db.service.GuildService;
|
import me.bobert.netneutrality.db.service.GuildService;
|
||||||
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
||||||
import me.bobert.netneutrality.db.service.GuildSettingService;
|
import me.bobert.netneutrality.db.service.GuildSettingService;
|
||||||
|
import me.bobert.netneutrality.music.MusicService;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||||
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;
|
||||||
@@ -82,7 +83,9 @@ class WebApiTest {
|
|||||||
when(settings.get("NETNEUTRALITY_API_PORT", "9000")).thenReturn(String.valueOf(port));
|
when(settings.get("NETNEUTRALITY_API_PORT", "9000")).thenReturn(String.valueOf(port));
|
||||||
when(settings.get("NETNEUTRALITY_API_KEY")).thenReturn(API_KEY);
|
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();
|
webApi.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user