Skip to content

Commit 93143a1

Browse files
RacciRacci
Racci
authored and
Racci
committed
feat: Add RoleSelector
1 parent 9ee092f commit 93143a1

File tree

4 files changed

+258
-4
lines changed

4 files changed

+258
-4
lines changed

src/main/kotlin/dev/racci/elixir/ElixirBot.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import dev.racci.elixir.extensions.commands.moderation.Report
1818
import dev.racci.elixir.extensions.commands.util.Custom
1919
import dev.racci.elixir.extensions.commands.util.Github
2020
import dev.racci.elixir.extensions.commands.util.Ping
21+
import dev.racci.elixir.extensions.RoleSelector
2122
import dev.racci.elixir.support.ThreadInviter
2223
import dev.racci.elixir.utils.BOT_TOKEN
2324
import dev.racci.elixir.utils.CONFIG_PATH
@@ -33,7 +34,7 @@ import org.kohsuke.github.GitHub
3334
import org.kohsuke.github.GitHubBuilder
3435

3536
val configPath: Path = Paths.get(CONFIG_PATH)
36-
val config: TomlTable = Toml.from(Files.newInputStream(configPath))
37+
val config: TomlTable = Toml.from(Files.newInputStream(Paths.get("$configPath/config.toml")))
3738
var github: GitHub? = null
3839
var bot by Delegates.notNull<ExtensibleBot>()
3940

@@ -49,7 +50,7 @@ suspend fun main() {
4950
}
5051
intents {
5152
+Intent.GuildMembers
52-
// +Intent.GuildMessageReactions
53+
+Intent.GuildMessageReactions
5354
}
5455

5556
chatCommands {
@@ -67,6 +68,7 @@ suspend fun main() {
6768
add(::Github)
6869
add(::Custom)
6970
add(::StatChannels)
71+
add(::RoleSelector)
7072

7173
extPhishing {
7274
appName = "Elixir Bot"

src/main/kotlin/dev/racci/elixir/database/DatabaseManager.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ object DatabaseManager {
4343
override val primaryKey = PrimaryKey(id)
4444
}
4545

46+
object RoleSelector: Table("role_selector") {
47+
48+
val name = text("name")
49+
val messageId = text("messageId")
50+
51+
override val primaryKey = PrimaryKey(name)
52+
53+
}
54+
4655
fun startDatabase() {
4756
try {
4857
val database = Path.of("database.db")
@@ -58,6 +67,7 @@ object DatabaseManager {
5867

5968
transaction {
6069
SchemaUtils.createMissingTablesAndColumns(Warn)
70+
SchemaUtils.createMissingTablesAndColumns(RoleSelector)
6171
}
6272
}
6373
}
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package dev.racci.elixir.extensions
2+
3+
import com.github.jezza.Toml
4+
import com.github.jezza.TomlArray
5+
import com.github.jezza.TomlTable
6+
import com.kotlindiscord.kord.extensions.components.buttons.PublicInteractionButtonContext
7+
import com.kotlindiscord.kord.extensions.components.components
8+
import com.kotlindiscord.kord.extensions.components.publicButton
9+
import com.kotlindiscord.kord.extensions.extensions.Extension
10+
import com.kotlindiscord.kord.extensions.utils.hasRole
11+
import com.kotlindiscord.kord.extensions.utils.parseBoolean
12+
import dev.kord.common.annotation.KordExperimental
13+
import dev.kord.common.annotation.KordPreview
14+
import dev.kord.common.annotation.KordUnsafe
15+
import dev.kord.common.entity.ButtonStyle
16+
import dev.kord.common.entity.DiscordPartialEmoji
17+
import dev.kord.common.entity.Snowflake
18+
import dev.kord.common.entity.optional.OptionalBoolean
19+
import dev.kord.core.behavior.MemberBehavior
20+
import dev.kord.core.behavior.RoleBehavior
21+
import dev.kord.core.behavior.channel.GuildMessageChannelBehavior
22+
import dev.kord.core.behavior.channel.createMessage
23+
import dev.kord.core.behavior.edit
24+
import dev.kord.core.behavior.interaction.followUpEphemeral
25+
import dev.kord.core.entity.Member
26+
import dev.kord.core.entity.Message
27+
import dev.racci.elixir.configPath
28+
import dev.racci.elixir.database.DatabaseManager
29+
import dev.racci.elixir.utils.GUILD_ID
30+
import java.nio.file.Files
31+
import java.nio.file.Paths
32+
import java.util.*
33+
import kotlin.properties.Delegates
34+
import kotlinx.coroutines.flow.count
35+
import org.jetbrains.exposed.sql.deleteWhere
36+
import org.jetbrains.exposed.sql.select
37+
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
38+
39+
class RoleSelector: Extension() {
40+
41+
override var name = "roles"
42+
43+
private val roles: TomlTable = Toml.from(Files.newInputStream(Paths.get("$configPath/roles.toml")))
44+
private var channel by Delegates.notNull<GuildMessageChannelBehavior>()
45+
46+
@OptIn(KordUnsafe::class, KordExperimental::class)
47+
override suspend fun setup() {
48+
val roleSelectors: TomlArray = roles.get("roleselector") as TomlArray
49+
channel = kord.unsafe.guildMessageChannel(GUILD_ID, (Snowflake(roles["roleChannel"] as ULong)))
50+
newSuspendedTransaction {
51+
DatabaseManager.RoleSelector.messageId.ddl.forEach {
52+
val message = channel.getMessageOrNull(Snowflake(it))
53+
if(message == null) {
54+
DatabaseManager.RoleSelector.deleteWhere { DatabaseManager.RoleSelector.messageId eq it }
55+
}
56+
}
57+
}
58+
for(rsls in roleSelectors) {
59+
val rsl = rsls as TomlTable
60+
addRoleSelector(
61+
rsl.get("name") as String,
62+
rsl.getOrDefault("attachment", "") as String,
63+
rsl.getOrDefault("limit", -1) as Int,
64+
rsl.getOrDefault("removable", true) as Boolean,
65+
rsl.get("role") as TomlArray?
66+
)
67+
}
68+
}
69+
70+
private suspend fun ensureExistingMessage(
71+
name: String,
72+
attachment: String
73+
): Message {
74+
return if(!DatabaseManager.RoleSelector.name.ddl.any { it == name }) {
75+
channel.createMessage {
76+
addFile(Paths.get(attachment))
77+
}
78+
} else {
79+
channel.getMessage(
80+
Snowflake(
81+
DatabaseManager.RoleSelector.select {
82+
DatabaseManager.RoleSelector.name eq name
83+
}.first()[DatabaseManager.RoleSelector.messageId]
84+
)
85+
)
86+
}
87+
}
88+
89+
private suspend fun removeCurrentRoleCheck(
90+
removable: Boolean,
91+
member: MemberBehavior,
92+
roleBehavior: RoleBehavior,
93+
context: PublicInteractionButtonContext
94+
): String? { // Return a nullable string for easily returning if we need to
95+
return if(member.asMember().hasRole(roleBehavior)) {
96+
if(removable) {
97+
member.removeRole(roleBehavior.id)
98+
} else {
99+
context.interactionResponse.followUpEphemeral {
100+
ephemeral = true
101+
content = "Sorry, You cannot the remove the ${roleBehavior.fetchRole().name} role."
102+
}
103+
}
104+
null
105+
} else ""
106+
}
107+
108+
private suspend fun roleLimitCheck(
109+
limit: Int,
110+
member: MemberBehavior,
111+
selectorRoles: List<RoleBehavior>,
112+
context: PublicInteractionButtonContext,
113+
): String? {
114+
return if(limit != -1 && member.asMember().roles.count(selectorRoles::contains) > limit) {
115+
context.interactionResponse.followUpEphemeral {
116+
ephemeral = true
117+
content = "Sorry, You already have the maximum amount of roles from this selector."
118+
}
119+
null
120+
} else ""
121+
}
122+
123+
private suspend fun roleIncompatibleWithCheck(
124+
incompatibleWith: Collection<RoleBehavior>,
125+
roleBehavior: RoleBehavior,
126+
member: MemberBehavior,
127+
context: PublicInteractionButtonContext,
128+
): String? {
129+
val incompatible = member.asMember().getAnyRole(incompatibleWith)
130+
return if(incompatible != null) {
131+
context.interactionResponse.followUpEphemeral {
132+
ephemeral = true
133+
content = "Sorry, You ${roleBehavior.fetchRole().name} is incompatible with ${incompatible.fetchRole().name}"
134+
}
135+
null
136+
} else ""
137+
}
138+
139+
@OptIn(KordPreview::class)
140+
@Suppress("UNCHECKED_CAST")
141+
private suspend fun addRoleSelector(
142+
name: String,
143+
attachment: String,
144+
limit: Int,
145+
removable: Boolean,
146+
roles: TomlArray?,
147+
) {
148+
val message = ensureExistingMessage(name, attachment)
149+
message.edit {
150+
components {
151+
// Remove all so we have the order and everything fully up to date
152+
removeAll()
153+
154+
if(roles == null) return@components
155+
val selectorRoles = roles.map { RoleBehavior(GUILD_ID, Snowflake((it as TomlTable)["roleId"] as ULong), kord) }
156+
157+
for(role in roles.asList() as List<TomlTable>) {
158+
val roleBehavior = selectorRoles.first { it.id.value == role["roleId"] as ULong }
159+
160+
publicButton {
161+
label = role["name"] as String
162+
val emoji = (role["emoji"] as String).split(':', limit = 3)
163+
partialEmoji = DiscordPartialEmoji.dsl {
164+
id = Snowflake(emoji[0])
165+
this.name = emoji[1]
166+
animated = OptionalBoolean.Value(emoji[2].parseBoolean(Locale.ENGLISH) ?: false)
167+
}
168+
style = ButtonStyle.Primary
169+
170+
val incompatibleWith = (role["incompatibleWith"] as Array<ULong>).map {
171+
RoleBehavior(GUILD_ID, Snowflake(it), kord)
172+
}
173+
174+
action {
175+
val member = member ?: return@action
176+
177+
removeCurrentRoleCheck(
178+
removable,
179+
member,
180+
roleBehavior,
181+
this
182+
) ?: return@action
183+
184+
roleLimitCheck(
185+
limit,
186+
member,
187+
selectorRoles,
188+
this,
189+
) ?: return@action
190+
191+
roleIncompatibleWithCheck(
192+
incompatibleWith,
193+
roleBehavior,
194+
member,
195+
this,
196+
) ?: return@action
197+
198+
member.addRole(roleBehavior.id, "Role Selections")
199+
}
200+
}
201+
}
202+
}
203+
}
204+
}
205+
}
206+
207+
fun Member.hasAnyRole(roles: Collection<RoleBehavior>): Boolean {
208+
return if (roles.isEmpty()) {
209+
true
210+
} else {
211+
val ids = roles.map { it.id }
212+
roleIds.any { ids.contains(id) }
213+
}
214+
}
215+
216+
fun Member.getAnyRole(roles: Collection<RoleBehavior>): RoleBehavior? {
217+
return if (roles.isEmpty()) {
218+
null
219+
} else {
220+
val ids = roles.map { it.id }
221+
roleBehaviors.firstOrNull { ids.contains(it.id) }
222+
}
223+
}
224+
class DiscordPartialEmojiDSL {
225+
226+
var id: Snowflake? = null
227+
var name: String? = null
228+
var animated: OptionalBoolean = OptionalBoolean.Missing
229+
230+
fun build(): DiscordPartialEmoji = DiscordPartialEmoji(id, name, animated)
231+
}
232+
233+
inline fun DiscordPartialEmoji.Companion.dsl(block: DiscordPartialEmojiDSL.() -> Unit): DiscordPartialEmoji {
234+
val dsl = DiscordPartialEmojiDSL()
235+
dsl.block()
236+
return dsl.build()
237+
}

src/main/kotlin/dev/racci/elixir/extensions/commands/util/Custom.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package dev.racci.elixir.extensions.commands.util
22

3+
import com.github.jezza.Toml
34
import com.github.jezza.TomlArray
45
import com.github.jezza.TomlTable
56
import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE
@@ -8,7 +9,9 @@ import com.kotlindiscord.kord.extensions.extensions.Extension
89
import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand
910
import com.kotlindiscord.kord.extensions.types.respond
1011
import dev.kord.rest.builder.message.create.embed
11-
import dev.racci.elixir.config
12+
import dev.racci.elixir.configPath
13+
import java.nio.file.Files
14+
import java.nio.file.Paths
1215
import kotlinx.datetime.Clock
1316

1417
/**
@@ -19,8 +22,10 @@ class Custom: Extension() {
1922

2023
override var name = "custom"
2124

25+
private val commands: TomlTable = Toml.from(Files.newInputStream(Paths.get("$configPath/commands.toml")))
26+
2227
override suspend fun setup() {
23-
val commands: TomlArray = config.get("command") as TomlArray
28+
val commands: TomlArray = commands.get("command") as TomlArray
2429
for(cmds in commands) {
2530
val cmd = cmds as TomlTable
2631
addCommand(cmd.get("name") as String,

0 commit comments

Comments
 (0)