diff --git a/gradle.properties b/gradle.properties index 001a6976..b9d2be15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=6.0.18-SNAPSHOT +version=6.0.20-SNAPSHOT diff --git a/src/main/kotlin/dev/slne/surf/discord/command/console/impl/SocialsMigrationCommand.kt b/src/main/kotlin/dev/slne/surf/discord/command/console/impl/SocialsMigrationCommand.kt new file mode 100644 index 00000000..f4b8a2f4 --- /dev/null +++ b/src/main/kotlin/dev/slne/surf/discord/command/console/impl/SocialsMigrationCommand.kt @@ -0,0 +1,23 @@ +package dev.slne.surf.discord.command.console.impl + +import dev.slne.surf.discord.command.console.ConsoleCommand +import dev.slne.surf.discord.ticket.database.whitelist.SocialsMigration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.springframework.stereotype.Component + +@Component +class SocialsMigrationCommand( + private val discordScope: CoroutineScope +) : ConsoleCommand { + override val name = "migrate-socials" + + override fun execute(args: List) { + discordScope.launch { + println("Starting social whitelist migration...") + SocialsMigration.migrateLegacy() + + println("\nMigration finished.") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/slne/surf/discord/config/DatabaseConfig.kt b/src/main/kotlin/dev/slne/surf/discord/config/DatabaseConfig.kt index b54b295f..a7058337 100644 --- a/src/main/kotlin/dev/slne/surf/discord/config/DatabaseConfig.kt +++ b/src/main/kotlin/dev/slne/surf/discord/config/DatabaseConfig.kt @@ -7,7 +7,8 @@ import dev.slne.surf.discord.ticket.database.messages.attachments.TicketAttachme import dev.slne.surf.discord.ticket.database.ticket.TicketTable import dev.slne.surf.discord.ticket.database.ticket.data.TicketDataTable import dev.slne.surf.discord.ticket.database.ticket.staff.TicketStaffTable -import dev.slne.surf.discord.ticket.database.whitelist.SocialsTable +import dev.slne.surf.discord.ticket.database.whitelist.FreebuildWhitelistTable +import dev.slne.surf.discord.ticket.database.whitelist.SocialConnectionsTable import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable import org.jetbrains.annotations.ApiStatus @@ -44,7 +45,8 @@ class DatabaseConfiguration { TicketStaffTable, TicketMessagesTable, TicketAttachmentsTable, - SocialsTable + SocialConnectionsTable, + FreebuildWhitelistTable ) } logger.info("Connected to database ${botConfig.database.database} at ${botConfig.database.hostname}:${botConfig.database.port}") diff --git a/src/main/kotlin/dev/slne/surf/discord/interaction/modal/impl/ticket/whitelist/SurvivalWhitelistEditModal.kt b/src/main/kotlin/dev/slne/surf/discord/interaction/modal/impl/ticket/whitelist/SurvivalWhitelistEditModal.kt index 1f7da3e4..67868293 100644 --- a/src/main/kotlin/dev/slne/surf/discord/interaction/modal/impl/ticket/whitelist/SurvivalWhitelistEditModal.kt +++ b/src/main/kotlin/dev/slne/surf/discord/interaction/modal/impl/ticket/whitelist/SurvivalWhitelistEditModal.kt @@ -4,6 +4,7 @@ import dev.slne.surf.discord.dsl.modal import dev.slne.surf.discord.interaction.modal.DiscordModal import dev.slne.surf.discord.messages.translatable import dev.slne.surf.discord.ticket.database.whitelist.SocialService +import dev.slne.surf.discord.util.PlayerLookupService import net.dv8tion.jda.api.components.selections.SelectOption import net.dv8tion.jda.api.components.selections.StringSelectMenu import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent @@ -12,7 +13,8 @@ import org.springframework.stereotype.Component @Component class SurvivalWhitelistEditModal( - private val socialService: SocialService + private val socialService: SocialService, + private val playerLookupService: PlayerLookupService ) : DiscordModal { override val id = "whitelist:modal:edit-survival" @@ -32,6 +34,13 @@ class SurvivalWhitelistEditModal( required = true } + textInput { + id = "whitelist:modal:edit-survival:old-discord-id" + label = "Interne Verwaltungs-ID (nicht ändern!)" + value = data[2] + required = true + } + selectMenu( translatable("whitelist.survival.edit.modal.blocked.label"), StringSelectMenu @@ -46,6 +55,9 @@ class SurvivalWhitelistEditModal( } override suspend fun onSubmit(event: ModalInteractionEvent) { + val oldDiscordId = + event.getValue("whitelist:modal:edit-survival:old-discord-id")?.asString?.toLongOrNull() + ?: return val minecraftName = event.getValue("whitelist:modal:edit-survival:minecraft-name")?.asString ?: return val discordId = @@ -56,11 +68,57 @@ class SurvivalWhitelistEditModal( ?.toBooleanStrictOrNull() ?: return val discordName = event.jda.getUserById(discordId)?.name ?: discordId.toString() + val oldWhitelist = socialService.getWhitelist(oldDiscordId) + if (oldWhitelist == null) { + event.reply(translatable("whitelist.embed.information.no_whitelist")) + .setEphemeral(true) + .queue() + return + } - socialService.updateWhitelist(discordId, minecraftName, blocked) + val minecraftUuid = playerLookupService.getUuid(minecraftName) - event.reply(translatable("whitelist.embed.information.successfully-edited", discordName)) + if (minecraftUuid == null) { + event.reply(translatable("whitelist.survival.edit.modal.invalid_minecraft_name")) + .setEphemeral(true) + .queue() + return + } + + if (oldWhitelist.minecraftUuid != minecraftUuid) { + if (!socialService.updateMinecraftName(oldDiscordId, minecraftName)) { + event.reply(translatable("whitelist.survival.edit.modal.failed.minecraft")) + .setEphemeral(true) + .queue() + return + } + } + + if (oldWhitelist.blocked != blocked) { + if (!socialService.updateBlocked(oldDiscordId, blocked)) { + event.reply(translatable("whitelist.survival.edit.modal.failed.blocked")) + .setEphemeral(true) + .queue() + return + } + } + + if (oldWhitelist.discordId != discordId) { + if (!socialService.updateDiscordId(oldDiscordId, discordId)) { + event.reply(translatable("whitelist.survival.edit.modal.failed.discord")) + .setEphemeral(true) + .queue() + return + } + } + + event.reply( + translatable( + "whitelist.embed.information.successfully-edited", + discordName + ) + ) .setEphemeral(true) .queue() } diff --git a/src/main/kotlin/dev/slne/surf/discord/permission/DiscordPermission.kt b/src/main/kotlin/dev/slne/surf/discord/permission/DiscordPermission.kt index 67532002..cf5b4874 100644 --- a/src/main/kotlin/dev/slne/surf/discord/permission/DiscordPermission.kt +++ b/src/main/kotlin/dev/slne/surf/discord/permission/DiscordPermission.kt @@ -16,6 +16,7 @@ enum class DiscordPermission { WHITELIST_BYPASS, WHITELIST_EDIT, WHITELIST_DELETE, + WHITELIST_CREATE, TICKET_CLOSE, TICKET_CLOSE_BYPASS_CLAIM, diff --git a/src/main/kotlin/dev/slne/surf/discord/permission/permission-util.kt b/src/main/kotlin/dev/slne/surf/discord/permission/permission-util.kt index 4f894b0b..ab75eee2 100644 --- a/src/main/kotlin/dev/slne/surf/discord/permission/permission-util.kt +++ b/src/main/kotlin/dev/slne/surf/discord/permission/permission-util.kt @@ -26,15 +26,6 @@ private val guildPermissionConfig: Map>> // Server Admin 949704206888079490 to setOf(*DiscordPermission.entries.toTypedArray()), - // Twitch Mod - 651104534529179660 to setOf( - DiscordPermission.TICKET_REPLY_DEADLINE, - DiscordPermission.TICKET_CLOSE, - DiscordPermission.TICKET_CLAIM, - DiscordPermission.TICKET_SUPPORT_TWITCH_VIEW - ), - - // Discord Moderation 156164562499010560 to setOf( DiscordPermission.COMMAND_TICKET_ADD, @@ -61,7 +52,8 @@ private val guildPermissionConfig: Map>> DiscordPermission.WHITELIST_EDIT, DiscordPermission.TICKET_APPLICATION_TWITCH_MODERATOR, DiscordPermission.WHITELIST_DELETE, - DiscordPermission.TICKET_COMPLAINT_VIEW + DiscordPermission.TICKET_COMPLAINT_VIEW, + DiscordPermission.WHITELIST_CREATE ), // Management @@ -89,7 +81,8 @@ private val guildPermissionConfig: Map>> DiscordPermission.WHITELIST_BYPASS, DiscordPermission.WHITELIST_EDIT, DiscordPermission.WHITELIST_DELETE, - DiscordPermission.TICKET_COMPLAINT_VIEW + DiscordPermission.TICKET_COMPLAINT_VIEW, + DiscordPermission.WHITELIST_CREATE ), // Developer @@ -117,7 +110,8 @@ private val guildPermissionConfig: Map>> DiscordPermission.WHITELIST_VIEW, DiscordPermission.WHITELIST_BYPASS, DiscordPermission.WHITELIST_EDIT, - DiscordPermission.WHITELIST_DELETE + DiscordPermission.WHITELIST_DELETE, + DiscordPermission.WHITELIST_CREATE ), // Moderator @@ -154,6 +148,14 @@ private val guildPermissionConfig: Map>> DiscordPermission.WHITELIST_VIEW ), + // Twitch Mod + 651104534529179660 to setOf( + DiscordPermission.TICKET_REPLY_DEADLINE, + DiscordPermission.TICKET_CLOSE, + DiscordPermission.TICKET_CLAIM, + DiscordPermission.TICKET_SUPPORT_TWITCH_VIEW + ), + // Community Management 1403107386415386736L to setOf( DiscordPermission.COMMAND_FAQ diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/TicketType.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/TicketType.kt index baea3b9c..df710307 100644 --- a/src/main/kotlin/dev/slne/surf/discord/ticket/TicketType.kt +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/TicketType.kt @@ -138,6 +138,10 @@ enum class TicketType( displayName = "Bug nicht reproduzierbar", description = "Der Fehler konnte nicht reproduziert werden." ), + TicketCloseReason.of( + displayName = "Externer Fehler", + description = "Der Fehler liegt außerhalb unseres Einflusses und wurde an zuständige Stellen weitergeleitet." + ), TicketCloseReason.of( displayName = "Bug behoben", description = "Der gemeldete Fehler wurde behoben. Danke für deinen Bugreport!" diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/command/whitelist/CreateWhitelistCommand.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/command/whitelist/CreateWhitelistCommand.kt new file mode 100644 index 00000000..6d58bd6d --- /dev/null +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/command/whitelist/CreateWhitelistCommand.kt @@ -0,0 +1,79 @@ +package dev.slne.surf.discord.ticket.command.whitelist + +import dev.minn.jda.ktx.coroutines.await +import dev.slne.surf.discord.command.CommandOption +import dev.slne.surf.discord.command.CommandOptionType +import dev.slne.surf.discord.command.DiscordCommand +import dev.slne.surf.discord.command.SlashCommand +import dev.slne.surf.discord.config.botConfig +import dev.slne.surf.discord.jda +import dev.slne.surf.discord.messages.translatable +import dev.slne.surf.discord.permission.DiscordPermission +import dev.slne.surf.discord.permission.hasPermission +import dev.slne.surf.discord.ticket.database.whitelist.SocialService +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent +import org.springframework.stereotype.Component + +@DiscordCommand( + name = "wl-create", + description = "Whitelist Eintrag erstellen", + options = [CommandOption( + name = "discord-user", + description = "Der Discord Nutzer, der gewhitelisted werden soll", + type = CommandOptionType.USER, + required = true + ), + CommandOption( + name = "minecraft-name", + description = "Der Minecraft Name, der gewhitelisted werden soll.", + type = CommandOptionType.STRING, + required = true + )] +) +@Component +class CreateWhitelistCommand( + private val socialService: SocialService +) : SlashCommand { + override suspend fun execute(event: SlashCommandInteractionEvent) { + if (!event.member.hasPermission(DiscordPermission.WHITELIST_CREATE)) { + event.reply(translatable("no-permission")).setEphemeral(true).queue() + return + } + + val userId = event.getOption("discord-user")?.asUser?.idLong ?: return + val minecraftName = event.getOption("minecraft-name")?.asString ?: return + + if (socialService.getWhitelist(userId) != null) { + event.reply(translatable("whitelist.command.already-whitelisted.discord")) + .setEphemeral(true) + .queue() + return + } + + if (socialService.getWhitelist(minecraftName) != null) { + event.reply(translatable("whitelist.command.already-whitelisted.minecraft")) + .setEphemeral(true) + .queue() + return + } + + val whitelist = socialService.whitelist(userId, minecraftName) + + val discordUser = event.jda.retrieveUserById(userId).await() + + jda.guilds.forEach { + val role = it.getRoleById(botConfig.whitelistedRoleId) ?: return@forEach + it.addRoleToMember(discordUser, role).queue() + } + + event.reply( + translatable( + "whitelist.command.create.success", + "<@${whitelist?.discordId}>", + whitelist?.getMinecraftName() ?: whitelist?.minecraftUuid.toString() + ) + ) + .setEphemeral(true) + .queue() + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/database/util/AuditableLongIdTable.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/database/util/AuditableLongIdTable.kt new file mode 100644 index 00000000..978c31fd --- /dev/null +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/database/util/AuditableLongIdTable.kt @@ -0,0 +1,9 @@ +package dev.slne.surf.discord.ticket.database.util + +import dev.slne.surf.discord.ticket.database.column.offsetDateTime +import org.jetbrains.exposed.v1.core.dao.id.LongIdTable + +open class AuditableLongIdTable(name: String) : LongIdTable(name) { + val createdAt = offsetDateTime("created_at") + val updatedAt = offsetDateTime("updated_at") +} \ No newline at end of file diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialRepository.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialRepository.kt index ec3c4091..5be0a8ef 100644 --- a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialRepository.kt +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialRepository.kt @@ -1,5 +1,6 @@ package dev.slne.surf.discord.ticket.database.whitelist +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.jetbrains.exposed.v1.core.eq @@ -14,84 +15,199 @@ import java.util.* @Repository class SocialRepository { + // Check if a discord id is whitelisted (exists in connections and whitelist table) suspend fun isWhitelisted(discordId: Long) = suspendTransaction { - SocialsTable.selectAll().where(SocialsTable.discordUserId eq discordId).count() > 0 + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() + + if (connectionId == null) return@suspendTransaction false + + FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull()?.let { blocked -> !blocked } ?: false } suspend fun isWhitelisted(minecraftUuid: UUID) = suspendTransaction { - SocialsTable.selectAll().where(SocialsTable.minecraftUuid eq minecraftUuid) - .count() > 0 + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.minecraftUuid eq minecraftUuid) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() + + if (connectionId == null) return@suspendTransaction false + + FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull()?.let { blocked -> !blocked } ?: false } suspend fun whitelist(discordId: Long, minecraftUuid: UUID): SocialEntry = suspendTransaction { - val entry = SocialEntry( + val now = OffsetDateTime.now() + + // Ensure connection exists (insert if missing) + val existingConnectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.minecraftUuid eq minecraftUuid) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() + + val connectionId = if (existingConnectionId == null) { + SocialConnectionsTable.insert { + it[this.discordUserId] = discordId + it[this.minecraftUuid] = minecraftUuid + it[this.createdAt] = now + it[this.updatedAt] = now + } + + // read back id by unique minecraft uuid + SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.minecraftUuid eq minecraftUuid) + .map { it[SocialConnectionsTable.id].value } + .first() + } else existingConnectionId + + // Ensure whitelist entry exists + val existingWhitelist = FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull() + + if (existingWhitelist == null) { + FreebuildWhitelistTable.insert { + it[this.socialConnectionId] = connectionId + it[this.blocked] = false + it[this.createdAt] = now + it[this.updatedAt] = now + } + } else { + FreebuildWhitelistTable.update(where = { FreebuildWhitelistTable.socialConnectionId eq connectionId }) { + it[this.blocked] = false + it[this.updatedAt] = now + } + } + + SocialEntry( discordId = discordId, minecraftUuid = minecraftUuid, - createdAt = OffsetDateTime.now(), - updatedAt = OffsetDateTime.now() + blocked = false, + createdAt = now, + updatedAt = now ) - SocialsTable.insert { - it[this.discordUserId] = discordId - it[this.minecraftUuid] = minecraftUuid - it[this.createdAt] = entry.createdAt - it[this.updatedAt] = entry.updatedAt - } - - entry } suspend fun blockWhitelist(discordId: Long) = suspendTransaction { - SocialsTable.update(where = { SocialsTable.discordUserId eq discordId }) { + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() ?: return@suspendTransaction false + + FreebuildWhitelistTable.update(where = { FreebuildWhitelistTable.socialConnectionId eq connectionId }) { it[this.blocked] = true it[this.updatedAt] = OffsetDateTime.now() } > 0 } suspend fun unblockWhitelist(discordId: Long) = suspendTransaction { - SocialsTable.update(where = { SocialsTable.discordUserId eq discordId }) { + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() ?: return@suspendTransaction false + + FreebuildWhitelistTable.update(where = { FreebuildWhitelistTable.socialConnectionId eq connectionId }) { it[this.blocked] = false it[this.updatedAt] = OffsetDateTime.now() } > 0 } suspend fun getWhitelist(discordId: Long) = suspendTransaction { - SocialsTable.selectAll().where(SocialsTable.discordUserId eq discordId).map { - SocialEntry( - discordId = it[SocialsTable.discordUserId], - minecraftUuid = it[SocialsTable.minecraftUuid], - blocked = it[SocialsTable.blocked], - createdAt = it[SocialsTable.createdAt], - updatedAt = it[SocialsTable.updatedAt] - ) - }.firstOrNull() + val connectionRow = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it } + .firstOrNull() ?: return@suspendTransaction null + + val connectionId = connectionRow[SocialConnectionsTable.id].value + + val blocked = FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull() ?: false + + SocialEntry( + discordId = connectionRow[SocialConnectionsTable.discordUserId]!!, + minecraftUuid = connectionRow[SocialConnectionsTable.minecraftUuid], + blocked = blocked, + createdAt = connectionRow[SocialConnectionsTable.createdAt], + updatedAt = connectionRow[SocialConnectionsTable.updatedAt] + ) } suspend fun getWhitelist(minecraftUuid: UUID) = suspendTransaction { - SocialsTable.selectAll().where(SocialsTable.minecraftUuid eq minecraftUuid).map { - SocialEntry( - discordId = it[SocialsTable.discordUserId], - minecraftUuid = it[SocialsTable.minecraftUuid], - blocked = it[SocialsTable.blocked], - createdAt = it[SocialsTable.createdAt], - updatedAt = it[SocialsTable.updatedAt] - ) - }.firstOrNull() + val connectionRow = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.minecraftUuid eq minecraftUuid) + .map { it } + .firstOrNull() ?: return@suspendTransaction null + + val connectionId = connectionRow[SocialConnectionsTable.id].value + + val blocked = FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull() ?: false + + SocialEntry( + discordId = connectionRow[SocialConnectionsTable.discordUserId] + ?: return@suspendTransaction null, + minecraftUuid = connectionRow[SocialConnectionsTable.minecraftUuid], + blocked = blocked, + createdAt = connectionRow[SocialConnectionsTable.createdAt], + updatedAt = connectionRow[SocialConnectionsTable.updatedAt] + ) } - suspend fun editWhitelist( + suspend fun editMinecraftName( discordId: Long, - minecraftUuid: UUID, - blocked: Boolean + minecraftUuid: UUID ) = suspendTransaction { - SocialsTable.update(where = { SocialsTable.discordUserId eq discordId }) { + SocialConnectionsTable.update(where = { SocialConnectionsTable.discordUserId eq discordId }) { it[this.minecraftUuid] = minecraftUuid - it[this.blocked] = blocked it[this.updatedAt] = OffsetDateTime.now() } } + suspend fun editDiscordId( + oldDiscordId: Long, + newDiscordId: Long + ) = suspendTransaction { + SocialConnectionsTable.update(where = { SocialConnectionsTable.discordUserId eq oldDiscordId }) { + it[this.discordUserId] = newDiscordId + it[this.updatedAt] = OffsetDateTime.now() + } + } + + suspend fun editBlocked( + discordId: Long, + blocked: Boolean + ) = suspendTransaction { + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() ?: return@suspendTransaction false + + FreebuildWhitelistTable.update(where = { FreebuildWhitelistTable.socialConnectionId eq connectionId }) { + it[this.blocked] = blocked + it[this.updatedAt] = OffsetDateTime.now() + } > 0 + } + suspend fun deleteWhitelist(discordId: Long) = suspendTransaction { - SocialsTable.deleteWhere { SocialsTable.discordUserId eq discordId } > 0 + val connectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.discordUserId eq discordId) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() ?: return@suspendTransaction false + + FreebuildWhitelistTable.deleteWhere { FreebuildWhitelistTable.socialConnectionId eq connectionId } > 0 } } \ No newline at end of file diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialService.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialService.kt index 9f968156..d72b2a2a 100644 --- a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialService.kt +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialService.kt @@ -1,5 +1,8 @@ package dev.slne.surf.discord.ticket.database.whitelist +import dev.minn.jda.ktx.coroutines.await +import dev.slne.surf.discord.config.botConfig +import dev.slne.surf.discord.jda import dev.slne.surf.discord.util.PlayerLookupService import org.springframework.stereotype.Service @@ -28,13 +31,32 @@ class SocialService( socialRepository.getWhitelist(it) } - suspend fun updateWhitelist( - discordId: Long, - minecraftName: String, - blocked: Boolean - ) { - playerLookupService.getUuid(minecraftName)?.let { - socialRepository.editWhitelist(discordId, it, blocked) + suspend fun updateBlocked(discordId: Long, blocked: Boolean): Boolean = + socialRepository.editBlocked(discordId, blocked) + + suspend fun updateMinecraftName(discordId: Long, minecraftName: String): Boolean { + return playerLookupService.getUuid(minecraftName)?.let { + socialRepository.editMinecraftName(discordId, it) + } != null + } + + suspend fun updateDiscordId(oldDiscordId: Long, discordId: Long): Boolean { + return runCatching { + val oldDiscordUser = jda.retrieveUserById(oldDiscordId).await() + val newDiscordUser = jda.retrieveUserById(discordId).await() + + jda.guilds.forEach { guild -> + val role = guild.getRoleById(botConfig.whitelistedRoleId) + ?: return@runCatching false + + guild.removeRoleFromMember(oldDiscordUser, role).queue() + guild.addRoleToMember(newDiscordUser, role).queue() + } + + socialRepository.editDiscordId(oldDiscordId, discordId) + true + }.getOrElse { + false } } diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsMigration.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsMigration.kt new file mode 100644 index 00000000..1b037a3b --- /dev/null +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsMigration.kt @@ -0,0 +1,117 @@ +package dev.slne.surf.discord.ticket.database.whitelist + +import dev.slne.surf.discord.ticket.database.column.nativeUuid +import dev.slne.surf.discord.ticket.database.column.offsetDateTime +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import org.jetbrains.exposed.v1.core.Table +import org.jetbrains.exposed.v1.core.eq +import org.jetbrains.exposed.v1.r2dbc.insert +import org.jetbrains.exposed.v1.r2dbc.insertReturning +import org.jetbrains.exposed.v1.r2dbc.selectAll +import org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction +import org.jetbrains.exposed.v1.r2dbc.update +import java.time.OffsetDateTime + +/** + * Migration helper to convert rows from the legacy `social_connections` table + * into the new [SocialConnectionsTable] and [FreebuildWhitelistTable]. + * + * The migration is idempotent – already-converted rows are skipped. + */ +object SocialsMigration { + private object LegacySocialsTable : Table("social_connections") { + val discordUserId = long("discord_user_id") + val minecraftUuid = nativeUuid("minecraft_uuid").nullable() + val twitchId = long("twitch_id").nullable() + val blocked = bool("blocked").default(false) + val createdAt = offsetDateTime("created_at") + val updatedAt = offsetDateTime("updated_at") + } + + /** + * Migrate all legacy rows. Prints a progress bar to stdout. + */ + suspend fun migrateLegacy() = suspendTransaction { + val rows = LegacySocialsTable.selectAll().toList() + val total = rows.size + + for ((index, row) in rows.withIndex()) { + val current = index + 1 + + val minecraftUuid = row[LegacySocialsTable.minecraftUuid] ?: run { + printProgress(current, total) + continue + } + + val discordId = row[LegacySocialsTable.discordUserId] + val twitchId = row[LegacySocialsTable.twitchId] + val blocked = row[LegacySocialsTable.blocked] + val createdAt = row[LegacySocialsTable.createdAt] + val updatedAt = row[LegacySocialsTable.updatedAt] + + val existingConnectionId = SocialConnectionsTable.selectAll() + .where(SocialConnectionsTable.minecraftUuid eq minecraftUuid) + .map { it[SocialConnectionsTable.id].value } + .firstOrNull() + + if (existingConnectionId != null) { + syncWhitelistEntry(existingConnectionId, blocked, createdAt, updatedAt) + printProgress(current, total) + continue + } + + val connectionId = SocialConnectionsTable.insertReturning { + it[this.minecraftUuid] = minecraftUuid + it[this.discordUserId] = discordId + twitchId?.let { id -> it[this.twitchId] = id } + it[this.createdAt] = createdAt + it[this.updatedAt] = updatedAt + }.first()[SocialConnectionsTable.id].value + + FreebuildWhitelistTable.insert { + it[this.socialConnectionId] = connectionId + it[this.blocked] = blocked + it[this.createdAt] = createdAt + it[this.updatedAt] = updatedAt + } + + printProgress(current, total) + } + } + + private suspend fun syncWhitelistEntry( + connectionId: Long, + blocked: Boolean, + createdAt: OffsetDateTime, + updatedAt: OffsetDateTime + ) { + val existingBlocked = FreebuildWhitelistTable.selectAll() + .where(FreebuildWhitelistTable.socialConnectionId eq connectionId) + .map { it[FreebuildWhitelistTable.blocked] } + .firstOrNull() + + if (existingBlocked == null) { + FreebuildWhitelistTable.insert { + it[this.socialConnectionId] = connectionId + it[this.blocked] = blocked + it[this.createdAt] = createdAt + it[this.updatedAt] = updatedAt + } + } else if (existingBlocked != blocked) { + FreebuildWhitelistTable.update(where = { FreebuildWhitelistTable.socialConnectionId eq connectionId }) { + it[this.blocked] = blocked + it[this.updatedAt] = updatedAt + } + } + } + + private fun printProgress(current: Int, total: Int) { + val percent = if (total == 0) 100 else (current * 100) / total + val filled = (percent / 2).coerceIn(0, 50) + val bar = "=".repeat(filled) + " ".repeat(50 - filled) + print("\r[$bar] $percent% ($current/$total)") + } +} diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsTable.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsTable.kt index cfec61c9..cea93095 100644 --- a/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsTable.kt +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/database/whitelist/SocialsTable.kt @@ -1,14 +1,17 @@ package dev.slne.surf.discord.ticket.database.whitelist import dev.slne.surf.discord.ticket.database.column.nativeUuid -import dev.slne.surf.discord.ticket.database.column.offsetDateTime -import org.jetbrains.exposed.v1.core.dao.id.LongIdTable +import dev.slne.surf.discord.ticket.database.util.AuditableLongIdTable -object SocialsTable : LongIdTable("social_connections") { - val discordUserId = long("discord_user_id").uniqueIndex() + +object SocialConnectionsTable : AuditableLongIdTable("social_connections_new") { val minecraftUuid = nativeUuid("minecraft_uuid").uniqueIndex() + val discordUserId = long("discord_user_id").uniqueIndex().nullable() val twitchId = long("twitch_id").uniqueIndex().nullable() +} + +object FreebuildWhitelistTable : AuditableLongIdTable("freebuild_whitelists") { + val socialConnectionId = + long("social_connection_id").references(SocialConnectionsTable.id).uniqueIndex() val blocked = bool("blocked").default(false) - val createdAt = offsetDateTime("created_at") - val updatedAt = offsetDateTime("updated_at") } \ No newline at end of file diff --git a/src/main/kotlin/dev/slne/surf/discord/ticket/listener/TicketLeaveListener.kt b/src/main/kotlin/dev/slne/surf/discord/ticket/listener/TicketLeaveListener.kt index 6adf8657..a4b32e35 100644 --- a/src/main/kotlin/dev/slne/surf/discord/ticket/listener/TicketLeaveListener.kt +++ b/src/main/kotlin/dev/slne/surf/discord/ticket/listener/TicketLeaveListener.kt @@ -1,6 +1,6 @@ package dev.slne.surf.discord.ticket.listener -import dev.minn.jda.ktx.coroutines.await +import dev.slne.surf.discord.dsl.embed import dev.slne.surf.discord.ticket.TicketMemberService import dev.slne.surf.discord.ticket.TicketService import kotlinx.coroutines.CoroutineScope @@ -19,12 +19,12 @@ class TicketLeaveListener( discordScope.launch { val ticket = ticketService.getTicketByThreadId(event.thread.idLong) ?: return@launch - if (ticketMemberService.isMember(ticket, event.threadMember.member.idLong)) { - val message = - event.thread.sendMessage("Readding ${event.threadMember.member.nickname}") - .await() - val edited = message.editMessage(event.threadMember.member.asMention).await() - edited.delete().queue() + if (ticketMemberService.isMember(ticket, event.threadMemberIdLong)) { + event.thread.sendMessage("<@${event.threadMemberIdLong}>").setEmbeds(embed { + title = "Willkommen zurück!" + description = + "Du wolltest flüchten - zum Glück habe ich dich an der Leine und konnte dich im Ticket behalten. Bitte habe Geduld, damit wir das Problem gemeinsam lösen können." + }).queue() } } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index c969905b..5ddc89dc 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -141,13 +141,20 @@ whitelist.survival.modal.user-not-found=Der Spieler wurde nicht gefunden. Bitte whitelist.survival.modal.success=Du wurdest gewhitelisted. whitelist.survival.edit.modal.title=Whitelist bearbeiten: {0} whitelist.survival.edit.modal.blocked.label=Blockiert -whitelist.survival.delete.modal.title=Whitelist Eintrag löschen: {0} +whitelist.survival.delete.modal.title=Whitelist löschen: {0} whitelist.survival.edit.modal.delete.label=Möchtest du diesen Whitelist-Eintrag löschen? whitelist.survival.delete.modal.cancelled=Der Löschvorgang wurde abgebrochen. whitelist.command.view.missing-parameters=Bitte gib den Minecraft Namen oder die Discord User des Spielers an, dessen Whitelist-Eintrag du ansehen möchtest. whitelist.embed.information.successfully-deleted=Der Whitelisteintrag wurde gelöscht. whitelist.embed.information.successfully-edited=Der Whitelisteintrag von {0} wurde erfolgreich bearbeitet. whitelist.embed.information.no_whitelist=Es wurde keine Whitelist gefunden. +whitelist.survival.edit.modal.invalid_minecraft_name=Der Minecraft Name ist ungültig. Bitte überprüfe die Schreibweise. +whitelist.survival.edit.modal.failed.blocked=Beim Bearbeiten des Blockiert-Status ist ein Fehler aufgetreten. +whitelist.survival.edit.modal.failed.minecraft=Beim Bearbeiten des Minecraft Namens ist ein Fehler aufgetreten. +whitelist.survival.edit.modal.failed.discord=Beim Bearbeiten des Discord Benutzers ist ein Fehler aufgetreten. +whitelist.command.already-whitelisted.discord=Ein Spieler mit diesem Discord Account ist bereits gewhitelisted. +whitelist.command.already-whitelisted.minecraft=Ein Spieler mit diesem Minecraft Namen ist bereits gewhitelisted. +whitelist.command.create.success=Der Whitelist-Eintrag für den Discord Benutzer {0} und den Minecraft Namen {1} wurde erfolgreich erstellt. whitelist.embed.information.title=Whitelistinformation whitelist.embed.information.minecraft=Minecraft Name whitelist.embed.information.discord=Discord Benutzer