Skip to content

Commit

Permalink
feat: add ability to choose roles with slash commands. (#351)
Browse files Browse the repository at this point in the history
  • Loading branch information
DuncanCasteleyn authored Oct 2, 2024
1 parent 4c21a53 commit 1cb9dcc
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 46 deletions.
3 changes: 1 addition & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ dependencies {
testImplementation(group = "org.springframework.boot", name = "spring-boot-starter-test")
testImplementation(group = "org.mockito.kotlin", name = "mockito-kotlin", version = mockitoKotlinVersion)

annotationProcessor(group = "org.springframework.boot", name = "spring-boot-configuration-processor")
kapt(group = "org.springframework.boot", name = "spring-boot-configuration-processor")
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}

configurations {
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3'

services:
database:
image: mariadb:11.5.2@sha256:4b812bbd9a025569fbe5a7a70e4a3cd3af53aa36621fecb1c2e108af2113450a
ports:
- "3306:3306"
volumes:
- .:/srv/jekyll
environment:
MARIADB_USER: spring
MARIADB_PASSWORD: test
MARIADB_DATABASE: discordmodbot
MARIADB_ROOT_PASSWORD: test
redis:
image: redis:7.4.0@sha256:878983f8f5045b28384fc300268cec62bca3b14d5e1a448bec21f28cfcc7bf78
ports:
- "6379:6379"

This file was deleted.

31 changes: 14 additions & 17 deletions src/main/kotlin/be/duncanc/discordmodbot/bot/commands/Ping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,36 @@
package be.duncanc.discordmodbot.bot.commands

import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.session.ReadyEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.interactions.commands.build.CommandData
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData
import org.springframework.stereotype.Component

@Component
class Ping : ListenerAdapter(), Command {
class Ping : ListenerAdapter(), SlashCommand {
companion object {
private const val COMMAND = "ping"
private const val DESCRIPTION = "responds with \"pong!\"."
}

override fun onReady(event: ReadyEvent) {
event.jda.upsertCommand("ping", DESCRIPTION).queue()
}

override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
if (event.name == "ping") {
event.deferReply().queue { hook ->
event.jda.restPing.queue { ping ->
hook.editOriginal(
"""pong!
if (event.name != "ping") return

event.deferReply().queue { hook ->
event.jda.restPing.queue { ping ->
hook.editOriginal(
"""pong!
It took Discord ${event.jda.gatewayPing} milliseconds to respond to our last heartbeat (gateway).
The REST API responded within $ping milliseconds"""
).queue()
}

).queue()
}

}

}

override fun getCommandsData(): List<CommandData> {
return listOf() // TODO rework
override fun getCommandsData(): List<SlashCommandData> {
return listOf(Commands.slash("ping", DESCRIPTION))
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package be.duncanc.discordmodbot.bot.commands

import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData

interface SlashCommand {
fun getCommandsData(): List<SlashCommandData>

fun onSlashCommandInteraction(event: SlashCommandInteractionEvent)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package be.duncanc.discordmodbot.bot.config

import be.duncanc.discordmodbot.bot.commands.Command
import be.duncanc.discordmodbot.bot.commands.SlashCommand
import be.duncanc.discordmodbot.data.configs.properties.DiscordModBotConfig
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
Expand Down Expand Up @@ -28,7 +28,7 @@ class BotConfiguration {
@Bean(destroyMethod = "shutdown")
fun jda(
listenerAdapters: Array<ListenerAdapter>,
commands: Array<Command>,
slashCommands: Array<SlashCommand>,
discordModBotConfig: DiscordModBotConfig
): JDA {
val jda = JDABuilder.create(discordModBotConfig.botToken, INTENTS)
Expand All @@ -39,7 +39,7 @@ class BotConfiguration {
.build()

val updateCommands = jda.updateCommands()
commands.forEach {
slashCommands.forEach {
updateCommands.addCommands(it.getCommandsData())
}
updateCommands.queue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit

@Deprecated(
message = """
This is being replaced with a new modern system Roles class slowly.
Only security and bug fixes should be applied to this class, slowly parts will be removed or disabled from this class.
"""
)
@Component
class IAmRoles
@Autowired constructor(
Expand Down
166 changes: 166 additions & 0 deletions src/main/kotlin/be/duncanc/discordmodbot/bot/services/Roles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package be.duncanc.discordmodbot.bot.services

import be.duncanc.discordmodbot.bot.commands.SlashCommand
import be.duncanc.discordmodbot.data.configs.properties.DiscordModBotConfig
import be.duncanc.discordmodbot.data.services.IAmRolesService
import net.dv8tion.jda.api.entities.Guild
import net.dv8tion.jda.api.entities.Role
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.interactions.InteractionHook
import net.dv8tion.jda.api.interactions.commands.build.Commands
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData
import net.dv8tion.jda.api.interactions.components.buttons.Button
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu
import org.springframework.stereotype.Component

private const val CATEGORY_COMPONENT_ID = "role-choose-category"
private const val ROLE_COMPONENT_ID = "role-choose-role"
private const val MORE_BUTTON_ID_PREFIX = "role-more-"

@Component
class Roles(
private val iAmRolesService: IAmRolesService,
private val discordModBotConfig: DiscordModBotConfig
) : ListenerAdapter(), SlashCommand {
override fun getCommandsData(): List<SlashCommandData> {
return listOf(
Commands.slash("role", "Allows you to select roles that you can freely assign to yourself")
.addSubcommands(
SubcommandData("assign", "assign yourself a role")
)
.setGuildOnly(true)
)
}


override fun onSlashCommandInteraction(event: SlashCommandInteractionEvent) {
val guild = event.guild

if (event.name != "role" || guild == null) return

when (event.subcommandName) {
"assign" -> {
event.deferReply(true).queue {
val categoryMenu = StringSelectMenu.create(CATEGORY_COMPONENT_ID)

iAmRolesService.getAllCategoriesForGuild(guild.idLong).forEach { category ->
categoryMenu.addOption(category.categoryName, category.categoryId.toString())
}

it.sendMessage("Select the category you would like a role from")
.addActionRow(categoryMenu.build())
.queue()
}
}

else -> {
event.deferReply(true).queue { reply ->
reply.sendMessage("That's not a valid subcommand for this command.").queue()
}
}
}
}

override fun onStringSelectInteraction(event: StringSelectInteractionEvent) {
val guild = event.guild
val componentId = event.componentId

if ((componentId != CATEGORY_COMPONENT_ID && componentId != ROLE_COMPONENT_ID) || guild == null) return

if (componentId == CATEGORY_COMPONENT_ID) {
event.deferReply(true).queue { reply ->
val categoryId = event.values.first().toLong()

replyWithRoleMenu(reply, guild, categoryId, 0)
}
}

if (componentId == ROLE_COMPONENT_ID) {
val ephemeral = event.user.idLong != discordModBotConfig.ownerId

event.deferReply(ephemeral).queue { reply ->
val roleId = event.values.first().toLong()

val roleCategory = iAmRolesService.getCategoryByRoleId(guild.idLong, roleId)

val member = event.member ?: return@queue

val assignedRolesFromCategory = member.roles.count { role: Role? ->
if (role == null) {
return@count false
}

roleCategory.roles.contains(role.idLong)
}

if (assignedRolesFromCategory >= roleCategory.allowedRoles) {
reply.sendMessage("You already have the max amount of roles from this category.").queue()
return@queue
}


guild.getRoleById(roleId)?.let {
guild.addRoleToMember(event.user, it)
.reason("User request role through \"/role assign\" slash command.").queue {
reply.sendMessage("You're role was assigned.").queue()
}
}
}

}
}

override fun onButtonInteraction(event: ButtonInteractionEvent) {
val guild = event.guild
val componentId = event.componentId

if (!componentId.startsWith(MORE_BUTTON_ID_PREFIX) || guild == null) return

val ephemeral = event.user.idLong != discordModBotConfig.ownerId

event.deferReply(ephemeral).queue { reply ->
val metaData = componentId.removePrefix(MORE_BUTTON_ID_PREFIX).split("-")

val categoryId = metaData[0].toLong()
val pageNumber = metaData[1].toInt() + 1

replyWithRoleMenu(reply, guild, categoryId, pageNumber)
}


}

private fun replyWithRoleMenu(
reply: InteractionHook,
guild: Guild,
categoryId: Long,
page: Int
) {
val chunkedRoles = getChunkedRoles(guild, categoryId)

val roleSelectMenu = StringSelectMenu.create(ROLE_COMPONENT_ID)

chunkedRoles[page].forEach {
roleSelectMenu.addOption(it.name, it.id)
}

reply.sendMessage("Select the role you would like to get assigned.")
.addActionRow(roleSelectMenu.build())
.addActionRow(
Button.primary("$MORE_BUTTON_ID_PREFIX$categoryId-$page", "More roles")
.withDisabled(chunkedRoles.size <= page + 1)
)
.queue()
}

private fun getChunkedRoles(
guild: Guild,
categoryId: Long
) = iAmRolesService.getRoleIds(guild.idLong, categoryId)
.mapNotNull { guild.getRoleById(it) }
.chunked(StringSelectMenu.OPTIONS_MAX_AMOUNT)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@ package be.duncanc.discordmodbot.data.entities
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
import java.io.Serializable
import java.util.*

@Entity
@Table(name = "i_am_roles_categories")
@IdClass(IAmRolesCategory.IAmRoleId::class)
data class IAmRolesCategory
constructor(
data class IAmRolesCategory(
@Id
@Column(updatable = false)
val guildId: Long,
Expand All @@ -46,7 +44,7 @@ constructor(
@Column(nullable = false)
@field:NotNull
@CollectionTable(name = "i_am_roles_category_roles")
@ElementCollection
@ElementCollection(fetch = FetchType.EAGER)
val roles: MutableSet<Long> = HashSet()
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ import org.springframework.transaction.annotation.Transactional
interface IAmRolesRepository : JpaRepository<IAmRolesCategory, IAmRolesCategory.IAmRoleId> {
@Transactional(readOnly = true)
fun findByGuildId(guildId: Long): Iterable<IAmRolesCategory>

@Transactional(readOnly = true)
fun findByRolesContainsAndGuildId(roles: MutableSet<Long>, guildId: Long): Iterable<IAmRolesCategory>
}
Loading

0 comments on commit 1cb9dcc

Please sign in to comment.