Skip to content

Commit

Permalink
🚧 complete some features of a polling session
Browse files Browse the repository at this point in the history
1) 取消表情 = 取消投票
2) 時間到之後取消 session
  • Loading branch information
Johnny850807 committed May 28, 2023
1 parent b91a667 commit 9009e25
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ private val log = KotlinLogging.logger {}
class MuteAudiences() : UtopiaListener() {
override fun commands(): List<CommandData> {
return listOf(
Commands.slash(MUTE_SLASH, "Mute")
.addSubcommands(
SubcommandData(AUDIENCES_SUBCOMMAND, "Mute Audiences")
.addOption(OptionType.USER, OPTION_AUDIENCE_NAME, "Allow who to voice", false)
.addOption(OptionType.ROLE, OPTION_ROLE_NAME, "Allow role to voice", false),
SubcommandData(REVOKED_SUB_COMMAND, "Unmute")
)
Commands.slash(MUTE_SLASH, "Mute")
.addSubcommands(
SubcommandData(AUDIENCES_SUBCOMMAND, "Mute Audiences")
.addOption(OptionType.USER, OPTION_AUDIENCE_NAME, "Allow who to voice", false)
.addOption(OptionType.ROLE, OPTION_ROLE_NAME, "Allow role to voice", false),
SubcommandData(REVOKED_SUB_COMMAND, "Unmute")
)
)
}

Expand All @@ -53,7 +53,7 @@ class MuteAudiences() : UtopiaListener() {
}

private fun SlashCommandInteractionEvent.isNotMuteCommand(): Boolean {
return fullCommandName != MUTE_AUDIENCES_COMMAND || fullCommandName != MUTE_REVOKED_COMMAND
return fullCommandName != MUTE_AUDIENCES_COMMAND && fullCommandName != MUTE_REVOKED_COMMAND
}

private fun SlashCommandInteractionEvent.isNotVoiceChannel(): Boolean {
Expand All @@ -80,15 +80,15 @@ class MuteAudiences() : UtopiaListener() {
val memberId = member?.id

member?.mute(true)
?.queue { log.info { "[$fullCommandName]: {\"muteLabel\":\"Mute\", \"memberName\":\"${memberName}\", \"memberId\":\"${memberId}\"}" } }
?.queue { log.info { "[$fullCommandName]: {\"muteLabel\":\"Mute\", \"memberName\":\"${memberName}\", \"memberId\":\"${memberId}\"}" } }
}

private fun SlashCommandInteractionEvent.unMuteMember() {
val memberName = member?.nickname ?: member?.effectiveName
val memberId = member?.id

member?.mute(false)
?.queue { log.info { "[$fullCommandName]: {\"muteLabel\":\"Unmute\", \"memberName\":\"${memberName}\", \"memberId\":\"${memberId}\"}" } }
?.queue { log.info { "[$fullCommandName]: {\"muteLabel\":\"Unmute\", \"memberName\":\"${memberName}\", \"memberId\":\"${memberId}\"}" } }
}

private fun SlashCommandInteractionEvent.executeMuteCommand(muteMemberAction: () -> Unit, replyMessage: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ fun GenericCommandInteractionEvent.getOptionAsPositiveInt(name: String): Int? {
}
}

fun GenericCommandInteractionEvent.getOptionAsLongInRange(name: String, range: LongRange): Long? {
return getOptionAsLongWithValidation(name, "a long that within ${range.first} ~ ${range.last}") {
range.contains(it)
}
}

fun GenericCommandInteractionEvent.getOptionAsIntInRange(name: String, range: IntRange): Int? {
return getOptionAsIntWithValidation(name, "a integer that within ${range.first} ~ ${range.last}") {
range.contains(it)
Expand All @@ -33,6 +39,12 @@ fun GenericCommandInteractionEvent.getOptionAsStringWithValidation(name: String,
return getOptionWithValidation(name, optionTypeName, validation) { it?.asString }
}

fun GenericCommandInteractionEvent.getOptionAsLongWithValidation(name: String,
optionTypeName: String,
validation: (Long) -> Boolean): Long? {
return getOptionWithValidation(name, optionTypeName, validation) { it?.asLong }
}

fun GenericCommandInteractionEvent.getOptionAsIntWithValidation(name: String,
optionTypeName: String,
validation: (Int) -> Boolean): Int? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration
import tw.waterballsa.utopia.jda.JdaInstance.compositeListener
import java.lang.reflect.Method

val log = KotlinLogging.logger {}
private val log = KotlinLogging.logger {}

internal class CompositeListener : EventListener {
internal val listeners: MutableList<UtopiaListener> = mutableListOf()
Expand Down
120 changes: 97 additions & 23 deletions poll/src/main/kotlin/tw.waterballsa.utopia.poll/PollCommandListener.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package tw.waterballsa.utopia.poll

import mu.KotlinLogging
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.emoji.Emoji
import net.dv8tion.jda.api.entities.emoji.EmojiUnion
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent
import net.dv8tion.jda.api.events.message.react.MessageReactionRemoveEvent
import net.dv8tion.jda.api.interactions.commands.OptionType
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
import tw.waterballsa.utopia.commons.config.WsaDiscordProperties
import tw.waterballsa.utopia.commons.extensions.scheduleDelay
import tw.waterballsa.utopia.jda.UtopiaListener
import tw.waterballsa.utopia.jda.extensions.getOptionAsIntInRange
import tw.waterballsa.utopia.jda.extensions.getOptionAsLongInRange
import tw.waterballsa.utopia.jda.extensions.getOptionAsStringWithValidation
import java.awt.Color
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit

private const val OPTION_TIME = "time"
Expand All @@ -26,16 +32,20 @@ private const val OPTION_QUESTION = "question"

private const val OPTION_OPTIONS = "options"

private val EMOJI_UNICODES: Array<String> = arrayOf("0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣")
private val EMOJI_UNICODES: Array<String> = arrayOf("0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "\uD83D\uDD1F")

private val log = KotlinLogging.logger {}

private val timer = Timer()

/**
* /poll time=1 timeUnit=minutes question="Which operating system do you prefer?" options="Windows,MacOS,Linux,Other"
* @author [email protected]
*/
@Component
class PollCommandListener(private val wsa: WsaDiscordProperties) : UtopiaListener() {
// embedded message's id to polling session
private val messageIdToSession: MutableMap<String, PollingSession> = mutableMapOf()
// embedded session id (message's id) to polling session
private val sessionIdToSession: ConcurrentHashMap<String, PollingSession> = ConcurrentHashMap()

override fun commands(): List<CommandData> {
return listOf(
Expand All @@ -52,42 +62,75 @@ class PollCommandListener(private val wsa: WsaDiscordProperties) : UtopiaListene
if (!fullCommandName.startsWith("poll")) {
return
}
val setting = getPollingSetting()
val pollingSetting = parsePollingSettingFromOptions() ?: return

val message = channel.sendMessageEmbeds(setting.toMessageEmbeds()).complete()
val session = PollingSession(id = message.id, setting = setting)
messageIdToSession[message.id] = session
options.forEachIndexed { i, _ ->
val message = channel.sendMessageEmbeds(pollingSetting.toMessageEmbeds()).complete()
val pollingSession = PollingSession(id = message.id, channelId = channel.id, setting = pollingSetting)
sessionIdToSession[message.id] = pollingSession

pollingSetting.options.forEachIndexed { i, _ ->
message.addReaction(Emoji.fromUnicode(EMOJI_UNICODES[i])).complete()
}

scheduleTaskToEndThePollingSession(wsa, jda, pollingSession)
}
}


private fun SlashCommandInteractionEvent.getPollingSetting(): PollingSetting {
val time = getOptionAsIntInRange(OPTION_TIME, 0..500)
val timeUnit = getOptionAsStringWithValidation(OPTION_TIMEUNIT, "should be one of (Day | Minute | Second)") {
private fun SlashCommandInteractionEvent.parsePollingSettingFromOptions(): PollingSetting? {
val time = getOptionAsLongInRange(OPTION_TIME, 0..500L)
val timeUnit = getOptionAsStringWithValidation(OPTION_TIMEUNIT, "should be one of (Days | Minutes | Seconds)") {
TimeUnit.values().any { unit -> unit.name == it.uppercase() }
}.let { TimeUnit.valueOf(it!!.uppercase()) }
}?.let { TimeUnit.valueOf(it.uppercase()) } ?: return null

val question = getOption(OPTION_QUESTION)?.asString
val options = getOption(OPTION_OPTIONS)?.asString?.split(Regex("\\s*,\\s*"))
val question = getOption(OPTION_QUESTION)!!.asString
val options = getOption(OPTION_OPTIONS)!!.asString.split(Regex("\\s*,\\s*"))
if (options.size > EMOJI_UNICODES.size) {
reply("The number of options cannot be greater than ${EMOJI_UNICODES.size}.").complete()
return null
}

return PollingSetting(time!!, timeUnit, question!!, options!!)
return PollingSetting(time!!, timeUnit, question, options)
}

// TODO:
// 一人一票的限制
override fun onMessageReactionAdd(event: MessageReactionAddEvent) {
with(event) {
val session = messageIdToSession[messageId] ?: return
val session = sessionIdToSession[messageId] ?: return
if (userId == jda.selfUser.id) {
return
}
session.vote(Vote(userId, emoji))
}
}

override fun onMessageReactionRemove(event: MessageReactionRemoveEvent) {
with(event) {
val session = sessionIdToSession[messageId] ?: return
if (userId == jda.selfUser.id) {
return
}
session.devote(Vote(userId, emoji))
}
}

private fun scheduleTaskToEndThePollingSession(wsa: WsaDiscordProperties, jda: JDA, pollingSession: PollingSession) {
val setting = pollingSession.setting
timer.scheduleDelay(setting.timeUnit.toMillis(setting.time)) {
val pollingResult = pollingSession.end()

sessionIdToSession.remove(pollingSession.id)
val channel = jda.getTextChannelById(pollingSession.channelId)!!
val message = channel.retrieveMessageById(pollingSession.id).complete()
message.reply("The polling session has ended. Result:\n${pollingResult.messageBody}")
.complete()
}
}
}

data class PollingSetting(val time: Int, val timeUnit: TimeUnit, val question: String, val options: List<String>) {

data class PollingSetting(val time: Long, val timeUnit: TimeUnit, val question: String, val options: List<String>) {
private val optionsMessageBody = options.mapIndexed { i, option ->
"${EMOJI_UNICODES[i]} $option"
}.joinToString("\n")
Expand All @@ -111,21 +154,52 @@ data class PollingSetting(val time: Int, val timeUnit: TimeUnit, val question: S
.setColor(Color.BLUE)
.build(),
)

fun getOption(index: Int): String = options[index]
}

data class Vote(val userId: String, val emoji: EmojiUnion)

class PollingSession(
val id: String, private val setting: PollingSetting) {
val id: String, val channelId: String, val setting: PollingSetting) {
private val voterIdToVotedOptionIndices = hashMapOf<String, MutableList<Int>>()

fun vote(vote: Vote) {
findVoterOptionIndices(vote) {
log.info { """[Voted] {"userId": ${vote.userId}, "optionIndex": $it}" }""" }
add(it)
}
}

fun devote(vote: Vote) {
findVoterOptionIndices(vote) {
log.info { """[Devoted] {"userId": ${vote.userId}, "optionIndex": $it}" }""" }
remove(it)
}
}

private fun findVoterOptionIndices(vote: Vote, newIndexConsumer: MutableList<Int>.(int: Int) -> Unit) {
val emojiIndex = EMOJI_UNICODES.indexOf(vote.emoji.name)
if (emojiIndex < 0) {
return
if (emojiIndex >= 0) {
newIndexConsumer.invoke(voterIdToVotedOptionIndices.computeIfAbsent(vote.userId) { mutableListOf() }, emojiIndex)
}
voterIdToVotedOptionIndices.computeIfAbsent(vote.userId) { mutableListOf() }
.add(emojiIndex)
}

fun end(): PollingResult {
return PollingResult(voterIdToVotedOptionIndices, setting)
}
}

class PollingResult(private val voterIdToVotedOptionIndices: Map<String, MutableList<Int>>, private val setting: PollingSetting) {
val messageBody: String
get() {
return voterIdToVotedOptionIndices.values
.flatten()
.groupingBy { it }
.eachCount()
.map { (index, count) -> "${setting.getOption(index)}: $count votes." }
.joinToString("\n")
}
}

private fun SlashCommandData.addRequiredOption(type: OptionType, name: String, description: String) =
Expand Down

0 comments on commit 9009e25

Please sign in to comment.