From 2531444102087cbff561f5ce949bfcc50492dd99 Mon Sep 17 00:00:00 2001 From: Panav Bindal Date: Mon, 1 Jan 2024 00:09:25 +0000 Subject: [PATCH] Discord server wrapped command Just run /wrapped to get some statistics about the year! --- src/main/kotlin/com/learnspigot/bot/Bot.kt | 2 +- .../bot/reputation/LeaderboardMessage.kt | 201 +++++++++--------- .../learnspigot/bot/stats/WrappedCommand.kt | 147 +++++++++++++ 3 files changed, 251 insertions(+), 99 deletions(-) create mode 100644 src/main/kotlin/com/learnspigot/bot/stats/WrappedCommand.kt diff --git a/src/main/kotlin/com/learnspigot/bot/Bot.kt b/src/main/kotlin/com/learnspigot/bot/Bot.kt index 66570c8..d7c7b97 100644 --- a/src/main/kotlin/com/learnspigot/bot/Bot.kt +++ b/src/main/kotlin/com/learnspigot/bot/Bot.kt @@ -22,7 +22,7 @@ import net.dv8tion.jda.api.utils.ChunkingFilter import net.dv8tion.jda.api.utils.MemberCachePolicy class Bot { - private val profileRegistry = ProfileRegistry() + val profileRegistry = ProfileRegistry() init { jda = JDABuilder.createDefault(Environment.get("BOT_TOKEN")) diff --git a/src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt b/src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt index 7af980c..df9d3d2 100644 --- a/src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt +++ b/src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt @@ -1,11 +1,8 @@ package com.learnspigot.bot.reputation -import com.learnspigot.bot.profile.ProfileRegistry import com.learnspigot.bot.Server +import com.learnspigot.bot.profile.ProfileRegistry import com.learnspigot.bot.util.embed -import net.dv8tion.jda.api.entities.Message -import net.dv8tion.jda.api.entities.MessageEmbed -import net.dv8tion.jda.api.entities.MessageHistory import java.time.Instant import java.time.YearMonth import java.time.ZoneOffset @@ -13,107 +10,115 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.function.Consumer +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.entities.MessageEmbed +import net.dv8tion.jda.api.entities.MessageHistory class LeaderboardMessage(private val profileRegistry: ProfileRegistry) { - private val medals: List = listOf(":first_place:", ":second_place:", ":third_place:") - - private val executorService = Executors.newSingleThreadScheduledExecutor() - - private val monthlyRewardMessage: Message - private val lifetimeMessage: Message - private val monthlyMessage: Message - - init { - Server.leaderboardChannel.apply { - MessageHistory.getHistoryFromBeginning(this).complete().retrievedHistory.apply { - /* - * If all 3 messages aren't there, delete any existing ones and send the new 3 - * Otherwise, just get them, edit to update, and store for constant updating like normal - */ - if (size != 3) { - forEach { it.delete().queue() } - monthlyRewardMessage = sendMessageEmbeds(buildPrizeEmbed()).complete() - lifetimeMessage = sendMessageEmbeds(buildLeaderboard(false)).complete() - monthlyMessage = sendMessageEmbeds(buildLeaderboard(true)).complete() - } else { - monthlyRewardMessage = get(2).editMessageEmbeds(buildPrizeEmbed()).complete() - lifetimeMessage = get(1).editMessageEmbeds(buildLeaderboard(false)).complete() - monthlyMessage = get(0).editMessageEmbeds(buildLeaderboard(true)).complete() - } - } + private val medals: List = listOf(":first_place:", ":second_place:", ":third_place:") + + private val executorService = Executors.newSingleThreadScheduledExecutor() + + private val monthlyRewardMessage: Message + private val lifetimeMessage: Message + private val monthlyMessage: Message + + init { + Server.leaderboardChannel.apply { + MessageHistory.getHistoryFromBeginning(this).complete().retrievedHistory.apply { + /* + * If all 3 messages aren't there, delete any existing ones and send the new 3 + * Otherwise, just get them, edit to update, and store for constant updating like normal + */ + if (size != 3) { + forEach { it.delete().queue() } + monthlyRewardMessage = sendMessageEmbeds(buildPrizeEmbed()).complete() + lifetimeMessage = sendMessageEmbeds(buildLeaderboard(false)).complete() + monthlyMessage = sendMessageEmbeds(buildLeaderboard(true)).complete() + } else { + monthlyRewardMessage = get(2).editMessageEmbeds(buildPrizeEmbed()).complete() + lifetimeMessage = get(1).editMessageEmbeds(buildLeaderboard(false)).complete() + monthlyMessage = get(0).editMessageEmbeds(buildLeaderboard(true)).complete() } - - executorService.scheduleAtFixedRate({ - lifetimeMessage.editMessageEmbeds(buildLeaderboard(false)).queue() - monthlyMessage.editMessageEmbeds(buildLeaderboard(true)).queue() - - if (isLastMin()){ - Server.managerChannel.sendMessageEmbeds(buildLeaderboard(true)).queue {println("Manager channel leaderboard message sent.")} - } - }, 1L, 1L, TimeUnit.MINUTES) + } } - private fun buildLeaderboard(monthly: Boolean): MessageEmbed { - val builder = StringBuilder() - - val i = AtomicInteger(1) - top10(monthly).forEach(Consumer { (id, reputation): ReputationWrapper -> - builder.append( - if (i.get() <= medals.size) medals[i.get() - 1] else i.get().toString() + "." - ).append(" <@").append(id).append("> - ").append(reputation.size).append("\n") - i.getAndIncrement() - }) - - return embed() - .setTitle((if (monthly) "Monthly" else "All-Time") + " Leaderboard") - .setDescription((if (monthly) "These stats are reset on the 1st of every month." else "These stats are never reset.") + "\n\n$builder") - .setFooter("Last updated") - .setTimestamp(Instant.now()) - .build() - } - - private fun buildPrizeEmbed() : MessageEmbed{ - return embed() - .setTitle("Current Monthly Rewards") - .setDescription("The top 3 on the Monthly Leaderboard will earn these rewards:" + - "\n\n${medals[0]} - $50 PayPal!" + - "\n${medals[1]} - \$20 PayPal!" + - "\n${medals[2]} - \$10 PayPal!") - .setFooter("* To qualify, you must be part of the Support Team. Message a Manager to apply.", "https://cdn.discordapp.com/avatars/928124622564655184/54b6c4735aff20a92a5bc6881fab4d64.webp?size=128") - .build() - } - - private fun top10(monthly: Boolean): List { - val reputation = mutableListOf() - for ((key, profile) in profileRegistry.profileCache) { - var repList = ArrayList(profile.reputation.values) - if (repList.isEmpty()) continue - - if (monthly) { - repList = repList.filter { rep -> - YearMonth.now().atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC) - .isBefore(Instant.ofEpochSecond(rep.timestamp)) - } as ArrayList - } - reputation.add(ReputationWrapper(key, repList)) + executorService.scheduleAtFixedRate( + { + lifetimeMessage.editMessageEmbeds(buildLeaderboard(false)).queue() + monthlyMessage.editMessageEmbeds(buildLeaderboard(true)).queue() + }, + 1L, + 1L, + TimeUnit.MINUTES) + } + + private fun buildLeaderboard(monthly: Boolean): MessageEmbed { + val builder = StringBuilder() + + val i = AtomicInteger(1) + top10(profileRegistry, monthly) + .forEach( + Consumer { (id, reputation): ReputationWrapper -> + builder + .append( + if (i.get() <= medals.size) medals[i.get() - 1] else i.get().toString() + ".") + .append(" <@") + .append(id) + .append("> - ") + .append(reputation.size) + .append("\n") + i.getAndIncrement() + }) + + return embed() + .setTitle((if (monthly) "Monthly" else "All-Time") + " Leaderboard") + .setDescription( + (if (monthly) "These stats are reset on the 1st of every month." + else "These stats are never reset.") + "\n\n$builder") + .setFooter("Last updated") + .setTimestamp(Instant.now()) + .build() + } + + private fun buildPrizeEmbed(): MessageEmbed { + return embed() + .setTitle("Current Monthly Rewards") + .setDescription( + "The top 3 on the Monthly Leaderboard will earn these rewards:" + + "\n\n${medals[0]} - $50 PayPal!" + + "\n${medals[1]} - \$20 PayPal!" + + "\n${medals[2]} - \$10 PayPal!") + .setFooter( + "* To qualify, you must be part of the Support Team. Message a Manager to apply.", + "https://cdn.discordapp.com/avatars/928124622564655184/54b6c4735aff20a92a5bc6881fab4d64.webp?size=128") + .build() + } + + companion object { + fun top10(profileRegistry: ProfileRegistry, monthly:Boolean): List { + val reputation = mutableListOf() + for ((key, profile) in profileRegistry.profileCache) { + var repList = ArrayList(profile.reputation.values) + if (repList.isEmpty()) continue + + if (monthly) { + repList = + repList.filter { rep -> + YearMonth.now() + .atDay(1) + .atStartOfDay() + .toInstant(ZoneOffset.UTC) + .isBefore(Instant.ofEpochSecond(rep.timestamp)) + } as ArrayList } - reputation.sortByDescending { it.reputation.size } - return reputation.take(10) + reputation.add(ReputationWrapper(key, repList)) + } + reputation.sortByDescending { it.reputation.size } + return reputation.take(10) } - - private fun isLastMin(): Boolean { - val now = Instant.now() - val startOfNextMonth = YearMonth.now().plusMonths(1).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC) - val lastMinOfCurrentMonth = startOfNextMonth.minusSeconds(60) - - val isLastMin = now.isAfter(lastMinOfCurrentMonth) - if (isLastMin){ println("This is the last minute of the month!")} - - return isLastMin - } - - + } data class ReputationWrapper(val id: String, val reputation: List) -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/learnspigot/bot/stats/WrappedCommand.kt b/src/main/kotlin/com/learnspigot/bot/stats/WrappedCommand.kt new file mode 100644 index 0000000..719b6d8 --- /dev/null +++ b/src/main/kotlin/com/learnspigot/bot/stats/WrappedCommand.kt @@ -0,0 +1,147 @@ +package com.learnspigot.bot.stats + +import com.learnspigot.bot.Bot +import com.learnspigot.bot.Environment +import com.learnspigot.bot.Server +import com.learnspigot.bot.reputation.LeaderboardMessage +import gg.flyte.neptune.annotation.Command +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.Permission +import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import java.awt.Color +import java.util.* + +class WrappedCommand { + + private val guild = Bot.jda.getGuildById(Environment.get("GUILD_ID"))!! + private val currentYear = 2023 + private val helpForum: ForumChannel = Bot.jda.getForumChannelById(Server.helpChannel.id)!! + + private val uniqueUsers = mutableMapOf() + private var totalMessages = 0 + private val channelsMessageCount = mutableMapOf() + private val emojisUsageCount = mutableMapOf() + private val wordsUsageCount = mutableMapOf() + private var studentsHelped = 0 + private val highestContributor: LeaderboardMessage.ReputationWrapper by lazy { + LeaderboardMessage.top10(Bot().profileRegistry(), false).firstOrNull()!! + } + + private lateinit var mostReactedMessage: net.dv8tion.jda.api.entities.Message + + private val blacklist = mutableListOf( + "the", "to", "it", "i", "a", "and", "is", "in", "that", "you", + "was", "for", "on", "are", "with", "as", "at", "be", "this", + "have", "from", "or", "an", "but", "not", "by", "we", "can", + "if", "they", "he", "she", "will", "all", "no", "there", "do", + "just", "has", "so", "what", "about", "up", "out", "up", "one", + "down", "into", "some", "your", "how", "like", "when", "his", + "her", "their", "would", "who", "which", "time", "than", "them", + ":", "of", "count", "last", "since" + ) + + @Command( + name = "wrapped", + description = "get this year's discord wrapped", + permissions = [Permission.MANAGE_ROLES] + ) + fun onWrappedCommand( + event: SlashCommandInteractionEvent, + ) { + event.reply("Fetching the stats, please hold a moment!").setEphemeral(true).queue() + performMessageIteration() + performHelpIteration() + event.channel.sendMessageEmbeds(buildRecapMessage().build()).queue() + } + + private fun performMessageIteration() { + guild.textChannels.forEach { textChannel -> + textChannel.iterableHistory.forEach { message -> + val messageYear = message.timeCreated.year + if (messageYear == currentYear) { + totalMessages++ + uniqueUsers.merge(message.author.id, 1, Int::plus) + + val reactions = message.reactions.size + if (!::mostReactedMessage.isInitialized || + reactions > mostReactedMessage.reactions.size + ) { + mostReactedMessage = message + } + + channelsMessageCount.merge(textChannel.id, 1, Int::plus) + + val emojis = guild.emojis + emojis.forEach { emoji -> + val emojiId = emoji.id + if (message.contentRaw.contains(emojiId)) { + emojisUsageCount.merge(emojiId, 1, Int::plus) + } + } + + val words = message.contentRaw.split("\\s+".toRegex()) + words.forEach { word -> + wordsUsageCount.merge(word, 1, Int::plus) + } + } + } + } + } + + private fun performHelpIteration() { + studentsHelped = helpForum.threadChannels.count { it.isArchived && it.timeCreated.year == currentYear } + } + + private fun buildRecapMessage(): EmbedBuilder { + val topUsers = uniqueUsers.entries.sortedByDescending { it.value }.take(5) + val topChannels = channelsMessageCount.entries.sortedByDescending { it.value }.take(3) + val topEmojis = emojisUsageCount.entries.sortedByDescending { it.value }.take(5) + val topWords = wordsUsageCount.entries + .filter { it.key.lowercase(Locale.getDefault()) !in blacklist } + .sortedByDescending { it.value } + .take(5) + + + val codeBlock = buildString { + append("```\n") + + append("Total Messages: $totalMessages\n\n") + + append("Top 5 Most Active Users:\n") + topUsers.forEach { entry -> + append("${Bot.jda.getUserById(entry.key)!!.asMention}: ${entry.value} messages\n") + } + + append("\nTop 3 Most Active Channels:\n") + topChannels.forEach { entry -> + val channel = guild.getTextChannelById(entry.key) + append("${channel!!.asMention}: ${entry.value} messages\n") + } + + append("\nTop 5 Most Used Emojis:\n") + topEmojis.forEach { entry -> + val emojiId = entry.key + val emoji = guild.getEmojiById(emojiId) + append("${emoji?.asMention ?: "<:$emojiId>"}: ${entry.value} uses\n") + } + + append("\nTop 5 Most Used Words:\n") + topWords.forEach { entry -> append("${entry.key}: ${entry.value} uses\n") } + + append("\nHelp Post Statistics:\n") + append("Help posts successfully closed: $studentsHelped\n") + append( + "Highest Contributor: ${Bot.jda.getUserById(highestContributor.id)!!.asMention} with ${highestContributor.reputation.size} reps!\n" + ) + + append("```\n") + } + + return EmbedBuilder() + .setColor(Color.BLUE) + .setTitle("Discord Wrapped $currentYear") + .setDescription("Here's a recap of this year's Discord activity:") + .addField("LearnSpigot Discord Wrapped $currentYear", codeBlock, false) + } +}