**Replace JDA with ShardProvider for improved sharding**
Refactored core classes and APIs to use `ShardProvider` instead of `JDA` directly. Added `ShardService` for managing shard connections and activity, improving scalability and separation of concerns. Integrated test coverage to validate shard-level operations and bot endpoints.
This commit is contained in:
1 parent
b0664e8835
commit
75b495b08c
12 files changed
+528
-224
No files matched your search
@@ -6,6 +6,8 @@ NETNEUTRALITY_BOT_PREFIX=$
|
|||||||
NETNEUTRALITY_BOT_OWNER=
|
NETNEUTRALITY_BOT_OWNER=
|
||||||
NETNEUTRALITY_BOT_INVITE=
|
NETNEUTRALITY_BOT_INVITE=
|
||||||
NETNEUTRALITY_API_PORT=9000
|
NETNEUTRALITY_API_PORT=9000
|
||||||
|
# Optional: number of gateway shards. Leave unset to use Discord's recommended count.
|
||||||
|
# NETNEUTRALITY_SHARD_TOTAL=1
|
||||||
|
|
||||||
MYSQL_HOST=127.0.0.1
|
MYSQL_HOST=127.0.0.1
|
||||||
MYSQL_PORT=3306
|
MYSQL_PORT=3306
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ package me.bobert.netneutrality;
|
|||||||
* https://discord.gg/qUa6zNFZsd
|
* https://discord.gg/qUa6zNFZsd
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
|
||||||
import io.sentry.Sentry;
|
import io.sentry.Sentry;
|
||||||
import me.bobert.netneutrality.bootstrap.ApplicationContext;
|
import me.bobert.netneutrality.bootstrap.ApplicationContext;
|
||||||
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
||||||
@@ -39,11 +38,8 @@ import me.bobert.netneutrality.db.repository.GuildSettingRepository;
|
|||||||
import me.bobert.netneutrality.db.repository.TaskRepository;
|
import me.bobert.netneutrality.db.repository.TaskRepository;
|
||||||
import me.bobert.netneutrality.utils.Settings;
|
import me.bobert.netneutrality.utils.Settings;
|
||||||
import me.bobert.netneutrality.http.WebModule;
|
import me.bobert.netneutrality.http.WebModule;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardService;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Activity;
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
|
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
|
||||||
import org.apache.logging.log4j.Level;
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@@ -53,8 +49,6 @@ public class App {
|
|||||||
private static final Logger logger = LogManager.getLogger(App.class);
|
private static final Logger logger = LogManager.getLogger(App.class);
|
||||||
private static App instance;
|
private static App instance;
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private JDA jda;
|
|
||||||
private MusicService music;
|
|
||||||
private EventPublisher events = NoopEventPublisher.INSTANCE;
|
private EventPublisher events = NoopEventPublisher.INSTANCE;
|
||||||
private final ApplicationContext context = new ApplicationContext();
|
private final ApplicationContext context = new ApplicationContext();
|
||||||
|
|
||||||
@@ -157,21 +151,21 @@ public class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Music sources (priority 40): initialise the shared player manager; disconnect on shutdown.
|
// Music sources (priority 40): initialise the shared player manager; disconnect on shutdown.
|
||||||
music = context.register(new MusicServiceImpl(settings, guildServices, tasks, executors, events));
|
MusicService music = context.register(new MusicServiceImpl(settings, guildServices, tasks, executors, events));
|
||||||
BotState.bind(guildServices, music);
|
BotState.bind(guildServices, music);
|
||||||
|
|
||||||
// JDA gateway; disconnected on shutdown.
|
// JDA shards (priority 60); the events handler is attached to every shard before it connects.
|
||||||
context.register(lifecycle(60, this::initJDA, this::disconnect));
|
ShardService shards = context.register(new ShardService(
|
||||||
|
settings, music, new EventsHandler(settings, guildServices, commands, tasks, events)));
|
||||||
|
|
||||||
// Register slash commands, attach the event handler and set the presence once JDA is ready.
|
// Register global slash commands once (shared by all shards) and set the presence.
|
||||||
context.register(lifecycle(65, () -> {
|
context.register(lifecycle(65, () -> {
|
||||||
commands.registerSlashCommands(jda);
|
commands.registerSlashCommands(shards.getAnyShard());
|
||||||
jda.addEventListener(new EventsHandler(settings, guildServices, commands, tasks, events));
|
shards.setActivity(Activity.watching("you"));
|
||||||
jda.getPresence().setActivity(Activity.watching("you"));
|
|
||||||
}, () -> { }));
|
}, () -> { }));
|
||||||
|
|
||||||
// Web API (priority 70) — Lifecycle with graceful stop.
|
// Web API (priority 70) — Lifecycle with graceful stop.
|
||||||
context.register(new WebApi(settings, guildServices, music, this::getJDA, this::shutdown));
|
context.register(new WebApi(settings, guildServices, music, shards, this::shutdown));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Lifecycle lifecycle(int priority, ThrowingRunnable start, Runnable stop) {
|
private static Lifecycle lifecycle(int priority, ThrowingRunnable start, Runnable stop) {
|
||||||
@@ -202,52 +196,10 @@ public class App {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initJDA() {
|
|
||||||
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
|
||||||
if (token == null) {
|
|
||||||
SessionManager sessions = context.get(SessionManager.class);
|
|
||||||
if (sessions != null) {
|
|
||||||
sessions.stop();
|
|
||||||
}
|
|
||||||
throw new RuntimeException("No bot token provided, exiting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
jda = JDABuilder
|
|
||||||
.createDefault(token)
|
|
||||||
.enableIntents(
|
|
||||||
GatewayIntent.GUILD_MEMBERS,
|
|
||||||
GatewayIntent.GUILD_MESSAGE_REACTIONS,
|
|
||||||
GatewayIntent.GUILD_VOICE_STATES,
|
|
||||||
GatewayIntent.MESSAGE_CONTENT)
|
|
||||||
// Voice state/server updates are handed to Lavalink instead of JDA's audio stack;
|
|
||||||
// DAVE encryption is handled by the node.
|
|
||||||
.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(music.getClient()))
|
|
||||||
.setActivity(Activity.watching("me load..."))
|
|
||||||
.build();
|
|
||||||
jda.awaitReady();
|
|
||||||
} catch (InvalidTokenException | IllegalArgumentException | InterruptedException e) {
|
|
||||||
if (e instanceof InterruptedException) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
LogHandler.log(e);
|
|
||||||
throw new IllegalStateException("Unable to connect to Discord", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
logger.info("[Net Neutrality] Shutting down...");
|
logger.info("[Net Neutrality] Shutting down...");
|
||||||
context.stop();
|
context.stop();
|
||||||
logger.info("[Net Neutrality] Shutdown complete");
|
logger.info("[Net Neutrality] Shutdown complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
public JDA getJDA() {
|
|
||||||
return jda;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void disconnect() {
|
|
||||||
if (jda != null) {
|
|
||||||
jda.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -14,12 +14,11 @@ 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 me.bobert.netneutrality.http.server.dto.StatusResponse;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the {@code /actions/*} endpoints on a {@link RoutesConfig} instance.
|
* Registers the {@code /actions/*} endpoints on a {@link RoutesConfig} instance.
|
||||||
@@ -34,10 +33,10 @@ public final class ActionRoutes {
|
|||||||
|
|
||||||
public static void register(
|
public static void register(
|
||||||
RoutesConfig routes,
|
RoutesConfig routes,
|
||||||
Supplier<JDA> jdaSupplier,
|
ShardProvider shards,
|
||||||
Runnable shutdownHook
|
Runnable shutdownHook
|
||||||
) {
|
) {
|
||||||
routes.get("/actions/status", ctx -> status(jdaSupplier, ctx));
|
routes.get("/actions/status", ctx -> status(shards, ctx));
|
||||||
|
|
||||||
Handler shutdown = ctx -> shutdown(ctx, shutdownHook);
|
Handler shutdown = ctx -> shutdown(ctx, shutdownHook);
|
||||||
routes.post("/actions/shutdown", shutdown);
|
routes.post("/actions/shutdown", shutdown);
|
||||||
@@ -50,11 +49,12 @@ public final class ActionRoutes {
|
|||||||
tags = {"Actions"},
|
tags = {"Actions"},
|
||||||
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = StatusResponse.class))
|
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = StatusResponse.class))
|
||||||
)
|
)
|
||||||
private static void status(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void status(ShardProvider shards, Context ctx) {
|
||||||
JDA jda = GuildRoutes.jda(jdaSupplier);
|
ShardProvider provider = GuildRoutes.shards(shards);
|
||||||
ctx.json(Map.of(
|
ctx.json(Map.of(
|
||||||
"health", String.valueOf(jda.getStatus()),
|
"health", String.valueOf(provider.getWorstStatus()),
|
||||||
"ping", String.valueOf(jda.getGatewayPing())
|
"ping", String.valueOf((long) provider.getAveragePing()),
|
||||||
|
"shard_count", provider.getShardCount()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import me.bobert.netneutrality.http.server.dto.RoleSummaryResponse;
|
|||||||
import me.bobert.netneutrality.http.server.dto.VoiceChannelSummaryResponse;
|
import me.bobert.netneutrality.http.server.dto.VoiceChannelSummaryResponse;
|
||||||
import me.bobert.netneutrality.utils.MusicAccess;
|
import me.bobert.netneutrality.utils.MusicAccess;
|
||||||
import me.bobert.netneutrality.utils.UserFunction;
|
import me.bobert.netneutrality.utils.UserFunction;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
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;
|
||||||
import net.dv8tion.jda.api.entities.Role;
|
import net.dv8tion.jda.api.entities.Role;
|
||||||
@@ -47,7 +47,6 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the read-only {@code /guilds/*} endpoints, as well as the debug/logging/log-channel
|
* Registers the read-only {@code /guilds/*} endpoints, as well as the debug/logging/log-channel
|
||||||
@@ -58,22 +57,22 @@ public final class GuildRoutes {
|
|||||||
private GuildRoutes() {
|
private GuildRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices) {
|
public static void register(RoutesConfig routes, ShardProvider shards, GuildServiceProvider guildServices) {
|
||||||
routes.get("/guilds", ctx -> guilds(jdaSupplier, guildServices, ctx));
|
routes.get("/guilds", ctx -> guilds(shards, guildServices, ctx));
|
||||||
routes.get("/guilds/owner", ctx -> owner(jdaSupplier, ctx));
|
routes.get("/guilds/owner", ctx -> owner(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/owner", ctx -> ownerOf(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/owner", ctx -> ownerOf(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/channels", ctx -> channels(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/channels", ctx -> channels(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/roles", ctx -> roles(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/roles", ctx -> roles(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/members/{userId}/access", ctx -> memberAccess(jdaSupplier, guildServices, ctx));
|
routes.get("/guilds/{guildId}/members/{userId}/access", ctx -> memberAccess(shards, guildServices, ctx));
|
||||||
routes.get("/guilds/{guildId}/members/{userId}", ctx -> member(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/members/{userId}", ctx -> member(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/members/{userId}/roles", ctx -> memberRoles(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/members/{userId}/roles", ctx -> memberRoles(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/members/{userId}/admin", ctx -> memberAdmin(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/members/{userId}/admin", ctx -> memberAdmin(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/voice-channels", ctx -> voiceChannels(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/voice-channels", ctx -> voiceChannels(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/voice-channels/{channelId}", ctx -> voiceChannel(jdaSupplier, ctx));
|
routes.get("/guilds/{guildId}/voice-channels/{channelId}", ctx -> voiceChannel(shards, ctx));
|
||||||
routes.get("/guilds/{guildId}/config", ctx -> config(jdaSupplier, guildServices, ctx));
|
routes.get("/guilds/{guildId}/config", ctx -> config(shards, guildServices, ctx));
|
||||||
routes.put("/guilds/{guildId}/debug", ctx -> updateDebug(jdaSupplier, guildServices, ctx));
|
routes.put("/guilds/{guildId}/debug", ctx -> updateDebug(shards, guildServices, ctx));
|
||||||
routes.put("/guilds/{guildId}/logging", ctx -> updateLogging(jdaSupplier, guildServices, ctx));
|
routes.put("/guilds/{guildId}/logging", ctx -> updateLogging(shards, guildServices, ctx));
|
||||||
routes.put("/guilds/{guildId}/log-channel", ctx -> updateLogChannel(jdaSupplier, guildServices, ctx));
|
routes.put("/guilds/{guildId}/log-channel", ctx -> updateLogChannel(shards, guildServices, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OpenApi(
|
@OpenApi(
|
||||||
@@ -87,15 +86,15 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "400", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void guilds(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void guilds(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
String userId = ctx.queryParam("user_id");
|
String userId = ctx.queryParam("user_id");
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
userGuilds(jda(jdaSupplier), guildServices, userId, ctx);
|
userGuilds(shards(shards), guildServices, userId, ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.json(
|
ctx.json(
|
||||||
jda(jdaSupplier).getGuilds().stream()
|
shards(shards).getGuilds().stream()
|
||||||
.map(GuildRoutes::guildSummary)
|
.map(GuildRoutes::guildSummary)
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
@@ -105,12 +104,12 @@ public final class GuildRoutes {
|
|||||||
* Lists the guilds shared by the bot and the given user, with the user's access flags. Members
|
* Lists the guilds shared by the bot and the given user, with the user's access flags. Members
|
||||||
* missing from the cache are resolved through REST; a failed lookup means "not a member".
|
* missing from the cache are resolved through REST; a failed lookup means "not a member".
|
||||||
*/
|
*/
|
||||||
private static void userGuilds(JDA jda, GuildServiceProvider guildServices, String userId, Context ctx) {
|
private static void userGuilds(ShardProvider shards, GuildServiceProvider guildServices, String userId, Context ctx) {
|
||||||
if (!userId.matches("\\d{1,20}")) {
|
if (!userId.matches("\\d{1,20}")) {
|
||||||
throw new BadRequestResponse("user_id must be a numeric Discord id");
|
throw new BadRequestResponse("user_id must be a numeric Discord id");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<CompletableFuture<Optional<Map<String, Object>>>> lookups = jda.getGuilds().stream()
|
List<CompletableFuture<Optional<Map<String, Object>>>> lookups = shards.getGuilds().stream()
|
||||||
.map(guild -> resolveMember(guild, userId)
|
.map(guild -> resolveMember(guild, userId)
|
||||||
.thenApply(member -> Optional.ofNullable(member)
|
.thenApply(member -> Optional.ofNullable(member)
|
||||||
.map(m -> userGuildEntry(guild, m, guildServices.get(guild.getId()))))
|
.map(m -> userGuildEntry(guild, m, guildServices.get(guild.getId()))))
|
||||||
@@ -176,9 +175,9 @@ public final class GuildRoutes {
|
|||||||
tags = {"Guilds"},
|
tags = {"Guilds"},
|
||||||
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildOwnerEntryResponse[].class))
|
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = GuildOwnerEntryResponse[].class))
|
||||||
)
|
)
|
||||||
private static void owner(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void owner(ShardProvider shards, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
jda(jdaSupplier).getGuilds().stream()
|
shards(shards).getGuilds().stream()
|
||||||
.map(guild -> ordered("guild_id", guild.getId(), "owner_id", guild.getOwnerId()))
|
.map(guild -> ordered("guild_id", guild.getId(), "owner_id", guild.getOwnerId()))
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
@@ -195,8 +194,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void ownerOf(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void ownerOf(ShardProvider shards, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
|
|
||||||
CacheOrFetch.resolve(
|
CacheOrFetch.resolve(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -217,9 +216,9 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void channels(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void channels(ShardProvider shards, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
guild(jdaSupplier, ctx).getTextChannels().stream()
|
guild(shards, ctx).getTextChannels().stream()
|
||||||
.map(channel -> ordered("channel_id", channel.getId(), "channel_name", channel.getName()))
|
.map(channel -> ordered("channel_id", channel.getId(), "channel_name", channel.getName()))
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
@@ -236,9 +235,9 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void roles(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void roles(ShardProvider shards, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
guild(jdaSupplier, ctx).getRoles().stream()
|
guild(shards, ctx).getRoles().stream()
|
||||||
.filter(role -> !role.isManaged() && !role.isPublicRole())
|
.filter(role -> !role.isManaged() && !role.isPublicRole())
|
||||||
.map(role -> ordered("role_id", role.getId(), "role_name", role.getName()))
|
.map(role -> ordered("role_id", role.getId(), "role_name", role.getName()))
|
||||||
.toList()
|
.toList()
|
||||||
@@ -259,8 +258,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void member(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void member(ShardProvider shards, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
CacheOrFetch.resolve(
|
CacheOrFetch.resolve(
|
||||||
@@ -285,8 +284,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void memberRoles(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void memberRoles(ShardProvider shards, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
CacheOrFetch.resolve(
|
CacheOrFetch.resolve(
|
||||||
@@ -311,8 +310,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void memberAdmin(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void memberAdmin(ShardProvider shards, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
CacheOrFetch.resolve(
|
CacheOrFetch.resolve(
|
||||||
@@ -343,8 +342,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void memberAccess(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void memberAccess(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
Function<Member, Map<String, Object>> toBody = member -> {
|
Function<Member, Map<String, Object>> toBody = member -> {
|
||||||
@@ -413,9 +412,9 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voiceChannels(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void voiceChannels(ShardProvider shards, Context ctx) {
|
||||||
ctx.json(
|
ctx.json(
|
||||||
guild(jdaSupplier, ctx).getVoiceChannels().stream()
|
guild(shards, ctx).getVoiceChannels().stream()
|
||||||
.map(GuildRoutes::voiceChannelSummary)
|
.map(GuildRoutes::voiceChannelSummary)
|
||||||
.toList()
|
.toList()
|
||||||
);
|
);
|
||||||
@@ -435,8 +434,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voiceChannel(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void voiceChannel(ShardProvider shards, Context ctx) {
|
||||||
ctx.json(voiceChannelSummary(voiceChannel(guild(jdaSupplier, ctx), ctx)));
|
ctx.json(voiceChannelSummary(voiceChannel(guild(shards, ctx), ctx)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -497,8 +496,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void config(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void config(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
ctx.json(guildConfig(guild, guildServices));
|
ctx.json(guildConfig(guild, guildServices));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,8 +514,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void updateDebug(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void updateDebug(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
boolean value = booleanValue(ctx);
|
boolean value = booleanValue(ctx);
|
||||||
|
|
||||||
GuildService guildService = guildServices.get(guild.getId());
|
GuildService guildService = guildServices.get(guild.getId());
|
||||||
@@ -538,8 +537,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void updateLogging(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void updateLogging(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
boolean value = booleanValue(ctx);
|
boolean value = booleanValue(ctx);
|
||||||
|
|
||||||
GuildService guildService = guildServices.get(guild.getId());
|
GuildService guildService = guildServices.get(guild.getId());
|
||||||
@@ -561,8 +560,8 @@ public final class GuildRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void updateLogChannel(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void updateLogChannel(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = guild(jdaSupplier, ctx);
|
Guild guild = guild(shards, ctx);
|
||||||
String channelId = ctx.bodyAsClass(Map.class).get("value") instanceof String s ? s : null;
|
String channelId = ctx.bodyAsClass(Map.class).get("value") instanceof String s ? s : null;
|
||||||
|
|
||||||
if (channelId != null && !channelId.isBlank() && guild.getTextChannelById(channelId) == null) {
|
if (channelId != null && !channelId.isBlank() && guild.getTextChannelById(channelId) == null) {
|
||||||
@@ -600,21 +599,20 @@ public final class GuildRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the JDA client or fails with 503 while it is not ready yet.
|
* Resolves the shard provider or fails with 503 while the shards are not ready yet.
|
||||||
*/
|
*/
|
||||||
static JDA jda(Supplier<JDA> jdaSupplier) {
|
static ShardProvider shards(ShardProvider shards) {
|
||||||
JDA jda = jdaSupplier.get();
|
if (shards == null || !shards.isReady()) {
|
||||||
if (jda == null) {
|
|
||||||
throw new ServiceUnavailableResponse("Discord client is not ready");
|
throw new ServiceUnavailableResponse("Discord client is not ready");
|
||||||
}
|
}
|
||||||
return jda;
|
return shards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the {@code guildId} path parameter or fails with 404.
|
* Resolves the {@code guildId} path parameter or fails with 404.
|
||||||
*/
|
*/
|
||||||
static Guild guild(Supplier<JDA> jdaSupplier, Context ctx) {
|
static Guild guild(ShardProvider shards, Context ctx) {
|
||||||
Guild guild = jda(jdaSupplier).getGuildById(ctx.pathParam("guildId"));
|
Guild guild = shards(shards).getGuildById(ctx.pathParam("guildId"));
|
||||||
if (guild == null) {
|
if (guild == null) {
|
||||||
throw new NotFoundResponse("Unknown guild");
|
throw new NotFoundResponse("Unknown guild");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import me.bobert.netneutrality.music.QueueActionResult;
|
|||||||
import me.bobert.netneutrality.music.SourceFinder;
|
import me.bobert.netneutrality.music.SourceFinder;
|
||||||
import me.bobert.netneutrality.music.TrackInfo;
|
import me.bobert.netneutrality.music.TrackInfo;
|
||||||
import me.bobert.netneutrality.utils.MusicUtils;
|
import me.bobert.netneutrality.utils.MusicUtils;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
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;
|
||||||
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
import net.dv8tion.jda.api.entities.channel.middleman.AudioChannel;
|
||||||
@@ -55,7 +55,6 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the {@code /music/*} endpoints on a {@link RoutesConfig} instance.
|
* Registers the {@code /music/*} endpoints on a {@link RoutesConfig} instance.
|
||||||
@@ -69,26 +68,26 @@ public final class MusicRoutes {
|
|||||||
private MusicRoutes() {
|
private MusicRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier, MusicService musicService) {
|
public static void register(RoutesConfig routes, ShardProvider shards, MusicService musicService) {
|
||||||
routes.get("/music/sources", MusicRoutes::sources);
|
routes.get("/music/sources", MusicRoutes::sources);
|
||||||
routes.get("/music/{guildId}/status", ctx -> status(jdaSupplier, musicService, ctx));
|
routes.get("/music/{guildId}/status", ctx -> status(shards, musicService, ctx));
|
||||||
routes.get("/music/{guildId}/queue", ctx -> queue(jdaSupplier, musicService, ctx));
|
routes.get("/music/{guildId}/queue", ctx -> queue(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/skip", ctx -> skip(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/skip", ctx -> skip(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/stop", ctx -> stop(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/stop", ctx -> stop(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/pause", ctx -> pause(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/pause", ctx -> pause(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/resume", ctx -> resumePlayback(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/resume", ctx -> resumePlayback(shards, musicService, ctx));
|
||||||
routes.get("/music/{guildId}/volume", ctx -> getVolume(jdaSupplier, musicService, ctx));
|
routes.get("/music/{guildId}/volume", ctx -> getVolume(shards, musicService, ctx));
|
||||||
routes.put("/music/{guildId}/volume", ctx -> volume(jdaSupplier, musicService, ctx));
|
routes.put("/music/{guildId}/volume", ctx -> volume(shards, musicService, ctx));
|
||||||
routes.put("/music/{guildId}/seek", ctx -> seek(jdaSupplier, musicService, ctx));
|
routes.put("/music/{guildId}/seek", ctx -> seek(shards, musicService, ctx));
|
||||||
routes.get("/music/{guildId}/vote-skip", ctx -> voteSkipStatus(jdaSupplier, musicService, ctx));
|
routes.get("/music/{guildId}/vote-skip", ctx -> voteSkipStatus(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/vote-skip/{userId}", ctx -> voteSkip(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/vote-skip/{userId}", ctx -> voteSkip(shards, musicService, ctx));
|
||||||
routes.get("/music/{guildId}/dj/{userId}", ctx -> dj(jdaSupplier, musicService, ctx));
|
routes.get("/music/{guildId}/dj/{userId}", ctx -> dj(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/queue", ctx -> play(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/queue", ctx -> play(shards, musicService, ctx));
|
||||||
routes.delete("/music/{guildId}/queue", ctx -> deleteQueue(jdaSupplier, musicService, ctx));
|
routes.delete("/music/{guildId}/queue", ctx -> deleteQueue(shards, musicService, ctx));
|
||||||
routes.patch("/music/{guildId}/queue/reorder", ctx -> reorder(jdaSupplier, musicService, ctx));
|
routes.patch("/music/{guildId}/queue/reorder", ctx -> reorder(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/search", ctx -> search(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/search", ctx -> search(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/leave", ctx -> leave(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/leave", ctx -> leave(shards, musicService, ctx));
|
||||||
routes.post("/music/{guildId}/shuffle", ctx -> shuffle(jdaSupplier, musicService, ctx));
|
routes.post("/music/{guildId}/shuffle", ctx -> shuffle(shards, musicService, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OpenApi(
|
@OpenApi(
|
||||||
@@ -118,8 +117,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void status(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void status(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
AudioChannel channel = manager.getConnectedChannel();
|
AudioChannel channel = manager.getConnectedChannel();
|
||||||
|
|
||||||
Map<String, Object> body = new LinkedHashMap<>();
|
Map<String, Object> body = new LinkedHashMap<>();
|
||||||
@@ -146,8 +145,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void queue(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void queue(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
|
|
||||||
ctx.json(
|
ctx.json(
|
||||||
manager.getQueueManager().getQueue().stream()
|
manager.getQueueManager().getQueue().stream()
|
||||||
@@ -167,8 +166,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void skip(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void skip(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
QueueActionResult result = manager.getQueueManager().skip();
|
QueueActionResult result = manager.getQueueManager().skip();
|
||||||
announce(manager, result.getMessage());
|
announce(manager, result.getMessage());
|
||||||
ctx.json(resultBody(result));
|
ctx.json(resultBody(result));
|
||||||
@@ -185,8 +184,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void stop(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void stop(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
manager.stopTrack();
|
manager.stopTrack();
|
||||||
announce(manager, "Stopping current track");
|
announce(manager, "Stopping current track");
|
||||||
ctx.json(Map.of("status", "ok", "message", "Stopping current track"));
|
ctx.json(Map.of("status", "ok", "message", "Stopping current track"));
|
||||||
@@ -203,8 +202,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void pause(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void pause(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
manager.pauseTrack();
|
manager.pauseTrack();
|
||||||
announce(manager, "Pausing current track");
|
announce(manager, "Pausing current track");
|
||||||
ctx.json(Map.of("status", "ok", "message", "Pausing current track"));
|
ctx.json(Map.of("status", "ok", "message", "Pausing current track"));
|
||||||
@@ -221,8 +220,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void resumePlayback(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void resumePlayback(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
manager.unpauseTrack();
|
manager.unpauseTrack();
|
||||||
ctx.json(Map.of("status", "ok", "message", "Resuming current track"));
|
ctx.json(Map.of("status", "ok", "message", "Resuming current track"));
|
||||||
}
|
}
|
||||||
@@ -238,8 +237,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void getVolume(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void getVolume(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
ctx.json(Map.of("status", "ok", "volume", manager.getVolume()));
|
ctx.json(Map.of("status", "ok", "volume", manager.getVolume()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,8 +255,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void volume(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void volume(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
int newVolume = volume(ctx);
|
int newVolume = volume(ctx);
|
||||||
|
|
||||||
manager.setVolume(newVolume);
|
manager.setVolume(newVolume);
|
||||||
@@ -297,8 +296,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void seek(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void seek(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
long positionMs = seekPosition(ctx, manager);
|
long positionMs = seekPosition(ctx, manager);
|
||||||
|
|
||||||
manager.seek(positionMs);
|
manager.seek(positionMs);
|
||||||
@@ -341,8 +340,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voteSkipStatus(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void voteSkipStatus(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
AudioChannel channel = manager.getConnectedChannel();
|
AudioChannel channel = manager.getConnectedChannel();
|
||||||
TrackInfo head = manager.getQueueManager().peekHeadOfQueue();
|
TrackInfo head = manager.getQueueManager().peekHeadOfQueue();
|
||||||
int threshold = manager.getVoteSkipThreshold();
|
int threshold = manager.getVoteSkipThreshold();
|
||||||
@@ -369,8 +368,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void voteSkip(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void voteSkip(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
MusicManager manager = musicService.get(guild);
|
MusicManager manager = musicService.get(guild);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
@@ -400,8 +399,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void dj(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void dj(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
MusicManager manager = musicService.get(guild);
|
MusicManager manager = musicService.get(guild);
|
||||||
String userId = ctx.pathParam("userId");
|
String userId = ctx.pathParam("userId");
|
||||||
|
|
||||||
@@ -426,8 +425,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void play(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void play(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
MusicManager manager = musicService.get(guild);
|
MusicManager manager = musicService.get(guild);
|
||||||
QueueRequest body = ctx.bodyAsClass(QueueRequest.class);
|
QueueRequest body = ctx.bodyAsClass(QueueRequest.class);
|
||||||
String userId = requireNonBlank(body.userId(), "userId");
|
String userId = requireNonBlank(body.userId(), "userId");
|
||||||
@@ -449,8 +448,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void search(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void search(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
MusicManager manager = musicService.get(guild);
|
MusicManager manager = musicService.get(guild);
|
||||||
SearchRequest body = ctx.bodyAsClass(SearchRequest.class);
|
SearchRequest body = ctx.bodyAsClass(SearchRequest.class);
|
||||||
String query = requireNonBlank(body.query(), "query");
|
String query = requireNonBlank(body.query(), "query");
|
||||||
@@ -480,7 +479,7 @@ public final class MusicRoutes {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues a previously returned search candidate for a guild member, invoked from {@link
|
* Queues a previously returned search candidate for a guild member, invoked from {@link
|
||||||
* #play(Supplier, MusicService, Context)} when the request body carries a {@code searchId}.
|
* #play(ShardProvider, MusicService, Context)} when the request body carries a {@code searchId}.
|
||||||
* Exposed via the {@code /music/{guildId}/queue} POST endpoint.
|
* Exposed via the {@code /music/{guildId}/queue} POST endpoint.
|
||||||
*/
|
*/
|
||||||
private static void playSelect(Guild guild, MusicManager manager, QueueRequest body, String userId, Context ctx) {
|
private static void playSelect(Guild guild, MusicManager manager, QueueRequest body, String userId, Context ctx) {
|
||||||
@@ -541,8 +540,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void deleteQueue(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void deleteQueue(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
String position = ctx.queryParam("position");
|
String position = ctx.queryParam("position");
|
||||||
|
|
||||||
QueueActionResult result;
|
QueueActionResult result;
|
||||||
@@ -578,8 +577,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void leave(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void leave(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
manager.disconnect();
|
manager.disconnect();
|
||||||
announce(manager, "Leaving voice channel");
|
announce(manager, "Leaving voice channel");
|
||||||
ctx.json(new LeaveResponse("ok", "Leaving voice channel"));
|
ctx.json(new LeaveResponse("ok", "Leaving voice channel"));
|
||||||
@@ -596,8 +595,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void shuffle(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void shuffle(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
QueueActionResult result = manager.getQueueManager().shuffle();
|
QueueActionResult result = manager.getQueueManager().shuffle();
|
||||||
announce(manager, result.getMessage());
|
announce(manager, result.getMessage());
|
||||||
ctx.json(resultBodyWithQueue(result, manager));
|
ctx.json(resultBodyWithQueue(result, manager));
|
||||||
@@ -616,8 +615,8 @@ public final class MusicRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void reorder(Supplier<JDA> jdaSupplier, MusicService musicService, Context ctx) {
|
private static void reorder(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
MusicManager manager = manager(jdaSupplier, musicService, ctx);
|
MusicManager manager = manager(shards, musicService, ctx);
|
||||||
ReorderRequest body = ctx.bodyAsClass(ReorderRequest.class);
|
ReorderRequest body = ctx.bodyAsClass(ReorderRequest.class);
|
||||||
|
|
||||||
QueueActionResult result = manager.getQueueManager().reorder(body.fromPosition(), body.toPosition());
|
QueueActionResult result = manager.getQueueManager().reorder(body.fromPosition(), body.toPosition());
|
||||||
@@ -717,8 +716,8 @@ public final class MusicRoutes {
|
|||||||
/**
|
/**
|
||||||
* Resolves the {@link MusicManager} for the guild path parameter, creating it on first use.
|
* 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) {
|
private static MusicManager manager(ShardProvider shards, MusicService musicService, Context ctx) {
|
||||||
return musicService.get(GuildRoutes.guild(jdaSupplier, ctx));
|
return musicService.get(GuildRoutes.guild(shards, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,13 +25,12 @@ import me.bobert.netneutrality.http.server.dto.SettingsCurrentResponse;
|
|||||||
import me.bobert.netneutrality.http.server.dto.SettingsReloadResponse;
|
import me.bobert.netneutrality.http.server.dto.SettingsReloadResponse;
|
||||||
import me.bobert.netneutrality.http.server.dto.SettingsUpdateRequest;
|
import me.bobert.netneutrality.http.server.dto.SettingsUpdateRequest;
|
||||||
import me.bobert.netneutrality.http.server.dto.SettingsUpdateResponse;
|
import me.bobert.netneutrality.http.server.dto.SettingsUpdateResponse;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the {@code /settings/*} endpoints on a {@link RoutesConfig} instance.
|
* Registers the {@code /settings/*} endpoints on a {@link RoutesConfig} instance.
|
||||||
@@ -41,14 +40,14 @@ public final class SettingsRoutes {
|
|||||||
private SettingsRoutes() {
|
private SettingsRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices) {
|
public static void register(RoutesConfig routes, ShardProvider shards, GuildServiceProvider guildServices) {
|
||||||
routes.get("/settings/catalog", SettingsRoutes::settingsCatalog);
|
routes.get("/settings/catalog", SettingsRoutes::settingsCatalog);
|
||||||
|
|
||||||
routes.get("/settings/current/{guildId}", ctx -> settingsCurrent(jdaSupplier, guildServices, ctx));
|
routes.get("/settings/current/{guildId}", ctx -> settingsCurrent(shards, guildServices, ctx));
|
||||||
|
|
||||||
routes.put("/settings/current/{guildId}/{name}", ctx -> settingsUpdate(jdaSupplier, guildServices, ctx));
|
routes.put("/settings/current/{guildId}/{name}", ctx -> settingsUpdate(shards, guildServices, ctx));
|
||||||
|
|
||||||
routes.put("/settings/reload/{guildId}", ctx -> settingsReload(jdaSupplier, guildServices, ctx));
|
routes.put("/settings/reload/{guildId}", ctx -> settingsReload(shards, guildServices, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OpenApi(
|
@OpenApi(
|
||||||
@@ -87,8 +86,8 @@ public final class SettingsRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsCurrent(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsCurrent(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
Map<String, String> settings = guildServices.get(guild.getId()).getGuildSettingService().snapshot();
|
Map<String, String> settings = guildServices.get(guild.getId()).getGuildSettingService().snapshot();
|
||||||
|
|
||||||
ctx.json(Map.of("status", "ok", "settings", settings));
|
ctx.json(Map.of("status", "ok", "settings", settings));
|
||||||
@@ -110,8 +109,8 @@ public final class SettingsRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsUpdate(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsUpdate(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
GSettings setting = setting(ctx.pathParam("name"));
|
GSettings setting = setting(ctx.pathParam("name"));
|
||||||
String value = ctx.bodyAsClass(Map.class).get("value") instanceof String s ? s : null;
|
String value = ctx.bodyAsClass(Map.class).get("value") instanceof String s ? s : null;
|
||||||
|
|
||||||
@@ -195,8 +194,8 @@ public final class SettingsRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void settingsReload(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void settingsReload(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
guildServices.get(guild.getId()).getGuildSettingService().reloadSettingsCache();
|
guildServices.get(guild.getId()).getGuildSettingService().reloadSettingsCache();
|
||||||
|
|
||||||
ctx.json(Map.of("status", true));
|
ctx.json(Map.of("status", true));
|
||||||
|
|||||||
@@ -18,11 +18,10 @@ import me.bobert.netneutrality.db.service.GuildServiceProvider;
|
|||||||
import me.bobert.netneutrality.http.server.dto.BotStatsResponse;
|
import me.bobert.netneutrality.http.server.dto.BotStatsResponse;
|
||||||
import me.bobert.netneutrality.http.server.dto.ErrorResponse;
|
import me.bobert.netneutrality.http.server.dto.ErrorResponse;
|
||||||
import me.bobert.netneutrality.http.server.dto.GuildStatsResponse;
|
import me.bobert.netneutrality.http.server.dto.GuildStatsResponse;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the {@code /stats/*} endpoints on a {@link RoutesConfig} instance.
|
* Registers the {@code /stats/*} endpoints on a {@link RoutesConfig} instance.
|
||||||
@@ -32,9 +31,9 @@ public final class StatsRoutes {
|
|||||||
private StatsRoutes() {
|
private StatsRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void register(RoutesConfig routes, Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices) {
|
public static void register(RoutesConfig routes, ShardProvider shards, GuildServiceProvider guildServices) {
|
||||||
routes.get("/stats/bot", ctx -> bot(jdaSupplier, ctx));
|
routes.get("/stats/bot", ctx -> bot(shards, ctx));
|
||||||
routes.get("/stats/guilds/{guildId}", ctx -> guild(jdaSupplier, guildServices, ctx));
|
routes.get("/stats/guilds/{guildId}", ctx -> guild(shards, guildServices, ctx));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OpenApi(
|
@OpenApi(
|
||||||
@@ -44,13 +43,14 @@ public final class StatsRoutes {
|
|||||||
tags = {"Stats"},
|
tags = {"Stats"},
|
||||||
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = BotStatsResponse.class))
|
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = BotStatsResponse.class))
|
||||||
)
|
)
|
||||||
private static void bot(Supplier<JDA> jdaSupplier, Context ctx) {
|
private static void bot(ShardProvider shards, Context ctx) {
|
||||||
JDA jda = GuildRoutes.jda(jdaSupplier);
|
ShardProvider provider = GuildRoutes.shards(shards);
|
||||||
|
|
||||||
ctx.json(Map.of(
|
ctx.json(Map.of(
|
||||||
"guild_count", jda.getGuilds().size(),
|
"guild_count", provider.getGuilds().size(),
|
||||||
"gateway_ping", jda.getGatewayPing(),
|
"gateway_ping", (long) provider.getAveragePing(),
|
||||||
"status", String.valueOf(jda.getStatus()),
|
"status", String.valueOf(provider.getWorstStatus()),
|
||||||
|
"shard_count", provider.getShardCount(),
|
||||||
"uptime_seconds", uptimeSeconds()
|
"uptime_seconds", uptimeSeconds()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -66,8 +66,8 @@ public final class StatsRoutes {
|
|||||||
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
@OpenApiResponse(status = "404", content = @OpenApiContent(from = ErrorResponse.class))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
private static void guild(Supplier<JDA> jdaSupplier, GuildServiceProvider guildServices, Context ctx) {
|
private static void guild(ShardProvider shards, GuildServiceProvider guildServices, Context ctx) {
|
||||||
Guild guild = GuildRoutes.guild(jdaSupplier, ctx);
|
Guild guild = GuildRoutes.guild(shards, ctx);
|
||||||
GuildService guildService = guildServices.get(guild.getId());
|
GuildService guildService = guildServices.get(guild.getId());
|
||||||
|
|
||||||
ctx.json(Map.of(
|
ctx.json(Map.of(
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ 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.music.MusicService;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embedded HTTP API exposed as a {@link Lifecycle} component (priority 70, after JDA).
|
* Embedded HTTP API exposed as a {@link Lifecycle} component (priority 70, after JDA).
|
||||||
@@ -32,7 +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 MusicService musicService;
|
||||||
private final Supplier<JDA> jdaSupplier;
|
private final ShardProvider shards;
|
||||||
private final Runnable shutdownHook;
|
private final Runnable shutdownHook;
|
||||||
private Javalin app;
|
private Javalin app;
|
||||||
|
|
||||||
@@ -40,13 +39,13 @@ public class WebApi implements Lifecycle {
|
|||||||
SettingsProvider settings,
|
SettingsProvider settings,
|
||||||
GuildServiceProvider guildServices,
|
GuildServiceProvider guildServices,
|
||||||
MusicService musicService,
|
MusicService musicService,
|
||||||
Supplier<JDA> jdaSupplier,
|
ShardProvider shards,
|
||||||
Runnable shutdownHook
|
Runnable shutdownHook
|
||||||
) {
|
) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.guildServices = guildServices;
|
this.guildServices = guildServices;
|
||||||
this.musicService = musicService;
|
this.musicService = musicService;
|
||||||
this.jdaSupplier = jdaSupplier;
|
this.shards = shards;
|
||||||
this.shutdownHook = shutdownHook;
|
this.shutdownHook = shutdownHook;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +76,11 @@ public class WebApi implements Lifecycle {
|
|||||||
config.routes.before(new ApiKeyFilter(settings));
|
config.routes.before(new ApiKeyFilter(settings));
|
||||||
|
|
||||||
config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML));
|
config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML));
|
||||||
GuildRoutes.register(config.routes, jdaSupplier, guildServices);
|
GuildRoutes.register(config.routes, shards, guildServices);
|
||||||
ActionRoutes.register(config.routes, jdaSupplier, shutdownHook);
|
ActionRoutes.register(config.routes, shards, shutdownHook);
|
||||||
SettingsRoutes.register(config.routes, jdaSupplier, guildServices);
|
SettingsRoutes.register(config.routes, shards, guildServices);
|
||||||
StatsRoutes.register(config.routes, jdaSupplier, guildServices);
|
StatsRoutes.register(config.routes, shards, guildServices);
|
||||||
MusicRoutes.register(config.routes, jdaSupplier, musicService);
|
MusicRoutes.register(config.routes, shards, 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,52 @@
|
|||||||
|
package me.bobert.netneutrality.shard;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only, shard-aggregating view of the Discord gateway connections.
|
||||||
|
*/
|
||||||
|
public interface ShardProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code true} once every shard has finished starting up and until shutdown
|
||||||
|
*/
|
||||||
|
boolean isReady();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks a guild up on whichever shard owns it.
|
||||||
|
*
|
||||||
|
* @return the guild, or {@code null} if no shard knows it
|
||||||
|
*/
|
||||||
|
Guild getGuildById(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the guilds of all shards
|
||||||
|
*/
|
||||||
|
List<Guild> getGuilds();
|
||||||
|
|
||||||
|
int getShardCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the mean gateway ping across shards, or {@code -1} if unknown
|
||||||
|
*/
|
||||||
|
double getAveragePing();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@code CONNECTED} when every shard is connected, otherwise the most degraded status
|
||||||
|
*/
|
||||||
|
JDA.Status getWorstStatus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return an arbitrary shard (for global operations such as command registration), or {@code null} if not ready
|
||||||
|
*/
|
||||||
|
JDA getAnyShard();
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package me.bobert.netneutrality.shard;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) anthorob
|
||||||
|
*
|
||||||
|
* https://discord.gg/qUa6zNFZsd
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
||||||
|
import me.bobert.netneutrality.bootstrap.Lifecycle;
|
||||||
|
import me.bobert.netneutrality.core.LogHandler;
|
||||||
|
import me.bobert.netneutrality.music.MusicService;
|
||||||
|
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.entities.Activity;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.exceptions.InvalidTokenException;
|
||||||
|
import net.dv8tion.jda.api.hooks.EventListener;
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
|
import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder;
|
||||||
|
import net.dv8tion.jda.api.sharding.ShardManager;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owns the JDA {@link ShardManager}. The shard count is Discord's recommendation unless
|
||||||
|
* {@code NETNEUTRALITY_SHARD_TOTAL} overrides it.
|
||||||
|
*/
|
||||||
|
public final class ShardService implements Lifecycle, ShardProvider {
|
||||||
|
public static final String SHARD_TOTAL_KEY = "NETNEUTRALITY_SHARD_TOTAL";
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger(ShardService.class);
|
||||||
|
|
||||||
|
private final SettingsProvider settings;
|
||||||
|
private final MusicService music;
|
||||||
|
private final EventListener eventsHandler;
|
||||||
|
private volatile ShardManager shardManager;
|
||||||
|
private volatile boolean ready;
|
||||||
|
|
||||||
|
public ShardService(SettingsProvider settings, MusicService music, EventListener eventsHandler) {
|
||||||
|
this.settings = Objects.requireNonNull(settings, "settings");
|
||||||
|
this.music = Objects.requireNonNull(music, "music");
|
||||||
|
this.eventsHandler = Objects.requireNonNull(eventsHandler, "eventsHandler");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps an already-built manager; used by tests.
|
||||||
|
*/
|
||||||
|
ShardService(ShardManager shardManager) {
|
||||||
|
this.settings = null;
|
||||||
|
this.music = null;
|
||||||
|
this.eventsHandler = null;
|
||||||
|
this.shardManager = shardManager;
|
||||||
|
this.ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws Exception {
|
||||||
|
String token = settings.get("NETNEUTRALITY_BOT_TOKEN");
|
||||||
|
if (token == null) {
|
||||||
|
throw new IllegalStateException("No bot token provided, exiting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DefaultShardManagerBuilder builder = DefaultShardManagerBuilder
|
||||||
|
.createDefault(token)
|
||||||
|
.enableIntents(
|
||||||
|
GatewayIntent.GUILD_MEMBERS,
|
||||||
|
GatewayIntent.GUILD_MESSAGE_REACTIONS,
|
||||||
|
GatewayIntent.GUILD_VOICE_STATES,
|
||||||
|
GatewayIntent.MESSAGE_CONTENT)
|
||||||
|
// Voice state/server updates are handed to Lavalink instead of JDA's audio stack;
|
||||||
|
// DAVE encryption is handled by the node.
|
||||||
|
.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(music.getClient()))
|
||||||
|
// Attached before connecting so no shard misses early events.
|
||||||
|
.addEventListeners(eventsHandler)
|
||||||
|
.setActivity(Activity.watching("me load..."));
|
||||||
|
|
||||||
|
int total = parseShardTotal(settings.get(SHARD_TOTAL_KEY));
|
||||||
|
if (total > 0) {
|
||||||
|
builder.setShardsTotal(total);
|
||||||
|
}
|
||||||
|
|
||||||
|
shardManager = builder.build();
|
||||||
|
awaitAllShards(shardManager);
|
||||||
|
ready = true;
|
||||||
|
logger.info("{} shard(s) ready", shardManager.getShardsTotal());
|
||||||
|
} catch (InvalidTokenException | IllegalArgumentException e) {
|
||||||
|
LogHandler.log(e);
|
||||||
|
throw new IllegalStateException("Unable to connect to Discord", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LogHandler.log(e);
|
||||||
|
throw new IllegalStateException("Unable to connect to Discord", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
ready = false;
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
if (manager != null) {
|
||||||
|
manager.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int priority() {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the configured shard count, or {@code -1} (automatic) when missing or invalid
|
||||||
|
*/
|
||||||
|
static int parseShardTotal(String raw) {
|
||||||
|
if (raw == null || raw.isBlank()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int value = Integer.parseInt(raw.trim());
|
||||||
|
if (value > 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// falls through to the warning below
|
||||||
|
}
|
||||||
|
logger.warn("Invalid {} '{}', using Discord's recommended shard count", SHARD_TOTAL_KEY, raw);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitAllShards(ShardManager manager) throws InterruptedException {
|
||||||
|
int total = manager.getShardsTotal();
|
||||||
|
for (int id = 0; id < total; id++) {
|
||||||
|
JDA shard = manager.getShardById(id);
|
||||||
|
// Shards are started asynchronously (one identify per 5s), so wait for each to appear.
|
||||||
|
while (shard == null) {
|
||||||
|
Thread.sleep(100);
|
||||||
|
shard = manager.getShardById(id);
|
||||||
|
}
|
||||||
|
shard.awaitReady();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActivity(Activity activity) {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
if (manager != null) {
|
||||||
|
manager.setActivity(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return ready && shardManager != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Guild getGuildById(String id) {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
return manager == null ? null : manager.getGuildById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Guild> getGuilds() {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
return manager == null ? List.of() : manager.getGuilds();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getShardCount() {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
return manager == null ? 0 : manager.getShardsTotal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getAveragePing() {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
return manager == null ? -1 : manager.getAverageGatewayPing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JDA.Status getWorstStatus() {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
if (manager == null) {
|
||||||
|
return JDA.Status.INITIALIZING;
|
||||||
|
}
|
||||||
|
return manager.getShards().stream()
|
||||||
|
.map(JDA::getStatus)
|
||||||
|
.filter(status -> status != JDA.Status.CONNECTED)
|
||||||
|
.max(Comparator.naturalOrder())
|
||||||
|
.orElse(JDA.Status.CONNECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JDA getAnyShard() {
|
||||||
|
ShardManager manager = shardManager;
|
||||||
|
if (manager == null || !ready) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JDA first = manager.getShardById(0);
|
||||||
|
return first != null ? first : manager.getShards().stream().findFirst().orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ 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.music.MusicService;
|
||||||
import me.bobert.netneutrality.utils.SettingsProvider;
|
import me.bobert.netneutrality.utils.SettingsProvider;
|
||||||
|
import me.bobert.netneutrality.shard.ShardProvider;
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
@@ -48,6 +49,7 @@ class WebApiTest {
|
|||||||
private WebApi webApi;
|
private WebApi webApi;
|
||||||
private int port;
|
private int port;
|
||||||
private GuildSettingService settingService;
|
private GuildSettingService settingService;
|
||||||
|
private ShardProvider shards;
|
||||||
private final HttpClient http = HttpClient.newHttpClient();
|
private final HttpClient http = HttpClient.newHttpClient();
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -79,13 +81,14 @@ class WebApiTest {
|
|||||||
when(failing.submit()).thenReturn(CompletableFuture.failedFuture(new IllegalStateException("unknown member")));
|
when(failing.submit()).thenReturn(CompletableFuture.failedFuture(new IllegalStateException("unknown member")));
|
||||||
when(guild.retrieveMemberById(any(String.class))).thenReturn(failing);
|
when(guild.retrieveMemberById(any(String.class))).thenReturn(failing);
|
||||||
|
|
||||||
JDA jda = mock(JDA.class);
|
shards = mock(ShardProvider.class);
|
||||||
when(jda.getGuilds()).thenReturn(List.of(guild));
|
when(shards.isReady()).thenReturn(true);
|
||||||
when(jda.getGuildById(GUILD_ID)).thenReturn(guild);
|
when(shards.getGuilds()).thenReturn(List.of(guild));
|
||||||
when(jda.getGuildById(any(String.class))).thenAnswer(inv ->
|
when(shards.getGuildById(any(String.class))).thenAnswer(inv ->
|
||||||
GUILD_ID.equals(inv.getArgument(0)) ? guild : null);
|
GUILD_ID.equals(inv.getArgument(0)) ? guild : null);
|
||||||
when(jda.getStatus()).thenReturn(JDA.Status.CONNECTED);
|
when(shards.getWorstStatus()).thenReturn(JDA.Status.CONNECTED);
|
||||||
when(jda.getGatewayPing()).thenReturn(12L);
|
when(shards.getAveragePing()).thenReturn(12.0);
|
||||||
|
when(shards.getShardCount()).thenReturn(3);
|
||||||
|
|
||||||
settingService = mock(GuildSettingService.class);
|
settingService = mock(GuildSettingService.class);
|
||||||
when(settingService.snapshot()).thenReturn(Map.of("prefix", "ü!"));
|
when(settingService.snapshot()).thenReturn(Map.of("prefix", "ü!"));
|
||||||
@@ -107,7 +110,7 @@ class WebApiTest {
|
|||||||
|
|
||||||
MusicService musicService = mock(MusicService.class);
|
MusicService musicService = mock(MusicService.class);
|
||||||
|
|
||||||
webApi = new WebApi(settings, provider, musicService, () -> jda, () -> { });
|
webApi = new WebApi(settings, provider, musicService, shards, () -> { });
|
||||||
webApi.start();
|
webApi.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +160,16 @@ class WebApiTest {
|
|||||||
assertEquals(200, response.statusCode());
|
assertEquals(200, response.statusCode());
|
||||||
assertTrue(response.body().contains("\"health\":\"CONNECTED\""));
|
assertTrue(response.body().contains("\"health\":\"CONNECTED\""));
|
||||||
assertTrue(response.body().contains("\"ping\":\"12\""));
|
assertTrue(response.body().contains("\"ping\":\"12\""));
|
||||||
|
assertTrue(response.body().contains("\"shard_count\":3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void routesReturnServiceUnavailableWhenShardsAreNotReady() throws Exception {
|
||||||
|
when(shards.isReady()).thenReturn(false);
|
||||||
|
|
||||||
|
assertEquals(503, get("/stats/bot").statusCode());
|
||||||
|
assertEquals(503, get("/actions/status").statusCode());
|
||||||
|
assertEquals(503, get("/guilds").statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -256,6 +269,7 @@ class WebApiTest {
|
|||||||
assertTrue(response.body().contains("\"guild_count\":1"));
|
assertTrue(response.body().contains("\"guild_count\":1"));
|
||||||
assertTrue(response.body().contains("\"status\":\"CONNECTED\""));
|
assertTrue(response.body().contains("\"status\":\"CONNECTED\""));
|
||||||
assertTrue(response.body().contains("\"gateway_ping\":12"));
|
assertTrue(response.body().contains("\"gateway_ping\":12"));
|
||||||
|
assertTrue(response.body().contains("\"shard_count\":3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package me.bobert.netneutrality.shard;
|
||||||
|
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.sharding.ShardManager;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class ShardServiceTest {
|
||||||
|
|
||||||
|
private static JDA shard(JDA.Status status) {
|
||||||
|
JDA jda = mock(JDA.class);
|
||||||
|
when(jda.getStatus()).thenReturn(status);
|
||||||
|
return jda;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void aggregatesAcrossShards() {
|
||||||
|
ShardManager manager = mock(ShardManager.class);
|
||||||
|
Guild a = mock(Guild.class);
|
||||||
|
Guild b = mock(Guild.class);
|
||||||
|
JDA shard0 = shard(JDA.Status.CONNECTED);
|
||||||
|
JDA shard1 = shard(JDA.Status.RECONNECT_QUEUED);
|
||||||
|
|
||||||
|
when(manager.getGuilds()).thenReturn(List.of(a, b));
|
||||||
|
when(manager.getGuildById("2")).thenReturn(b);
|
||||||
|
when(manager.getShardsTotal()).thenReturn(2);
|
||||||
|
when(manager.getAverageGatewayPing()).thenReturn(42.5);
|
||||||
|
when(manager.getShards()).thenReturn(List.of(shard0, shard1));
|
||||||
|
when(manager.getShardById(0)).thenReturn(shard0);
|
||||||
|
|
||||||
|
ShardService service = new ShardService(manager);
|
||||||
|
|
||||||
|
assertTrue(service.isReady());
|
||||||
|
assertEquals(2, service.getGuilds().size());
|
||||||
|
assertSame(b, service.getGuildById("2"));
|
||||||
|
assertNull(service.getGuildById("missing"));
|
||||||
|
assertEquals(2, service.getShardCount());
|
||||||
|
assertEquals(42.5, service.getAveragePing());
|
||||||
|
assertEquals(JDA.Status.RECONNECT_QUEUED, service.getWorstStatus());
|
||||||
|
assertSame(shard0, service.getAnyShard());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allConnectedReportsConnected() {
|
||||||
|
ShardManager manager = mock(ShardManager.class);
|
||||||
|
JDA connected = shard(JDA.Status.CONNECTED);
|
||||||
|
when(manager.getShards()).thenReturn(List.of(connected));
|
||||||
|
|
||||||
|
assertEquals(JDA.Status.CONNECTED, new ShardService(manager).getWorstStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void notReadyBeforeStartAndAfterStop() {
|
||||||
|
ShardManager manager = mock(ShardManager.class);
|
||||||
|
ShardService service = new ShardService(manager);
|
||||||
|
service.stop();
|
||||||
|
|
||||||
|
assertFalse(service.isReady());
|
||||||
|
assertNull(service.getAnyShard());
|
||||||
|
verify(manager).shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void parsesShardTotal() {
|
||||||
|
assertEquals(-1, ShardService.parseShardTotal(null));
|
||||||
|
assertEquals(-1, ShardService.parseShardTotal(""));
|
||||||
|
assertEquals(-1, ShardService.parseShardTotal("abc"));
|
||||||
|
assertEquals(-1, ShardService.parseShardTotal("0"));
|
||||||
|
assertEquals(1, ShardService.parseShardTotal("1"));
|
||||||
|
assertEquals(4, ShardService.parseShardTotal(" 4 "));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user