diff --git a/build.gradle.kts b/build.gradle.kts index 537b185..3140546 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.flywaydb:flyway-core") developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/AboutService.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/AboutService.kt similarity index 98% rename from src/main/kotlin/dev/shiron/kotodiscord/service/command/AboutService.kt rename to src/main/kotlin/dev/shiron/kotodiscord/command/AboutService.kt index e7e7037..a16c6ef 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/AboutService.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/AboutService.kt @@ -1,4 +1,4 @@ -package dev.shiron.kotodiscord.service.command +package dev.shiron.kotodiscord.command import dev.shiron.kotodiscord.i18n.I18n import dev.shiron.kotodiscord.util.data.action.BotSlashCommandData diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/HelloService.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/HelloService.kt similarity index 94% rename from src/main/kotlin/dev/shiron/kotodiscord/service/command/HelloService.kt rename to src/main/kotlin/dev/shiron/kotodiscord/command/HelloService.kt index 4eca6b1..fd9e4f0 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/HelloService.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/HelloService.kt @@ -1,4 +1,4 @@ -package dev.shiron.kotodiscord.service.command +package dev.shiron.kotodiscord.command import dev.shiron.kotodiscord.i18n.I18n import dev.shiron.kotodiscord.util.data.action.BotSlashCommandData diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/HelpService.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/HelpService.kt similarity index 98% rename from src/main/kotlin/dev/shiron/kotodiscord/service/command/HelpService.kt rename to src/main/kotlin/dev/shiron/kotodiscord/command/HelpService.kt index c94e6df..24c62fa 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/HelpService.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/HelpService.kt @@ -1,4 +1,4 @@ -package dev.shiron.kotodiscord.service.command +package dev.shiron.kotodiscord.command import dev.shiron.kotodiscord.i18n.I18n import dev.shiron.kotodiscord.util.data.action.BotSlashCommandData diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpCommand.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpCommand.kt similarity index 99% rename from src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpCommand.kt rename to src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpCommand.kt index c01a383..5d4d2ec 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpCommand.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpCommand.kt @@ -1,4 +1,4 @@ -package dev.shiron.kotodiscord.service.command.bump +package dev.shiron.kotodiscord.command.bump import dev.shiron.kotodiscord.bot.KotoMain import dev.shiron.kotodiscord.domain.BumpConfigData diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpService.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpService.kt similarity index 97% rename from src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpService.kt rename to src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpService.kt index 0113b67..1c269bd 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/bump/BumpService.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/bump/BumpService.kt @@ -1,4 +1,4 @@ -package dev.shiron.kotodiscord.service.command.bump +package dev.shiron.kotodiscord.command.bump import dev.shiron.kotodiscord.domain.BumpJobQueueData import dev.shiron.kotodiscord.repository.BumpConfigDataRepository diff --git a/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCCommand.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCCommand.kt new file mode 100644 index 0000000..d5802c5 --- /dev/null +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCCommand.kt @@ -0,0 +1,249 @@ +package dev.shiron.kotodiscord.command.vc.notify + +import dev.shiron.kotodiscord.domain.VCNotificationData +import dev.shiron.kotodiscord.i18n.I18n +import dev.shiron.kotodiscord.util.data.action.* +import dev.shiron.kotodiscord.util.meta.SingleCommandEnum +import dev.shiron.kotodiscord.util.service.SingleCommandServiceClass +import net.dv8tion.jda.api.entities.Guild +import net.dv8tion.jda.api.entities.channel.ChannelType +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion +import net.dv8tion.jda.api.interactions.components.ActionComponent +import net.dv8tion.jda.api.interactions.components.buttons.Button +import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu +import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Component + +@Component +class VCCommand + @Autowired + constructor( + private val vcService: VCService, + private val i18n: I18n, + ) : SingleCommandServiceClass( + SingleCommandEnum.VC_NOTIFICATION, + i18n, + ) { + fun genBtnSet(shared: Boolean): Button { + return Button.secondary( + genComponentId("set", shared, ComponentSendType.EDIT), + i18n.format("button.vc_notification.set"), + ) + } + + fun genBtnSetAll(shared: Boolean): Button { + return Button.secondary( + genComponentId("set_all", shared, ComponentSendType.EDIT), + i18n.format("button.vc_notification.set_all"), + ) + } + + fun genBtnRemove(shared: Boolean): Button { + return Button.secondary( + genComponentId("remove", shared, ComponentSendType.EDIT), + i18n.format("button.vc_notification.remove"), + ) + } + + fun genBtnSetSmart(shared: Boolean): Button { + return Button.secondary( + genComponentId("set_smart", shared, ComponentSendType.EDIT), + i18n.format("button.vc_notification.set_smart"), + ) + } + + override fun onSlashCommand(cmd: BotSlashCommandData) { + val dataList = vcService.listVCNotification(cmd.guild.idLong) + + var msg = i18n.format("command.message.vc_notification.main") + "\n\n" + val components = mutableListOf() + + if (dataList.isEmpty()) { + msg += i18n.format("command.message.vc_notification.empty") + components.add(genBtnSetAll(cmd.shared)) + components.add(genBtnSet(cmd.shared)) + } else { + msg += i18n.format("command.message.vc_notification.exist") + "\n" + + if (dataList.size < 10) { + if (dataList.find { it.vcCategoryId == null && it.vcChannelId == null && it.textChannelId == cmd.event.channel.idLong } == null) { + components.add(genBtnSetAll(cmd.shared)) + } + + components.add(genBtnSet(cmd.shared)) + } else { + msg += i18n.format("command.message.vc_notification.set.limit") + } + + msg += + dataList.joinToString("\n") { + "- " + getConfString(it) + } + + components.add(genBtnSetSmart(cmd.shared)) + components.add(genBtnRemove(cmd.shared)) + } + + cmd.send(msg, components.ifEmpty { null }) + } + + override fun onButton(event: BotButtonData) { + when (event.actionData.key) { + "set" -> { + event.send( + i18n.format("command.message.vc_notification.set.vc"), + listOf( + EntitySelectMenu.create( + genComponentId("vc", event.actionData.isShow, ComponentSendType.EDIT), + EntitySelectMenu.SelectTarget.CHANNEL, + ).build(), + ), + ) + } + "set_all" -> { + setVCNotification(event.guild, null, event.event.channel, event) + } + "set_smart" -> { + val dataList = vcService.listVCNotification(event.guild.idLong) + + event.send( + i18n.format("command.message.vc_notification.set_smart"), + listOf(buildOptionSelection("set_smart", dataList, event.guild, event.actionData.isShow)), + ) + } + "remove" -> { + val dataList = vcService.listVCNotification(event.guild.idLong) + + event.send( + i18n.format("command.message.vc_notification.remove"), + listOf(buildOptionSelection("rm", dataList, event.guild, event.actionData.isShow)), + ) + } + } + } + + override fun onEntitySelect(event: BotEntitySelectData) { + when (event.actionData.key) { + "vc" -> { + val vcChannelId = event.event.values.first().idLong + setVCNotification(event.guild, vcChannelId, event.event.channel, event) + } + } + } + + override fun onStringSelect(event: BotStringSelectData) { + when (event.actionData.key) { + "rm" -> { + val index = event.values.first().toIntOrNull()?.minus(1) ?: return + val data = vcService.listVCNotification(event.guild.idLong)[index] + vcService.removeVCNotification(data) + event.send( + i18n.format( + "command.message.vc_notification.remove.success", + (index + 1).toString(), + data.vcName, + "<#${data.textChannelId}>", + ), + ) + } + "set_smart" -> { + val index = event.values.first().toIntOrNull()?.minus(1) ?: return + val data = vcService.listVCNotification(event.guild.idLong)[index] + vcService.setVCNotification(data.copy(isSmart = !data.isSmart)) + event.send( + i18n.format( + "command.message.vc_notification.set_smart.success", + (index + 1).toString(), + data.vcName, + "<#${data.textChannelId}>", + if (!data.isSmart) { + i18n.format("command.message.vc_notification.set_smart.true") + } else { + i18n.format("command.message.vc_notification.set_smart.false") + }, + ), + ) + } + } + } + + private fun setVCNotification( + guild: Guild, + vcChannelId: Long?, + textChannel: MessageChannelUnion, + event: BotSendEventClass, + ) { + val vcChannel = vcChannelId?.let { guild.getVoiceChannelById(it) } + val categoryChannel = vcChannelId?.let { guild.getCategoryById(it) } + + if (vcChannelId != null && (vcChannel == null && categoryChannel == null)) { + event.send(i18n.format("command.message.vc_notification.set.error.vc")) + return + } + if (textChannel.type != ChannelType.TEXT) { + event.send(i18n.format("command.message.vc_notification.set.error.text")) + return + } + + vcService.setVCNotification( + VCNotificationData( + guildId = guild.idLong, + vcCategoryId = categoryChannel?.idLong, + vcChannelId = vcChannel?.idLong, + textChannelId = textChannel.idLong, + ), + ) + event.send( + i18n.format( + "command.message.vc_notification.set.success", + vcChannel?.asMention ?: categoryChannel?.asMention ?: "サーバー全体", + textChannel.asMention, + ), + ) + } + + private fun buildOptionSelection( + key: String, + dataList: List, + guild: Guild, + isShow: Boolean, + ): StringSelectMenu { + return StringSelectMenu.create(genComponentId(key, isShow, ComponentSendType.EDIT)).apply { + dataList.forEachIndexed { index, vcNotificationData -> + val vcId = vcNotificationData.vcCategoryId ?: vcNotificationData.vcChannelId + val vcName = vcId.let { guild.channels.find { it.idLong == vcId }?.name }?.let { "#$it" } ?: "サーバー全体" + val textName = guild.channels.find { it.idLong == vcNotificationData.textChannelId }?.name?.let { "#$it" } ?: "不明" + addOption( + "${index + 1}" + + i18n.format( + "command.message.vc_notification.conf", + vcName, + textName, + if (vcNotificationData.isSmart) { + i18n.format("command.message.vc_notification.set_smart.true") + } else { + i18n.format("command.message.vc_notification.set_smart.false") + }, + ), + "${index + 1}", + ) + } + }.build() + } + + private fun getConfString( + data: VCNotificationData, + ): String { + return i18n.format( + "command.message.vc_notification.conf", + data.vcName, + "<#${data.textChannelId}>", + if (data.isSmart) { + i18n.format("command.message.vc_notification.set_smart.true") + } else { + i18n.format("command.message.vc_notification.set_smart.false") + }, + ) + } + } diff --git a/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCService.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCService.kt new file mode 100644 index 0000000..45c33f0 --- /dev/null +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCService.kt @@ -0,0 +1,203 @@ +package dev.shiron.kotodiscord.command.vc.notify + +import dev.shiron.kotodiscord.domain.VCNotificationData +import dev.shiron.kotodiscord.i18n.I18n +import dev.shiron.kotodiscord.repository.VCNotificationDataRepository +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.entities.Member +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import java.awt.Color +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit +import kotlin.math.min + +@Service +class VCService + @Autowired + constructor( + private val vcRepository: VCNotificationDataRepository, + private val i18n: I18n, + ) : ListenerAdapter() { + fun setVCNotification(data: VCNotificationData): VCNotificationData { + VCSmartNotifyManager.remove(data) + return vcRepository.save(data) + } + + fun removeVCNotification(data: VCNotificationData) { + VCSmartNotifyManager.remove(data) + vcRepository.delete(data) + } + + fun listVCNotification(guildId: Long): List { + return vcRepository.findAllByGuildId(guildId) ?: emptyList() + } + + override fun onGuildVoiceUpdate(event: GuildVoiceUpdateEvent) { + val guild = event.guild + val allData = vcRepository.findAllByGuildId(guild.idLong) ?: return + + data class NonSmartTextData( + val textChannelId: Long, + val isJoin: Boolean, + val isLeft: Boolean, + ) + + data class SmartTextData( + val textChannelId: Long, + val firstJoin: Boolean, + val lastLeft: Boolean, + val members: List, + val config: VCNotificationData, + ) + + val nonSmartTextDataList = mutableListOf() + val smartTextDataList = mutableListOf() + + for (data in allData) { + if (!data.isSmart) { + val isJoin = + event.channelJoined != null && ( + data.vcChannelId == event.channelJoined?.idLong || + data.vcCategoryId == event.channelJoined?.parentCategoryIdLong || + (data.vcCategoryId == null && data.vcChannelId == null) + ) + val isLeft = + event.channelLeft != null && ( + data.vcChannelId == event.channelLeft?.idLong || + data.vcCategoryId == event.channelLeft?.parentCategoryIdLong || + (data.vcCategoryId == null && data.vcChannelId == null) + ) + + if (isJoin || isLeft) { + nonSmartTextDataList.add( + NonSmartTextData( + data.textChannelId, + isJoin, + isLeft, + ), + ) + } + } else { + val channels = listOfNotNull(event.channelLeft, event.channelJoined) + for (channel in channels) { + val members = channel.members + smartTextDataList.add( + SmartTextData( + data.textChannelId, + members.size == 1 && event.channelJoined != null, + members.isEmpty() && event.channelLeft != null, + members, + data, + ), + ) + } + } + } + + val date = LocalDateTime.now() + val fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss") + + for (textData in nonSmartTextDataList) { + val eb = EmbedBuilder() + eb.setAuthor(event.member.effectiveName, null, event.member.effectiveAvatarUrl) + eb.setFooter(fmt.format(date)) + + if (textData.isJoin && textData.isLeft) { + eb.setTitle("Change") + eb.setDescription( + i18n.format( + "service.message.vc_notification.change", + event.guild.name, + event.member.effectiveName, + event.channelJoined?.name ?: "", + event.member.asMention, + event.channelLeft?.asMention ?: "", + event.channelJoined?.asMention ?: "", + ), + ) + } else if (textData.isJoin) { + eb.setTitle("Join") + eb.setDescription( + i18n.format( + "service.message.vc_notification.join", + event.guild.name, + event.member.effectiveName, + event.channelJoined?.name ?: "", + event.member.asMention, + event.channelJoined?.asMention ?: "", + ), + ) + } else if (textData.isLeft) { + eb.setTitle("Left") + eb.setDescription( + i18n.format( + "service.message.vc_notification.left", + event.guild.name, + event.member.effectiveName, + event.channelLeft?.name ?: "", + event.member.asMention, + event.channelLeft?.asMention ?: "", + ), + ) + } + guild.getTextChannelById(textData.textChannelId)?.sendMessage("")?.setEmbeds(eb.build())?.queue() + } + + for (textData in smartTextDataList) { + val eb = EmbedBuilder() + eb.setAuthor(event.member.effectiveName, null, event.member.effectiveAvatarUrl) + eb.setFooter(fmt.format(date)) + + val names = + run { + val names = textData.members.joinToString(", ") { it.effectiveName } + val last = if (names.length > 100) "..." else "" + return@run names.substring(0, min(names.length, 100)) + last + } + + if (!textData.lastLeft) { + eb.setTitle(i18n.format("service.message.smart.status.active")) + eb.setColor(Color.GREEN) + } else { + eb.setTitle(i18n.format("service.message.smart.status.inactive")) + eb.setColor(Color.RED) + } + + val started = VCSmartNotifyManager[textData.config]?.startDate ?: date + + eb.setDescription( + i18n.format( + "service.message.smart.message", + (event.channelJoined ?: event.channelLeft)?.asMention ?: "#不明", + event.guild.name, + textData.members.size.toString(), + names, + fmt.format(started), + started.until(date, ChronoUnit.MINUTES).toString(), + ), + ) + + val message = VCSmartNotifyManager[textData.config]?.message + if (message == null) { + guild.getTextChannelById(textData.textChannelId)?.sendMessage("")?.setEmbeds(eb.build())?.queue { + VCSmartNotifyManager.new( + VCSmartNotifyData( + textData.config, + it, + date, + ), + ) + } + } else { + message.editMessageEmbeds(eb.build()).queue() + } + if (textData.lastLeft) { + VCSmartNotifyManager.remove(textData.config) + } + } + } + } diff --git a/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyData.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyData.kt new file mode 100644 index 0000000..0807eec --- /dev/null +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyData.kt @@ -0,0 +1,11 @@ +package dev.shiron.kotodiscord.command.vc.notify + +import dev.shiron.kotodiscord.domain.VCNotificationData +import net.dv8tion.jda.api.entities.Message +import java.time.LocalDateTime + +data class VCSmartNotifyData( + val configData: VCNotificationData, + val message: Message, + val startDate: LocalDateTime, +) diff --git a/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyManager.kt b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyManager.kt new file mode 100644 index 0000000..478a008 --- /dev/null +++ b/src/main/kotlin/dev/shiron/kotodiscord/command/vc/notify/VCSmartNotifyManager.kt @@ -0,0 +1,19 @@ +package dev.shiron.kotodiscord.command.vc.notify + +import dev.shiron.kotodiscord.domain.VCNotificationData + +object VCSmartNotifyManager { + private val smartNotifyData: MutableMap = mutableMapOf() + + fun new(data: VCSmartNotifyData) { + smartNotifyData[data.configData] = data + } + + fun remove(data: VCNotificationData) { + smartNotifyData.remove(data) + } + + operator fun get(data: VCNotificationData): VCSmartNotifyData? { + return smartNotifyData[data] + } +} diff --git a/src/main/kotlin/dev/shiron/kotodiscord/controller/CommandController.kt b/src/main/kotlin/dev/shiron/kotodiscord/controller/CommandController.kt index cfb0efe..4347998 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/controller/CommandController.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/controller/CommandController.kt @@ -9,6 +9,7 @@ import dev.shiron.kotodiscord.util.service.RunnableCommandServiceClass import dev.shiron.kotodiscord.util.service.SingleCommandServiceClass import dev.shiron.kotodiscord.util.service.SubCommandServiceClass import dev.shiron.kotodiscord.vars.properties.AppProperties +import net.dv8tion.jda.api.entities.Guild import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent @@ -17,6 +18,7 @@ import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionE import net.dv8tion.jda.api.hooks.ListenerAdapter import net.dv8tion.jda.api.interactions.commands.build.Commands import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData +import net.dv8tion.jda.api.interactions.components.ComponentInteraction import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Controller @@ -79,19 +81,7 @@ class CommandController } override fun onButtonInteraction(event: ButtonInteractionEvent) { - val guild = event.guild - val actionData = ActionDataManager[event.componentId] - if (actionData == null) { - event.reply(i18n.format("command.error.action")).setEphemeral(true).queue() - return - } - val command = getCommandFromComponentId(actionData.componentIdData) - if (guild == null || command == null) { - event.reply(i18n.format("command.error.internal")).queue() - return - } - - ActionDataManager.removeActionData(event.componentId) + val (guild, actionData, command) = getDataFromInteractionOrSendError(event) ?: return val data = BotButtonData( @@ -123,19 +113,7 @@ class CommandController } override fun onStringSelectInteraction(event: StringSelectInteractionEvent) { - val guild = event.guild - val actionData = ActionDataManager[event.componentId] - if (actionData == null) { - event.reply(i18n.format("command.error.action")).setEphemeral(true).queue() - return - } - val command = getCommandFromComponentId(actionData.componentIdData) - if (guild == null || command == null) { - event.reply(i18n.format("command.error.internal")).queue() - return - } - - ActionDataManager.removeActionData(event.componentId) + val (guild, actionData, command) = getDataFromInteractionOrSendError(event) ?: return val data = BotStringSelectData( @@ -168,19 +146,7 @@ class CommandController } override fun onEntitySelectInteraction(event: EntitySelectInteractionEvent) { - val guild = event.guild - val actionData = ActionDataManager[event.componentId] - if (actionData == null) { - event.reply(i18n.format("command.error.action")).setEphemeral(true).queue() - return - } - val command = getCommandFromComponentId(actionData.componentIdData) - if (guild == null || command == null) { - event.reply(i18n.format("command.error.internal")).queue() - return - } - - ActionDataManager.removeActionData(event.componentId) + val (guild, actionData, command) = getDataFromInteractionOrSendError(event) ?: return val data = BotEntitySelectData( @@ -212,6 +178,24 @@ class CommandController command.onEntitySelect(data) } + private fun getDataFromInteractionOrSendError(event: ComponentInteraction): Triple? { + val guild = event.guild + val actionData = ActionDataManager[event.componentId] + if (actionData == null) { + event.reply(i18n.format("command.error.action")).setEphemeral(true).queue() + return null + } + val command = getCommandFromComponentId(actionData.componentIdData) + if (guild == null || command == null) { + event.reply(i18n.format("command.error.internal")).queue() + return null + } + + ActionDataManager.removeActionData(event.componentId) + + return Triple(guild, actionData, command) + } + fun getCommand( name: String, subcommandName: String?, diff --git a/src/main/kotlin/dev/shiron/kotodiscord/domain/VCNotificationData.kt b/src/main/kotlin/dev/shiron/kotodiscord/domain/VCNotificationData.kt index 2a5d903..84aa620 100644 --- a/src/main/kotlin/dev/shiron/kotodiscord/domain/VCNotificationData.kt +++ b/src/main/kotlin/dev/shiron/kotodiscord/domain/VCNotificationData.kt @@ -13,14 +13,9 @@ data class VCNotificationData( val vcChannelId: Long?, val vcCategoryId: Long?, val textChannelId: Long, + @Column(columnDefinition = "boolean default true") + val isSmart: Boolean = true, ) { - fun like(other: VCNotificationData): Boolean { - return guildId == other.guildId && - vcChannelId == other.vcChannelId && - vcCategoryId == other.vcCategoryId && - textChannelId == other.textChannelId - } - val vcName: String get() = run { diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCCommand.kt b/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCCommand.kt deleted file mode 100644 index 7d4bed4..0000000 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCCommand.kt +++ /dev/null @@ -1,193 +0,0 @@ -package dev.shiron.kotodiscord.service.command.vc.notify - -import dev.shiron.kotodiscord.domain.VCNotificationData -import dev.shiron.kotodiscord.i18n.I18n -import dev.shiron.kotodiscord.util.data.action.* -import dev.shiron.kotodiscord.util.meta.SingleCommandEnum -import dev.shiron.kotodiscord.util.service.SingleCommandServiceClass -import net.dv8tion.jda.api.interactions.components.ActionComponent -import net.dv8tion.jda.api.interactions.components.buttons.Button -import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu -import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Component - -@Component -class VCCommand - @Autowired - constructor( - private val vcService: VCService, - private val i18n: I18n, - ) : SingleCommandServiceClass( - SingleCommandEnum.VC_NOTIFICATION, - i18n, - ) { - fun genBtnSet(shared: Boolean): Button { - return Button.secondary( - genComponentId("set", shared, ComponentSendType.EDIT), - i18n.format("button.vc_notification.set"), - ) - } - - fun genBtnSetAll(shared: Boolean): Button { - return Button.secondary( - genComponentId("set_all", shared, ComponentSendType.EDIT), - i18n.format("button.vc_notification.set_all"), - ) - } - - fun genBtnRemove(shared: Boolean): Button { - return Button.secondary( - genComponentId("remove", shared, ComponentSendType.EDIT), - i18n.format("button.vc_notification.remove"), - ) - } - - override fun onSlashCommand(cmd: BotSlashCommandData) { - val dataList = vcService.listVCNotification(cmd.guild.idLong) - - var msg = i18n.format("command.message.vc_notification.main") + "\n\n" - val componets = mutableListOf() - - if (dataList.isEmpty()) { - msg += i18n.format("command.message.vc_notification.empty") - componets.add(genBtnSetAll(cmd.shared)) - componets.add(genBtnSet(cmd.shared)) - } else { - msg += i18n.format("command.message.vc_notification.exist") + "\n" - - if (dataList.size < 10) { - if (dataList.find { it.vcCategoryId == null && it.vcChannelId == null } == null) { - componets.add(genBtnSetAll(cmd.shared)) - } - - componets.add(genBtnSet(cmd.shared)) - } else { - msg += i18n.format("command.message.vc_notification.set.limit") - } - - msg += dataList.joinToString("\n") { "- ${it.vcName} -> <#${it.textChannelId}>" } - - componets.add(genBtnRemove(cmd.shared)) - } - - cmd.send(msg, componets.ifEmpty { null }) - } - - override fun onButton(event: BotButtonData) { - when (event.actionData.key) { - "set" -> { - event.send( - i18n.format("command.message.vc_notification.set.vc"), - listOf( - EntitySelectMenu.create( - genComponentId("vc", event.actionData.isShow, ComponentSendType.EDIT), - EntitySelectMenu.SelectTarget.CHANNEL, - ).build(), - ), - ) - } - "set_all" -> { - event.send( - i18n.format("command.message.vc_notification.set.text"), - listOf( - EntitySelectMenu.create( - genComponentId( - "text", - event.actionData.isShow, - ComponentSendType.EDIT, - null, - ), - EntitySelectMenu.SelectTarget.CHANNEL, - ).build(), - ), - ) - } - "remove" -> { - val dataList = vcService.listVCNotification(event.guild.idLong) - - val option = - StringSelectMenu.create(genComponentId("rm", event.actionData.isShow, ComponentSendType.EDIT)).apply { - dataList.forEachIndexed { index, vcNotificationData -> - val vcId = vcNotificationData.vcCategoryId ?: vcNotificationData.vcChannelId - val vcName = vcId.let { event.guild.channels.find { it.idLong == vcId }?.name }?.let { "#$it" } ?: "サーバー全体" - val textName = event.guild.channels.find { it.idLong == vcNotificationData.textChannelId }?.name?.let { "#$it" } ?: "不明" - addOption( - "${index + 1} $vcName -> $textName", - "${index + 1}", - ) - } - }.build() - event.send( - i18n.format("command.message.vc_notification.remove"), - listOf(option), - ) - } - } - } - - override fun onEntitySelect(event: BotEntitySelectData) { - when (event.actionData.key) { - "vc" -> { - event.send( - i18n.format("command.message.vc_notification.set.text"), - listOf( - EntitySelectMenu.create( - genComponentId( - "text", - event.actionData.isShow, - ComponentSendType.EDIT, - event.values.first().idLong, - ), - EntitySelectMenu.SelectTarget.CHANNEL, - ).build(), - ), - ) - } - "text" -> { - val vcChannelId = event.actionData.data - if (vcChannelId is Long?) { - val vcChannel = vcChannelId?.let { event.guild.getVoiceChannelById(it) } - val categoryChannel = vcChannelId?.let { event.guild.getCategoryById(it) } - val textChannel = event.guild.getTextChannelById(event.values.first().idLong) - - if (vcChannelId != null && (vcChannel == null && categoryChannel == null)) { - event.send(i18n.format("command.message.vc_notification.set.error.vc")) - return - } - if (textChannel == null) { - event.send(i18n.format("command.message.vc_notification.set.error.text")) - return - } - - vcService.setVCNotification( - VCNotificationData( - guildId = event.guild.idLong, - vcCategoryId = categoryChannel?.idLong, - vcChannelId = vcChannel?.idLong, - textChannelId = textChannel.idLong, - ), - ) - event.send( - i18n.format( - "command.message.vc_notification.set.success", - vcChannel?.asMention ?: categoryChannel?.asMention ?: "サーバー全体", - textChannel.asMention, - ), - ) - } - } - } - } - - override fun onStringSelect(event: BotStringSelectData) { - when (event.actionData.key) { - "rm" -> { - val index = event.values.first().toIntOrNull()?.minus(1) ?: return - val data = vcService.listVCNotification(event.guild.idLong)[index] - vcService.removeVCNotification(data) - event.send("${index + 1} : ${data.vcName} -> <#${data.textChannelId}>\n" + "の設定を削除しました") - } - } - } - } diff --git a/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCService.kt b/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCService.kt deleted file mode 100644 index 0c4f76d..0000000 --- a/src/main/kotlin/dev/shiron/kotodiscord/service/command/vc/notify/VCService.kt +++ /dev/null @@ -1,121 +0,0 @@ -package dev.shiron.kotodiscord.service.command.vc.notify - -import dev.shiron.kotodiscord.domain.VCNotificationData -import dev.shiron.kotodiscord.i18n.I18n -import dev.shiron.kotodiscord.repository.VCNotificationDataRepository -import net.dv8tion.jda.api.EmbedBuilder -import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent -import net.dv8tion.jda.api.hooks.ListenerAdapter -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Service -import java.text.SimpleDateFormat -import java.util.* - -@Service -class VCService - @Autowired - constructor( - private val vcRepository: VCNotificationDataRepository, - private val i18n: I18n, - ) : ListenerAdapter() { - fun setVCNotification(data: VCNotificationData): VCNotificationData { - return vcRepository.save(data) - } - - fun removeVCNotification(data: VCNotificationData) { - vcRepository.delete(data) - } - - fun listVCNotification(guildId: Long): List { - return vcRepository.findAllByGuildId(guildId) ?: emptyList() - } - - override fun onGuildVoiceUpdate(event: GuildVoiceUpdateEvent) { - val guild = event.guild - val allData = vcRepository.findAllByGuildId(guild.idLong) ?: return - - data class TextData( - val textChannelId: Long, - val isJoin: Boolean, - val isLeft: Boolean, - ) - - val textDataList = mutableListOf() - - for (data in allData) { - val isJoin = - event.channelJoined != null && ( - data.vcChannelId == event.channelJoined?.idLong || - data.vcCategoryId == event.channelJoined?.parentCategoryIdLong || - (data.vcCategoryId == null && data.vcChannelId == null) - ) - val isLeft = - event.channelLeft != null && ( - data.vcChannelId == event.channelLeft?.idLong || - data.vcCategoryId == event.channelLeft?.parentCategoryIdLong || - (data.vcCategoryId == null && data.vcChannelId == null) - ) - - if (isJoin || isLeft) { - textDataList.add( - TextData( - data.textChannelId, - isJoin, - isLeft, - ), - ) - } - } - - val eb = EmbedBuilder() - eb.setAuthor(event.member.effectiveName, null, event.member.effectiveAvatarUrl) - - val date = Date() - val timeZoneJP = TimeZone.getTimeZone("Asia/Tokyo") - val fmt = SimpleDateFormat() - fmt.timeZone = timeZoneJP - eb.setFooter(fmt.format(date)) - - for (textData in textDataList) { - if (textData.isJoin && textData.isLeft) { - eb.setTitle("Change") - eb.setDescription( - i18n.format( - "service.message.vc_notification.change", - event.guild.name, - event.member.effectiveName, - event.channelJoined?.name ?: "", - event.member.asMention, - event.channelLeft?.asMention ?: "", - event.channelJoined?.asMention ?: "", - ), - ) - } else if (textData.isJoin) { - eb.setTitle("Join") - eb.setDescription( - i18n.format( - "service.message.vc_notification.join", - event.guild.name, - event.member.effectiveName, - event.channelJoined?.name ?: "", - event.member.asMention, - event.channelJoined?.asMention ?: "", - ), - ) - } else if (textData.isLeft) { - eb.setTitle("Left") - eb.setDescription( - i18n.format( - "service.message.vc_notification.left", - event.guild.name, - event.member.effectiveName, - event.channelLeft?.name ?: "", - event.member.asMention, - event.channelLeft?.asMention ?: "", - ), - ) - } - guild.getTextChannelById(textData.textChannelId)?.sendMessage("")?.setEmbeds(eb.build())?.queue() - } - } - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5e40f43..9adb9a4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,6 +9,11 @@ spring: properties: hibernate: dialect: org.hibernate.community.dialect.SQLiteDialect + flyway: + baseline-on-migrate: true + enabled: true + url: jdbc:sqlite:./db.sqlite3 + validate-on-migrate: true profiles: include: env app: diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b99dccc..1bb3dff 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -30,6 +30,7 @@ command.message.help.notfound = `{0}`が見つかりませんでした。\n\ # VC通知 # command +command.message.vc_notification.conf = {0} -> {1} {2} command.message.vc_notification.main = ボイスチャンネルの入退出を通知します。 command.message.vc_notification.exist = 以下の通知設定がサーバーに存在します。 command.message.vc_notification.empty = サーバーに通知設定が存在しません。\n\ @@ -47,10 +48,16 @@ command.message.vc_notification.set.error.vc = :warning:ボイスチャンネル ボイスチャンネルかカテゴリを指定してください。\n\ もう一度コマンドを実行し直してください。 command.message.vc_notification.remove = 削除したい通知を選択してください。 +command.message.vc_notification.remove.success = {0}: {1} -> {2} の通知設定を削除しました。 +command.message.vc_notification.set_smart = スマート通知を設定/解除したい通知を選択してください。 +command.message.vc_notification.set_smart.success = {0}: {1} -> {2} を**{3}**に設定しました。 +command.message.vc_notification.set_smart.true = スマート通知有効 +command.message.vc_notification.set_smart.false = スマート通知無効 # button -button.vc_notification.set = 通知設定を行う/追加する -button.vc_notification.set_all = サーバー全体の入退出を通知する +button.vc_notification.set = 現在のチャンネルに特定VCの通知設定を行う/追加する +button.vc_notification.set_all = 現在のチャンネルにサーバー全体の入退出を通知する button.vc_notification.remove = 通知設定を削除する +button.vc_notification.set_smart = スマート通知設定/解除を行う # service service.message.vc_notification.join = VC通知 {0} @{1} #{2}\n\n\ {3}が{4}に入室しました。 @@ -60,6 +67,11 @@ service.message.vc_notification.change = VC通知 {0} @{1} #{2}\n\n\ {3}が{4}から{5}に移動しました。 service.message.bump = bumpしてください!\n\ ここからもコマンドが実行できます: {0} +service.message.smart.status.active = 活動中 +service.message.smart.status.inactive = 終了 +service.message.smart.message = {0} ({1}) {2}人\n\ + {3}\n\ + 開始時間:{4}~ 経過時間:{5}分 command.message.about = > {0}について\n\ 使い方: `/help`でヘルプを表示します。\n\