├── keys.csv ├── gradle.properties ├── settings.gradle.kts ├── src └── main │ └── kotlin │ └── com │ └── learnspigot │ └── bot │ ├── Main.kt │ ├── util │ ├── Embed.kt │ ├── PostRegistry.kt │ ├── PermissionRole.kt │ └── Mongo.kt │ ├── starboard │ ├── StarboardUtil.kt │ ├── StarboardEntry.kt │ ├── StarboardListener.kt │ └── StarboardRegistry.kt │ ├── Environment.kt │ ├── lecture │ ├── Lecture.kt │ ├── LectureCommand.kt │ ├── WordMatcher.kt │ └── LectureRegistry.kt │ ├── intellijkey │ ├── IJUltimateKeyRegistry.kt │ ├── KeysLeftCommand.kt │ └── GetKeyCommand.kt │ ├── reputation │ ├── Reputation.kt │ ├── command │ │ ├── AddReputationCommand.kt │ │ ├── RemoveReputationCommand.kt │ │ └── ReputationCommand.kt │ └── LeaderboardMessage.kt │ ├── knowledgebase │ ├── KnowledgebasePostRegistry.kt │ ├── KnowledgebaseCommand.kt │ └── KnowledgebaseListener.kt │ ├── help │ ├── search │ │ ├── HelpPostRegistry.kt │ │ └── SearchHelpCommand.kt │ ├── ThreadListener.kt │ ├── PasteCommand.kt │ ├── MultiplierCommand.kt │ ├── PingCommand.kt │ ├── CloseListener.kt │ ├── HastebinListener.kt │ └── CloseCommand.kt │ ├── showcase │ └── ShowcaseListener.kt │ ├── suggestion │ ├── SuggestionListener.kt │ └── grammar │ │ └── MessageListener.kt │ ├── voicechat │ ├── VCListener.kt │ └── VCCommand.kt │ ├── Server.kt │ ├── embed │ └── EmbedCommand.kt │ ├── counting │ ├── CountingRegistry.kt │ ├── CountingCommand.kt │ ├── CountingListener.kt │ └── CountingInsults.kt │ ├── verification │ ├── VerificationMessage.kt │ └── VerificationListener.kt │ ├── profile │ ├── ProfileCommand.kt │ ├── ProfileRegistry.kt │ ├── Profile.kt │ └── ProfileListener.kt │ ├── vote │ └── VoteListener.kt │ └── Bot.kt ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ ├── suggestion.yml │ └── bug.yml ├── .gitignore ├── .env.example ├── README.md ├── gradlew.bat └── gradlew /keys.csv: -------------------------------------------------------------------------------- 1 | TEST -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bot" 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/Main.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot 2 | 3 | fun main() { 4 | Bot() 5 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flytegg/ls-discord-bot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issus_enabled: true 2 | contact_links: 3 | - name: LearnSpigot Discord Server 4 | url: https://learnspigot.com/discord 5 | - name: Project Manager 6 | about: stephen.gg 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/util/Embed.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.util 2 | 3 | import net.dv8tion.jda.api.EmbedBuilder 4 | 5 | fun embed(): EmbedBuilder { 6 | return EmbedBuilder().setColor(0x2B2D31) 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/starboard/StarboardUtil.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.starboard 2 | 3 | import net.dv8tion.jda.api.entities.Message 4 | import net.dv8tion.jda.api.entities.emoji.Emoji 5 | 6 | object StarboardUtil { 7 | fun Message.getEmojiReactionCount(emoji: Emoji): Int = this.getReaction(emoji)?.count ?: 0 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/Environment.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot 2 | 3 | import io.github.cdimascio.dotenv.Dotenv 4 | 5 | object Environment { 6 | private val dotenv = Dotenv.configure() 7 | .systemProperties() 8 | .load() 9 | 10 | fun get(variable: String): String { 11 | return dotenv.get(variable) 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/lecture/Lecture.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.lecture 2 | 3 | data class Lecture( 4 | val id: String, 5 | val title: String, 6 | val description: String) { 7 | 8 | fun url(): String { 9 | return "https://www.udemy.com/course/develop-minecraft-plugins-java-programming/learn/lecture/$id" 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/intellijkey/IJUltimateKeyRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.intellijkey 2 | 3 | import java.io.File 4 | 5 | class IJUltimateKeyRegistry { 6 | 7 | private val csvFile = File("keys.csv") 8 | 9 | val keys = csvFile.readLines().toMutableList() 10 | 11 | fun getKey(): String? { 12 | return keys.removeFirstOrNull() 13 | } 14 | 15 | fun readdKey(key: String) { 16 | keys.add(key) 17 | } 18 | 19 | fun removeKeyFromFile(key: String) { 20 | csvFile.writeText(keys.joinToString("\n")) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/reputation/Reputation.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.reputation 2 | 3 | import org.bson.Document 4 | 5 | data class Reputation( 6 | val timestamp: Long, 7 | val fromMemberId: String?, 8 | val fromPostId: String? 9 | ) { 10 | fun document(): Document { 11 | val document = Document() 12 | document["timestamp"] = timestamp 13 | if (fromMemberId != null) document["fromMemberId"] = fromMemberId 14 | if (fromPostId != null) document["fromPostId"] = fromPostId 15 | return document 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/knowledgebase/KnowledgebasePostRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.knowledgebase 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.util.PostRegistry 5 | import java.util.concurrent.CompletableFuture 6 | import java.util.concurrent.Executors 7 | 8 | class KnowledgebasePostRegistry : PostRegistry() { 9 | 10 | init { 11 | CompletableFuture.runAsync({ 12 | Server.knowledgebaseChannel.threadChannels.forEach { 13 | posts[it.name] = it.id 14 | } 15 | }, Executors.newCachedThreadPool()) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | gradle 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | 41 | ### ENV ### 42 | .env 43 | .env.dev 44 | 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/util/PostRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.util 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.lecture.WordMatcher 5 | import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel 6 | 7 | open class PostRegistry { 8 | 9 | val posts = mutableMapOf() // name, id 10 | private val matcher: WordMatcher = WordMatcher() 11 | 12 | fun findTop4Posts(query: String): MutableList { 13 | return matcher.getTopMatches(query, posts.keys.toList(), 4).mapNotNull { 14 | Server.guild.getThreadChannelById(posts[it]!!) 15 | }.toMutableList() 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/suggestion.yml: -------------------------------------------------------------------------------- 1 | name: Enhancement 2 | description: File a suggestion on how to enhance the bot. 3 | labels: ["enhancement"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for your ideas, fill the boxes below and we'll get back to you soon. 9 | - type: textarea 10 | id: explanation 11 | attributes: 12 | label: Explain your idea. 13 | description: What's going through your head? 14 | placeholder: Explain it clearly... 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: other-info 19 | attributes: 20 | label: Anything else? 21 | description: Anything you missed? 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/search/HelpPostRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help.search 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.util.PostRegistry 5 | import java.util.concurrent.CompletableFuture 6 | import java.util.concurrent.Executors 7 | 8 | class HelpPostRegistry : PostRegistry() { 9 | 10 | init { 11 | CompletableFuture.runAsync({ 12 | // Get open help posts 13 | Server.helpChannel.threadChannels.forEach { posts[it.name] = it.id } 14 | // Get closed help posts 15 | Server.helpChannel.retrieveArchivedPublicThreadChannels().forEach { posts[it.name] = it.id } 16 | }, Executors.newCachedThreadPool()) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/util/PermissionRole.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.util 2 | 3 | import net.dv8tion.jda.api.Permission 4 | 5 | // Maps Discord permission to Role which has perm on server, accounting for Discord making command access permission based and not role based 6 | // These need implemented in commands when somebody can be bothered, and also updated to the relevant perm 7 | object PermissionRole { 8 | 9 | val STUDENT = Permission.MESSAGE_SEND 10 | val TRIAL_HELPER = Permission.MESSAGE_SEND 11 | val HELPER = Permission.MESSAGE_SEND 12 | val SUPPORT = Permission.MESSAGE_SEND 13 | val SPECIALIST = Permission.MESSAGE_SEND 14 | val EXPERT = Permission.PRIORITY_SPEAKER 15 | val MANAGER = Permission.MESSAGE_SEND 16 | 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/starboard/StarboardEntry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.starboard 2 | 3 | import org.bson.Document 4 | 5 | 6 | data class StarboardEntry(val originalMessageId: String, val startboardMessageId: String) { 7 | fun document(): Document { 8 | val document = Document() 9 | document["originalMessageId"] = originalMessageId 10 | document["startboardMessageId"] = startboardMessageId 11 | 12 | return document 13 | } 14 | 15 | companion object { 16 | fun fromDocument(document: Document): StarboardEntry = StarboardEntry( 17 | originalMessageId = document.getString("originalMessageId"), 18 | startboardMessageId = document.getString("startboardMessageId") 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MONGO_URI= 2 | MONGO_DATABASE= 3 | 4 | BOT_TOKEN= 5 | GUILD_ID= 6 | 7 | SUGGESTIONS_CHANNEL_ID= 8 | LEADERBOARD_CHANNEL_ID= 9 | VERIFY_CHANNEL_ID= 10 | HELP_CHANNEL_ID= 11 | QUESTIONS_CHANNEL_ID= 12 | GET_COURSE_CHANNEL_ID= 13 | SUPPORT_CHANNEL_ID= 14 | GENERAL_CHANNEL_ID= 15 | MANAGER_CHANNEL_ID= 16 | KNOWLEDGEBASE_CHANNEL_ID= 17 | PROJECTS_CHANNEL_ID= 18 | STARBOARD_CHANNEL_ID= 19 | SHOWCASE_CHANNEL_ID= 20 | NEWS_CHANNEL_ID= 21 | COUNTING_CHANNEL_ID= 22 | VOICE_CHANNEL_ID= 23 | VERIFICATION_CHANNEL_ID= 24 | 25 | CHAT_CATEGORY= 26 | 27 | STUDENT_ROLE_ID= 28 | SUPPORT_ROLE_ID= 29 | STAFF_ROLE_ID= 30 | MANAGEMENT_ROLE_ID= 31 | VERIFIER_ROLE_ID= 32 | 33 | RIGHT_ARROW_EMOJI_ID= 34 | UPVOTE_EMOJI_ID= 35 | DOWNVOTE_EMOJI_ID= 36 | NOSTARBOARD_EMOJI_ID= 37 | 38 | STARBOARD_AMOUNT= 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/showcase/ShowcaseListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.showcase 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import net.dv8tion.jda.api.entities.emoji.Emoji 6 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent 7 | import net.dv8tion.jda.api.hooks.ListenerAdapter 8 | 9 | class ShowcaseListener : ListenerAdapter() { 10 | override fun onMessageReceived(event: MessageReceivedEvent) { 11 | if (event.author.isBot) return 12 | if (!event.isFromGuild) return 13 | if (event.guild.id != Server.guildId) return 14 | if (event.channel.id != Environment.get("SHOWCASE_CHANNEL_ID")) return 15 | 16 | event.message.addReaction(Emoji.fromUnicode("❤️")).queue() 17 | event.message.createThreadChannel("Showcase from ${event.author.effectiveName}").queue() 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/suggestion/SuggestionListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.suggestion 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent 6 | import net.dv8tion.jda.api.hooks.ListenerAdapter 7 | 8 | class SuggestionListener : ListenerAdapter() { 9 | 10 | override fun onMessageReceived(e: MessageReceivedEvent) { 11 | if (e.author.isBot) return 12 | if (!e.isFromGuild) return 13 | if (e.guild.id != Server.guildId) return 14 | if (e.channel.id != Environment.get("SUGGESTIONS_CHANNEL_ID")) return 15 | 16 | e.message.apply { 17 | addReaction(Server.upvoteEmoji).queue() 18 | addReaction(Server.downvoteEmoji).queue() 19 | createThreadChannel("Suggestion from ${e.author.effectiveName}").queue() 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/voicechat/VCListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.voicechat 2 | 3 | import com.learnspigot.bot.Environment 4 | import net.dv8tion.jda.api.entities.channel.concrete.StageChannel 5 | import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent 6 | import net.dv8tion.jda.api.hooks.ListenerAdapter 7 | 8 | class VCListener : ListenerAdapter() { 9 | override fun onGuildVoiceUpdate(event: GuildVoiceUpdateEvent){ 10 | val guild = event.guild 11 | val voiceChannel = guild.getVoiceChannelById(Environment.get("VOICE_CHANNEL_ID")) 12 | val leftChannel = event.channelLeft 13 | 14 | if (leftChannel != null && leftChannel.members.isEmpty()) { 15 | if ((leftChannel == voiceChannel) || (leftChannel is StageChannel) || (leftChannel.parentCategoryId != Environment.get("CHAT_CATEGORY"))) return 16 | leftChannel.delete().queue() 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | labels: ["bug"] 4 | assignees: 5 | - sttephen 6 | - joshbker 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thank you for creating a bug report. Please fill in the boxes below and we will respond asap! 12 | - type: textarea 13 | id: explanation 14 | attributes: 15 | label: Explain the issue. 16 | description: What is going wrong? 17 | placeholder: Explain it clearly... 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: recreation 22 | attributes: 23 | label: How to recreate? 24 | description: Give us an easy to follow recreation of how to recreate this bug 25 | placeholder: Make this as easy to follow as possible 26 | - type: textarea 27 | id: other-info 28 | attributes: 29 | label: Anything else? 30 | description: Anything you missed? 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/util/Mongo.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.util 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.mongodb.client.MongoClients 5 | import com.mongodb.client.MongoCollection 6 | import org.bson.Document 7 | import org.litote.kmongo.KMongo 8 | 9 | object Mongo { 10 | 11 | private val client = KMongo.createClient(Environment.get("MONGO_URI")).also { 12 | println("Connected to MongoDB with URI: ${Environment.get("MONGO_URI")}") 13 | } 14 | private val database = client.getDatabase(Environment.get("MONGO_DATABASE")) 15 | 16 | val userCollection: MongoCollection = database.getCollection("users") 17 | val starboardCollection: MongoCollection = database.getCollection("starboard") 18 | val docsCollection: MongoCollection = database.getCollection("spigot-docs") 19 | val countingCollection: MongoCollection = database.getCollection("counting") 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/suggestion/grammar/MessageListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.suggestion.grammar 2 | 3 | import net.dv8tion.jda.api.entities.emoji.Emoji 4 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent 5 | import net.dv8tion.jda.api.hooks.ListenerAdapter 6 | 7 | class MessageListener : ListenerAdapter() { 8 | 9 | private val idiotGrammerFixerBroLikeActuallyStfuOngUCantSpellDumbAss = hashSetOf( 10 | "I m ", 11 | "I ma ", 12 | ) 13 | 14 | private val theIdiot = "1071963283332018177" 15 | 16 | override fun onMessageReceived(event: MessageReceivedEvent) { 17 | if (event.author.id == theIdiot) { 18 | if (idiotGrammerFixerBroLikeActuallyStfuOngUCantSpellDumbAss.any { event.message.contentRaw.contains(it, true) }) { 19 | event.channel.sendMessage("<@${theIdiot}> I'm*").queue() { 20 | it.addReaction(Emoji.fromFormatted(":regional_indicator_l:")).queue() 21 | } 22 | 23 | event.message.delete().queue() 24 | } 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/intellijkey/KeysLeftCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.intellijkey 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Inject 8 | import gg.flyte.neptune.annotation.Optional 9 | import net.dv8tion.jda.api.Permission 10 | import net.dv8tion.jda.api.entities.User 11 | import net.dv8tion.jda.api.entities.channel.Channel 12 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 13 | 14 | class KeysLeftCommand { 15 | 16 | @Inject 17 | private lateinit var keyRegistry: IJUltimateKeyRegistry 18 | 19 | @Command( 20 | name = "keysleft", 21 | description = "View amount of IntelliJ Ultimate keys remaining", 22 | permissions = [Permission.MANAGE_ROLES] 23 | ) 24 | fun onKeysLeftCommand(event: SlashCommandInteractionEvent) { 25 | val amount = keyRegistry.keys.size 26 | event.reply("There are **$amount** IntelliJ Ultimate keys left in this batch." + if (amount < 100) " Maybe time to restock?" else "").setEphemeral(true).queue() 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/ThreadListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.util.embed 5 | import net.dv8tion.jda.api.entities.channel.ChannelType 6 | import net.dv8tion.jda.api.events.channel.ChannelCreateEvent 7 | import net.dv8tion.jda.api.hooks.ListenerAdapter 8 | 9 | class ThreadListener : ListenerAdapter() { 10 | override fun onChannelCreate(event: ChannelCreateEvent) { 11 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 12 | if (event.channel.asThreadChannel().parentChannel.id != Server.helpChannel.id) return 13 | 14 | val closeId = event.guild!!.retrieveCommands().complete() 15 | .firstOrNull { it.name == "close" } 16 | ?.id 17 | 18 | event.channel.asThreadChannel().sendMessageEmbeds( 19 | embed() 20 | .setTitle("Thank you for creating a post!") 21 | .setDescription(""" 22 | Please allow someone to read through your post and answer it! 23 | 24 | If you fixed your problem, please run ${if (closeId == null) "/close" else ""}. 25 | """.trimIndent()) 26 | .build() 27 | ).queue() 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/Server.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot 2 | 3 | import net.dv8tion.jda.api.entities.emoji.Emoji 4 | 5 | object Server { 6 | 7 | private val jda = Bot.jda 8 | 9 | val guildId = Environment.get("GUILD_ID") 10 | val guild = jda.getGuildById(Environment.get("GUILD_ID"))!! 11 | 12 | val managementRole = guild.getRoleById(Environment.get("MANAGEMENT_ROLE_ID"))!! 13 | 14 | val leaderboardChannel = guild.getTextChannelById(Environment.get("LEADERBOARD_CHANNEL_ID"))!! 15 | val verifyChannel = guild.getTextChannelById(Environment.get("VERIFY_CHANNEL_ID"))!! 16 | val managerChannel = guild.getTextChannelById(Environment.get("MANAGER_CHANNEL_ID"))!! 17 | val starboardChannel = guild.getTextChannelById(Environment.get("STARBOARD_CHANNEL_ID"))!! 18 | val helpChannel = guild.getForumChannelById(Environment.get("HELP_CHANNEL_ID"))!! 19 | val knowledgebaseChannel = guild.getForumChannelById(Environment.get("KNOWLEDGEBASE_CHANNEL_ID"))!! 20 | val countingChannel = guild.getTextChannelById(Environment.get("COUNTING_CHANNEL_ID"))!! 21 | 22 | val upvoteEmoji = Emoji.fromCustom("upvote", Environment.get("UPVOTE_EMOJI_ID").toLong(), false) 23 | val downvoteEmoji = Emoji.fromCustom("downvote", Environment.get("DOWNVOTE_EMOJI_ID").toLong(), false) 24 | val nostarboardEmoji = Emoji.fromCustom("nostarboard", Environment.get("NOSTARBOARD_EMOJI_ID").toLong(), false) 25 | val rightEmoji = Emoji.fromCustom("right", Environment.get("RIGHT_ARROW_EMOJI_ID").toLong(), false) 26 | val starEmoji = Emoji.fromUnicode("⭐") 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/lecture/LectureCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.lecture 2 | 3 | import com.learnspigot.bot.util.embed 4 | import gg.flyte.neptune.annotation.Command 5 | import gg.flyte.neptune.annotation.Description 6 | import gg.flyte.neptune.annotation.Inject 7 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 8 | import java.util.function.Consumer 9 | 10 | class LectureCommand { 11 | 12 | @Inject 13 | private lateinit var lectureRegistry: LectureRegistry 14 | 15 | @Command( 16 | name = "lecture", 17 | description = "Search for a lecture in the course" 18 | ) 19 | fun onLectureCommand( 20 | event: SlashCommandInteractionEvent, 21 | @Description("Lecture title or keywords") query: String 22 | ) { 23 | val lectures = lectureRegistry.findLectures(query, 4) 24 | val topLecture = lectures.removeFirst() 25 | val suggestions = StringBuilder() 26 | lectures.forEach(Consumer { lecture: Lecture -> 27 | suggestions.append("- [").append(lecture.title).append("](").append(lecture.url()).append(")\n") 28 | }) 29 | event.replyEmbeds( 30 | embed() 31 | .setTitle(topLecture.title, topLecture.url()) 32 | .setDescription((topLecture.description + "\n\n[Click here to watch the lecture](" + topLecture.url()) + ")") 33 | .addField("Not quite what you're looking for?", suggestions.toString(), false) 34 | .build() 35 | ).queue() 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/search/SearchHelpCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help.search 2 | 3 | import com.learnspigot.bot.util.embed 4 | import gg.flyte.neptune.annotation.Command 5 | import gg.flyte.neptune.annotation.Description 6 | import gg.flyte.neptune.annotation.Inject 7 | import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 9 | 10 | class SearchHelpCommand { 11 | 12 | @Inject 13 | private lateinit var helpPostRegistry: HelpPostRegistry 14 | 15 | @Command( 16 | name = "searchhelp", 17 | description = "Search the Help channel for a post" 18 | ) 19 | fun onSearchHelpCommand( 20 | event: SlashCommandInteractionEvent, 21 | @Description("Post title or keywords") query: String 22 | ) { 23 | val posts = helpPostRegistry.findTop4Posts(query) 24 | if (posts.size == 0) { 25 | event.reply("No post was found. This is probably an error and should not happen.").setEphemeral(true) 26 | .queue() 27 | return 28 | } 29 | 30 | val topPost = posts.removeFirst() 31 | val suggestions = StringBuilder() 32 | posts.forEach { post: ThreadChannel -> 33 | suggestions.append("- ").append(post.asMention).append("\n") 34 | } 35 | 36 | event.replyEmbeds( 37 | embed() 38 | .setTitle(topPost.name) 39 | .setDescription(topPost.asMention) 40 | .addField("Not quite what you're looking for?", suggestions.toString(), false) 41 | .build() 42 | ).queue() 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/reputation/command/AddReputationCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.reputation.command 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Inject 8 | import gg.flyte.neptune.annotation.Optional 9 | import net.dv8tion.jda.api.Permission 10 | import net.dv8tion.jda.api.entities.User 11 | import net.dv8tion.jda.api.entities.channel.Channel 12 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 13 | 14 | class AddReputationCommand { 15 | 16 | @Inject 17 | private lateinit var profileRegistry: ProfileRegistry 18 | 19 | @Command( 20 | name = "addrep", 21 | description = "Add reputation to a user", 22 | permissions = [Permission.MANAGE_ROLES] 23 | ) 24 | fun onManageRepAddCommand( 25 | event: SlashCommandInteractionEvent, 26 | @Description("User to add reputation to") user: User, 27 | @Description("User who's adding the reputation") @Optional fromUser: User?, 28 | @Description("Channel the reputation is being added from") @Optional fromChannel: Channel?, 29 | @Description("Amount of reputation the user should receive") @Optional amount: Int? 30 | ) { 31 | val profile = profileRegistry.findByUser(user) 32 | profile.addReputation(user, fromUser?.id ?: event.user.id, fromChannel?.id ?: event.channel.id, amount ?: 1) 33 | event.replyEmbeds( 34 | embed() 35 | .setTitle("Operation successful") 36 | .setDescription("Added " + (amount ?: 1) + " reputation to " + user.name + " (" + user.asMention + ")") 37 | .build() 38 | ).setEphemeral(true).queue() 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/knowledgebase/KnowledgebaseCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.knowledgebase 2 | 3 | import com.learnspigot.bot.help.CloseCommand 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Inject 8 | import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel 9 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 10 | 11 | class KnowledgebaseCommand { 12 | 13 | @Inject 14 | private lateinit var knowledgebasePostRegistry: KnowledgebasePostRegistry 15 | 16 | @Command( 17 | name = "knowledgebase", 18 | description = "Search the Knowledgebase channel for a post" 19 | ) 20 | fun onKnowledgebaseCommand( 21 | event: SlashCommandInteractionEvent, 22 | @Description("Post title or keywords") query: String 23 | ) { 24 | val posts = knowledgebasePostRegistry.findTop4Posts(query) 25 | if (posts.size == 0) { 26 | event.reply("No post was found. This is likely an error and shouldn't happen.").setEphemeral(true).queue() 27 | return 28 | } 29 | 30 | val topPost = posts.removeFirst() 31 | val suggestions = StringBuilder() 32 | posts.forEach { post: ThreadChannel -> 33 | suggestions.append("- ").append(post.asMention).append("\n") 34 | } 35 | 36 | event.replyEmbeds( 37 | embed() 38 | .setTitle(topPost.name) 39 | .setDescription(topPost.asMention) 40 | .addField("Not quite what you're looking for?", suggestions.toString(), false) 41 | .build() 42 | ).queue() 43 | 44 | CloseCommand.knowledgebasePostsUsed.getOrPut(event.channel.id) { mutableSetOf() }.add(topPost.id) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/reputation/command/RemoveReputationCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.reputation.command 2 | 3 | import com.learnspigot.bot.profile.Profile 4 | import com.learnspigot.bot.profile.ProfileRegistry 5 | import com.learnspigot.bot.util.embed 6 | import gg.flyte.neptune.annotation.Command 7 | import gg.flyte.neptune.annotation.Description 8 | import gg.flyte.neptune.annotation.Inject 9 | import gg.flyte.neptune.annotation.Optional 10 | import net.dv8tion.jda.api.Permission 11 | import net.dv8tion.jda.api.entities.User 12 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 13 | 14 | class RemoveReputationCommand { 15 | 16 | @Inject 17 | private lateinit var profileRegistry: ProfileRegistry 18 | 19 | @Command( 20 | name = "removerep", 21 | description = "Remove reputation from a user", 22 | permissions = [Permission.MANAGE_ROLES] 23 | ) 24 | fun onManageRepRemoveCommand( 25 | event: SlashCommandInteractionEvent, 26 | @Description("User to remove reputation from") user: User, 27 | @Description("ID of the reputation entry to be removed") id: Int, 28 | @Description("Ending ID range of reputation entries to be removed") @Optional ifRangeEndId: Int? 29 | ) { 30 | val profile: Profile = profileRegistry.findByUser(user) 31 | profile.removeReputation(id, ifRangeEndId ?: id) 32 | val repRemoveOutput = 33 | (if (id == ifRangeEndId) "Removed reputation with ID $id" else "Removed reputation within ID range $id - $ifRangeEndId") + " from " + user.name + " (" + user.asMention + ")" 34 | event.interaction.replyEmbeds( 35 | embed() 36 | .setTitle("Operation successful") 37 | .setDescription(repRemoveOutput) 38 | .build() 39 | ).setEphemeral(true).queue() 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/PasteCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import net.dv8tion.jda.api.entities.channel.ChannelType 7 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 8 | 9 | class PasteCommand { 10 | 11 | @Command( 12 | name = "pastebin", 13 | description = "Share the link to the custom pastebin" 14 | ) 15 | fun onPasteCommand(event: SlashCommandInteractionEvent) { 16 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) { 17 | event.reply("This can only be used in a help thread!").setEphemeral(true).queue() 18 | return 19 | } 20 | 21 | val channel = event.guildChannel.asThreadChannel() 22 | if (channel.parentChannel.id != Server.helpChannel.id) { 23 | return event.reply("This can only be used in a help thread!").setEphemeral(true).queue() 24 | } 25 | 26 | event.replyEmbeds(getNewPasteBinEmbed()).queue() 27 | } 28 | 29 | companion object { 30 | fun getNewPasteBinEmbed() = embed() 31 | .setTitle("LearnSpigot Pastebin") 32 | .setDescription("${Server.rightEmoji.asMention} https://paste.learnspigot.com/") 33 | .addField( 34 | "How do I use this?", 35 | "Copy paste your code/error directly from your IDE/console, save it and share the link from the search bar into this chat so we can help.", 36 | false 37 | ) 38 | .addField( 39 | "Important notes:", 40 | "When sharing code with an error, send the identical class without any changes. Use only one class in each pastebin.", 41 | true 42 | ) 43 | .build() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Discord Banner 2](https://discordapp.com/api/guilds/397526357191557121/widget.png?style=banner2) 2 | 3 | # LearnSpigot Discord Bot 4 | This is the Discord bot for the [LearnSpigot Discord Server](https://learnspigot.com/discord). LearnSpigot is the most sold Minecraft course in the world, and this bot aids the provision of exclusive support for all students in the server. 5 | 6 | This bot powers systems such as verification, tickets and suggestions. 7 | 8 | ## Technologies 9 | Written in Kotlin/JVM. Using Gradle (Kotlin DSL) build tool. 10 | 11 | ## Libraries 12 | - [JDA](https://github.com/DV8FromTheWorld/JDA) (Java Discord API) 13 | - [MongoDB Java Driver](https://github.com/mongodb/mongo-java-drive) (Database) 14 | - [Neptune](https://github.com/flytegg/neptune/) (Command framework) 15 | 16 | ## Contributing 17 | 18 | Contributions are always welcome. If you have no coding knowledge, please create an issue in the Issues tab so we can track it. Otherwise, please use the following steps to begin contributing to the code: 19 | 20 | 1. Fork the repository, and then clone it to your local git 21 | 2. Open the project in your IDE of choice 22 | 3. We use environment variables for sensitive data such as Mongo URI's and bot tokens, as well as constants such as channel IDs or role IDs. You will see an .env.example in the root folder. You should rename this to .env, and populate it 23 | 4. Make your changes, and please maintain a similar code style and quality 24 | 5. Create a Pull Request into the master branch of this repository 25 | 26 | We review pull requests as soon as possible. Please feel free to get in touch if it's urgent. 27 | 28 | If you are an active contributor or close to the [Flyte](https://flyte.gg) team, you may be offered access to the official LearnSpigot bot testing server where preconfigured .env files are provided with bot tokens and a database. Otherwise, all the tools are provided to work locally. 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/knowledgebase/KnowledgebaseListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.knowledgebase 2 | 3 | import com.learnspigot.bot.Environment 4 | import gg.flyte.neptune.annotation.Inject 5 | import net.dv8tion.jda.api.entities.channel.ChannelType 6 | import net.dv8tion.jda.api.events.channel.ChannelCreateEvent 7 | import net.dv8tion.jda.api.events.channel.ChannelDeleteEvent 8 | import net.dv8tion.jda.api.events.channel.update.ChannelUpdateArchivedEvent 9 | import net.dv8tion.jda.api.hooks.ListenerAdapter 10 | 11 | class KnowledgebaseListener : ListenerAdapter() { 12 | 13 | @Inject 14 | private lateinit var knowledgebasePostRegistry: KnowledgebasePostRegistry 15 | 16 | override fun onChannelCreate(e: ChannelCreateEvent) { 17 | if (e.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 18 | if (e.channel.asThreadChannel().parentChannel.id != Environment.get("KNOWLEDGEBASE_CHANNEL_ID")) return 19 | 20 | knowledgebasePostRegistry.posts[e.channel.name] = e.channel.id 21 | } 22 | 23 | override fun onChannelDelete(e: ChannelDeleteEvent) { 24 | if (e.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 25 | if (e.channel.asThreadChannel().parentChannel.id != Environment.get("KNOWLEDGEBASE_CHANNEL_ID")) return 26 | 27 | knowledgebasePostRegistry.posts.remove(e.channel.name) 28 | } 29 | 30 | override fun onChannelUpdateArchived(e: ChannelUpdateArchivedEvent) { 31 | if (e.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 32 | if (e.channel.asThreadChannel().parentChannel.id != Environment.get("KNOWLEDGEBASE_CHANNEL_ID") && e.channel.asThreadChannel().parentChannel.id != Environment.get("PROJECTS_CHANNEL_ID")) return 33 | 34 | val channel = e.channel.asThreadChannel() 35 | if (channel.isArchived){ 36 | channel.manager.setArchived(false).setLocked(false).queue() 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/MultiplierCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.embed 6 | import gg.flyte.neptune.annotation.Command 7 | import net.dv8tion.jda.api.Permission 8 | import net.dv8tion.jda.api.entities.channel.ChannelType 9 | import net.dv8tion.jda.api.entities.emoji.Emoji 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 11 | 12 | class MultiplierCommand { 13 | 14 | @Command( 15 | name = "multiplier", 16 | description = "Set a reputation multiplier", 17 | permissions = [Permission.MANAGE_ROLES] 18 | ) 19 | fun onMultiplierCommand(event: SlashCommandInteractionEvent, multiplier: Int) { 20 | if (!event.isFromGuild) return 21 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 22 | 23 | val channel = event.guildChannel.asThreadChannel() 24 | if (channel.parentChannel.id != Server.helpChannel.id) return 25 | 26 | if (multiplier !in 1..9) { 27 | event.reply("Multiplier must be 1-9.").setEphemeral(true).queue() 28 | return 29 | } 30 | 31 | event.channel.asThreadChannel().getHistoryFromBeginning(1).complete().retrievedHistory[0].apply { 32 | clearReactions().complete() 33 | addReaction(Emoji.fromUnicode("${multiplier}\u20E3")).complete() 34 | } 35 | 36 | event.replyEmbeds(embed() 37 | .setTitle("${multiplier}x reputation multiplier set") 38 | .setDescription("Everyone listed as contributor will receive $multiplier reputation once this post is closed.") 39 | .build()).queue() 40 | 41 | event.jda.getTextChannelById(Environment.get("SUPPORT_CHANNEL_ID"))!!.sendMessageEmbeds(embed() 42 | .setTitle("${multiplier}x reputation multiplier set") 43 | .setDescription(channel.asMention) 44 | .build()).queue() 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/PingCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.Server 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import net.dv8tion.jda.api.Permission 7 | import net.dv8tion.jda.api.entities.channel.ChannelType 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 9 | 10 | class PingCommand { 11 | 12 | @Command( 13 | name = "ping", 14 | description = "Remind a student who has abandoned their ticket", 15 | permissions = [Permission.MANAGE_EMOJIS_AND_STICKERS] 16 | ) 17 | fun onPingCommand(event: SlashCommandInteractionEvent) { 18 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) { 19 | event.reply("This can only be used in a help thread!").setEphemeral(true).queue() 20 | return 21 | } 22 | 23 | val channel = event.guildChannel.asThreadChannel() 24 | if (channel.parentChannel.id != Server.helpChannel.id) { 25 | event.reply("This can only be used in a help thread!").setEphemeral(true).queue() 26 | return 27 | } 28 | 29 | val closeId = event.guild!!.retrieveCommands().complete() 30 | .firstOrNull { it.name == "close" } 31 | ?.id 32 | 33 | event.replyEmbeds( 34 | embed() 35 | .setTitle("Are there any updates?") 36 | .setDescription(" ") 37 | .addField( 38 | "I have new code/error", 39 | "Paste it @ https://paste.learnspigot.com and send it so we can help.", 40 | false 41 | ) 42 | .addField("I figured it out", "Great job! Run ${if (closeId == null) "/close" else ""} and select contributors.", false) 43 | .build()) 44 | .setContent(channel.owner!!.asMention + " - You haven't responded in a while!") 45 | .queue() 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/embed/EmbedCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.embed 2 | 3 | import com.learnspigot.bot.Bot 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Optional 8 | import net.dv8tion.jda.api.Permission 9 | import net.dv8tion.jda.api.entities.channel.Channel 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 11 | 12 | class EmbedCommand { 13 | @Command( 14 | name = "embed", 15 | description = "Create an embed message", 16 | permissions = [Permission.MANAGE_PERMISSIONS] 17 | ) 18 | fun onEmbedCommand( 19 | event: SlashCommandInteractionEvent, 20 | @Description("Channel to send embed") channel: Channel, 21 | @Description("Embed title") title: String, 22 | @Description("Embed description") description: String, 23 | @Description("Embed footer") @Optional footer: String?, 24 | @Description("Embed thumbnail") @Optional thumbnail: String?, 25 | @Description("Embed image") @Optional image: String?, 26 | @Description("Embed author") @Optional author: String?, 27 | @Description("Embed color") @Optional color: Int?, 28 | ) { 29 | event.replyEmbeds( 30 | embed() 31 | .setTitle("Successfully created embed") 32 | .setDescription("The embed has been sent in ${channel.asMention}.") 33 | .build() 34 | ).setEphemeral(true).queue() 35 | 36 | Bot.jda.getTextChannelById(channel.id)?.sendMessageEmbeds( 37 | embed() 38 | .setTitle(title) 39 | .setDescription(description.replace("\\n", "\n")) 40 | .setFooter(footer) 41 | .setThumbnail(thumbnail) 42 | .setImage(image) 43 | .setAuthor(author) 44 | .setColor(color ?: 0x2B2D31) 45 | .build() 46 | )?.queue() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/counting/CountingRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.counting 2 | 3 | import com.learnspigot.bot.Bot 4 | import com.learnspigot.bot.profile.Profile 5 | import com.learnspigot.bot.util.Mongo 6 | import com.mongodb.client.model.Filters 7 | import net.dv8tion.jda.api.entities.User 8 | import org.bson.Document 9 | 10 | class CountingRegistry(val bot: Bot) { 11 | private inline val profileRegistry get() = bot.profileRegistry() 12 | private val mongoCollection = Mongo.countingCollection 13 | 14 | var topServerCount: Int = 0 15 | var serverTotalCounts: Int = 0 16 | var currentCount = 0 17 | 18 | fun getTop5(): List = profileRegistry.profileCache.values 19 | .filter { it.totalCounts > 0 } 20 | .sortedByDescending { it.totalCounts } 21 | .take(5) 22 | 23 | init { 24 | val document = mongoCollection.find().first() 25 | if (document == null) { 26 | val newDoc = Document() 27 | newDoc["highestCount"] = 0 28 | newDoc["currentCount"] = 0 29 | newDoc["serverTotalCounts"] = 0 30 | mongoCollection.insertOne(newDoc) 31 | } else { 32 | topServerCount = document.getInteger("highestCount", 0) 33 | currentCount = document.getInteger("currentCount", 0) 34 | serverTotalCounts = document.getInteger("serverTotalCounts", 0) 35 | } 36 | } 37 | 38 | fun incrementCount(user: User) { 39 | currentCount++ 40 | serverTotalCounts++ 41 | if (currentCount > topServerCount) topServerCount = currentCount 42 | profileRegistry.findByUser(user).incrementCount(currentCount) 43 | val newDoc = mongoCollection.find().first()!! 44 | newDoc["highestCount"] = topServerCount 45 | newDoc["currentCount"] = currentCount 46 | newDoc["serverTotalCounts"] = serverTotalCounts 47 | mongoCollection.replaceOne(Filters.eq("_id", newDoc.getObjectId("_id")), newDoc) 48 | } 49 | 50 | fun fuckedUp(user: User) { 51 | currentCount = 0 52 | profileRegistry.findByUser(user).fuckedUpCounting() 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/reputation/command/ReputationCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.reputation.command 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.reputation.Reputation 5 | import com.learnspigot.bot.util.embed 6 | import gg.flyte.neptune.annotation.Command 7 | import gg.flyte.neptune.annotation.Description 8 | import gg.flyte.neptune.annotation.Inject 9 | import gg.flyte.neptune.annotation.Optional 10 | import net.dv8tion.jda.api.entities.User 11 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 12 | 13 | class ReputationCommand { 14 | 15 | @Inject 16 | private lateinit var profileRegistry: ProfileRegistry 17 | 18 | @Command(name = "rep", description = "View a user's reputation") 19 | fun onReputationCommand( 20 | event: SlashCommandInteractionEvent, 21 | @Description("User to see reputation") @Optional user: User? 22 | ) { 23 | val finalUser = user ?: event.user 24 | val profile = profileRegistry.findByUser(finalUser) 25 | val reputation = StringBuilder() 26 | val repMap: Map = profile.reputation.descendingMap() 27 | val i = intArrayOf(0) 28 | repMap.forEach { (id: Int?, rep: Reputation) -> 29 | if (i[0] == 5) return@forEach 30 | reputation.append("- ") 31 | if (rep.fromMemberId != null) reputation.append("From <@").append(rep.fromMemberId).append(">, on ") else reputation.append("On ") 34 | if (rep.fromPostId != null) reputation.append(" in <#").append(rep.fromPostId).append(">") 35 | reputation.append(" (").append(id).append(")\n") 36 | i[0]++ 37 | } 38 | event.replyEmbeds( 39 | embed() 40 | .setTitle(finalUser.name + "'s reputation") 41 | .setDescription("${profile.reputation.size} reputation points") 42 | .addField( 43 | "Last 5 reputation", 44 | if (reputation.isEmpty()) "No reputation" else reputation.toString(), 45 | false 46 | ) 47 | .build() 48 | ).setEphemeral(true).queue() 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/voicechat/VCCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.voicechat 2 | 3 | import com.learnspigot.bot.Environment 4 | import gg.flyte.neptune.annotation.Command 5 | import gg.flyte.neptune.annotation.Description 6 | import gg.flyte.neptune.annotation.Optional 7 | import net.dv8tion.jda.api.Permission 8 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 9 | import net.dv8tion.jda.api.exceptions.ContextException 10 | import java.util.concurrent.Executors 11 | import java.util.concurrent.ScheduledExecutorService 12 | import java.util.concurrent.TimeUnit 13 | 14 | class VCCommand { 15 | 16 | private val scheduledExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor() 17 | 18 | @Command( 19 | name = "createvoice", 20 | description = "Create a temporary voice channel!", 21 | permissions = [Permission.CREATE_PUBLIC_THREADS] 22 | ) 23 | fun onCreateVoiceCommand( 24 | event: SlashCommandInteractionEvent, 25 | @Description("Max user limit") @Optional limit: Int?, 26 | ) { 27 | val guild = event.guild ?: return 28 | val member = event.member ?: return 29 | 30 | if (guild.getVoiceChannelsByName("${member.effectiveName}'s channel", true).isNotEmpty()) { 31 | event.reply("You already have a voice channel!").setEphemeral(true).queue() 32 | return 33 | } 34 | 35 | if (limit != null && limit < 1) { 36 | event.reply("The max user limit must be 1 or higher.").setEphemeral(true).queue() 37 | return 38 | } 39 | 40 | val newChannel = guild.createVoiceChannel( 41 | "${member.effectiveName}'s channel", 42 | guild.getCategoryById(Environment.get("CHAT_CATEGORY")) 43 | ).complete() 44 | 45 | if (limit != null) { 46 | newChannel.manager.setUserLimit(limit).queue() 47 | } 48 | 49 | if (member.voiceState?.inAudioChannel() == true) 50 | guild.moveVoiceMember(member, newChannel).queue() 51 | 52 | event.reply("Your voice channel has been created - ${newChannel.asMention}").setEphemeral(true).queue() 53 | 54 | scheduledExecutor.schedule({ 55 | try { 56 | if (newChannel.members.isEmpty()) newChannel.delete().queue() 57 | } catch (_: ContextException) {} 58 | }, 5, TimeUnit.MINUTES) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/counting/CountingCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.counting 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Inject 8 | import gg.flyte.neptune.annotation.Optional 9 | import net.dv8tion.jda.api.entities.User 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 11 | 12 | class CountingCommand { 13 | 14 | @Inject private lateinit var profileRegistry: ProfileRegistry 15 | @Inject private lateinit var countingRegistry: CountingRegistry 16 | 17 | @Command(name = "countingstats", description = "View counting statistics") 18 | fun onCountingCommand( 19 | event: SlashCommandInteractionEvent, 20 | @Description("User's stats to view") @Optional user: User? 21 | ) { 22 | if (user == null) { // Server Stats 23 | event.replyEmbeds( 24 | embed() 25 | .setTitle("Server counting statistics") 26 | .setDescription(""" 27 | - Last Count: ${countingRegistry.currentCount} 28 | - Total Counts: ${countingRegistry.serverTotalCounts} 29 | - Highest Count: ${countingRegistry.topServerCount} 30 | """.trimIndent()) 31 | .addField( 32 | "Top 5 counters", 33 | countingRegistry.getTop5().joinToString("") { profile -> 34 | "\n- <@${profile.id}>: ${profile.totalCounts}" 35 | }, 36 | false 37 | ) 38 | .build() 39 | ).setEphemeral(true).queue() 40 | } else { // Individual Stats 41 | val profile = profileRegistry.findByUser(user) 42 | event.replyEmbeds( 43 | embed() 44 | .setTitle(user.name + "'s counting statistics") 45 | .setDescription(""" 46 | - Total Counts: ${profile.totalCounts} 47 | - Highest Count: ${profile.highestCount} 48 | - Mistakes: ${profile.countingFuckUps} 49 | """.trimIndent()) 50 | .build() 51 | ).setEphemeral(true).queue() 52 | } 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/verification/VerificationMessage.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.verification 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.embed 6 | import net.dv8tion.jda.api.entities.Guild 7 | import net.dv8tion.jda.api.entities.MessageHistory 8 | import net.dv8tion.jda.api.interactions.components.buttons.Button 9 | 10 | class VerificationMessage(guild: Guild) { 11 | 12 | init { 13 | val history = MessageHistory.getHistoryFromBeginning(Server.verifyChannel).complete().retrievedHistory 14 | if (history.isEmpty()) 15 | Server.verifyChannel.sendMessageEmbeds( 16 | embed() 17 | .setTitle("VERIFY YOU OWN THE COURSE") 18 | .setDescription( 19 | """ 20 | Welcome to the Discord for the LearnSpigot course! 21 | 22 | :disappointed: **Don't own the course? See """.trimIndent() + guild.getTextChannelById( 23 | Environment.get("GET_COURSE_CHANNEL_ID") 24 | )!!.asMention + """ 25 | ** 26 | 27 | The URL you need to use is the link to your public profile, to get this: 28 | :one: Hover over your profile picture in the top right on Udemy 29 | :two: Select "Public profile" from the dropdown menu 30 | :three: Copy the link from your browser 31 | 32 | Please make sure that you have [privacy settings](https://www.udemy.com/instructor/profile/privacy/) enabled so that we can verify you own the course. 33 | 34 | **On Udemy Personal Plan or Udemy For Business?** When verifying, indicate this by typing "Yes" in the provided field. (If you purchased the course directly and don't know what these are, simply answer "No")""".trimIndent() 35 | ) 36 | .setFooter("Once you've verified, you'll have access to our 50-person support team, hundreds of additional tutorials, and a supportive community.") 37 | .build() 38 | ) 39 | .addActionRow(Button.success("verify", "Click to Verify")) 40 | .queue() 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/profile/ProfileCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.profile 2 | 3 | import com.learnspigot.bot.Bot 4 | import com.learnspigot.bot.util.embed 5 | import gg.flyte.neptune.annotation.Command 6 | import gg.flyte.neptune.annotation.Description 7 | import gg.flyte.neptune.annotation.Inject 8 | import gg.flyte.neptune.annotation.Optional 9 | import net.dv8tion.jda.api.EmbedBuilder 10 | import net.dv8tion.jda.api.Permission 11 | import net.dv8tion.jda.api.entities.MessageEmbed 12 | import net.dv8tion.jda.api.entities.User 13 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 14 | 15 | class ProfileCommand { 16 | 17 | @Inject 18 | private lateinit var profileRegistry: ProfileRegistry 19 | 20 | @Command( 21 | name = "profile", 22 | description = "View a user's profile", 23 | permissions = [Permission.MANAGE_ROLES] 24 | ) 25 | fun onProfileCommand( 26 | event: SlashCommandInteractionEvent, 27 | @Description("User to show profile") @Optional user: User?, 28 | @Description("URL to show profile") @Optional url: String? 29 | ) { 30 | val profileByURL = profileRegistry.findByURL(url ?: "") 31 | 32 | val embed: MessageEmbed = when { 33 | user == null && url == null -> userProfileEmbed(event.user) 34 | user != null && url != null -> createProfileLookupEmbed("Please choose", "Please make a " + 35 | "choice whether you want to use a user or use a URL.") 36 | profileByURL != null -> userProfileEmbed(Bot.jda.getUserById(profileByURL.id)!!) 37 | user != null -> userProfileEmbed(user) 38 | else -> createProfileLookupEmbed("Something went wrong", "Something went wrong while " + 39 | "trying to find a profile matching your query. Please contact a manager (or higher) to look at " + 40 | "this issue.") 41 | } 42 | 43 | event.replyEmbeds(embed).setEphemeral(true).queue() 44 | } 45 | 46 | private fun userProfileEmbed( 47 | user: User 48 | ): MessageEmbed { 49 | val profile = profileRegistry.findByUser(user) 50 | 51 | return embed() 52 | .setTitle("Profile Lookup") 53 | .addField("Discord", user.name + " (" + user.asMention + ")", false) 54 | .addField("Udemy", profile.udemyProfileUrl ?: "Not linked", false) 55 | .addField("Reputation", profile.reputation.size.toString(), true) 56 | .addField("(Notifications)", profile.notifyOnRep.toString(), true) 57 | .setThumbnail(user.effectiveAvatarUrl) 58 | .build() 59 | } 60 | 61 | private fun createProfileLookupEmbed( 62 | title: String, 63 | description: String) 64 | : MessageEmbed = embed() 65 | .setTitle("Profile Lookup") 66 | .addField(title, description, false) 67 | .build() 68 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/profile/ProfileRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.profile 2 | 3 | import com.learnspigot.bot.reputation.Reputation 4 | import com.learnspigot.bot.util.Mongo 5 | import net.dv8tion.jda.api.entities.Message 6 | import net.dv8tion.jda.api.entities.User 7 | import org.bson.Document 8 | import java.util.* 9 | 10 | class ProfileRegistry { 11 | 12 | val profileCache: MutableMap = TreeMap(String.CASE_INSENSITIVE_ORDER) 13 | private val urlProfiles: MutableMap = TreeMap() 14 | 15 | val contributorSelectorCache: MutableMap> = HashMap() 16 | val messagesToRemove: MutableMap = HashMap() 17 | 18 | init { 19 | Mongo.userCollection.find().forEach { document -> 20 | val reputation: NavigableMap = TreeMap() 21 | document.get("reputation", Document::class.java).forEach { id, rep -> 22 | val repDocument = rep as Document 23 | reputation[id.toInt()] = Reputation( 24 | convertToLongTimestamp(repDocument["timestamp"]!!), 25 | repDocument.getString("fromMemberId"), 26 | repDocument.getString("fromPostId")) 27 | } 28 | 29 | Profile( 30 | document.getString("_id"), 31 | document.getString("tag"), 32 | document.getString("udemyProfileUrl"), 33 | reputation, 34 | document.getBoolean("notifyOnRep", true), 35 | document.getBoolean("intellijKeyGiven", false), 36 | document.getInteger("highestCount", 0), 37 | document.getInteger("totalCounts", 0), 38 | document.getInteger("countingFuckUps", 0) 39 | ).let { 40 | profileCache[it.id] = it 41 | if (it.udemyProfileUrl != null) 42 | urlProfiles[it.udemyProfileUrl!!] = it 43 | } 44 | } 45 | } 46 | 47 | // I don't even care enough to sort this bug so have this function instead 48 | // Basically at some point they've been saving as Ints and some points Longs. So now we must read both. .-. 49 | private fun convertToLongTimestamp(timestamp: Any): Long { 50 | return when (timestamp) { 51 | is Int -> timestamp.toLong() 52 | is Long -> timestamp.toLong() 53 | is String -> timestamp.toLongOrNull() ?: throw IllegalArgumentException("Invalid timestamp format") 54 | else -> throw IllegalArgumentException("Unsupported timestamp format") 55 | } 56 | } 57 | 58 | fun findById(id: String): Profile? { 59 | return profileCache[id] 60 | } 61 | 62 | fun findByUser(user: User): Profile { 63 | return findById(user.id) ?: run { 64 | Profile( 65 | user.id, 66 | user.name, 67 | null, 68 | TreeMap(), 69 | true, 70 | false, 71 | 0, 72 | 0, 73 | 0, 74 | ).apply { 75 | profileCache[user.id] = this 76 | save() 77 | } 78 | } 79 | } 80 | 81 | fun findByURL(udemyURL: String): Profile? { 82 | return urlProfiles[udemyURL] 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/profile/Profile.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.profile 2 | 3 | import com.learnspigot.bot.reputation.Reputation 4 | import com.learnspigot.bot.util.Mongo 5 | import com.learnspigot.bot.util.embed 6 | import com.mongodb.client.model.Filters 7 | import com.mongodb.client.model.ReplaceOptions 8 | import net.dv8tion.jda.api.entities.User 9 | import net.dv8tion.jda.api.exceptions.ErrorHandler 10 | import net.dv8tion.jda.api.requests.ErrorResponse 11 | import org.bson.Document 12 | import java.time.Instant 13 | import java.util.* 14 | 15 | data class Profile( 16 | val id: String, 17 | val tag: String?, 18 | var udemyProfileUrl: String?, 19 | val reputation: NavigableMap, 20 | val notifyOnRep: Boolean, 21 | var intellijKeyGiven: Boolean, 22 | var highestCount: Int, 23 | var totalCounts: Int, 24 | var countingFuckUps: Int 25 | ) { 26 | 27 | fun addReputation(user: User, fromUserId: String, fromPostId: String, amount: Int) { 28 | for (i in 0 until amount) 29 | reputation[if (reputation.isEmpty()) 0 else reputation.lastKey() + 1] = 30 | Reputation(Instant.now().epochSecond, fromUserId, fromPostId) 31 | 32 | save() 33 | 34 | user.openPrivateChannel().complete().let { 35 | it.sendMessageEmbeds( 36 | embed() 37 | .setAuthor("You have ${reputation.size} reputation in total") 38 | .setTitle("You earned ${if (amount == 1) "" else "$amount "}reputation") 39 | .setDescription("You gained reputation from <@$fromUserId> in <#$fromPostId>.") 40 | .build() 41 | ).queue(null, ErrorHandler().handle(ErrorResponse.CANNOT_SEND_TO_USER) {}) 42 | } 43 | } 44 | 45 | fun removeReputation(startId: Int, endId: Int) { 46 | for (i in startId..endId) { 47 | reputation.remove(i) 48 | } 49 | save() 50 | } 51 | 52 | fun save() { 53 | val document = Document() 54 | document["_id"] = id 55 | document["tag"] = tag 56 | document["udemyProfileUrl"] = udemyProfileUrl 57 | val reputationDocument = Document() 58 | reputation.forEach { (id, rep) -> 59 | reputationDocument[id.toString()] = rep.document() 60 | } 61 | document["reputation"] = reputationDocument 62 | document["notifyOnRep"] = notifyOnRep 63 | document["intellijKeyGiven"] = intellijKeyGiven 64 | document["highestCount"] = highestCount 65 | document["totalCounts"] = totalCounts 66 | document["countingFuckUps"] = countingFuckUps 67 | Mongo.userCollection.replaceOne(Filters.eq("_id", id), document, ReplaceOptions().upsert(true)) 68 | } 69 | 70 | fun incrementCount(currentCount: Int) { 71 | totalCounts++ 72 | if (currentCount > highestCount) highestCount = currentCount 73 | saveCounting() 74 | } 75 | 76 | fun fuckedUpCounting() { 77 | countingFuckUps++ 78 | saveCounting() 79 | } 80 | 81 | private fun saveCounting() { 82 | val doc = Mongo.userCollection.find(Filters.eq("_id", id)).first()!! 83 | doc["highestCount"] = highestCount 84 | doc["totalCounts"] = totalCounts 85 | doc["countingFuckUps"] = countingFuckUps 86 | Mongo.userCollection.replaceOne(Filters.eq("_id", id), doc, ReplaceOptions().upsert(true)) 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/starboard/StarboardListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.starboard 2 | 3 | import com.learnspigot.bot.Server 4 | import gg.flyte.neptune.annotation.Inject 5 | import net.dv8tion.jda.api.entities.Message 6 | import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion 7 | import net.dv8tion.jda.api.events.message.MessageBulkDeleteEvent 8 | import net.dv8tion.jda.api.events.message.MessageDeleteEvent 9 | import net.dv8tion.jda.api.events.message.MessageUpdateEvent 10 | import net.dv8tion.jda.api.events.message.react.* 11 | import net.dv8tion.jda.api.hooks.ListenerAdapter 12 | 13 | class StarboardListener : ListenerAdapter() { 14 | @Inject private lateinit var starboardRegistry: StarboardRegistry 15 | 16 | private fun getMessage(messageId: String, channel: MessageChannelUnion): Message { 17 | return channel.retrieveMessageById(messageId).complete() 18 | } 19 | 20 | override fun onMessageReactionRemoveEmoji(event: MessageReactionRemoveEmojiEvent) { 21 | if (!event.isFromGuild) return 22 | if (event.channel == Server.starboardChannel) return 23 | 24 | when (event.emoji) { 25 | Server.starEmoji -> starboardRegistry.updateStarboard(getMessage(event.messageId, event.channel), 0) 26 | Server.nostarboardEmoji -> starboardRegistry.updateNoStarboard(getMessage(event.messageId, event.channel)) 27 | } 28 | } 29 | 30 | override fun onMessageReactionAdd(event: MessageReactionAddEvent) { 31 | if (!event.isFromGuild) return 32 | if (event.channel == Server.starboardChannel) return 33 | 34 | when (event.emoji) { 35 | Server.starEmoji -> starboardRegistry.updateStarboard(getMessage(event.messageId, event.channel)) 36 | Server.nostarboardEmoji -> starboardRegistry.updateNoStarboard(getMessage(event.messageId, event.channel)) 37 | } 38 | } 39 | 40 | override fun onMessageReactionRemove(event: MessageReactionRemoveEvent) { 41 | if (!event.isFromGuild) return 42 | if (event.channel == Server.starboardChannel) return 43 | 44 | when (event.emoji) { 45 | Server.starEmoji -> starboardRegistry.updateStarboard(getMessage(event.messageId, event.channel)) 46 | Server.nostarboardEmoji -> starboardRegistry.updateNoStarboard(getMessage(event.messageId, event.channel)) 47 | } 48 | } 49 | 50 | override fun onMessageReactionRemoveAll(event: MessageReactionRemoveAllEvent) { 51 | if (!event.isFromGuild) return 52 | if (event.channel == Server.starboardChannel) return 53 | 54 | starboardRegistry.updateStarboard(getMessage(event.messageId, event.channel), 0) 55 | } 56 | 57 | override fun onMessageDelete(event: MessageDeleteEvent) { 58 | if (event.channel == Server.starboardChannel) return 59 | 60 | starboardRegistry.removeStarboardEntry(event.messageId) 61 | } 62 | 63 | override fun onMessageBulkDelete(event: MessageBulkDeleteEvent) { 64 | if (event.channel == Server.starboardChannel) return 65 | 66 | event.messageIds.forEach(starboardRegistry::removeStarboardEntry) 67 | } 68 | 69 | override fun onMessageUpdate(event: MessageUpdateEvent) { 70 | if (!event.isFromGuild) return 71 | if (event.channel == Server.starboardChannel) return 72 | 73 | runCatching { 74 | starboardRegistry.updateStarboard(getMessage(event.messageId, event.channel), true) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/profile/ProfileListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.profile 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.Mongo 6 | import com.learnspigot.bot.util.embed 7 | import com.mongodb.client.model.Filters 8 | import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent 9 | import net.dv8tion.jda.api.exceptions.ErrorHandler 10 | import net.dv8tion.jda.api.hooks.ListenerAdapter 11 | import net.dv8tion.jda.api.requests.ErrorResponse 12 | 13 | class ProfileListener : ListenerAdapter() { 14 | 15 | override fun onGuildMemberJoin(e: GuildMemberJoinEvent) { 16 | if (e.guild.id != Server.guildId) return 17 | 18 | val document = Mongo.userCollection.find(Filters.eq("_id", e.user.id)).first() 19 | if (document != null && document.containsKey("udemyProfileUrl")) { 20 | e.user.openPrivateChannel().complete().let { 21 | it.sendMessageEmbeds( 22 | embed() 23 | .setTitle("Welcome to the Discord! :tada:") 24 | .setDescription( 25 | """ 26 | You have already verified previously so your Student role has been restored. 27 | 28 | *PS: Use [our pastebin](https://paste.learnspigot.com) - pastes never expire!* 29 | 30 | """.trimIndent() 31 | ) 32 | .build() 33 | ).queue(null, ErrorHandler().handle(ErrorResponse.CANNOT_SEND_TO_USER) {}) 34 | } 35 | 36 | e.guild.addRoleToMember(e.user, e.guild.getRoleById(Environment.get("STUDENT_ROLE_ID"))!!).queue() 37 | } else { 38 | e.user.openPrivateChannel().complete().let { 39 | it.sendMessageEmbeds( 40 | embed() 41 | .setTitle("Welcome to the Discord! :tada:") 42 | .setDescription( 43 | """ 44 | You have joined the exclusive support community for the [Develop Minecraft Plugins (Java)](https://learnspigot.com) Udemy course. 45 | 46 | :question: Don't have the course? Grab it at 47 | 48 | :thinking: Not convinced? Take a look at what everyone else has to say at 49 | 50 | :star: Have it? Follow the instructions in """.trimIndent() + e.guild.getTextChannelById(Environment.get("VERIFY_CHANNEL_ID"))!!.asMention + """ 51 | 52 | 53 | *PS: Use [our pastebin](https://paste.learnspigot.com) - pastes never expire!* 54 | 55 | """.trimIndent() 56 | ) 57 | .setFooter("Without verifying, you can still read the server but won't have access to our 24/7 support team and dozens of tutorials and projects.") 58 | .build() 59 | ).queue(null, ErrorHandler().handle(ErrorResponse.CANNOT_SEND_TO_USER) {}) 60 | } 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/lecture/WordMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.lecture 2 | 3 | class WordMatcher { 4 | fun getTopLectures(query: String, source: List, amount: Int = 1): List { 5 | val top = getTopMatches(query, source.map { it.title }, amount) 6 | val map = source.associateBy { it.title } 7 | return top.map { map[it]!! } 8 | } 9 | 10 | fun getTopMatches(word: String, source: List, amount: Int = 1) = getSorted(word, source).take(amount) 11 | 12 | fun getBestMatch(word: String, source: List): String { 13 | val matcher = WordMatcher() 14 | return source.maxBy { matcher.getMatchScore(word, it) } 15 | } 16 | 17 | private fun getSorted(word: String, source: List): List { 18 | val matcher = WordMatcher() 19 | return source.sortedByDescending { matcher.getMatchScore(word, it) } 20 | } 21 | 22 | /** 23 | * Generate a score between 0 and 4.5 determining how similar "query" is to "check". 24 | * @param query the word being scored 25 | * @param check the word being scored against 26 | */ 27 | fun getMatchScore(query: String, check: String): Double { 28 | var test = query.lowercase() 29 | var check = check.lowercase() 30 | val wordMatchPercentage = test.split(" ").let { words -> words.count { check.contains(it) } / words.size.toDouble() } 31 | val FULL_WORD_BIAS = 3 32 | val LENGTH_BIAS = 0.25 33 | 34 | return average( 35 | FULL_WORD_BIAS*wordMatchPercentage, // Increase score significantly if the title contains fully any words from the query. 36 | getAdjustedCharacterSimilarity(test, check), // Checks spelling mistakes, using an unordered character similarity. 37 | LENGTH_BIAS * getAmbiguousPortion(test.length, check.length) // Slightly pioritise words with similar length. 38 | ) 39 | } 40 | 41 | /** 42 | * Compares two strings by comparing the words within them. 43 | * This will ensure that matching the query 'Scoreboard' to 'Custom Scoreboards' does not yield undesired results 44 | */ 45 | private fun getAdjustedCharacterSimilarity(query: String, title: String): Double { 46 | var total = 0.0 47 | val titleWords = title.split(" ") 48 | val queryWords = query.split(" ") 49 | // For each word in the query, add the similarity of the most similar word in the title. 50 | for (word in queryWords) total += titleWords.maxOf { getRawCharacterSimilarity(word, it) } 51 | return total/queryWords.size 52 | } 53 | 54 | /** 55 | * Uses a raw character similarity to compare two words 56 | */ 57 | private fun getRawCharacterSimilarity(word1: String, word2: String): Double { 58 | val uniqueChars: HashSet = HashSet() 59 | val chars1 = word1.toCharArray() 60 | val chars2 = word2.toCharArray() 61 | uniqueChars.addAll(chars1.distinct()) 62 | uniqueChars.addAll(chars2.distinct()) 63 | 64 | var total = 0.0 65 | for (character in uniqueChars){ // Add the proportion of each distinct character in t 66 | total += getAmbiguousPortion(chars1.count { it == character }, chars2.count { it == character }) 67 | } 68 | return total/uniqueChars.size 69 | } 70 | 71 | private fun average(vararg vals: Number): Double { 72 | var total = 0.0 73 | vals.forEach { total += it.toDouble() } 74 | return total/vals.size 75 | } 76 | 77 | private fun getAmbiguousPortion(amount1: Number, amount2: Number) = minOf(amount1.toDouble(), amount2.toDouble()) / maxOf(amount1.toDouble(), amount2.toDouble()) 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/vote/VoteListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.vote 2 | 3 | import com.google.common.cache.CacheBuilder 4 | import com.learnspigot.bot.Environment 5 | import com.learnspigot.bot.Server 6 | import net.dv8tion.jda.api.entities.emoji.Emoji 7 | import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent 8 | import net.dv8tion.jda.api.hooks.ListenerAdapter 9 | import java.util.concurrent.TimeUnit 10 | 11 | class VoteListener : ListenerAdapter() { 12 | 13 | val cooldown = CacheBuilder.newBuilder() 14 | .expireAfterWrite(15, TimeUnit.MINUTES) 15 | .build() 16 | 17 | override fun onMessageContextInteraction(event: MessageContextInteractionEvent) { 18 | if (event.channel!!.id == Environment.get("NEWS_CHANNEL_ID")) { 19 | event.reply("You cannot use this in the News channel.").setEphemeral(true).queue() 20 | return 21 | } 22 | 23 | println(event.member!!.effectiveName + " added vote") 24 | 25 | when (event.name) { 26 | "Set vote" -> event.run { 27 | if (event.channel!!.id == Server.countingChannel.id) // Stop fake counting bullshit 28 | return event.reply("You cannot use that in this channel.").setEphemeral(true).queue() 29 | 30 | val member = event.member!! 31 | val roles = member.roles 32 | if (cooldown.asMap().containsKey(member.id) && 33 | !roles.contains(event.jda.getRoleById(Environment.get("MANAGEMENT_ROLE_ID"))) && 34 | !roles.contains(event.jda.getRoleById(Environment.get("STAFF_ROLE_ID")))) { 35 | event.reply("You are on cooldown! Please wait.").setEphemeral(true).queue() 36 | return 37 | } 38 | 39 | target.apply { 40 | addReaction(Server.upvoteEmoji).queue() 41 | addReaction(Server.downvoteEmoji).queue() 42 | } 43 | reply("Vote reactions were added!").setEphemeral(true).queue() 44 | 45 | cooldown.put(member.id, "Dummy") 46 | } 47 | "Set Tutorial vote" -> event.run { 48 | target.apply { 49 | addReaction(Emoji.fromUnicode("1\uFE0F⃣")).queue() 50 | addReaction(Emoji.fromUnicode("2\uFE0F⃣")).queue() 51 | addReaction(Emoji.fromUnicode("3\uFE0F⃣")).queue() 52 | addReaction(Emoji.fromUnicode("4\uFE0F⃣")).queue() 53 | addReaction(Emoji.fromUnicode("5\uFE0F⃣")).queue() 54 | } 55 | reply("Vote reactions were added!").setEphemeral(true).queue() 56 | } 57 | "Set Project vote" -> event.run { 58 | target.apply { 59 | addReaction(Emoji.fromUnicode("1\uFE0F⃣")).queue() 60 | addReaction(Emoji.fromUnicode("2\uFE0F⃣")).queue() 61 | addReaction(Emoji.fromUnicode("3\uFE0F⃣")).queue() 62 | addReaction(Emoji.fromUnicode("4\uFE0F⃣")).queue() 63 | addReaction(Emoji.fromUnicode("5\uFE0F⃣")).queue() 64 | addReaction(Emoji.fromUnicode("6\uFE0F⃣")).queue() 65 | addReaction(Emoji.fromUnicode("7\uFE0F⃣")).queue() 66 | addReaction(Emoji.fromUnicode("8\uFE0F⃣")).queue() 67 | addReaction(Emoji.fromUnicode("9\uFE0F⃣")).queue() 68 | addReaction(Emoji.fromUnicode("\uD83D\uDD1F")).queue() 69 | } 70 | reply("Vote reactions were added!").setEphemeral(true).queue() 71 | } 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/CloseListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.embed 6 | import gg.flyte.neptune.annotation.Inject 7 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent 8 | import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent 9 | import net.dv8tion.jda.api.hooks.ListenerAdapter 10 | 11 | class CloseListener : ListenerAdapter() { 12 | 13 | @Inject 14 | private lateinit var profileRegistry: ProfileRegistry 15 | 16 | override fun onStringSelectInteraction(event: StringSelectInteractionEvent) { 17 | if (event.componentId != event.channel.id + "-contributor-selector") return 18 | val channel = event.channel.asThreadChannel() 19 | 20 | if (event.member!!.id != channel.ownerId && !event.member!!.roles.contains(Server.managementRole)) { 21 | event.reply("You cannot close this thread!").setEphemeral(true).queue() 22 | return 23 | } 24 | 25 | event.interaction.deferEdit().queue() 26 | 27 | profileRegistry.contributorSelectorCache[event.channel.id] = event.values 28 | } 29 | 30 | override fun onButtonInteraction(event: ButtonInteractionEvent) { 31 | if (!event.componentId.endsWith("-close-button")) return 32 | val channel = event.channel.asThreadChannel() 33 | 34 | if (event.member!!.id != channel.ownerId && !event.member!!.roles.contains(Server.managementRole)) { 35 | event.reply("You cannot close this thread!").setEphemeral(true).queue() 36 | return 37 | } 38 | 39 | event.editButton(event.button.asDisabled()).complete() 40 | 41 | val contributors = profileRegistry.contributorSelectorCache[event.channel.id] ?: mutableListOf() 42 | 43 | var reputation = 1 44 | channel.getHistoryFromBeginning(1).complete().retrievedHistory[0].reactions.forEach { 45 | if (it.isSelf && it.emoji.asUnicode().name.toCharArray()[0].isDigit()) { 46 | reputation = it.emoji.asUnicode().name.toCharArray()[0].toInt() - '0'.toInt() 47 | } 48 | return@forEach 49 | } 50 | 51 | contributors.forEach { contributor -> 52 | if (contributor.startsWith("knowledgebase:")) { 53 | val post = Server.guild.getThreadChannelById(contributor.removePrefix("knowledgebase:")) 54 | post?.owner?.user?.let { user -> 55 | profileRegistry.findByUser(user).addReputation(user, channel.ownerId, channel.id, reputation) 56 | } 57 | } else { 58 | val user = event.guild!!.retrieveMemberById(contributor).complete().user 59 | profileRegistry.findByUser(user).addReputation(user, channel.ownerId, channel.id, reputation) 60 | } 61 | } 62 | 63 | profileRegistry.messagesToRemove[channel.id]?.delete()?.queue() 64 | CloseCommand.knowledgebasePostsUsed.remove(channel.id) 65 | 66 | event.channel.asThreadChannel().getHistoryFromBeginning(2).complete().retrievedHistory[0].delete().complete() 67 | 68 | event.channel.sendMessageEmbeds(embed() 69 | .setTitle(event.member!!.effectiveName + " has closed the thread") 70 | .setDescription("Listing ${if (contributors.isEmpty()) "no contributors." else contributors.joinToString(", ") { 71 | if (it.startsWith("knowledgebase:")) "<#${it.removePrefix("knowledgebase:")}>" else "<@$it>" 72 | } + " as contributors."}") 73 | .build()).complete() 74 | 75 | channel.manager.setArchived(true).setLocked(true).complete() 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/Bot.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot 2 | 3 | import com.learnspigot.bot.counting.CountingRegistry 4 | import com.learnspigot.bot.help.search.HelpPostRegistry 5 | import com.learnspigot.bot.intellijkey.IJUltimateKeyRegistry 6 | import com.learnspigot.bot.knowledgebase.KnowledgebasePostRegistry 7 | import com.learnspigot.bot.lecture.LectureRegistry 8 | import com.learnspigot.bot.profile.ProfileRegistry 9 | import com.learnspigot.bot.reputation.LeaderboardMessage 10 | import com.learnspigot.bot.starboard.StarboardRegistry 11 | import com.learnspigot.bot.util.PermissionRole 12 | import com.learnspigot.bot.verification.VerificationMessage 13 | import gg.flyte.neptune.Neptune 14 | import gg.flyte.neptune.annotation.Instantiate 15 | import net.dv8tion.jda.api.JDA 16 | import net.dv8tion.jda.api.JDABuilder 17 | import net.dv8tion.jda.api.entities.Activity 18 | import net.dv8tion.jda.api.interactions.commands.Command 19 | import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions 20 | import net.dv8tion.jda.api.interactions.commands.build.Commands 21 | import net.dv8tion.jda.api.requests.GatewayIntent 22 | import net.dv8tion.jda.api.utils.ChunkingFilter 23 | import net.dv8tion.jda.api.utils.MemberCachePolicy 24 | 25 | class Bot { 26 | private val profileRegistry = ProfileRegistry() 27 | private val countingRegistry = CountingRegistry(this) 28 | 29 | init { 30 | jda = JDABuilder.createDefault(Environment.get("BOT_TOKEN")) 31 | .setActivity(Activity.watching("learnspigot.com")) 32 | .enableIntents( 33 | GatewayIntent.GUILD_MESSAGES, 34 | GatewayIntent.GUILD_INVITES, 35 | GatewayIntent.GUILD_MEMBERS, 36 | GatewayIntent.DIRECT_MESSAGES, 37 | GatewayIntent.MESSAGE_CONTENT 38 | ) 39 | .setMemberCachePolicy(MemberCachePolicy.ALL) 40 | .setChunkingFilter(ChunkingFilter.ALL) 41 | .build() 42 | .awaitReady() 43 | 44 | run { Server } // intentional to initialize vals 45 | 46 | val guild = jda.getGuildById(Environment.get("GUILD_ID"))!! 47 | VerificationMessage(guild) 48 | LeaderboardMessage(profileRegistry) 49 | 50 | guild.updateCommands().addCommands( 51 | Commands.context(Command.Type.MESSAGE, "Set vote").setDefaultPermissions(DefaultMemberPermissions.enabledFor(PermissionRole.STUDENT)), 52 | Commands.context(Command.Type.MESSAGE, "Set Tutorial vote").setDefaultPermissions(DefaultMemberPermissions.enabledFor(PermissionRole.EXPERT)), 53 | Commands.context(Command.Type.MESSAGE, "Set Project vote").setDefaultPermissions(DefaultMemberPermissions.enabledFor(PermissionRole.EXPERT)) 54 | ).complete() 55 | 56 | Neptune.Builder(jda, this) 57 | .addGuilds(guild) 58 | .clearCommands(false) 59 | .registerAllListeners(true) 60 | .create() 61 | } 62 | 63 | @Instantiate 64 | fun profileRegistry(): ProfileRegistry { 65 | return profileRegistry 66 | } 67 | 68 | @Instantiate 69 | fun lectureRegistry(): LectureRegistry { 70 | return LectureRegistry() 71 | } 72 | 73 | @Instantiate 74 | fun starboardRegistry(): StarboardRegistry { 75 | return StarboardRegistry() 76 | } 77 | 78 | @Instantiate 79 | fun keyRegistry(): IJUltimateKeyRegistry { 80 | return IJUltimateKeyRegistry() 81 | } 82 | 83 | @Instantiate 84 | fun knowledgebasePostRegistry(): KnowledgebasePostRegistry { 85 | return KnowledgebasePostRegistry() 86 | } 87 | 88 | @Instantiate 89 | fun helpPostRegistry(): HelpPostRegistry { 90 | return HelpPostRegistry() 91 | } 92 | 93 | @Instantiate fun countingRegistry(): CountingRegistry = countingRegistry 94 | 95 | companion object { 96 | lateinit var jda: JDA 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/counting/CountingListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.counting 2 | 3 | import com.github.mlgpenguin.mathevaluator.Evaluator 4 | import com.learnspigot.bot.Environment 5 | import com.learnspigot.bot.Server 6 | import gg.flyte.neptune.annotation.Inject 7 | import net.dv8tion.jda.api.entities.Message 8 | import net.dv8tion.jda.api.entities.User 9 | import net.dv8tion.jda.api.entities.channel.Channel 10 | import net.dv8tion.jda.api.entities.emoji.Emoji 11 | import net.dv8tion.jda.api.events.message.MessageDeleteEvent 12 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent 13 | import net.dv8tion.jda.api.events.message.MessageUpdateEvent 14 | import net.dv8tion.jda.api.hooks.ListenerAdapter 15 | import net.dv8tion.jda.api.entities.Member 16 | 17 | class CountingListener: ListenerAdapter() { 18 | 19 | @Inject private lateinit var countingRegistry: CountingRegistry 20 | 21 | val currentCount: Int get() = countingRegistry.currentCount 22 | 23 | var lastCount: Message? = null 24 | 25 | fun fuckedUp(user: User) { 26 | lastCount = null 27 | countingRegistry.fuckedUp(user) 28 | } 29 | 30 | 31 | private fun Channel.isCounting() = id == Environment.get("COUNTING_CHANNEL_ID") 32 | private fun Message.millisSinceLastCount() = timeCreated.toInstant().toEpochMilli() - (lastCount?.timeCreated?.toInstant()?.toEpochMilli() ?: 0) 33 | 34 | private val thinking = Emoji.fromUnicode("🤔") 35 | private val oneHundred = Emoji.fromUnicode("💯") 36 | 37 | override fun onMessageReceived(event: MessageReceivedEvent) { 38 | if (event.author.isBot || !event.isFromGuild || !event.channel.isCounting() || event.guild.id != Server.guildId) return 39 | if (event.message.attachments.isNotEmpty()) return 40 | 41 | val msg = event.message.contentRaw 42 | val userId = event.author.id 43 | if (Evaluator.isValidSyntax(msg)) { 44 | val evaluated = Evaluator.eval(msg).intValue() 45 | if (evaluated == currentCount + 1) { 46 | if (userId.equals(lastCount?.author?.id, true)) return run { 47 | event.message.addReaction(Server.downvoteEmoji) 48 | 49 | val insultMessage = CountingInsults.doubleCountInsults.random() 50 | 51 | event.message.reply("$insultMessage ${event.author.asMention}, The count has been reset to 1.").queue() 52 | 53 | fuckedUp(event.author) 54 | } 55 | val reactionEmoji = if (evaluated % 100 == 0) oneHundred else Server.upvoteEmoji 56 | 57 | 58 | lastCount = event.message 59 | event.message.addReaction(reactionEmoji).queue() 60 | countingRegistry.incrementCount(event.author) 61 | 62 | } else { 63 | if (evaluated == currentCount && event.message.millisSinceLastCount() < 600) { 64 | // ( 600ms delay ) - Arbitrary value based on superficial testing 65 | event.message.addReaction(thinking).queue() 66 | event.message.reply("I'll let this one slide").queue() 67 | return 68 | } 69 | 70 | val next = currentCount + 1 71 | fuckedUp(event.author) 72 | event.message.addReaction(Server.downvoteEmoji).queue() 73 | 74 | val insultMessage = CountingInsults.fuckedUpInsults.random() 75 | 76 | event.message.reply("$insultMessage ${event.author.asMention}, The next number was $next, not $evaluated.").queue() 77 | } 78 | } 79 | } 80 | 81 | override fun onMessageDelete(event: MessageDeleteEvent) { 82 | if (!event.channel.isCounting()) return 83 | if (event.messageId == lastCount?.id) { 84 | Server.countingChannel.sendMessage("${lastCount?.author?.asMention} deleted their count of $currentCount").queue() 85 | } 86 | } 87 | 88 | override fun onMessageUpdate(event: MessageUpdateEvent) { 89 | if (!event.channel.isCounting()) return 90 | if (event.messageId == lastCount?.id) { 91 | Server.countingChannel.sendMessage("${event.author.asMention} edited their count of $currentCount").queue() 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/HastebinListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.JsonObject 5 | import com.learnspigot.bot.Server 6 | import com.learnspigot.bot.util.embed 7 | import net.dv8tion.jda.api.entities.channel.ChannelType 8 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent 9 | import net.dv8tion.jda.api.hooks.ListenerAdapter 10 | import java.net.URI 11 | import java.net.http.HttpClient 12 | import java.net.http.HttpRequest 13 | import java.net.http.HttpResponse 14 | import java.util.concurrent.CompletableFuture 15 | 16 | class HastebinListener : ListenerAdapter() { 17 | 18 | private val GSON = Gson() 19 | 20 | private val KNOWN_PASTEBINS = listOf( 21 | "pastebin.com", 22 | "paste.md-5.net", 23 | "paste.helpch.at" 24 | ) 25 | 26 | private val LS_PASTEBIN = "https://paste.learnspigot.com" 27 | 28 | override fun onMessageReceived(event: MessageReceivedEvent) { 29 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 30 | if (event.author.isBot) return 31 | if (event.guildChannel.asThreadChannel().parentChannel.id != Server.helpChannel.id) return 32 | 33 | val rawLinks = getBinLinks(event.message.contentRaw) ?: return 34 | 35 | event.message.suppressEmbeds(true).queue() 36 | 37 | if (rawLinks.size > 6) { // No reason for someone to be sending over SIX pastebins (probably) 38 | event.channel.sendMessageEmbeds(PasteCommand.getNewPasteBinEmbed()).queue() 39 | return 40 | } 41 | val lsLinks = convertToLSBins(rawLinks).takeIf { it.isNotEmpty() } ?: return 42 | val description = StringBuilder() 43 | .appendLine("We highly recommend using our custom pastebin next time you need to paste some code. Your paste will never expire!") 44 | .appendLine() 45 | .appendLines(lsLinks.map { "${Server.rightEmoji.asMention} $it" }) 46 | 47 | event.channel.sendMessageEmbeds( 48 | embed() 49 | .setTitle("Converted to LearnSpigot pastebin") 50 | .setDescription(description) 51 | .setFooter("PS: If you ever forget the link to the website, just run /pastebin.").build() 52 | ).queue() 53 | } 54 | 55 | private fun StringBuilder.appendLines(lines: List) = lines.forEach(::appendLine).let { this } 56 | 57 | private val regex = "https?://(?:${KNOWN_PASTEBINS.joinToString("|")})/([a-zA-Z0-9]+)".toRegex() 58 | private fun String.toUrlRaw() = lastIndexOf("/").takeIf { it != -1 }?.let { index -> replaceRange(index, index +1, "/raw/") } 59 | 60 | private fun getBinLinks(rawMessage: String): List? = regex 61 | .findAll(rawMessage) 62 | .toList() 63 | .mapNotNull { it.value.toUrlRaw() } 64 | .takeIf { it.isNotEmpty() } 65 | 66 | 67 | private fun startLSBinConversion(client: HttpClient, link: String) = CompletableFuture.supplyAsync { 68 | val rawText = client.send(HttpRequest.newBuilder().uri(URI.create(link)).build(), HttpResponse.BodyHandlers.ofString()).body() 69 | val urlRequest = HttpRequest.newBuilder() 70 | .uri(URI.create("$LS_PASTEBIN/documents")) 71 | .header("Content-Type", "application/json") 72 | .POST(HttpRequest.BodyPublishers.ofString(rawText)) 73 | .build() 74 | 75 | runCatching { 76 | val response = client.send(urlRequest, HttpResponse.BodyHandlers.ofString()).takeIf { it.statusCode() == 200 } 77 | ?: return@runCatching null 78 | val keyObject = GSON.fromJson(response.body(), JsonObject::class.java) 79 | return@runCatching "$LS_PASTEBIN/${keyObject.get("key")?.asString}" 80 | } 81 | .onFailure { it.printStackTrace() } 82 | .getOrNull() 83 | } 84 | 85 | private fun convertToLSBins(links: List): List { 86 | val client = HttpClient.newHttpClient() 87 | val futures = links.map { startLSBinConversion(client, it) } 88 | CompletableFuture.allOf(*futures.toTypedArray()).join() 89 | val results = futures.mapNotNull { it.join() } 90 | if (results.size != futures.size) println("Conversion to LS Pastebin failed") 91 | return results 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/help/CloseCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.help 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.embed 6 | import gg.flyte.neptune.annotation.Command 7 | import gg.flyte.neptune.annotation.Inject 8 | import net.dv8tion.jda.api.entities.Member 9 | import net.dv8tion.jda.api.entities.Message 10 | import net.dv8tion.jda.api.entities.ThreadMember 11 | import net.dv8tion.jda.api.entities.channel.ChannelType 12 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 13 | import net.dv8tion.jda.api.interactions.components.buttons.Button 14 | import net.dv8tion.jda.api.interactions.components.selections.SelectOption 15 | import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu 16 | 17 | class CloseCommand { 18 | 19 | @Inject 20 | private lateinit var profileRegistry: ProfileRegistry 21 | 22 | companion object { 23 | val knowledgebasePostsUsed = mutableMapOf>() 24 | } 25 | 26 | @Command( 27 | name = "close", description = "Close a help post" 28 | ) 29 | fun onCloseCommand(event: SlashCommandInteractionEvent) { 30 | if (!event.isFromGuild) return 31 | if (event.channelType != ChannelType.GUILD_PUBLIC_THREAD) return 32 | 33 | val channel = event.guildChannel.asThreadChannel() 34 | if (channel.parentChannel.id != Server.helpChannel.id) return 35 | 36 | if (event.member!!.id != channel.ownerId && !event.member!!.roles.contains(Server.managementRole)) { 37 | event.reply("You cannot close this thread!").setEphemeral(true).queue() 38 | return 39 | } 40 | 41 | event.deferReply().queue() 42 | 43 | val contributors: List = 44 | channel.retrieveThreadMembers().complete().asSequence() 45 | // excludes the author of the channel 46 | .filter { member: ThreadMember -> member.id != channel.ownerId } 47 | // excludes bots 48 | .filter { member: ThreadMember -> !member.user.isBot } 49 | // excludes users that haven't sent a single message to this channel (i.e: users that clicked the 'follow post' button) 50 | .filter { member: ThreadMember -> 51 | val messageHistory = channel.iterableHistory.complete() 52 | messageHistory.any { it.author.id == member.id }} 53 | .take(25).map { it.member }.toList() 54 | 55 | if (contributors.isEmpty()) { 56 | event.hook.sendMessageEmbeds( 57 | embed().setTitle(event.member!!.effectiveName + " has closed the thread") 58 | .setDescription("Listing no contributors.").build() 59 | ).complete() 60 | channel.manager.setArchived(true).setLocked(true).complete() 61 | return 62 | } 63 | 64 | val owner = channel.owner 65 | if (owner != null) channel.sendMessage(owner.asMention).queue { it.delete().queue() } 66 | 67 | event.hook.sendMessageEmbeds( 68 | embed().setTitle("Who helped you solve your issue?").setDescription( 69 | """ 70 | Please select people from the dropdown who helped solve your issue. 71 | 72 | Once you've selected contributors, click the Close button to close your post. 73 | """ 74 | ).build() 75 | ).addActionRow( 76 | StringSelectMenu.create(channel.id + "-contributor-selector") 77 | .setPlaceholder("Select the people that helped solve your issue").setRequiredRange(0, 25) 78 | .addOptions(contributors.map { member: Member -> 79 | SelectOption.of( 80 | member.effectiveName, member.id 81 | ).withDescription(member.user.name) 82 | }) 83 | .addOptions( 84 | knowledgebasePostsUsed[channel.id]?.map { postId -> 85 | SelectOption.of( 86 | Server.guild.getThreadChannelById(postId)?.name ?: "", "knowledgebase:$postId" 87 | ).withDescription("Knowledgebase Post") 88 | } ?: listOf() 89 | ).build() 90 | ).addActionRow(Button.danger(channel.id + "-close-button", "Close")) 91 | .queue { message: Message -> profileRegistry.messagesToRemove[event.channel.id] = message } 92 | } 93 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/intellijkey/GetKeyCommand.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.intellijkey 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.profile.ProfileRegistry 6 | import com.learnspigot.bot.util.embed 7 | import gg.flyte.neptune.annotation.Command 8 | import gg.flyte.neptune.annotation.Inject 9 | import net.dv8tion.jda.api.Permission 10 | import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent 11 | import java.time.OffsetDateTime 12 | import java.time.ZoneOffset 13 | 14 | class GetKeyCommand { 15 | 16 | @Inject 17 | private lateinit var profileRegistry: ProfileRegistry 18 | 19 | @Inject 20 | private lateinit var keyRegistry: IJUltimateKeyRegistry 21 | 22 | @Command( 23 | name = "getkey", 24 | description = "Unlock your free 6 months IntelliJ Ultimate key", 25 | permissions = [Permission.MESSAGE_SEND] 26 | ) 27 | fun onGetKeyCommand(event: SlashCommandInteractionEvent) { 28 | // First, defer the reply to prevent timeout 29 | event.deferReply(true).queue() // 'true' makes the reply ephemeral 30 | 31 | val member = event.member!! 32 | val isManager = member.roles.contains(Server.managementRole) 33 | 34 | if (!member.roles.contains(event.jda.getRoleById(Environment.get("STUDENT_ROLE_ID")))) { 35 | event.hook.sendMessage("You don't have the Student role! You must show you own the course through the verify channel.").queue() 36 | return 37 | } 38 | 39 | if (!isManager && member.timeJoined.isBefore(OffsetDateTime.of(2023, 8, 21, 0, 0, 0, 0, ZoneOffset.UTC))) { 40 | event.hook.sendMessage("You joined the server before this automated distribution system was added. As such, please DM <@676926873669992459> for your key.").queue() 41 | return 42 | } 43 | 44 | val profile = profileRegistry.findByUser(event.user) 45 | if (!isManager && profile.intellijKeyGiven) { 46 | event.hook.sendMessage("You have already unlocked your free 6 months IntelliJ Ultimate key!").queue() 47 | return 48 | } 49 | 50 | val key = keyRegistry.getKey() 51 | if (key == null) { 52 | event.hook.sendMessage("Sorry - there are no more keys left to send! Contact a Manager if this is an issue.").queue() 53 | return 54 | } 55 | 56 | member.user.openPrivateChannel().queue({ channel -> 57 | channel.sendMessageEmbeds( 58 | embed() 59 | .setTitle("IntelliJ Ultimate Key") 60 | .setDescription(""" 61 | Thanks to our partnership with our friends over at JetBrains, as a free perk for buying the course you receive a 6 months IntelliJ Ultimate license! 62 | 63 | Your key: $key 64 | Redeem @ 65 | 66 | Note: IntelliJ Community version is free and used throughout the course. This key is to unlock the Ultimate version, which is loaded with extra features. 67 | """) 68 | .setFooter("PS: If you ever need help, come use the #help channel in the server.") 69 | .build() 70 | ).queue({ 71 | profile.apply { 72 | intellijKeyGiven = true 73 | save() 74 | } 75 | 76 | keyRegistry.removeKeyFromFile(key) 77 | 78 | event.hook.sendMessage("I have privately messaged your key!").queue() 79 | 80 | val logChannel = event.jda.getTextChannelById(Environment.get("KEYLOG_CHANNEL_ID")) 81 | logChannel?.sendMessageEmbeds( 82 | embed() 83 | .setTitle("Key Given") 84 | .setDescription("Given IntelliJ Ultimate key to ${event.user.asMention}") 85 | .addField("Key", key, false) 86 | .setTimestamp(OffsetDateTime.now()) 87 | .build() 88 | )?.queue() 89 | }) { 90 | keyRegistry.readdKey(key) 91 | event.hook.sendMessage("I am unable to DM you! Please open your DMs so I can privately send your key.").queue() 92 | } 93 | }, { 94 | keyRegistry.readdKey(key) 95 | event.hook.sendMessage("I am unable to open a DM channel with you. Please check your privacy settings and try again.").queue() 96 | }) 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/starboard/StarboardRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.starboard 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.starboard.StarboardUtil.getEmojiReactionCount 5 | import com.learnspigot.bot.util.Mongo 6 | import com.learnspigot.bot.Server 7 | import com.learnspigot.bot.util.embed 8 | import com.mongodb.client.model.Filters 9 | import net.dv8tion.jda.api.entities.Message 10 | import net.dv8tion.jda.api.entities.MessageEmbed 11 | import net.dv8tion.jda.api.entities.User 12 | 13 | class StarboardRegistry { 14 | private val starboardEntries = mutableMapOf() 15 | 16 | init { 17 | Mongo.starboardCollection.find().forEach { 18 | val starboardMessage = StarboardEntry.fromDocument(it) 19 | starboardEntries[starboardMessage.originalMessageId] = starboardMessage 20 | } 21 | } 22 | 23 | fun removeStarboardEntry(messageId: String) { 24 | val starboardEntry = starboardEntries[messageId] ?: return 25 | 26 | Mongo.starboardCollection.deleteOne(Filters.eq("originalMessageId", messageId)) 27 | Server.starboardChannel.deleteMessageById(starboardEntry.startboardMessageId).queue { 28 | starboardEntries.remove(messageId) 29 | } 30 | } 31 | 32 | 33 | private fun addStarboardEntry(message: Message) { 34 | Server.starboardChannel.sendMessageEmbeds(createStarboardEntryEmbed(message, message.isEdited)).queue { 35 | if (it === null) return@queue 36 | val starboardEntry = StarboardEntry(message.id, it.id) 37 | 38 | Mongo.starboardCollection.insertOne(starboardEntry.document()) 39 | starboardEntries[message.id] = starboardEntry 40 | } 41 | } 42 | 43 | private fun createStarboardEntryEmbed(message: Message, edited: Boolean): MessageEmbed { 44 | return embed().apply { 45 | setAuthor(message.author.name, null, message.author.effectiveAvatarUrl) 46 | setDescription(message.contentRaw) 47 | addField("Stars", "⭐️ ${message.getEmojiReactionCount(Server.starEmoji)}", true) 48 | addField("Original Message", message.jumpUrl, true) 49 | setFooter(if (edited) "This message has been edited." else "") 50 | if (message.attachments.isNotEmpty()) setImage(message.attachments.first().proxyUrl) 51 | }.build() 52 | } 53 | 54 | private fun Message.hasNoStarboardEmoji(): Boolean { 55 | val noStarboardReaction = this.reactions.find { 56 | it.emoji == Server.nostarboardEmoji 57 | } ?: return false 58 | 59 | val users = noStarboardReaction.retrieveUsers().complete() 60 | return usersAreAuthorOrManagement(users, this.author) 61 | } 62 | 63 | private fun usersAreAuthorOrManagement(users: List, author: User): Boolean { 64 | return users.any { 65 | if (it.id == author.id) return@any true 66 | val member = Server.guild.getMember(it) ?: return@any false 67 | return@any member.roles.contains(Server.managementRole) 68 | } 69 | } 70 | 71 | fun updateNoStarboard(message: Message) { 72 | if (message.getEmojiReactionCount(Server.nostarboardEmoji) >= 1) { 73 | val noStarboardReaction = message.getReaction(Server.nostarboardEmoji) 74 | val users = noStarboardReaction?.retrieveUsers()?.complete() 75 | 76 | val hasNoStarboardEmoji = usersAreAuthorOrManagement(users ?: listOf(), message.author) 77 | if (hasNoStarboardEmoji) { 78 | removeStarboardEntry(message.id) 79 | } 80 | 81 | users?.forEach { 82 | val member = Server.guild.getMember(it) 83 | if (it.id == message.author.id || member?.roles?.contains(Server.managementRole) == true) return@forEach 84 | noStarboardReaction.removeReaction(it).queue() 85 | } 86 | } else { 87 | updateStarboard(message, false) 88 | } 89 | } 90 | 91 | fun updateStarboard(message: Message, amount: Int, edited: Boolean = false) { 92 | if (message.hasNoStarboardEmoji()) return updateNoStarboard(message) 93 | 94 | val starboardEntry = starboardEntries[message.id] 95 | if (starboardEntry !== null) { 96 | if (amount < amountOfStarsNeeded) { 97 | return removeStarboardEntry(message.id) 98 | } 99 | Server.starboardChannel.editMessageEmbedsById( 100 | starboardEntry.startboardMessageId, createStarboardEntryEmbed(message, edited) 101 | ).queue() 102 | } else if (amount >= amountOfStarsNeeded) { 103 | addStarboardEntry(message) 104 | } 105 | } 106 | 107 | fun updateStarboard(message: Message) { 108 | updateStarboard(message, message.getEmojiReactionCount(Server.starEmoji)) 109 | } 110 | 111 | fun updateStarboard(message: Message, edited: Boolean) { 112 | updateStarboard(message, message.getEmojiReactionCount(Server.starEmoji), edited) 113 | } 114 | 115 | companion object { 116 | val amountOfStarsNeeded: Int = Environment.get("STARBOARD_AMOUNT").toInt() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/reputation/LeaderboardMessage.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.reputation 2 | 3 | import com.learnspigot.bot.profile.ProfileRegistry 4 | import com.learnspigot.bot.Server 5 | import com.learnspigot.bot.util.embed 6 | import net.dv8tion.jda.api.entities.Message 7 | import net.dv8tion.jda.api.entities.MessageEmbed 8 | import net.dv8tion.jda.api.entities.MessageHistory 9 | import java.time.Instant 10 | import java.time.YearMonth 11 | import java.time.ZoneOffset 12 | import java.util.concurrent.Executors 13 | import java.util.concurrent.TimeUnit 14 | import java.util.concurrent.atomic.AtomicInteger 15 | import java.util.function.Consumer 16 | 17 | class LeaderboardMessage(private val profileRegistry: ProfileRegistry) { 18 | 19 | private val medals: List = listOf(":first_place:", ":second_place:", ":third_place:") 20 | 21 | private val executorService = Executors.newSingleThreadScheduledExecutor() 22 | 23 | private val monthlyRewardMessage: Message 24 | private val lifetimeMessage: Message 25 | private val monthlyMessage: Message 26 | 27 | init { 28 | Server.leaderboardChannel.apply { 29 | MessageHistory.getHistoryFromBeginning(this).complete().retrievedHistory.apply { 30 | /* 31 | * If all 3 messages aren't there, delete any existing ones and send the new 3 32 | * Otherwise, just get them, edit to update, and store for constant updating like normal 33 | */ 34 | if (size != 3) { 35 | forEach { it.delete().queue() } 36 | monthlyRewardMessage = sendMessageEmbeds(buildPrizeEmbed()).complete() 37 | lifetimeMessage = sendMessageEmbeds(buildLeaderboard(false)).complete() 38 | monthlyMessage = sendMessageEmbeds(buildLeaderboard(true)).complete() 39 | } else { 40 | monthlyRewardMessage = get(2).editMessageEmbeds(buildPrizeEmbed()).complete() 41 | lifetimeMessage = get(1).editMessageEmbeds(buildLeaderboard(false)).complete() 42 | monthlyMessage = get(0).editMessageEmbeds(buildLeaderboard(true)).complete() 43 | } 44 | } 45 | } 46 | 47 | executorService.scheduleAtFixedRate({ 48 | lifetimeMessage.editMessageEmbeds(buildLeaderboard(false)).queue() 49 | monthlyMessage.editMessageEmbeds(buildLeaderboard(true)).queue() 50 | 51 | if (isLastMin()){ 52 | Server.managerChannel.sendMessageEmbeds(buildLeaderboard(true)).queue {println("Manager channel leaderboard message sent.")} 53 | } 54 | }, 1L, 1L, TimeUnit.MINUTES) 55 | } 56 | 57 | private fun buildLeaderboard(monthly: Boolean): MessageEmbed { 58 | val builder = StringBuilder() 59 | 60 | val i = AtomicInteger(1) 61 | top10(monthly).forEach(Consumer { (id, reputation): ReputationWrapper -> 62 | builder.append( 63 | if (i.get() <= medals.size) medals[i.get() - 1] else i.get().toString() + "." 64 | ).append(" <@").append(id).append("> - ").append(reputation.size).append("\n") 65 | i.getAndIncrement() 66 | }) 67 | 68 | return embed() 69 | .setTitle((if (monthly) "Monthly" else "All-Time") + " Leaderboard") 70 | .setDescription((if (monthly) "These stats are reset on the 1st of every month." else "These stats are never reset.") + "\n\n$builder") 71 | .setFooter("Last updated") 72 | .setTimestamp(Instant.now()) 73 | .build() 74 | } 75 | 76 | private fun buildPrizeEmbed() : MessageEmbed{ 77 | return embed() 78 | .setTitle("Current Monthly Rewards") 79 | .setDescription("The top 3 on the Monthly Leaderboard will earn these rewards:" + 80 | "\n\n${medals[0]} - $50 PayPal!" + 81 | "\n${medals[1]} - \$20 PayPal!" + 82 | "\n${medals[2]} - \$10 PayPal!") 83 | .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") 84 | .build() 85 | } 86 | 87 | private fun top10(monthly: Boolean): List { 88 | val reputation = mutableListOf() 89 | for ((key, profile) in profileRegistry.profileCache) { 90 | var repList = ArrayList(profile.reputation.values) 91 | if (repList.isEmpty()) continue 92 | 93 | if (monthly) { 94 | repList = repList.filter { rep -> 95 | YearMonth.now().atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC) 96 | .isBefore(Instant.ofEpochSecond(rep.timestamp)) 97 | } as ArrayList 98 | } 99 | reputation.add(ReputationWrapper(key, repList)) 100 | } 101 | reputation.sortByDescending { it.reputation.size } 102 | return reputation.take(10) 103 | } 104 | 105 | private fun isLastMin(): Boolean { 106 | val now = Instant.now() 107 | val startOfNextMonth = YearMonth.now().plusMonths(1).atDay(1).atStartOfDay().toInstant(ZoneOffset.UTC) 108 | val lastMinOfCurrentMonth = startOfNextMonth.minusSeconds(60) 109 | 110 | val isLastMin = now.isAfter(lastMinOfCurrentMonth) 111 | if (isLastMin){ println("This is the last minute of the month!")} 112 | 113 | return isLastMin 114 | } 115 | 116 | 117 | data class ReputationWrapper(val id: String, val reputation: List) 118 | 119 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/counting/CountingInsults.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.counting 2 | 3 | object CountingInsults { 4 | val fuckedUpInsults = listOf( 5 | "Were you counting with your eyes closed?", 6 | "Did you mistake numbers for alphabet letters?", 7 | "Are you allergic to numbers or just incompetent?", 8 | "Did you skip math class for skipping numbers?", 9 | "Your counting skills are like a broken record - stuck on repeat.", 10 | "Were you counting with your fingers in your ears?", 11 | "Even a toddler could count better than that.", 12 | "I've seen smoother counting from a malfunctioning calculator.", 13 | "Did you confuse counting with alphabetizing?", 14 | "Did you lose count because you ran out of fingers?", 15 | "You must have a PhD in miscounting.", 16 | "Were you using an abacus with missing beads?", 17 | "Your counting skills are about as accurate as a blindfolded archer.", 18 | "I've heard better counting from a parrot with a limited vocabulary.", 19 | "Did you forget to engage your brain while counting?", 20 | "Your counting proficiency rivals that of a distracted squirrel.", 21 | "Did you fall asleep halfway through counting?", 22 | "You're counting like you're trying to find your way through a maze blindfolded.", 23 | "I didn't know counting could be performed with such remarkable ineptitude.", 24 | "I've seen smoother counting from a broken clock.", 25 | "Were you distracted by counting sheep instead of numbers?", 26 | "Did you skip numbers like a stone on a pond?", 27 | "Your counting abilities are as shaky as a Jenga tower in an earthquake.", 28 | "Did you misplace your counting skills along with your common sense?", 29 | "Even a GPS would have trouble navigating your counting route.", 30 | "You're counting like you're allergic to reaching the correct number.", 31 | "Your counting skills are like a GPS without satellite reception - hopelessly lost.", 32 | "Did you confuse counting with a game of hopscotch?", 33 | "You're counting like you're trying to break a world record for most mistakes in a minute.", 34 | "Did you mistakenly count your own mistakes?", 35 | "Your counting proficiency is comparable to a toddler on roller skates - all over the place.", 36 | "Did you accidentally count backwards instead of forwards?", 37 | "Your counting skills are like a Picasso painting - abstract and confusing.", 38 | "Did you mistake counting for a memory test?", 39 | "You're counting like you're trying to decipher hieroglyphics.", 40 | "Did you misplace your counting marbles?", 41 | "Your counting abilities are as reliable as a fortune told by a Magic 8-Ball.", 42 | "Were you trying to count while riding a roller coaster?", 43 | "You're counting like you're trying to break the sound barrier - fast but completely out of control.", 44 | "Did you confuse counting with a game of roulette?", 45 | "Your counting skills are like a GPS without batteries - going absolutely nowhere.", 46 | "Were you trying to count while juggling flaming torches?", 47 | "Did you accidentally count in a foreign language?", 48 | "Your counting abilities are as consistent as the weather in spring - all over the place.", 49 | "Did you mistake counting for a game of Whac-A-Mole?", 50 | "You're counting like you're trying to solve a Rubik's Cube blindfolded.", 51 | "Did you forget how to count and decide to guess instead?", 52 | "Your counting skills are like a broken compass - pointing in every direction except the right one.", 53 | "Were you trying to count while riding a unicycle on a tightrope?", 54 | "Did you mistakenly count imaginary friends instead of numbers?" 55 | ) 56 | 57 | val doubleCountInsults = listOf( 58 | "Did you forget you already counted that?", 59 | "Are you hoping the numbers will change if you count them again?", 60 | "You must really love that number to count it twice.", 61 | "Did you hit the replay button on your counting?", 62 | "Is double counting your secret strategy for getting the right answer?", 63 | "I didn't realize we were playing a counting remix.", 64 | "Did you accidentally press the repeat button on your counting machine?", 65 | "I guess counting once just wasn't enough for you.", 66 | "You're like a broken record, but with numbers.", 67 | "Are you trying to see if the numbers change their minds?", 68 | "Your counting is like déjà vu - I feel like I've heard it before.", 69 | "Are you practicing for the counting Olympics by doing extra laps?", 70 | "Did you think we wouldn't notice if you counted it again?", 71 | "I've heard of double trouble, but this is ridiculous.", 72 | "Did you get lost on the way back to the first number?", 73 | "You're like a dog chasing its tail, but with numbers.", 74 | "Did you accidentally hit the rewind button on your counting?", 75 | "Congratulations, you just doubled your counting effort for no reason.", 76 | "You're like a broken clock, but instead of being right twice a day, you're just counting twice.", 77 | "Is this your way of making sure the numbers didn't disappear while you weren't looking?", 78 | "I didn't realize we were in a counting loop.", 79 | "Did you think the numbers were playing hide and seek?", 80 | "Is this your version of counting insurance - counting it twice just in case?", 81 | "You're like a magician pulling the same rabbit out of the hat twice.", 82 | "Did you accidentally hit the repeat button on your counting playlist?", 83 | "Your counting is like a bad sequel - nobody asked for it.", 84 | "Did you think the numbers would change their minds if you asked them nicely?", 85 | "You're like a detective solving the case of the missing numbers by counting them twice.", 86 | "Did you think the numbers were playing a disappearing act?", 87 | "I guess you just wanted to give the numbers a second chance to be counted.", 88 | "Is this your way of ensuring job security for counters everywhere?", 89 | "Your counting is like Groundhog Day - the same numbers over and over again.", 90 | "Did you accidentally press the refresh button on your counting app?", 91 | "You're like a broken vending machine - you keep giving out the same numbers.", 92 | "Did you think the numbers were playing hide and seek with you?", 93 | "Your counting is like a rerun of a bad TV show - nobody wants to see it twice.", 94 | "Did you think the numbers were going to disappear if you didn't count them again?", 95 | "You're like a child asking 'are we there yet?' but with numbers.", 96 | "Did you think the numbers were going to change their minds if you counted them twice?", 97 | "Your counting is like a bad sequel - nobody asked for it.", 98 | "Did you think the numbers were playing a disappearing act?", 99 | "I guess you just wanted to give the numbers a second chance to be counted.", 100 | "Is this your way of ensuring job security for counters everywhere?", 101 | "Your counting is like Groundhog Day - the same numbers over and over again.", 102 | "Did you accidentally press the refresh button on your counting app?", 103 | "You're like a broken vending machine - you keep giving out the same numbers.", 104 | "Did you think the numbers were playing hide and seek with you?", 105 | "Your counting is like a rerun of a bad TV show - nobody wants to see it twice.", 106 | "Did you think the numbers were going to disappear if you didn't count them again?", 107 | "You're like a child asking 'are we there yet?' but with numbers.", 108 | "Did you think the numbers were going to change their minds if you counted them twice?" 109 | ) 110 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/verification/VerificationListener.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.verification 2 | 3 | import com.learnspigot.bot.Environment 4 | import com.learnspigot.bot.profile.ProfileRegistry 5 | import com.learnspigot.bot.util.Mongo 6 | import com.learnspigot.bot.util.embed 7 | import com.mongodb.client.model.Filters 8 | import gg.flyte.neptune.annotation.Inject 9 | import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent 10 | import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent 11 | import net.dv8tion.jda.api.exceptions.ErrorHandler 12 | import net.dv8tion.jda.api.hooks.ListenerAdapter 13 | import net.dv8tion.jda.api.interactions.InteractionType 14 | import net.dv8tion.jda.api.interactions.components.ActionRow 15 | import net.dv8tion.jda.api.interactions.components.buttons.Button 16 | import net.dv8tion.jda.api.interactions.components.text.TextInput 17 | import net.dv8tion.jda.api.interactions.components.text.TextInputStyle 18 | import net.dv8tion.jda.api.interactions.modals.Modal 19 | import net.dv8tion.jda.api.requests.ErrorResponse 20 | import java.util.regex.Pattern 21 | 22 | class VerificationListener : ListenerAdapter() { 23 | 24 | @Inject 25 | private lateinit var profileRegistry: ProfileRegistry 26 | 27 | override fun onButtonInteraction(e: ButtonInteractionEvent) { 28 | if (e.button.id == null) return 29 | 30 | if (e.button.id.equals("verify")) { 31 | if (e.member!!.roles.contains(e.jda.getRoleById(Environment.get("STUDENT_ROLE_ID")))) { 32 | e.reply("You're already a student!").setEphemeral(true).queue() 33 | return 34 | } 35 | 36 | val verifyModal = Modal.create("verify", "Verify Your Profile") 37 | .addActionRows( 38 | ActionRow.of( 39 | TextInput.create("url", "Udemy Profile URL", TextInputStyle.SHORT) 40 | .setPlaceholder("https://www.udemy.com/user/example") 41 | .setMinLength(10) 42 | .setMaxLength(70) 43 | .setRequired(true) 44 | .build() 45 | ), 46 | ActionRow.of( 47 | TextInput.create("personal_plan", "On Personal/Business Subscription?", TextInputStyle.SHORT) 48 | .setPlaceholder("Yes/No - If you purchased the course directly, answer No") 49 | .setMinLength(2) 50 | .setMaxLength(3) 51 | .setRequired(false) 52 | .build() 53 | ) 54 | ) 55 | .build() 56 | 57 | e.replyModal(verifyModal).queue() 58 | return 59 | } 60 | 61 | val info = e.button.id!!.split("|") 62 | val action = info[1] 63 | 64 | if (e.button.id!!.startsWith("v|")) { 65 | val guild = e.guild!! 66 | 67 | val allowedRoles = listOf( 68 | Environment.get("SUPPORT_ROLE_ID"), 69 | Environment.get("STAFF_ROLE_ID"), 70 | Environment.get("MANAGEMENT_ROLE_ID"), 71 | Environment.get("VERIFIER_ROLE_ID") 72 | ) 73 | val memberRoles = e.member!!.roles.map { it.id } 74 | 75 | if (allowedRoles.none { it in memberRoles }) { 76 | e.reply("Sorry, you can't verify student profiles.").setEphemeral(true).queue() 77 | return 78 | } 79 | 80 | val url = info[2] 81 | val member = guild.getMemberById(info[3]) ?: return 82 | val questionChannel = guild.getTextChannelById(Environment.get("QUESTIONS_CHANNEL_ID")) 83 | 84 | var description = "" 85 | 86 | when (action) { 87 | "a" -> { 88 | description = "has approved :mention:'s profile" 89 | 90 | guild.addRoleToMember(member, guild.getRoleById(Environment.get("STUDENT_ROLE_ID"))!!).queue() 91 | 92 | guild.getTextChannelById(Environment.get("GENERAL_CHANNEL_ID"))!!.sendMessageEmbeds( 93 | embed() 94 | .setTitle("Welcome") 95 | .setDescription("Please welcome " + member.asMention + " as a new Student! :heart:").build() 96 | ).queue() 97 | 98 | member.user.openPrivateChannel().queue({ channel -> 99 | channel.sendMessageEmbeds( 100 | embed() 101 | .setTitle("Profile Verification") 102 | .setDescription("Your profile was approved! Go ahead and enjoy our community :heart:") 103 | .setFooter("PS: Want your free 6 months IntelliJ Ultimate key? Run /getkey in the Discord server!") 104 | .build() 105 | ).queue(null, ErrorHandler().handle(ErrorResponse.CANNOT_SEND_TO_USER) { 106 | }) 107 | }, null) 108 | 109 | profileRegistry.findByUser(member.user).let { 110 | it.udemyProfileUrl = url 111 | it.save() 112 | } 113 | } 114 | 115 | "wl" -> { 116 | description = "hasn't approved :mention:, as they specified an invalid link" 117 | 118 | questionChannel!!.sendMessage(member.asMention).setEmbeds( 119 | embed() 120 | .setTitle("Profile Verification") 121 | .setDescription( 122 | """ 123 | Staff looked at your profile and found that you have sent the wrong profile link! 124 | 125 | The URL you need to use is the link to your public profile, to get this: 126 | :one: Hover over your profile picture in the top right on Udemy 127 | :two: Select "Public profile" from the dropdown menu 128 | :three: Copy the link from your browser 129 | """ 130 | ) 131 | .build() 132 | ).queue() 133 | } 134 | 135 | "ch" -> { 136 | description = "hasn't approved :mention:, as they're unable to view their courses" 137 | 138 | questionChannel!!.sendMessage(member.asMention).setEmbeds( 139 | embed() 140 | .setTitle("Profile Verification") 141 | .setDescription(""" 142 | Staff looked at your profile and found that you have privacy settings disabled which means we can't see your courses. 143 | 144 | Change here: 145 | 146 | Enable "Show courses you're taking on your profile page" and verify again! 147 | """) 148 | .build() 149 | ).queue() 150 | } 151 | 152 | "no" -> { 153 | description = "hasn't approved :mention:, as they do not own the course" 154 | 155 | questionChannel!!.sendMessage(member.asMention).setEmbeds( 156 | embed() 157 | .setTitle("Profile Verification") 158 | .setDescription("Staff looked at your profile and found that you do not own the course. If you have purchased the course, please make sure it's visible on your public profile.") 159 | .build() 160 | ).queue() 161 | } 162 | 163 | "u" -> { 164 | val originalActionTaker = info[4] 165 | if (e.member!!.id != originalActionTaker && !e.member!!.roles.contains(e.guild!!.getRoleById(Environment.get("MANAGEMENT_ROLE_ID"))!!)) { 166 | e.reply("Sorry, you can't undo that verification decision.").setEphemeral(true).queue() 167 | return 168 | } 169 | 170 | guild.removeRoleFromMember(member, guild.getRoleById(Environment.get("STUDENT_ROLE_ID"))!!).queue() 171 | e.message.editMessageEmbeds( 172 | embed() 173 | .setTitle("Profile Verification") 174 | .setDescription( 175 | "Please verify that " + member.asMention + " owns the course." + 176 | "\n\nPrevious action reverted by: ${e.member!!.asMention}" 177 | ) 178 | .addField("Udemy Link", url, false) 179 | .build() 180 | ) 181 | .setActionRow( 182 | Button.success("v|a|" + url + "|" + member.id, "Approve"), 183 | Button.danger("v|wl|" + url + "|" + member.id, "Wrong Link"), 184 | Button.danger("v|ch|" + url + "|" + member.id, "Courses Hidden"), 185 | Button.danger("v|no|" + url + "|" + member.id, "Not Owned") 186 | ) 187 | .queue() 188 | 189 | e.interaction.deferEdit().queue() 190 | 191 | questionChannel!!.sendMessage(member.asMention).setEmbeds( 192 | embed() 193 | .setTitle("Profile Verification") 194 | .setDescription( 195 | "Please disregard the previous message regarding your verification status - a staff member has reverted the action. Please remain patient while waiting for a corrected decision.\n\n" + 196 | "If you were previously verified and granted the Student role, the role has been removed pending the corrected decision from staff." 197 | ) 198 | .build() 199 | ).queue() 200 | 201 | return 202 | } 203 | } 204 | 205 | e.message.editMessageEmbeds( 206 | embed() 207 | .setTitle("Profile Verification") 208 | .setDescription( 209 | e.member!!.asMention + " " + description.replace( 210 | ":mention:", 211 | member.asMention 212 | ) + "." 213 | ) 214 | .build() 215 | ) 216 | .setActionRow( 217 | Button.danger("v|u|" + url + "|" + member.id + "|" + e.member!!.id, "Undo") 218 | ) 219 | .queue() 220 | 221 | e.interaction.deferEdit().queue() 222 | } 223 | } 224 | 225 | override fun onModalInteraction(e: ModalInteractionEvent) { 226 | if (e.interaction.type != InteractionType.MODAL_SUBMIT) return 227 | if (e.modalId != "verify") return 228 | 229 | var url = e.getValue("url")!!.asString 230 | val isPersonalPlan = e.getValue("personal_plan")?.asString?.lowercase() == "yes" 231 | 232 | if (url.contains("|") || url.startsWith("https://www.udemy.com/course")) { 233 | e.reply("Invalid profile link.").setEphemeral(true).queue() 234 | return 235 | } 236 | 237 | if (e.member!!.roles.contains(e.jda.getRoleById(Environment.get("STUDENT_ROLE_ID")))) { 238 | e.reply("You're already a Student!").setEphemeral(true).queue() 239 | return 240 | } 241 | 242 | if (url.endsWith("/")) { 243 | url = url.substring(0, url.length - 1) 244 | } 245 | 246 | if (Mongo.userCollection.countDocuments( 247 | Filters.eq( 248 | "udemyProfileUrl", 249 | Pattern.compile(url, Pattern.CASE_INSENSITIVE) 250 | ) 251 | ) > 0 252 | ) { 253 | e.reply("Somebody has already verified with this profile. Was this not you? Let staff know.") 254 | .setEphemeral(true).queue() 255 | return 256 | } 257 | 258 | e.replyEmbeds( 259 | embed() 260 | .setTitle("Your profile has been received!") 261 | .setDescription( 262 | """ 263 | Please wait a short while as staff verify that you own the course! Once verified, this channel will disappear and you'll be able to talk in the rest of the server. 264 | 265 | If you have any concerns, please ask in <#${Environment.get("QUESTIONS_CHANNEL_ID")}>.""" 266 | ) 267 | .build() 268 | ).setEphemeral(true).queue() 269 | 270 | val supportChannel = e.jda.getTextChannelById(Environment.get("SUPPORT_CHANNEL_ID"))!! 271 | val verificationEmbed = embed() 272 | .setTitle("Profile Verification") 273 | .setDescription("Verify that " + e.member!!.asMention + " owns the course." + 274 | (if (isPersonalPlan) "\n\nNote: Student claims to be on Udemy Personal or Business Plan." else "")) 275 | .addField("Udemy Link", url, false) 276 | .build() 277 | 278 | val mentionContent = if (isPersonalPlan) { 279 | "<@${Environment.get("STEPHEN_USER_ID")}>" 280 | } else { 281 | "<@&${Environment.get("VERIFIER_ROLE_ID")}> New verification request." 282 | } 283 | 284 | supportChannel.sendMessage(mentionContent) 285 | .addEmbeds(verificationEmbed) 286 | .addActionRow( 287 | Button.success("v|a|" + url + "|" + e.member!!.id, "Approve"), 288 | Button.danger("v|wl|" + url + "|" + e.member!!.id, "Wrong Link"), 289 | Button.danger("v|ch|" + url + "|" + e.member!!.id, "Courses Hidden"), 290 | Button.danger("v|no|" + url + "|" + e.member!!.id, "Not Owned") 291 | ).queue() 292 | } 293 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/learnspigot/bot/lecture/LectureRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.learnspigot.bot.lecture 2 | 3 | import java.util.* 4 | 5 | class LectureRegistry { 6 | 7 | private val lectures = mutableListOf() 8 | private val matcher: WordMatcher = WordMatcher() 9 | 10 | fun findLectures(query: String, amount: Int): MutableList { 11 | return matcher.getTopLectures( 12 | query.lowercase(Locale.getDefault()) 13 | .replace("\n", " ") // Replace line breaks with spaces 14 | .replace("\\b(the|a|an)\\b".toRegex(), "") // Remove determiners 15 | .replace("[^\\w\\s]".toRegex(), "") // Only leave valid characters 16 | .replace("\\s+".toRegex(), " "), // Clean up stacked spaces 17 | lectures, 18 | amount).toMutableList() 19 | } 20 | 21 | init { 22 | lectures.addAll( 23 | mutableListOf( 24 | Lecture( 25 | "29860442", 26 | "Introduction", 27 | "I'll explain all about the course, the content within it, my credibility, the support networks available and how to make the most of this resource!" 28 | ), 29 | Lecture( 30 | "29860444", 31 | "Installing IntelliJ (Windows)", 32 | "You'll learn how to install IntelliJ, the software we'll be using throughout, using Windows OS." 33 | ), 34 | Lecture( 35 | "29860448", 36 | "Installing IntelliJ (Mac)", 37 | "You'll learn how to install IntelliJ, the software we'll be using throughout, using Mac OS." 38 | ), 39 | Lecture( 40 | "35191380", 41 | "Installing IntelliJ (Linux)", 42 | "You'll learn how to install IntelliJ, the software we'll be using throughout, using Linux-based Ubuntu (22.04)." 43 | ), 44 | Lecture( 45 | "32006130", 46 | "Optimizing IntelliJ", 47 | "You'll learn the basics of how IntelliJ works, how to get my theme and some other top tips." 48 | ), 49 | Lecture( 50 | "29860450", 51 | "Creating Spigot Server (Windows)", 52 | "You'll learn how to setup your very own local Spigot server using Windows OS." 53 | ), 54 | Lecture( 55 | "29860452", 56 | "Creating Spigot Server (Mac)", 57 | "You'll learn how to setup your very own local Spigot server using Mac OS." 58 | ), 59 | Lecture( 60 | "35191378", 61 | "Creating Spigot Server (Linux)", 62 | "You'll learn how to setup your very own local Spigot server using Linux-based Ubuntu (22.04)." 63 | ), 64 | Lecture( 65 | "29860454", 66 | "Discord", 67 | "I'll introduce you to our exclusive student-only Discord community and support server! Come join us @ discord.gg/9WuhJJkCsa" 68 | ), 69 | Lecture( 70 | "29860422", 71 | "First Plugin", 72 | "You'll make your very first plugin! We start basic, with a simple console message, after setting up the whole environment for future lectures." 73 | ) 74 | ) 75 | ) 76 | 77 | lectures.addAll( 78 | mutableListOf( 79 | Lecture( 80 | "29860426", 81 | "Events", 82 | "You'll begin to understand what events are and how to listen to them - in this lecture we look at the PlayerMoveEvent and the PlayerEggThrowEvent!" 83 | ), 84 | Lecture( 85 | "29860428", 86 | "Commands", 87 | "You'll learn how to create basic commands. We'll make a /heal command to start off!" 88 | ), 89 | Lecture( 90 | "29860430", 91 | "Command Arguments", 92 | "You'll learn how to start expanding your command possibilities using optional arguments from the player." 93 | ), 94 | Lecture( 95 | "29860434", 96 | "Console Commands", 97 | "You'll learn how to allow your console users to run your commands!" 98 | ), 99 | Lecture( 100 | "29860438", 101 | "Configuration File (config.yml)", 102 | "You'll learn how to create a configuration file, read and set data." 103 | ), 104 | Lecture( 105 | "29860440", 106 | "Permissions", 107 | "You'll learn how to incorporate permission check into your plugin (i.e. allowing actions based on whether user has X permission)." 108 | ), 109 | Lecture( 110 | "29860590", 111 | "Javadocs", 112 | "You'll learn the importance of the Javadocs throughout the course and your development career, and understand how to read and search them." 113 | ), 114 | Lecture( 115 | "29860592", 116 | "Reading Errors & Debugging", 117 | "You'll learn the essential skills of reading errors and debugging which will be used throughout the course!" 118 | ), 119 | Lecture( 120 | "29860594", 121 | "Entities", 122 | "You'll learn some of the key methods and events relating to entities." 123 | ), 124 | Lecture( 125 | "29860596", 126 | "Blocks, Materials & ItemStacks", 127 | "You'll learn the key methods related to block manipulation, ItemStacks and the Material's behind them." 128 | ) 129 | ) 130 | ) 131 | 132 | lectures.addAll( 133 | mutableListOf( 134 | Lecture( 135 | "31034476", 136 | "Additional ItemMeta (Dyed Leather Armor etc.)", 137 | "You'll learn how to use other forms of ItemMeta such as LeatherArmorMeta to make dyed armor." 138 | ), 139 | Lecture( 140 | "29860598", 141 | "Action Bar, Titles & Tablist", 142 | "You'll learn how to send custom action bars (1.9+), titles (1.9) and player list header and footers (1.13+). For lower versions, see packets section." 143 | ), 144 | Lecture( 145 | "29860600", 146 | "Boss Bars", 147 | "You'll learn how to create boss bars with lots of customization, and also how to edit them in real time!" 148 | ), 149 | Lecture( 150 | "29860604", 151 | "Fireworks", 152 | "You'll learn how to create and give extremely customizable fireworks to players." 153 | ), 154 | Lecture( 155 | "29860606", 156 | "Potion Effects", 157 | "You'll learn the key methods regarding potion effects and how to apply them to players with different settings." 158 | ), 159 | Lecture( 160 | "29860610", 161 | "Worlds (Weather, Time etc.)", 162 | "You'll learn all the key methods regarding worlds; including how to get them, the methods you can use on them and the key events." 163 | ), 164 | Lecture( 165 | "29860612", 166 | "1.16 Hex Color Codes", 167 | "In version 1.16+, you'll learn how to send custom Hex and RGB colours. As well as a bonus gradient and translation tutorial!" 168 | ), 169 | Lecture( 170 | "29860614", 171 | "Sounds", 172 | "You'll learn how to send sounds to single or all players and the key settings involved." 173 | ), 174 | Lecture( 175 | "30742712", 176 | "Note Block Sounds & Music Discs", 177 | "You'll learn how to play every Note Block sound and music discs." 178 | ), 179 | Lecture( 180 | "29860616", 181 | "Projectiles", 182 | "You'll learn the key events related to projectiles and also create a gun-like Egg-shooting Diamond Hoe!" 183 | ) 184 | ) 185 | ) 186 | 187 | lectures.addAll( 188 | mutableListOf( 189 | Lecture("30195558", "Particles", "You'll learn how to spawn particles to players & worlds."), 190 | Lecture( 191 | "29860618", 192 | "Toggling", 193 | "You'll learn the importance of toggling logic and how it works in practise!" 194 | ), 195 | Lecture( 196 | "29860622", 197 | "Vanish", 198 | "You'll learn how to vanish players and even incorporate it into a custom command!" 199 | ), 200 | Lecture( 201 | "29860624", 202 | "PROJECT: Guns", 203 | "We'll bring together a load of content from the earlier videos and implement some guns into Minecraft!" 204 | ), 205 | Lecture( 206 | "29860626", 207 | "Custom Books", 208 | "You'll learn how to create a fully custom book - including setting the title, author, pages and other settings." 209 | ), 210 | Lecture( 211 | "29860628", 212 | "Custom Banners", 213 | "You'll learn how to create custom banners and give them to players." 214 | ), 215 | Lecture( 216 | "29860630", 217 | "Moderation Tools (Kick, Ban etc.)", 218 | "You'll learn how to kick, ban and temporarily ban players using Bukkit's build-in punishment system!" 219 | ), 220 | Lecture( 221 | "29860632", 222 | "Setting Resource Packs", 223 | "You'll learn the key events related to resource packs and also how to force them from the server." 224 | ), 225 | Lecture( 226 | "29860634", 227 | "Riding Entities", 228 | "You'll learn the key methods that allows any entity to ride any entity (really!)." 229 | ), 230 | Lecture( 231 | "31038296", 232 | "Player Statistics", 233 | "You'll learn how to retrieve all the default-stored Minecraft statistics and how to change them." 234 | ) 235 | ) 236 | ) 237 | 238 | lectures.addAll( 239 | mutableListOf( 240 | Lecture( 241 | "29860636", 242 | "Setting MOTD, Server Icon & Player Count", 243 | "You'll learn the methods used in the ServerPingEvent to set the server MOTD, icon and max player count." 244 | ), 245 | Lecture( 246 | "29860638", 247 | "PROJECT: Private Message System", 248 | "We'll bring together a load of content from this section and create a functioning message system between players." 249 | ), 250 | Lecture( 251 | "29869446", 252 | "Runnables", 253 | "You'll learn all about runnables and their importance, aswell as the key ones we'll be using and how to use them!" 254 | ), 255 | Lecture( 256 | "29869450", 257 | "GUI Creation", 258 | "You'll learn how to create your very first GUI by making a cool staff/moderation menu! It'll also teach how to be sustainable, scalable and how to make your own ideas come to life." 259 | ), 260 | Lecture( 261 | "29869452", 262 | "GUI Interaction", 263 | "You'll learn how to make GUI's clickable and user friendly, while keeping safety measures in mind. We'll make our GUI from last lecture fully functional and responsive!" 264 | ), 265 | Lecture( 266 | "29869456", 267 | "Command Tab Complete", 268 | "You'll learn how to add custom tab complete options to your commands!" 269 | ), 270 | Lecture( 271 | "33779108", 272 | "Attribute Modifiers (1.16+)", 273 | "This lecture you'll learn how to modify attributes on entities and items." 274 | ), 275 | Lecture( 276 | "29869458", 277 | "Block Data (Doors, Signs etc.)", 278 | "You'll learn how to set specifc block data about all supported blocks, using a Rail, Cake & Glass Panes as examples!" 279 | ), 280 | Lecture( 281 | "29869462", 282 | "Per-Player Blocks & Signs", 283 | "You'll learn how to send block and sign information to just one player rather than the whole server!" 284 | ), 285 | Lecture( 286 | "29869466", 287 | "Custom Skulls (Players & Textures)", 288 | "You'll learn how to create fully custom skulls - by (a) setting it to a player's head, or (b) setting it to a custom texture." 289 | ) 290 | ) 291 | ) 292 | 293 | lectures.addAll( 294 | mutableListOf( 295 | Lecture( 296 | "29869468", 297 | "Custom YML Files", 298 | "You'll learn how to generate custom files for data storage, extra configuration, languages etc." 299 | ), 300 | Lecture( 301 | "30195612", 302 | "Custom JSON Files", 303 | "You'll learn how to generate files for data storage in the JSON format!" 304 | ), 305 | Lecture( 306 | "29869470", 307 | "Custom Maps (Text, Images etc.)", 308 | "You'll learn how to draw coloured pixels & shapes, create custom text and add any image to maps!" 309 | ), 310 | Lecture( 311 | "29869472", 312 | "Custom Crafting Recipes", 313 | "You'll learn how to create custom crafting recipes using three different examples!" 314 | ), 315 | Lecture( 316 | "34165536", 317 | "Persistent Data Containers", 318 | "You'll learn how to use persistent data containers with entities, tile entities, itemstacks and chunks. This data persists over server restart." 319 | ), 320 | Lecture( 321 | "29869474", 322 | "Cooldowns", 323 | "You'll learn about the logic behind cooldowns and how to make the most efficient and effective system." 324 | ), 325 | Lecture( 326 | "29869476", 327 | "Holograms", 328 | "You'll learn how to create cool-looking single and multi-line holograms and how to make them clickable!" 329 | ), 330 | Lecture( 331 | "37363462", 332 | "Display Entities (1.19.4+)", 333 | "You'll learn how to create amazing floating text, blocks and items without Texture Packs." 334 | ), 335 | Lecture( 336 | "29869478", 337 | "Setting Permissions", 338 | "You'll learn the (annoyingly complicated) way of manually applying and removing specific permissions on players." 339 | ), 340 | Lecture( 341 | "29869482", 342 | "Scoreboard #1 - Static", 343 | "You'll learn how to display information that is unlikely to change on a player's sidebar!" 344 | ), 345 | Lecture( 346 | "29869486", 347 | "Scoreboard #2 - Dynamic", 348 | "You'll learn how to add smooth seamless data which changes in your sidebar, alongside the static lines!" 349 | ) 350 | ) 351 | ) 352 | 353 | lectures.addAll( 354 | mutableListOf( 355 | Lecture( 356 | "29869488", 357 | "Nametags", 358 | "You'll learn how to set player prefixes and suffixes (i.e. nametags) which show up on the tablist and above the player." 359 | ), 360 | Lecture( 361 | "29869490", 362 | "PROJECT: Rank System", 363 | "You'll learn how to make a fully custom rank system, with all of your own savable ranks that work with nametags, chat formats & setting permissions." 364 | ), 365 | Lecture( 366 | "29961814", 367 | "Clickable/Hoverable Chat", 368 | "You'll learn how to make clickable and hoverable text show in chat!" 369 | ), 370 | Lecture( 371 | "29961816", 372 | "Clickable/Hoverable Books", 373 | "You'll learn how to make optional clickable and hoverable text within custom books." 374 | ), 375 | Lecture( 376 | "29961818", 377 | "Forcing Custom Skins", 378 | "You'll learn the (slightly-complex) way of forcing a player to have a custom skin." 379 | ), 380 | Lecture( 381 | "29961822", 382 | "Custom Events", 383 | "You'll learn how to create a fully custom event, with optional cancellable implementation, and how to call it and listen to it." 384 | ), 385 | Lecture( 386 | "29961824", 387 | "Using Plugin APIs", 388 | "You'll learn how to connect to other plugins using theirs APIs - namely WorldEdit in this example!" 389 | ), 390 | Lecture( 391 | "29961826", 392 | "Creating Custom API", 393 | "You'll learn how to convert your plugin into an API usable by others, following the best conventions and practises." 394 | ), 395 | Lecture( 396 | "30742714", 397 | "Creating & Playing Note Block Music", 398 | "You'll learn (a) how to create your very own Note Block music and (b) how to play this or any other public Note Block song to players!" 399 | ), 400 | Lecture( 401 | "35190590", 402 | "Anvil Text Input", 403 | "You'll learn how to accept player text input through an anvil and how to do things with the input!" 404 | ) 405 | ) 406 | ) 407 | 408 | lectures.addAll( 409 | mutableListOf( 410 | Lecture( 411 | "29961834", 412 | "Regions", 413 | "You'll learn how to create a custom 3d region between two bounds, allowing for mass block updates, player tracking etc." 414 | ), 415 | Lecture( 416 | "32195856", 417 | "Custom Model Data (w/ Resource Packs)", 418 | "You'll learn how resource packs work and how to utilise custom model data in 1.14+ to add (near) unlimited models." 419 | ), 420 | Lecture( 421 | "35258896", 422 | "Custom Enchantments", 423 | "You'll learn how to create your own unique enchantment, how to apply it to an item and set the impact of it." 424 | ), 425 | Lecture( 426 | "35440392", 427 | "AI Chat", 428 | "You'll learn how to make it so you can have conversations with an AI bot and give it certain characters to act as." 429 | ), 430 | Lecture( 431 | "29961836", 432 | "GUI Pages", 433 | "You'll learn how to make pages for GUIs which dynamically generate depending on contents!" 434 | ), 435 | Lecture( 436 | "30742708", 437 | "Discord/Minecraft Bridge", 438 | "You'll learn how to create a link between your Minecraft and Discord bot to allow for limitless possibilities; from syncing data, to sending messages, to sharing events etc." 439 | ), 440 | Lecture( 441 | "29961840", 442 | "PROJECT: Command Manager (No Plugin.yml)", 443 | "You'll learn how to bring together all the command and tab completion content to make an efficient, scalable command manager which requires no plugin.yml info!" 444 | ), 445 | Lecture( 446 | "29860486", 447 | "Creating & Building Database", 448 | "You'll learn all about databases, their benefits and use-cases, aswell as creating and building your own." 449 | ), 450 | Lecture( 451 | "29860488", 452 | "Connecting to Database", 453 | "You'll learn how to connect to your database - whether local or externally hosted - using the the correct settings." 454 | ), 455 | Lecture( 456 | "29860490", 457 | "Key SQL Commands (Querying, Updating etc.)", 458 | "You'll learn the essential SQL commands, everything from setting, updating, deleting and retrieving information from your database!" 459 | ) 460 | ) 461 | ) 462 | 463 | lectures.addAll( 464 | mutableListOf( 465 | Lecture( 466 | "29860494", 467 | "Player Profiles", 468 | "You'll learn how to make a wrapper for each player which provides useful functionality to connect to the database in setting and retrieving information." 469 | ), 470 | Lecture( 471 | "29860496", 472 | "Using HikariCP", 473 | "You'll learn how to use HikariCP - a connection pooling API - in order to speed up requests and make your life easier!" 474 | ), 475 | Lecture( 476 | "34168474", 477 | "Using MongoDB (Installation, Connecting, Using)", 478 | "You'll learn how to use MongoDB as an alternative to an SQL database. You'll download the relevant software and learn how to push and pull data." 479 | ), 480 | Lecture( 481 | "30007374", 482 | "Mechanics #1", 483 | "You'll learn about what we'll be making and creating the configuration file which will govern the rest of our minigame." 484 | ), 485 | Lecture( 486 | "30007376", 487 | "Mechanics #2", 488 | "You'll create the arena manager and arena classes which control the games." 489 | ), 490 | Lecture( 491 | "30007380", 492 | "Mechanics #3", 493 | "You'll create the countdown and game classes which compliment the arena class to provide the minigame experience." 494 | ), 495 | Lecture( 496 | "30007384", 497 | "Mechanics #4", 498 | "You'll create the required listeners and an arena command to ensure the most user-friendly approach. Then we're done!" 499 | ), 500 | Lecture( 501 | "30126494", 502 | "PROJECT: Bedwars", 503 | "You'll learn how to convert our simple minigame mechanics framework into a fully functional Bedwars game!" 504 | ), 505 | Lecture( 506 | "30126466", 507 | "Kits (w/ Selection GUI)", 508 | "You'll learn how to implement kits and a selection GUI to the minigame." 509 | ), 510 | Lecture( 511 | "30126470", 512 | "Teams (w/ Selection GUI)", 513 | "You'll learn how to implement teams and a selection GUI to the minigame." 514 | ) 515 | ) 516 | ) 517 | 518 | lectures.addAll( 519 | mutableListOf( 520 | Lecture( 521 | "30126476", 522 | "Resetting Maps", 523 | "You'll learn how to add multiple maps and make them reset after each game." 524 | ), 525 | Lecture( 526 | "30126478", 527 | "Arena Signs", 528 | "You'll learn how to create multiple scalable signs which allow users to join specific arenas." 529 | ), 530 | Lecture( 531 | "30126480", 532 | "NPC Join", 533 | "You'll learn how to create multiple scalable NPCs which allow users to join specific arenas." 534 | ), 535 | Lecture( 536 | "30126484", 537 | "Customisable Messages File", 538 | "You'll learn how to make all of the messages within the minigame fully configurable." 539 | ), 540 | Lecture( 541 | "30126488", 542 | "Network Compatability", 543 | "You'll learn how to convert our setup to a BungeeCord network to have an unlimited server system." 544 | ), 545 | Lecture( 546 | "30126490", 547 | "Supporting Multiple Games (Converting to Engine)", 548 | "You'll learn how to convert our setup to an engine which supports unlimited games which can be added extremely easily." 549 | ), 550 | Lecture( 551 | "29979080", 552 | "Creating Cosmetic Foundation", 553 | "You'll learn about the nature of cosmetics and how we're going to proceed in this section! We make a base GUI, command and listener." 554 | ), 555 | Lecture( 556 | "29979082", 557 | "Cosmetic #1 - Hats", 558 | "You'll learn how to add custom player skulls onto peoples heads - as well as creating a fully functioning toggling system and paving the way for future lectures." 559 | ), 560 | Lecture( 561 | "30299092", 562 | "Cosmetic #2 - Trails", 563 | "You'll learn how to spawn cool particles behind players as they move!" 564 | ), 565 | Lecture( 566 | "30276474", 567 | "Saving Cosmetic Data (YML Files)", 568 | "You'll learn how to save what cosmetics people own in local YML files." 569 | ) 570 | ) 571 | ) 572 | 573 | lectures.addAll( 574 | mutableListOf( 575 | Lecture( 576 | "29860512", 577 | "Creating BungeeCord Network (Windows)", 578 | "You'll learn how to create your very own BungeeCord network using Windows OS, setting up two Spigot servers to connect to each other!" 579 | ), 580 | Lecture( 581 | "29860514", 582 | "Creating BungeeCord Network (Mac)", 583 | "You'll learn to create your very own BungeeCord network using Mac OS!" 584 | ), 585 | Lecture( 586 | "35192316", 587 | "Creating BungeeCord Network (Linux)", 588 | "You'll learn to create your very own BungeeCord network using Linux (Ubuntu 22.04)!" 589 | ), 590 | Lecture( 591 | "29860516", 592 | "First BungeeCord Plugin", 593 | "You'll learn how to create your very first BungeeCord plugin using Maven & IntelliJ!" 594 | ), 595 | Lecture( 596 | "29860518", 597 | "Bungee Commands, Events & Schedulers", 598 | "You'll learn how to make basic commands, listen to events and run schedulers using the BungeeCord API." 599 | ), 600 | Lecture( 601 | "29860520", 602 | "Bungee Command Tab Complete", 603 | "You'll learn how to add custom tab complete options to your commands in BungeeCord!" 604 | ), 605 | Lecture( 606 | "29860522", 607 | "Bungee Setting MOTD, Network Icon & Player Count", 608 | "You'll learn how to set a custom MOTD, server favicon, online player count, max player count and version information." 609 | ), 610 | Lecture( 611 | "31079722", 612 | "PROJECT: Network Private Messaging", 613 | "You'll learn how to make a private messaging system across servers by bringing together a load of content from this section." 614 | ), 615 | Lecture( 616 | "29961838", 617 | "Plugin Messaging (Cross-Server Communication)", 618 | "You'll learn how to use the BungeeCord Plugin Messaging Channel in order to communicate between Spigot servers." 619 | ), 620 | Lecture( 621 | "33985306", 622 | "UUID/Name Conversion (Mojang API)", 623 | "This lecture you'll learn how to convert names and UUIDs to be used across the network." 624 | ) 625 | ) 626 | ) 627 | 628 | lectures.addAll( 629 | mutableListOf( 630 | Lecture( 631 | "35042092", 632 | "Understanding NMS", 633 | "You'll learn everything about NMS. What it is, why it exists and how to use it. We'll use this info in the following videos to do some cool things!" 634 | ), 635 | Lecture( 636 | "35416526", 637 | "Sending Packets", 638 | "You'll learn how to find, interpret and send packets to players." 639 | ), 640 | Lecture( 641 | "35042646", 642 | "PROJECT: Player NPCs", 643 | "You'll learn how to spawn player NPCs with custom skins and locations, as well as giving their custom armor or items to hold." 644 | ), 645 | Lecture( 646 | "35247942", 647 | "Custom Packet Listener (Clickable NPCs)", 648 | "You'll learn how to create a custom packet listener to read incoming and outgoing packets. The example in this lecture is making a Player NPC clickable." 649 | ), 650 | Lecture( 651 | "32065494", 652 | "Finding Ideas, Planning & Staying Motivated", 653 | "You'll learn the best tips on finding ideas, how to setup the best planning environment and how to stay motivated on those long painful projects." 654 | ), 655 | Lecture( 656 | "32129970", 657 | "Writing & Keeping Code Clean", 658 | "You'll learn all the best industry-standard practises on keeping your code clean and readable." 659 | ), 660 | Lecture( 661 | "29860562", 662 | "Optimised Start.bat Flags", 663 | "You'll learn the most optimized and stable flags to incorporate in your start.bat instructions to help the server run smoother and with higher TPS!" 664 | ), 665 | Lecture( 666 | "35189142", 667 | "PlaceholderAPI (Using & Creating)", 668 | "You'll learn how to hook into an extremely valuable API named PlaceholderAPI which is used by all the big plugins. You'll know how to use existing placeholders and even create your own for your own plugin!" 669 | ), 670 | Lecture( 671 | "30196408", 672 | "Creating Multi-Version Plugins", 673 | "You'll learn the best advice for creating plugins which support multiple/different Minecraft versions." 674 | ), 675 | Lecture( 676 | "31972738", 677 | "Supporting Multiple Languages", 678 | "You'll learn the best practises and methods of supporting multiple speaking languages to allow for an easier user experience." 679 | ) 680 | ) 681 | ) 682 | 683 | lectures.addAll( 684 | mutableListOf( 685 | Lecture( 686 | "33642386", 687 | "Adding Plugin Metrics (bStats)", 688 | "You'll learn how to start collecting information about where and how your plugin is used using popular software called bStats." 689 | ), 690 | Lecture( 691 | "29860564", 692 | "Considering Spigot Forks", 693 | "You'll learn about alternatives to Spigot, such as Paper and Waterfall, and why you should and shouldn't use them." 694 | ), 695 | Lecture( 696 | "35191976", 697 | "Using Gradle", 698 | "You'll learn how to generate Minecraft projects with Gradle on IntelliJ, how to use dependencies and how to build your projects." 699 | ), 700 | Lecture( 701 | "29860566", 702 | "Using GitHub", 703 | "You'll learn how to push your projects to GitHub, manage them and take advantage of GitHub's features." 704 | ), 705 | Lecture( 706 | "35179306", 707 | "Publishing to Maven & Gradle", 708 | "You'll learn how to make your plugin API accessable to other developers through Maven and Gradle build tools." 709 | ), 710 | Lecture( 711 | "34395636", 712 | "Student Discounts", 713 | "You'll learn how to enjoy the awesome student discounts offered by GitHub." 714 | ), 715 | Lecture( 716 | "29860568", 717 | "Plugin Licensing", 718 | "You'll learn how to add a mandatory license system to your plugin which helps protect it from piracy." 719 | ), 720 | Lecture( 721 | "29860570", 722 | "Maximizing Plugin Sales", 723 | "You'll learn my tried and tested strategies on pricing, presenting and how to best market your Spigot plugin to make the most $$$. I use Spigot as the case study marketplace, but the tips & tricks remain the same." 724 | ), 725 | Lecture( 726 | "29860572", 727 | "Ending...", 728 | "The final lecture... it's been a long ride! Let's talk about the future..." 729 | ), 730 | Lecture( 731 | "29860548", 732 | "Java Basics #1", 733 | "You will learn:\n- Introduction to Java & OOP\n- Variables & Objects\n- Data Types (Primitive & Non Primitive)\n- Methods (including Parameters & Returning)" 734 | ) 735 | ) 736 | ) 737 | 738 | lectures.addAll( 739 | mutableListOf( 740 | Lecture( 741 | "29860550", 742 | "Java Basics #2", 743 | "You will learn:\n- If Statements & Operators\n- Null\n- Exceptions & Try/Catch\n- Classes, Instances & Constructors" 744 | ), 745 | Lecture( 746 | "29860554", 747 | "Java Basics #4", 748 | "You will learn:\n- Inheritance (Superclass & Subclass)\n- Abstract Methods & Classes\n- Enumerators\n- Static Keyword" 749 | ), 750 | Lecture( 751 | "29860556", 752 | "Java Basics #5", 753 | "You will learn:\n- Switch Statement\n- Date & Time\n- Randomness" 754 | ), 755 | Lecture( 756 | "29860552", 757 | "Java Basics #3", 758 | "You will learn:\n- Arrays\n- List (& LinkedList)\n- HashMap (& LinkedHashMap)\n- Loops (For & While)" 759 | ) 760 | ) 761 | ) 762 | } 763 | 764 | } --------------------------------------------------------------------------------