-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #224 from Pan-nav/master
Discord server wrapped command
- Loading branch information
Showing
3 changed files
with
251 additions
and
99 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 103 additions & 98 deletions
201
src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,119 +1,124 @@ | ||
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 | ||
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<String> = 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<String> = 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<ReputationWrapper> { | ||
val reputation = mutableListOf<ReputationWrapper>() | ||
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> | ||
} | ||
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<ReputationWrapper> { | ||
val reputation = mutableListOf<ReputationWrapper>() | ||
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> | ||
} | ||
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<Reputation>) | ||
|
||
} | ||
} |
147 changes: 147 additions & 0 deletions
147
src/main/kotlin/com/learnspigot/bot/stats/WrappedCommand.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, Int>() | ||
private var totalMessages = 0 | ||
private val channelsMessageCount = mutableMapOf<String, Int>() | ||
private val emojisUsageCount = mutableMapOf<String, Int>() | ||
private val wordsUsageCount = mutableMapOf<String, Int>() | ||
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) | ||
} | ||
} |