From 62fd9fd76eed9299f66bc948b103d040bc95504e Mon Sep 17 00:00:00 2001 From: WinG4merBR <68250074+WinG4merBR@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:17:33 -0300 Subject: [PATCH] EXPERIMENTAL: Implements sharding manager --- .../kotlin/net/cakeyfox/foxy/FoxyInstance.kt | 76 +++++++++++++------ .../foxy/command/FoxyCommandManager.kt | 31 ++++---- .../command/vanilla/social/MarryExecutor.kt | 2 +- .../command/vanilla/utils/HelpExecutor.kt | 2 +- .../command/vanilla/utils/PingExecutor.kt | 24 ++++++ .../command/vanilla/utils/TopCakesExecutor.kt | 3 + .../vanilla/utils/declarations/PingCommand.kt | 16 ++++ .../foxy/listeners/MajorEventListener.kt | 3 - .../cakeyfox/foxy/utils/ActivityUpdater.kt | 7 +- .../foxy/utils/analytics/TopggStatsSender.kt | 2 +- .../cakeyfox/foxy/utils/config/FoxyConfig.kt | 4 + .../foxy/utils/profile/badge/BadgeUtils.kt | 12 ++- foxy/src/main/resources/foxy.conf | 20 ++--- .../main/resources/locales/en-us/general.yml | 4 + .../main/resources/locales/pt-br/general.yml | 4 + 15 files changed, 151 insertions(+), 59 deletions(-) create mode 100644 foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/PingExecutor.kt create mode 100644 foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/declarations/PingCommand.kt diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt index 615ee436..7faf1b65 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/FoxyInstance.kt @@ -5,6 +5,7 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.Job import mu.KotlinLogging import net.cakeyfox.artistry.ArtistryClient import net.cakeyfox.foxy.command.FoxyCommandManager @@ -13,19 +14,22 @@ import net.cakeyfox.foxy.listeners.GuildEventListener import net.cakeyfox.foxy.listeners.InteractionEventListener import net.cakeyfox.foxy.listeners.MajorEventListener import net.cakeyfox.foxy.utils.ActivityUpdater -import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.JDABuilder import net.cakeyfox.foxy.utils.config.FoxyConfig import net.cakeyfox.foxy.utils.FoxyUtils import net.cakeyfox.foxy.utils.database.MongoDBClient +import net.dv8tion.jda.api.entities.User import net.dv8tion.jda.api.requests.GatewayIntent +import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder +import net.dv8tion.jda.api.sharding.ShardManager import net.dv8tion.jda.api.utils.cache.CacheFlag +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.concurrent.thread import kotlin.reflect.jvm.jvmName class FoxyInstance( val config: FoxyConfig ) { - lateinit var jda: JDA + lateinit var jda: ShardManager lateinit var mongoClient: MongoDBClient lateinit var commandHandler: FoxyCommandManager lateinit var artistryClient: ArtistryClient @@ -33,8 +37,9 @@ class FoxyInstance( lateinit var interactionManager: FoxyComponentManager lateinit var environment: String lateinit var httpClient: HttpClient + lateinit var selfUser: User - fun start() { + suspend fun start() { val logger = KotlinLogging.logger(this::class.jvmName) val activityUpdater = ActivityUpdater(this) mongoClient = MongoDBClient() @@ -54,33 +59,54 @@ class FoxyInstance( } mongoClient.start(this) - jda = JDABuilder.createDefault(config.discordToken) - .setEnabledIntents( - GatewayIntent.GUILD_MEMBERS, - GatewayIntent.MESSAGE_CONTENT, - GatewayIntent.GUILD_MESSAGES, - GatewayIntent.GUILD_EMOJIS_AND_STICKERS, - GatewayIntent.SCHEDULED_EVENTS - ) - .enableCache(CacheFlag.SCHEDULED_EVENTS) - .enableCache(CacheFlag.MEMBER_OVERRIDES) - .disableCache(CacheFlag.VOICE_STATE) - .build() - - jda.addEventListener( + jda = DefaultShardManagerBuilder.create( + GatewayIntent.GUILD_MEMBERS, + GatewayIntent.MESSAGE_CONTENT, + GatewayIntent.GUILD_MESSAGES, + GatewayIntent.GUILD_EMOJIS_AND_STICKERS, + GatewayIntent.SCHEDULED_EVENTS + ).addEventListeners( MajorEventListener(this), GuildEventListener(this), InteractionEventListener(this) ) + .setShardsTotal(config.totalShards) + .setShards(config.minClusterShard, config.maxClusterShard) + .disableCache(CacheFlag.entries) + .enableCache( + CacheFlag.EMOJI, + CacheFlag.STICKER, + CacheFlag.MEMBER_OVERRIDES + ) + .setToken(config.discordToken) + .setEnableShutdownHook(false) + .build() - jda.awaitReady() + this.commandHandler.handle() - Runtime.getRuntime().addShutdownHook(Thread { - logger.info { "Shutting down..." } - jda.shutdown() - httpClient.close() - mongoClient.close() - activityUpdater.shutdown() + selfUser = jda.getShardById(0)?.selfUser!! + + Runtime.getRuntime().addShutdownHook(thread(false) { + try { + logger.info { "Foxy is shutting down..." } + jda.shards.forEach { shard -> + shard.removeEventListener(*shard.registeredListeners.toTypedArray()) + logger.info { "Shutting down shard #${shard.shardInfo.shardId}..."} + shard.shutdown() + } + httpClient.close() + mongoClient.close() + activityUpdater.shutdown() + + activeJobs.forEach { + logger.info { "Cancelling job $it" } + it.cancel() + } + } catch (e: Exception) { + logger.error(e) { "Error during shutdown process" } + } }) } + + private val activeJobs = ConcurrentLinkedQueue() } \ No newline at end of file diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/FoxyCommandManager.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/FoxyCommandManager.kt index 5587c21c..e85ec3ba 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/FoxyCommandManager.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/FoxyCommandManager.kt @@ -1,6 +1,7 @@ package net.cakeyfox.foxy.command import dev.minn.jda.ktx.coroutines.await +import mu.KotlinLogging import net.cakeyfox.foxy.FoxyInstance import net.cakeyfox.foxy.command.structure.FoxyCommandDeclarationWrapper import net.cakeyfox.foxy.command.vanilla.actions.declarations.ActionsCommand @@ -13,11 +14,14 @@ import net.cakeyfox.foxy.command.vanilla.social.declarations.MarryCommand import net.cakeyfox.foxy.command.vanilla.social.declarations.ProfileCommand import net.cakeyfox.foxy.command.vanilla.utils.declarations.DblCommand import net.cakeyfox.foxy.command.vanilla.utils.declarations.HelpCommand +import net.cakeyfox.foxy.command.vanilla.utils.declarations.PingCommand import net.cakeyfox.foxy.command.vanilla.utils.declarations.TopCommand import net.dv8tion.jda.api.interactions.commands.Command +import kotlin.reflect.jvm.jvmName class FoxyCommandManager(private val foxy: FoxyInstance) { private val commands = mutableListOf() + private val logger = KotlinLogging.logger(this::class.jvmName) operator fun get(name: String): FoxyCommandDeclarationWrapper? { return commands.find { it.create().name == name } @@ -27,25 +31,25 @@ class FoxyCommandManager(private val foxy: FoxyInstance) { commands.add(command) } - suspend fun handle(): MutableList? { - val action = foxy.jda.updateCommands() - val privateGuild = foxy.jda.getGuildById(foxy.config.guildId)!! + suspend fun handle(): MutableList { + val allCommands = mutableListOf() + logger.info { "Starting command handling for ${foxy.jda.shards.size + 1} shards" } + foxy.jda.shards.forEach { shard -> + val action = shard.updateCommands() - commands.forEach { command -> - if (command.create().isPrivate) { - privateGuild.updateCommands().addCommands( - command.create().build() - ).await() - } else { - action.addCommands( - command.create().build() - ) + commands.forEach { command -> + action.addCommands(command.create().build()) } + + val registeredCommands = action.await() + allCommands.addAll(registeredCommands) + logger.info { "${commands.size} commands registered on shard ${shard.shardInfo.shardId}" } } - return action.await() + return allCommands } + init { /* ---- [Roleplay] ---- */ register(ActionsCommand()) @@ -71,5 +75,6 @@ class FoxyCommandManager(private val foxy: FoxyInstance) { /* ---- [Utils] ---- */ register(HelpCommand()) register(DblCommand()) + register(PingCommand()) } } \ No newline at end of file diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/social/MarryExecutor.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/social/MarryExecutor.kt index 28902cf5..ce002225 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/social/MarryExecutor.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/social/MarryExecutor.kt @@ -24,7 +24,7 @@ class MarryExecutor : FoxyCommandExecutor() { return } - if (user.id == context.foxy.jda.selfUser.id) { + if (user.id == context.foxy.selfUser.id) { context.reply(true) { content = pretty( FoxyEmotes.FoxyCry, diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/HelpExecutor.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/HelpExecutor.kt index 5da9b94c..0a61ffdf 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/HelpExecutor.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/HelpExecutor.kt @@ -17,7 +17,7 @@ class HelpExecutor : FoxyCommandExecutor() { ) color = Colors.FOXY_DEFAULT - thumbnail = context.foxy.jda.selfUser.effectiveAvatarUrl + thumbnail = context.foxy.selfUser.effectiveAvatarUrl // Yes, using "emoji" instead of emoji name will work field { diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/PingExecutor.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/PingExecutor.kt new file mode 100644 index 00000000..58aef768 --- /dev/null +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/PingExecutor.kt @@ -0,0 +1,24 @@ +package net.cakeyfox.foxy.command.vanilla.utils + +import net.cakeyfox.common.FoxyEmotes +import net.cakeyfox.foxy.command.FoxyInteractionContext +import net.cakeyfox.foxy.command.structure.FoxyCommandExecutor +import net.cakeyfox.foxy.utils.pretty +import java.net.InetAddress + +class PingExecutor : FoxyCommandExecutor() { + override suspend fun execute(context: FoxyInteractionContext) { + val gatewayPing = context.jda.gatewayPing + val currentShardId = context.jda.shardInfo.shardId + 1 + val totalShards = context.jda.shardInfo.shardTotal + + val response = pretty(FoxyEmotes.FoxyHowdy, "Ping\n") + + pretty(FoxyEmotes.FoxyWow, "Gateway Ping: ${gatewayPing}ms\n") + + pretty(FoxyEmotes.FoxyThink, "Shard ID: ${currentShardId}/${totalShards}\n") + + pretty(FoxyEmotes.FoxyCupcake, "Cluster: `${InetAddress.getLocalHost().hostName}`") + + context.reply { + content = response + } + } +} \ No newline at end of file diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/TopCakesExecutor.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/TopCakesExecutor.kt index a7dd75a6..ab2a91f6 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/TopCakesExecutor.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/TopCakesExecutor.kt @@ -1,10 +1,13 @@ package net.cakeyfox.foxy.command.vanilla.utils +import com.github.benmanes.caffeine.cache.Caffeine import dev.minn.jda.ktx.coroutines.await import net.cakeyfox.common.Colors import net.cakeyfox.common.FoxyEmotes import net.cakeyfox.foxy.command.FoxyInteractionContext import net.cakeyfox.foxy.command.structure.FoxyCommandExecutor +import net.cakeyfox.serializable.database.data.FoxyUser +import okhttp3.Cache class TopCakesExecutor : FoxyCommandExecutor() { override suspend fun execute(context: FoxyInteractionContext) { diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/declarations/PingCommand.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/declarations/PingCommand.kt new file mode 100644 index 00000000..aa870e75 --- /dev/null +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/command/vanilla/utils/declarations/PingCommand.kt @@ -0,0 +1,16 @@ +package net.cakeyfox.foxy.command.vanilla.utils.declarations + +import net.cakeyfox.foxy.command.structure.FoxyCommandDeclarationBuilder +import net.cakeyfox.foxy.command.structure.FoxyCommandDeclarationWrapper +import net.cakeyfox.foxy.command.vanilla.utils.PingExecutor + +class PingCommand : FoxyCommandDeclarationWrapper { + override fun create(): FoxyCommandDeclarationBuilder = command( + "ping", + "ping.description", + + block = { + executor = PingExecutor() + } + ) +} \ No newline at end of file diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/listeners/MajorEventListener.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/listeners/MajorEventListener.kt index 4c5cb645..d81adeaf 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/listeners/MajorEventListener.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/listeners/MajorEventListener.kt @@ -25,9 +25,6 @@ class MajorEventListener(private val foxy: FoxyInstance): ListenerAdapter() { OnlineStatus.ONLINE, Activity.customStatus(Constants.DEFAULT_ACTIVITY(foxy.environment))) - val commands = foxy.commandHandler.handle() - logger.info { "Registered ${commands?.size} commands" } - if (foxy.environment == "production") { topggStats.send(event.jda.guildCache.size()) } diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/ActivityUpdater.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/ActivityUpdater.kt index b2370309..432045d1 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/ActivityUpdater.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/ActivityUpdater.kt @@ -45,15 +45,16 @@ class ActivityUpdater( return@post } - foxy.jda.presence.setPresence( - request.status?.let { OnlineStatus.fromKey(it) } ?: OnlineStatus.ONLINE, + foxy.jda.shards.forEach { + request.status?.let { OnlineStatus.fromKey(it) } ?: OnlineStatus.ONLINE Activity.of( ActivityType.fromKey(request.type), request.name, request.url ) - ) + logger.info { "Updating status for shard: #${it.shardInfo.shardId}" } + } call.respond(HttpStatusCode.OK, "Activity updated") } } diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/analytics/TopggStatsSender.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/analytics/TopggStatsSender.kt index f297b9e7..855d1565 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/analytics/TopggStatsSender.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/analytics/TopggStatsSender.kt @@ -18,7 +18,7 @@ class TopggStatsSender( ): StatsSender { private val logger = KotlinLogging.logger(this::class.jvmName) private val token = foxy.config.dblToken - private val clientId = foxy.jda.selfUser.id + private val clientId = foxy.config.applicationId override suspend fun send(guildCount: Long): Boolean { return withContext(Dispatchers.IO) { diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/config/FoxyConfig.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/config/FoxyConfig.kt index 2bb04df1..d6bbd2a3 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/config/FoxyConfig.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/config/FoxyConfig.kt @@ -4,10 +4,14 @@ import kotlinx.serialization.Serializable @Serializable data class FoxyConfig( + val applicationId: String, val ownerId: String, val guildId: String, val environment: String, val discordToken: String, + val minShards: Int, + val maxShards: Int, + val totalShards: Int, val mongoUri: String, val dbName: String, val mongoTimeout: Long, diff --git a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/profile/badge/BadgeUtils.kt b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/profile/badge/BadgeUtils.kt index 1b27872e..0ee23034 100644 --- a/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/profile/badge/BadgeUtils.kt +++ b/foxy/src/main/kotlin/net/cakeyfox/foxy/utils/profile/badge/BadgeUtils.kt @@ -6,6 +6,8 @@ import net.dv8tion.jda.api.entities.Member import java.time.Instant object BadgeUtils { + private val twelveHoursAgo = System.currentTimeMillis() - 12 * 60 * 60 * 1000 + fun getBadges(member: Member, defaultBadges: List, data: FoxyUser): List { val userBadges = mutableListOf() @@ -17,7 +19,6 @@ object BadgeUtils { } userBadges.addAll(roleBadges) - val twelveHoursAgo = System.currentTimeMillis() - 12 * 60 * 60 * 1000 val additionalBadges = listOf( BadgeCondition("married", data.marryStatus.marriedWith != null), BadgeCondition("upvoter", data.lastVote?.let { @@ -41,13 +42,20 @@ object BadgeUtils { } } + defaultBadges.filter { it.isFromGuild != null }.forEach { badge -> + if (userBadges.none { + it.id == badge.id || it.isFromGuild == badge.isFromGuild + }) { + userBadges.add(badge) + } + } + return userBadges.distinctBy { it.id }.sortedByDescending { it.priority } } fun getFallbackBadges(defaultBadges: List, userData: FoxyUser): List { val userBadges = mutableListOf() - val twelveHoursAgo = System.currentTimeMillis() - 12 * 60 * 60 * 1000 val additionalBadges = listOf( BadgeCondition("married", userData.marryStatus.marriedWith != null), BadgeCondition("upvoter", userData.lastVote?.let { diff --git a/foxy/src/main/resources/foxy.conf b/foxy/src/main/resources/foxy.conf index 9dcde0c2..5df70793 100644 --- a/foxy/src/main/resources/foxy.conf +++ b/foxy/src/main/resources/foxy.conf @@ -1,20 +1,20 @@ -# Basic settings ownerId= guildId= +applicationId= -# Environment type environment=development -# Basic Authentication discordToken= -# Database Settings -mongoUri="YOUR-MONGODB-URI" -dbName=Foxy +minShards = 0 +maxShards = 0 +totalShards = 1 + +mongoUri= +dbName=foxy mongoTimeout=5000 -# Others -foxyApiKey=key -dblToken=key -artistryKey=key +foxyApiKey= +dblToken= +artistryKey= activityPort=8080 \ No newline at end of file diff --git a/foxy/src/main/resources/locales/en-us/general.yml b/foxy/src/main/resources/locales/en-us/general.yml index 4df69c03..9803e0da 100644 --- a/foxy/src/main/resources/locales/en-us/general.yml +++ b/foxy/src/main/resources/locales/en-us/general.yml @@ -7,6 +7,10 @@ commands: name: "daily" description: "[Economy] Claim your daily cakes" + ping: + name: "ping" + description: "pings the bot" + fun: name: "fun" description: "Fun commands to enjoy with your friends" diff --git a/foxy/src/main/resources/locales/pt-br/general.yml b/foxy/src/main/resources/locales/pt-br/general.yml index a0d6ccb4..d3c70d13 100644 --- a/foxy/src/main/resources/locales/pt-br/general.yml +++ b/foxy/src/main/resources/locales/pt-br/general.yml @@ -8,6 +8,10 @@ commands: name: "daily" description: "[Economia] Receba seus cakes diários" + ping: + name: "ping" + description: "pings the bot" + fun: name: "fun" description: "Comandos divertidos para você se divertir com seus amigos"