From b5d731d7899bc0d5c3bd34e9658f990964cb95c1 Mon Sep 17 00:00:00 2001 From: NoComment Date: Tue, 9 May 2023 22:19:23 +0100 Subject: [PATCH 01/13] Make several crimes against the clear commands --- docs/commands.md | 40 ++- .../moderation/ModerationCommands.kt | 340 ++++++++++++++---- 2 files changed, 306 insertions(+), 74 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 27da9e17..9998cd10 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -313,12 +313,44 @@ Description: Kicks a user. * `image` - An image you'd like to provide as extra context for the action - Optional Attachment --- -### Command name: `clear` -Description: Clears messages from a channel. -**Required Member Permissions**: Manage Messages +#### Command name: `clear count` +**Description**: Clear a specific count of messages +Required Member Permissions: Manage Messages -* Arguments: +* **Arguments**: * `messages` - Number of messages to delete - Int + * `author` - The author of the messages to clear - Optional User + +--- +#### Command name: `clear before` +**Description**: Clear messages before a given message ID +Required Member Permissions: Manage Messages + +* **Arguments**: + * `before` - The ID of the message to clear before - Snowflake + * `message-count` - The number of messages to clear - Optional Int/Long + * `author` - The author of the messages to clear - Optional User + +--- +#### Command name: `clear after` +**Description**: Clear messages before a given message ID +Required Member Permissions: Manage Messages + +* **Arguments**: + * `after` - The ID of the message to clear after - Snowflake + * `message-count` - The number of messages to clear - Optional Int/Long + * `author` - The author of the messages to clear - Optional User + +--- +#### Command name: `clear between` +**Description**: Clear messages between 2 message IDs +Required Member Permissions: Manage Messages + +* **Arguments**: + * `after` - The ID of the message to clear after - Snowflake + * `before` - The ID of the message to clear before - Snowflake + * `message-count` - The number of messages to clear - Optional Int/Long + * `author` - The author of the messages to clear - Optional User --- ### Command name: `timeout` diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt index fcad7f10..8e694a14 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt @@ -9,12 +9,16 @@ import com.kotlindiscord.kord.extensions.checks.hasPermission import com.kotlindiscord.kord.extensions.checks.hasPermissions import com.kotlindiscord.kord.extensions.checks.types.CheckContextWithCache import com.kotlindiscord.kord.extensions.commands.Arguments +import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSlashCommandContext +import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingOptionalDuration import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingString import com.kotlindiscord.kord.extensions.commands.converters.impl.int import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalAttachment import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalInt +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalUser +import com.kotlindiscord.kord.extensions.commands.converters.impl.snowflake import com.kotlindiscord.kord.extensions.commands.converters.impl.user import com.kotlindiscord.kord.extensions.components.components import com.kotlindiscord.kord.extensions.components.ephemeralSelectMenu @@ -47,8 +51,10 @@ import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.builder.message.create.embed import dev.kord.rest.request.KtorRequestException +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.toSet import kotlinx.datetime.Clock import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone @@ -79,8 +85,8 @@ class ModerationCommands : Extension() { override val name = "moderation" private val warnSuffix = "Please consider your actions carefully.\n\n" + - "For more information about the warn system, please see [this document]" + - "($HYACINTH_GITHUB/LilyBot/blob/main/docs/commands.md#name-warn)" + "For more information about the warn system, please see [this document]" + + "($HYACINTH_GITHUB/LilyBot/blob/main/docs/commands.md#name-warn)" @OptIn(DoNotChain::class) override suspend fun setup() { @@ -181,14 +187,14 @@ class ModerationCommands : Extension() { embed { title = "Banned." description = "${sender.mention} user was banned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Banned." description = "${sender.mention} user was banned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -219,7 +225,7 @@ class ModerationCommands : Extension() { title = "You have been soft-banned from ${guild?.asGuildOrNull()?.name}" description = "Quick soft-banned $reasonSuffix. This is a soft-ban, you are " + - "free to rejoin at any time" + "free to rejoin at any time" } } @@ -238,14 +244,14 @@ class ModerationCommands : Extension() { embed { title = "Soft-banned." description = "${sender.mention} user was soft-banned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Soft-Banned." description = "${sender.mention} user was soft-banned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -288,14 +294,14 @@ class ModerationCommands : Extension() { embed { title = "Kicked." description = "${sender.mention} user was kicked " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Kicked." description = "${sender.mention} user was kicked " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -350,14 +356,14 @@ class ModerationCommands : Extension() { embed { title = "Timed-out." description = "${sender.mention} user was timed-out for " + - "${timeoutTime.interval()} for sending this message." + "${timeoutTime.interval()} for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Timed-out." description = "${sender.mention} user was timed-out for " + - "${timeoutTime.interval()} for sending a deleted message." + "${timeoutTime.interval()} for sending a deleted message." } } } @@ -450,14 +456,14 @@ class ModerationCommands : Extension() { embed { title = "Warned." description = "${sender.mention} user was warned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Warned." description = "${sender.mention} user was warned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -596,7 +602,7 @@ class ModerationCommands : Extension() { dmEmbed { title = "You have been soft-banned from ${guild?.fetchGuild()?.name}" description = "**Reason:**\n${arguments.reason}\n\n" + - "You are free to rejoin without the need to be unbanned" + "You are free to rejoin without the need to be unbanned" } } @@ -719,57 +725,80 @@ class ModerationCommands : Extension() { } } - ephemeralSlashCommand(::ClearArgs) { + ephemeralSlashCommand { name = "clear" - description = "Clears messages from a channel." + description = "Parent command for clear commands" - requirePermission(Permission.ManageMessages) + ephemeralSubCommand(ClearCommandArgs::Count) { + name = "count" + description = "Clear a specific count of messages" - check { - modCommandChecks(Permission.ManageMessages) - requireBotPermissions(Permission.ManageMessages) - botHasChannelPerms(Permissions(Permission.ManageMessages)) + requirePermission(Permission.ManageMessages) + + check { + modCommandChecks(Permission.ManageMessages) + requireBotPermissions(Permission.ManageMessages) + botHasChannelPerms(Permissions(Permission.ManageMessages)) + } + + action { + clearMessages(arguments.count, null, null, arguments.author) + } } - action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val messageAmount = arguments.messages - val textChannel = channel.asChannelOfOrNull() + ephemeralSubCommand(ClearCommandArgs::Before) { + name = "before" + description = "Clear messages before a given message ID" - if (textChannel == null) { - respond { - content = "Could not get the channel to clear messages from." - } - return@action + requirePermission(Permission.ManageMessages) + + check { + modCommandChecks(Permission.ManageMessages) + requireBotPermissions(Permission.ManageMessages) + botHasChannelPerms(Permissions(Permission.ManageMessages)) + } + + action { + clearMessages(arguments.count, Snowflake(arguments.before.value + 1u), null, arguments.author) } + } - // Get the specified amount of messages into an array list of Snowflakes and delete them - val messages = channel.withStrategy(EntitySupplyStrategy.rest).getMessagesBefore( - Snowflake.max, min(messageAmount, 100) - ).map { it.id }.toList() + ephemeralSubCommand(ClearCommandArgs::After) { + name = "after" + description = "Clear messages before a given message ID" - textChannel.bulkDelete(messages) + requirePermission(Permission.ManageMessages) - respond { - content = "Messages cleared." + check { + modCommandChecks(Permission.ManageMessages) + requireBotPermissions(Permission.ManageMessages) + botHasChannelPerms(Permissions(Permission.ManageMessages)) } - if (config.publicLogging != null && config.publicLogging == true) { - channel.createEmbed { - title = "$messageAmount messages have been cleared." - color = DISCORD_BLACK - } + action { + clearMessages(arguments.count, null, Snowflake(arguments.after.value - 1u), arguments.author) } + } - val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action - actionLog.createEmbed { - title = "$messageAmount messages have been cleared." - description = "Action occurred in ${textChannel.mention}" - footer { - text = user.asUserOrNull()?.tag ?: "Unable to get user tag" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - color = DISCORD_BLACK + ephemeralSubCommand(ClearCommandArgs::Between) { + name = "between" + description = "Clear messages between 2 message IDs" + + requirePermission(Permission.ManageMessages) + + check { + modCommandChecks(Permission.ManageMessages) + requireBotPermissions(Permission.ManageMessages) + botHasChannelPerms(Permissions(Permission.ManageMessages)) + } + + action { + clearMessages( + arguments.count, + Snowflake(arguments.before.value + 1u), + Snowflake(arguments.after.value - 1u), + arguments.author + ) } } } @@ -1171,14 +1200,6 @@ class ModerationCommands : Extension() { } } - inner class ClearArgs : Arguments() { - /** The number of messages the user wants to remove. */ - val messages by int { - name = "messages" - description = "Number of messages to delete" - } - } - inner class TimeoutArgs : Arguments() { /** The requested user to timeout. */ val userArgument by user { @@ -1285,22 +1306,29 @@ private suspend fun CheckContextWithCache<*>.modCommandChecks(actionPermission: hasPermission(actionPermission) } -private fun EmbedBuilder.warnTimeoutLog(timeoutNumber: Int, moderator: User, targetUser: User, reason: String) { - when (timeoutNumber) { +/** + * Creates a log for timeouts produced by a number of warnings. + * + * @param warningNumber The number of warning strikes the user has + * @param moderator The moderator that actioned the warning + * @param targetUser The User that was warned + * @param reason The reason for the warning + * @author NoComment1105 + * @since 4.4.0 + */ +private fun EmbedBuilder.warnTimeoutLog(warningNumber: Int, moderator: User, targetUser: User, reason: String) { + when (warningNumber) { 1 -> {} - 2 -> - description = "${targetUser.mention} has been timed-out for 3 hours due to 2 warn strikes" + 2 -> description = "${targetUser.mention} has been timed-out for 3 hours due to 2 warn strikes" - 3 -> - description = "${targetUser.mention} has been timed-out for 12 hours due to 3 warn strikes" + 3 -> description = "${targetUser.mention} has been timed-out for 12 hours due to 3 warn strikes" else -> - description = "${targetUser.mention} has been timed-out for 3 days due to $timeoutNumber warn " + - "strikes\nIt might be time to consider other " + - "action." + description = "${targetUser.mention} has been timed-out for 3 days due to $warningNumber warn " + + "strikes\nIt might be time to consider other action." } - if (timeoutNumber != 1) { + if (warningNumber != 1) { title = "Timeout" field { name = "User" @@ -1318,3 +1346,175 @@ private fun EmbedBuilder.warnTimeoutLog(timeoutNumber: Int, moderator: User, tar timestamp = Clock.System.now() } } + +/** + * A function to use clear messages based on the count, before and after, as well as a user. + * + * @param count The number of messages to clear, or null + * @param before The ID of the message to clear messages before + * @param after The ID of the message to clear messages after + * @param author The author of the messages that should be cleared + * @author NoComment1105 + * @since 4.8.6 + */ +private suspend fun EphemeralSlashCommandContext<*, *>.clearMessages( + count: Int?, + before: Snowflake?, + after: Snowflake?, + author: User? +) { + val config = ModerationConfigCollection().getConfig(guild!!.id)!! + val textChannel = channel.asChannelOfOrNull() + + if (textChannel == null) { + respond { + content = "Could not get the channel to clear messages from." + } + return + } + + if ((before != null && after != null) && (before < after)) { + respond { + content = "Before cannot be more recent than after!" + } + return + } + + // Get the specified amount of messages into an array list of Snowflakes and delete them + // Send help + val messageFlow = if (before == null && after == null) { + channel.withStrategy(EntitySupplyStrategy.rest).getMessagesBefore(Snowflake.max, count?.let { min(it, 100) }) + } else if (after != null && before == null) { + channel.withStrategy(EntitySupplyStrategy.rest).getMessagesAfter(after, count?.let { min(it, 100) }) + } else if (after == null && before != null) { + channel.withStrategy(EntitySupplyStrategy.rest).getMessagesBefore(before, count?.let { min(it, 100) }) + } else if (after != null && before != null) { + channel.withStrategy(EntitySupplyStrategy.rest).getMessagesBefore(before, count?.let { min(it, 100) }) + .filter { it.id > after } + } else { + flowOf() + } + + val messages = if (author == null) { + messageFlow.map { it.id }.toSet() + } else { + messageFlow.filter { it.author == author }.map { it.id }.toSet() + } + + textChannel.bulkDelete(messages) + + respond { + content = "Messages cleared." + } + + if (config.publicLogging != null && config.publicLogging == true) { + channel.createEmbed { + title = "$count messages have been cleared." + color = DISCORD_BLACK + } + } + + val actionLog = + getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return + actionLog.createEmbed { + title = "${count ?: messages.size} messages have been cleared." + description = "Action occurred in ${textChannel.mention}" + footer { + text = user.asUserOrNull()?.tag ?: "Unable to get user tag" + icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } + color = DISCORD_BLACK + } +} + +/** + * An object containing the arguments for clear commands. + * + * @since 4.8.6 + */ +@Suppress("MemberNameEqualsClassName") // Cope +object ClearCommandArgs { + /** Clear a specific count of messages. */ + class Count : Arguments() { + /** The number of messages the user wants to remove. */ + val count by int { + name = "messages" + description = "Number of messages to delete" + } + + /** The author of the messages that need clearing. */ + val author by optionalUser { + name = "author" + description = "The author of the messages to clear" + } + } + + /** Clear messages after a specific one. */ + class After : Arguments() { + /** The ID of the message to start clearing from. */ + val after by snowflake { + name = "after" + description = "The ID of the message to clear after" + } + + /** The number of messages the user wants to remove. */ + val count by optionalInt { + name = "message-count" + description = "The number of messages to clear" + } + + /** The author of the messages that need clearing. */ + val author by optionalUser { + name = "author" + description = "The author of the messages to clear" + } + } + + /** Clear messages before a specific one. */ + class Before : Arguments() { + /** The ID of the message to start clearing before. */ + val before by snowflake { + name = "before" + description = "The ID of the message to clear before" + } + + /** The number of messages the user wants to remove. */ + val count by optionalInt { + name = "message-count" + description = "The number of messages to clear" + } + + /** The author of the messages that need clearing. */ + val author by optionalUser { + name = "author" + description = "The author of the messages to clear" + } + } + + /** Clear messages between 2 specific ones. */ + class Between : Arguments() { + /** The ID of the message to start clearing from. */ + val after by snowflake { + name = "after" + description = "The ID of the message to clear after" + } + + /** The ID of the message to start clearing before. */ + val before by snowflake { + name = "before" + description = "The ID of the message to clear before" + } + + /** The number of messages the user wants to remove. */ + val count by optionalInt { + name = "message-count" + description = "The number of messages to clear" + } + + /** The author of the messages that need clearing. */ + val author by optionalUser { + name = "author" + description = "The author of the messages to clear" + } + } +} From 0d3c1157abc46be7db0c1bc79840bd541b0db691 Mon Sep 17 00:00:00 2001 From: NoComment Date: Sat, 8 Jul 2023 12:06:29 +0100 Subject: [PATCH 02/13] Upgradle to 8.2 and update some deps --- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 5 ++++- libs.versions.toml | 8 ++++---- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fa430ee0..b2a6de24 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -108,7 +108,7 @@ tasks { detekt { buildUponDefaultConfig = true - config = files("$rootDir/detekt.yml") + config.setFrom("$rootDir/detekt.yml") autoCorrect = true } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 16170 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 delta 14892 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3..62f495df 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..fcb6fca1 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. diff --git a/libs.versions.toml b/libs.versions.toml index 4cd93427..1d823963 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -8,13 +8,13 @@ grgit = "5.2.0" blossom = "1.3.1" # Libraries -kord-extensions = "1.5.7-20230506.104616-11" +kord-extensions = "1.5.8-20230701.153318-4" logging = "3.0.5" -logback = "1.4.7" -github-api = "1.314" +logback = "1.4.8" +github-api = "1.315" kmongo = "4.9.0" cozy-welcome = "1.0.1-SNAPSHOT" -dma = "0.2.0-SNAPSHOT" +dma = "v0.2.1" docgenerator = "0.1.2-SNAPSHOT" [libraries] From 8be156002e0158fbca37073523a337ef069db60c Mon Sep 17 00:00:00 2001 From: NoComment Date: Tue, 15 Aug 2023 06:10:52 +0100 Subject: [PATCH 03/13] Remove Old Log-uploading DB Code --- .../lilybot/database/entities/Config.kt | 21 ------------ .../database/migrations/config/configV5.kt | 32 +++++++++---------- 2 files changed, 15 insertions(+), 38 deletions(-) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt index 07cdef37..8ee311dd 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt @@ -56,27 +56,6 @@ data class ModerationConfigData( val publicLogging: Boolean?, ) -/** - * The data for miscellaneous configuration. The miscellaneous config stores the data for enabling or disabling log - * uploading. - * - * @property guildId The ID of the guild the config is for - * @property disableLogUploading If log uploading is enabled or not - * @property utilityLogChannel The channel to log various utility actions too - * @since 4.0.0 - */ -@Deprecated( - "Replaced by org.hyacinthbots.database.entities.UtilityConfigData", - ReplaceWith("UtilityConfigData", "import org.hyacinthbots.lilybot.database.entities.UtilityConfigData"), - DeprecationLevel.ERROR -) -@Serializable -data class UtilityConfigDataOld( - val guildId: Snowflake, - val disableLogUploading: Boolean, - val utilityLogChannel: Snowflake? -) - /** * The data for miscellaneous configuration. The miscellaneous config stores the data for enabling or disabling log * uploading. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt index 35d0b4e8..1a264ed1 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt @@ -1,24 +1,22 @@ -@file:Suppress("DEPRECATION_ERROR") +@file:Suppress("DEPRECATION_ERROR", "UnusedPrivateMember", "UNUSED_PARAMETER", "RedundantSuspendModifier") package org.hyacinthbots.lilybot.database.migrations.config -import org.hyacinthbots.lilybot.database.entities.UtilityConfigData -import org.hyacinthbots.lilybot.database.entities.UtilityConfigDataOld import org.litote.kmongo.coroutine.CoroutineDatabase suspend fun configV5(db: CoroutineDatabase) { - val collection = db.getCollection("utilityConfigData") - val oldConfigs = collection.find().toList() - val newConfigs = mutableListOf() - - oldConfigs.forEach { - newConfigs.add(UtilityConfigData(it.guildId, it.utilityLogChannel)) - } - - db.dropCollection("utilityConfigData") - db.createCollection("utilityConfigData") - - with(db.getCollection("utilityConfigData")) { - insertMany(newConfigs) - } +// val collection = db.getCollection("utilityConfigData") +// val oldConfigs = collection.find().toList() +// val newConfigs = mutableListOf() +// +// oldConfigs.forEach { +// newConfigs.add(UtilityConfigData(it.guildId, it.utilityLogChannel)) +// } +// +// db.dropCollection("utilityConfigData") +// db.createCollection("utilityConfigData") +// +// with(db.getCollection("utilityConfigData")) { +// insertMany(newConfigs) +// } } From 7a3341cb0514e10cb9c9c79278c1c4a6b01ca716 Mon Sep 17 00:00:00 2001 From: NoComment Date: Tue, 15 Aug 2023 06:37:17 +0100 Subject: [PATCH 04/13] Allow the addition of a custom message to send with bans, intended for appeal server linking --- docs/commands.md | 3 +- .../lilybot/database/entities/Config.kt | 2 + .../lilybot/database/migrations/Migrator.kt | 2 + .../database/migrations/config/configV6.kt | 15 +++++ .../lilybot/extensions/config/Config.kt | 18 +++++- .../moderation/ModerationCommands.kt | 56 +++++++++++-------- 6 files changed, 70 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt diff --git a/docs/commands.md b/docs/commands.md index 6ab5f523..36756b4b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -46,6 +46,7 @@ Required Member Permissions: Manage Server * `quick-timeout-length` - The length of timeouts to use for quick timeouts - Coalescing Optional Duration * `warn-auto-punishments` - Whether to automatically punish users for reach a certain threshold on warns - Optional Boolean * `log-publicly` - Whether to log moderation publicly or not. - Optional Boolean + * `ban-dm-message` - A custom message to send to users when they are banned. - Optional String --- #### Command name: `config logging` @@ -645,7 +646,7 @@ Description: Check whether a given domain is a known phishing domain. **Description**: Set a custom API URL, "reset" to reset * **Arguments**: - * `api-url` - Set an alternative API url, or "reset" to use the default - Optional String + * `api-url` - Set an alternative API URL, or "reset" to use the default - Optional String --- #### Command name: `pluralkit bot` diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt index 8ee311dd..84a256af 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt @@ -43,6 +43,7 @@ data class LoggingConfigData( * @property quickTimeoutLength The length of timeout to apply when using the moderate menu * @property autoPunishOnWarn Whether to automatically apply punishments for reaching certain warn strike counts * @property publicLogging Whether to log moderation actions publicly in the channel the command was run in + * @property banDmMessage The message to send in a DM to a user when they are banned. * @since 4.0.0 */ @Serializable @@ -54,6 +55,7 @@ data class ModerationConfigData( val quickTimeoutLength: DateTimePeriod?, val autoPunishOnWarn: Boolean?, val publicLogging: Boolean?, + val banDmMessage: String?, ) /** diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt index c02838bd..3c8af692 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt @@ -23,6 +23,7 @@ import org.hyacinthbots.lilybot.database.migrations.config.configV2 import org.hyacinthbots.lilybot.database.migrations.config.configV3 import org.hyacinthbots.lilybot.database.migrations.config.configV4 import org.hyacinthbots.lilybot.database.migrations.config.configV5 +import org.hyacinthbots.lilybot.database.migrations.config.configV6 import org.hyacinthbots.lilybot.database.migrations.main.mainV1 import org.hyacinthbots.lilybot.database.migrations.main.mainV2 import org.hyacinthbots.lilybot.database.migrations.main.mainV3 @@ -119,6 +120,7 @@ object Migrator : KordExKoinComponent { 3 -> ::configV3 4 -> ::configV4 5 -> ::configV5 + 6 -> ::configV6 else -> break }(db.configDatabase) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt new file mode 100644 index 00000000..da9c7cd0 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt @@ -0,0 +1,15 @@ +package org.hyacinthbots.lilybot.database.migrations.config + +import org.hyacinthbots.lilybot.database.entities.ModerationConfigData +import org.litote.kmongo.coroutine.CoroutineDatabase +import org.litote.kmongo.exists +import org.litote.kmongo.setValue + +suspend fun configV6(db: CoroutineDatabase) { + with(db.getCollection("moderationConfigData")) { + updateMany( + ModerationConfigData::banDmMessage exists false, + setValue(ModerationConfigData::banDmMessage, null) + ) + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt index 818eb1fb..20bef24f 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt @@ -10,6 +10,7 @@ import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingOpti import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalBoolean import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalChannel import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalRole +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString import com.kotlindiscord.kord.extensions.components.forms.ModalForm import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand @@ -79,6 +80,7 @@ class Config : Extension() { null, null, null, + null, null ) ) @@ -150,6 +152,10 @@ class Config : Extension() { null -> "Disabled" } } + field { + name = "Ban DM Message" + value = arguments.banDmMessage ?: "No custom Ban DM message set" + } footer { text = "Configured by ${user.asUserOrNull()?.username}" } @@ -169,7 +175,8 @@ class Config : Extension() { arguments.moderatorRole?.id, arguments.quickTimeoutLength, arguments.warnAutoPunishments, - arguments.logPublicly + arguments.logPublicly, + arguments.banDmMessage ) ) @@ -637,6 +644,10 @@ class Config : Extension() { null -> "Disabled" } } + field { + name = "Ban DM Message" + value = config.banDmMessage ?: "None" + } timestamp = Clock.System.now() } } @@ -750,6 +761,11 @@ class Config : Extension() { name = "log-publicly" description = "Whether to log moderation publicly or not." } + + val banDmMessage by optionalString { + name = "ban-dm-message" + description = "A custom message to send to users when they are banned." + } } inner class LoggingArgs : Arguments() { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt index f4be0cd5..942e8f75 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt @@ -79,8 +79,8 @@ class ModerationCommands : Extension() { override val name = "moderation" private val warnSuffix = "Please consider your actions carefully.\n\n" + - "For more information about the warn system, please see [this document]" + - "($HYACINTH_GITHUB/LilyBot/blob/main/docs/commands.md#name-warn)" + "For more information about the warn system, please see [this document]" + + "($HYACINTH_GITHUB/LilyBot/blob/main/docs/commands.md#name-warn)" @OptIn(DoNotChain::class) override suspend fun setup() { @@ -110,8 +110,8 @@ class ModerationCommands : Extension() { if (targetMessage.author.isNullOrBot()) { val proxiedMessage = PluralKit().getMessageOrNull(targetMessage.id) proxiedMessage ?: run { - respond { content = "Unable to find user" } - return@action + respond { content = "Unable to find user" } + return@action } senderId = proxiedMessage.sender } else { @@ -119,8 +119,8 @@ class ModerationCommands : Extension() { } val sender = guild!!.getMemberOrNull(senderId) ?: run { - respond { content = "Unable to find user" } - return@action + respond { content = "Unable to find user" } + return@action } isBotOrModerator(event.kord, sender.asUserOrNull(), guild, "moderate") ?: return@action @@ -168,8 +168,8 @@ class ModerationCommands : Extension() { val dm = sender.dm { embed { title = "You have been banned from ${guild?.asGuildOrNull()?.name}" - description = - "Quick banned $reasonSuffix" + description = modConfig?.banDmMessage ?: "Quick banned $reasonSuffix" + color = DISCORD_GREEN } } @@ -187,14 +187,14 @@ class ModerationCommands : Extension() { embed { title = "Banned." description = "${sender.mention} user was banned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Banned." description = "${sender.mention} user was banned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -225,7 +225,7 @@ class ModerationCommands : Extension() { title = "You have been soft-banned from ${guild?.asGuildOrNull()?.name}" description = "Quick soft-banned $reasonSuffix. This is a soft-ban, you are " + - "free to rejoin at any time" + "free to rejoin at any time" } } @@ -244,14 +244,14 @@ class ModerationCommands : Extension() { embed { title = "Soft-banned." description = "${sender.mention} user was soft-banned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Soft-Banned." description = "${sender.mention} user was soft-banned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -294,14 +294,14 @@ class ModerationCommands : Extension() { embed { title = "Kicked." description = "${sender.mention} user was kicked " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Kicked." description = "${sender.mention} user was kicked " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -356,14 +356,14 @@ class ModerationCommands : Extension() { embed { title = "Timed-out." description = "${sender.mention} user was timed-out for " + - "${timeoutTime.interval()} for sending this message." + "${timeoutTime.interval()} for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Timed-out." description = "${sender.mention} user was timed-out for " + - "${timeoutTime.interval()} for sending a deleted message." + "${timeoutTime.interval()} for sending a deleted message." } } } @@ -456,14 +456,14 @@ class ModerationCommands : Extension() { embed { title = "Warned." description = "${sender.mention} user was warned " + - "for sending this message." + "for sending this message." } } } catch (e: KtorRequestException) { channel.createEmbed { title = "Warned." description = "${sender.mention} user was warned " + - "for sending a deleted message." + "for sending a deleted message." } } } @@ -496,9 +496,11 @@ class ModerationCommands : Extension() { return@action } + val modConfig = ModerationConfigCollection().getConfig(guild!!.id) + val action = ban(arguments.userArgument) { reason = arguments.reason - logPublicly = ModerationConfigCollection().getConfig(guild!!.id)?.publicLogging + logPublicly = modConfig?.publicLogging sendActionLog = true sendDm = arguments.dm removeTimeout = true @@ -531,7 +533,13 @@ class ModerationCommands : Extension() { dmEmbed { title = "You have been banned from ${guild?.asGuildOrNull()?.name}" - description = "**Reason:**\n${arguments.reason}" + description = "**Reason:**\n${arguments.reason} ${ + if (modConfig?.banDmMessage != null) { + "\n${modConfig.banDmMessage}" + } else { + "" + } + }" } } @@ -602,7 +610,7 @@ class ModerationCommands : Extension() { dmEmbed { title = "You have been soft-banned from ${guild?.fetchGuild()?.name}" description = "**Reason:**\n${arguments.reason}\n\n" + - "You are free to rejoin without the need to be unbanned" + "You are free to rejoin without the need to be unbanned" } } @@ -1302,8 +1310,8 @@ private fun EmbedBuilder.warnTimeoutLog(timeoutNumber: Int, moderator: User, tar else -> description = "${targetUser.mention} has been timed-out for 3 days due to $timeoutNumber warn " + - "strikes\nIt might be time to consider other " + - "action." + "strikes\nIt might be time to consider other " + + "action." } if (timeoutNumber != 1) { From ce2f23d2bf65361ea332302038bb4d89acdd4283 Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 22 Aug 2023 23:23:31 +0100 Subject: [PATCH 05/13] Move version catalogue --- libs.versions.toml => gradle/libs.versions.toml | 0 settings.gradle.kts | 8 -------- 2 files changed, 8 deletions(-) rename libs.versions.toml => gradle/libs.versions.toml (100%) diff --git a/libs.versions.toml b/gradle/libs.versions.toml similarity index 100% rename from libs.versions.toml rename to gradle/libs.versions.toml diff --git a/settings.gradle.kts b/settings.gradle.kts index 5746ff1b..9e813ec3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,9 +1 @@ rootProject.name = "LilyBot" - -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("libs.versions.toml")) - } - } -} From d03a5e3cf260b8f1f2d595b86ee16ca06551f114 Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 22 Aug 2023 23:26:41 +0100 Subject: [PATCH 06/13] Enable dependabot --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7e6e322b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +enable-beta-ecosystems: true + +updates: + - package-ecosystem: "gradle" + target-branch: "develop" + directory: "/" + + schedule: + interval: "weekly" + + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + + schedule: + interval: "weekly" From 66d6128f41d57a87795b0a296dd63c4b293b74cc Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Mon, 4 Sep 2023 08:51:50 +0100 Subject: [PATCH 07/13] Remove redundant argument from clear between command and fix ranges for deleting --- docs/commands.md | 1 - .../extensions/moderation/ModerationCommands.kt | 12 +++--------- .../hyacinthbots/lilybot/utils/_PermissionUtils.kt | 4 +--- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index ff6239bd..53e00423 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -328,7 +328,6 @@ Required Member Permissions: Manage Messages * **Arguments**: * `after` - The ID of the message to clear after - Snowflake * `before` - The ID of the message to clear before - Snowflake - * `message-count` - The number of messages to clear - Optional Int/Long * `author` - The author of the messages to clear - Optional User --- diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt index 136ab828..5de7aab4 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt @@ -808,9 +808,9 @@ class ModerationCommands : Extension() { action { clearMessages( - arguments.count, - Snowflake(arguments.before.value + 1u), - Snowflake(arguments.after.value - 1u), + null, + Snowflake(arguments.before.value - 1u), + Snowflake(arguments.after.value + 1u), arguments.author ) } @@ -1519,12 +1519,6 @@ object ClearCommandArgs { description = "The ID of the message to clear before" } - /** The number of messages the user wants to remove. */ - val count by optionalInt { - name = "message-count" - description = "The number of messages to clear" - } - /** The author of the messages that need clearing. */ val author by optionalUser { name = "author" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt index 20af83b3..ca2af49a 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt @@ -135,9 +135,7 @@ suspend inline fun CheckContext<*>.botHasChannelPerms(permissions: Permissions) permissionsSet.add( permissions.values.toString() .split(",")[count] - .split(".")[4] - .split("$")[1] - .split("@")[0] + .split(".")[1] .replace("[", "`") .replace("]", "`") ) From ee7a972c5245a0402a32d295cd98c7ed39d4ff3e Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Wed, 6 Sep 2023 12:53:24 +0100 Subject: [PATCH 08/13] Remove old tag code from clear command --- .../lilybot/extensions/moderation/ModerationCommands.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt index 5de7aab4..6e69d773 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt @@ -1434,7 +1434,7 @@ private suspend fun EphemeralSlashCommandContext<*, *>.clearMessages( title = "${count ?: messages.size} messages have been cleared." description = "Action occurred in ${textChannel.mention}" footer { - text = user.asUserOrNull()?.tag ?: "Unable to get user tag" + text = user.asUserOrNull()?.username ?: "Unable to get username" icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() } color = DISCORD_BLACK From 8557d4f9c6813d84ad7808dfbcd6d5686bfa5aef Mon Sep 17 00:00:00 2001 From: NoComment <67918617+NoComment1105@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:03:34 +0100 Subject: [PATCH 09/13] Switch to official mongo driver (#351) * Switch to the Official MongoDB Driver. Needs testing * Some fixes, still broken though * Switch to kordex mongodb data adapter * Update kordex to fix mongo bug * Fix meta breaking the whole thing * Use a constant to determine the collection name * Revert "Use a constant to determine the collection name" This reverts commit f61306ce10f277bd879a52362f4e1c5702a5cc7b. * Use a constant to determine the collection name but do it better * Simplify filtering in cleanup * Update the README.md --- README.md | 2 +- build.gradle.kts | 3 +- gradle/libs.versions.toml | 8 +- .../org/hyacinthbots/lilybot/LilyBot.kt | 4 +- .../hyacinthbots/lilybot/database/Cleanups.kt | 9 +- .../lilybot/database/Collection.kt | 22 +++ .../hyacinthbots/lilybot/database/Database.kt | 15 +- .../lilybot/database/_DatabaseUtils.kt | 18 +++ .../collections/AutoThreadingCollection.kt | 16 +- .../database/collections/ConfigCollection.kt | 27 ++-- .../collections/GalleryChannelCollection.kt | 19 ++- .../database/collections/GithubCollection.kt | 11 +- .../collections/GuildLeaveTimeCollection.kt | 6 +- .../database/collections/MetaCollection.kt | 15 +- .../NewsChannelPublishingCollection.kt | 21 ++- .../collections/ReminderCollection.kt | 12 +- .../collections/RoleMenuCollection.kt | 15 +- .../collections/RoleSubscriptionCollection.kt | 22 +-- .../database/collections/StatusCollection.kt | 4 +- .../database/collections/TagsCollection.kt | 27 ++-- .../database/collections/ThreadsCollection.kt | 16 +- .../database/collections/UptimeCollection.kt | 3 +- .../database/collections/WarnCollection.kt | 16 +- .../collections/WelcomeChannelCollection.kt | 15 +- .../lilybot/database/entities/AdaptedData.kt | 3 +- .../database/entities/AutoThreadingData.kt | 5 +- .../lilybot/database/entities/Config.kt | 13 +- .../database/entities/GalleryChannelData.kt | 5 +- .../lilybot/database/entities/GithubData.kt | 5 +- .../database/entities/GuildLeaveTimeData.kt | 5 +- .../lilybot/database/entities/MetaData.kt | 9 +- .../entities/NewsChannelPublishingData.kt | 5 +- .../database/entities/PublicMemberLogData.kt | 5 +- .../lilybot/database/entities/ReminderData.kt | 5 +- .../lilybot/database/entities/RoleMenuData.kt | 5 +- .../database/entities/RoleSubscriptionData.kt | 5 +- .../lilybot/database/entities/StatusData.kt | 5 +- .../lilybot/database/entities/TagsData.kt | 5 +- .../lilybot/database/entities/ThreadData.kt | 5 +- .../lilybot/database/entities/UptimeData.kt | 5 +- .../lilybot/database/entities/WarnData.kt | 5 +- .../database/entities/WelcomeChannelData.kt | 5 +- .../lilybot/database/migrations/Migrator.kt | 2 + .../database/migrations/config/configV1.kt | 20 +-- .../database/migrations/config/configV2.kt | 18 +-- .../database/migrations/config/configV3.kt | 22 +-- .../database/migrations/config/configV4.kt | 4 +- .../database/migrations/config/configV5.kt | 4 +- .../database/migrations/config/configV6.kt | 14 +- .../database/migrations/config/configV7.kt | 9 ++ .../database/migrations/main/mainV1.kt | 10 +- .../database/migrations/main/mainV2.kt | 12 +- .../database/migrations/main/mainV3.kt | 9 +- .../database/migrations/main/mainV4.kt | 10 +- .../database/migrations/main/mainV5.kt | 15 +- .../database/migrations/main/mainV6.kt | 15 +- .../database/migrations/main/mainV7.kt | 7 +- .../database/migrations/main/mainV8.kt | 7 +- .../database/migrations/main/mainV9.kt | 7 +- .../database/storage/MongoDBDataAdapter.kt | 146 ------------------ 60 files changed, 389 insertions(+), 368 deletions(-) create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/Collection.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/_DatabaseUtils.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt delete mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt diff --git a/README.md b/README.md index 14ce77be..d94dd38d 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Lily makes use of the following tools and frameworks. * [Kord](https://github.com/kordlib/kord), the Kotlin API for Discord. * [KordEx](https://github.com/Kord-Extensions/kord-extensions), an integrated commands and extensions framework for Kord. * KordEx's [Phishing](https://github.com/Kord-Extensions/kord-extensions/tree/develop/extra-modules/extra-phishing), [PluralKit](https://github.com/Kord-Extensions/kord-extensions/tree/develop/extra-modules/extra-pluralkit) and [Unsafe](https://github.com/Kord-Extensions/kord-extensions/tree/develop/modules/unsafe) modules. -* [MongoDB](https://www.mongodb.com/) and [KMongo](https://litote.org/kmongo/) +* [MongoDB](https://www.mongodb.com/)'s [Kotlin Driver](https://www.mongodb.com/docs/drivers/kotlin/) to manage the database. * [Logback](https://github.com/qos-ch/logback), a library that makes logging prettier * [Kotlin Logging](https://github.com/MicroUtils/kotlin-logging), a lightweight logging that wraps SLF4J with kotlin extensions diff --git a/build.gradle.kts b/build.gradle.kts index 23e36554..f55c55f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(libs.kord.extensions.phishing) implementation(libs.kord.extensions.pluralkit) implementation(libs.kord.extensions.unsafe) + implementation(libs.kord.extensions.mongodb) implementation(libs.kotlin.stdlib) @@ -64,7 +65,7 @@ dependencies { implementation(libs.github.api) // KMongo - implementation(libs.kmongo) + implementation(libs.mongo.driver) // Cozy's welcome module implementation(libs.cozy.welcome) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62ed2b78..68e9ef53 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,11 +8,12 @@ grgit = "5.2.0" blossom = "1.3.1" # Libraries -kord-extensions = "1.5.9-20230809.104126-1" +#kord-extensions = "1.5.9-20230820.204324-8" +kord-extensions = "1.5.9-SNAPSHOT" logging = "5.1.0" logback = "1.4.9" github-api = "1.315" -kmongo = "4.9.0" +mongo-driver = "4.10.1" cozy-welcome = "1.0.1-SNAPSHOT" dma = "v0.2.1" docgenerator = "0.1.2-SNAPSHOT" @@ -22,11 +23,12 @@ kord-extensions-core = { module = "com.kotlindiscord.kord.extensions:kord-extens kord-extensions-phishing = { module = "com.kotlindiscord.kord.extensions:extra-phishing", version.ref = "kord-extensions" } kord-extensions-pluralkit = { module = "com.kotlindiscord.kord.extensions:extra-pluralkit", version.ref = "kord-extensions"} kord-extensions-unsafe = { module = "com.kotlindiscord.kord.extensions:unsafe", version.ref = "kord-extensions"} +kord-extensions-mongodb = { module = "com.kotlindiscord.kord.extensions:adapter-mongodb", version.ref = "kord-extensions" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logging = { module = "io.github.oshai:kotlin-logging", version.ref = "logging" } github-api = { module = "org.kohsuke:github-api", version.ref = "github-api" } -kmongo = { module = "org.litote.kmongo:kmongo-coroutine-serialization", version.ref = "kmongo" } +mongo-driver = { module = "org.mongodb:mongodb-driver-kotlin-coroutine", version.ref = "mongo-driver" } detekt = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt"} cozy-welcome = {module = "org.quiltmc.community:module-welcome", version.ref = "cozy-welcome"} dma = { module = "org.hyacinthbots:discord-moderation-actions", version.ref = "dma"} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt index c70d2413..321fd211 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt @@ -3,6 +3,7 @@ package org.hyacinthbots.lilybot import com.kotlindiscord.kord.extensions.ExtensibleBot +import com.kotlindiscord.kord.extensions.adapters.mongodb.mongoDB import com.kotlindiscord.kord.extensions.checks.hasPermission import com.kotlindiscord.kord.extensions.modules.extra.phishing.DetectionAction import com.kotlindiscord.kord.extensions.modules.extra.phishing.extPhishing @@ -15,7 +16,6 @@ import org.hyacinthbots.docgenerator.docsGenerator import org.hyacinthbots.docgenerator.enums.CommandTypes import org.hyacinthbots.docgenerator.enums.SupportedFileFormat import org.hyacinthbots.lilybot.database.collections.WelcomeChannelCollection -import org.hyacinthbots.lilybot.database.storage.MongoDBDataAdapter import org.hyacinthbots.lilybot.extensions.config.Config import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.extensions.config.GuildLogging @@ -60,7 +60,7 @@ val docFile = Path("./docs/commands.md") suspend fun main() { val bot = ExtensibleBot(BOT_TOKEN) { database(true) - dataAdapter(::MongoDBDataAdapter) + mongoDB() members { lockMemberRequests = true // Collect members one at a time to avoid hitting rate limits diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt index 7d5671ac..a9975248 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt @@ -1,10 +1,12 @@ package org.hyacinthbots.lilybot.database import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.core.Kord import dev.kord.core.behavior.getChannelOfOrNull import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.rest.request.KtorRequestException +import kotlinx.coroutines.flow.toList import kotlinx.datetime.Clock import mu.KotlinLogging import org.hyacinthbots.lilybot.database.Cleanups.cleanupGuildData @@ -24,7 +26,6 @@ import org.hyacinthbots.lilybot.database.collections.WelcomeChannelCollection import org.hyacinthbots.lilybot.database.entities.GuildLeaveTimeData import org.hyacinthbots.lilybot.database.entities.ThreadData import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This object contains the Database clean up functions, for removing old data from the database that Lily no longer @@ -38,10 +39,10 @@ object Cleanups : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val guildLeaveTimeCollection = db.mainDatabase.getCollection() + internal val guildLeaveTimeCollection = db.mainDatabase.getCollection(GuildLeaveTimeData.name) @PublishedApi - internal val threadDataCollection = db.mainDatabase.getCollection() + internal val threadDataCollection = db.mainDatabase.getCollection(ThreadData.name) @PublishedApi internal val cleanupsLogger = KotlinLogging.logger("Database Cleanups") @@ -75,7 +76,7 @@ object Cleanups : KordExKoinComponent { UtilityConfigCollection().clearConfig(it.guildId) WarnCollection().clearWarns(it.guildId) WelcomeChannelCollection().removeWelcomeChannelsForGuild(it.guildId, kord) - guildLeaveTimeCollection.deleteOne(GuildLeaveTimeData::guildId eq it.guildId) + guildLeaveTimeCollection.deleteOne(eq(GuildLeaveTimeData::guildId.name, it.guildId)) deletedGuildData += 1 // Increment the counter for logging } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/Collection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/Collection.kt new file mode 100644 index 00000000..41413e0e --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/Collection.kt @@ -0,0 +1,22 @@ +package org.hyacinthbots.lilybot.database + +/** + * This class stores the name of a collection, so it can be referenced easily, avoiding string duplication + * + * An example of how it should be used can be found below. The name property used in the get collection line comes from + * this class + * ```kt + * class MagicCollection : KordExKoinComponent { + * private val db: Database by inject() + * + * val collection = db.mainDatabase.getCollection(name) + * + * suspend fun get(): List = collection.find().toList() + * + * companion object : Collection("magicCollection") + * } + * ``` + * + * @property name The name of the collection + */ +abstract class Collection(val name: String) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/Database.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/Database.kt index 01e2d82c..dd3c19ea 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/Database.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/Database.kt @@ -1,22 +1,29 @@ package org.hyacinthbots.lilybot.database +import com.kotlindiscord.kord.extensions.adapters.mongodb.kordExCodecRegistry import com.mongodb.ConnectionString import com.mongodb.MongoClientSettings +import com.mongodb.kotlin.client.coroutine.MongoClient import org.bson.UuidRepresentation +import org.bson.codecs.configuration.CodecRegistries import org.hyacinthbots.lilybot.database.migrations.Migrator import org.hyacinthbots.lilybot.utils.MONGO_URI -import org.litote.kmongo.coroutine.coroutine -import org.litote.kmongo.reactivestreams.KMongo class Database { + private val codecRegistries = CodecRegistries.fromRegistries( + kordExCodecRegistry, + MongoClientSettings.getDefaultCodecRegistry() + ) + // Connect to the database using the provided connection URL private val settings = MongoClientSettings .builder() .uuidRepresentation(UuidRepresentation.STANDARD) .applyConnectionString(ConnectionString(MONGO_URI)) + .codecRegistry(codecRegistries) .build() - private val client = KMongo.createClient(settings).coroutine + private val client = MongoClient.create(settings) /** The main database for storing data. */ val mainDatabase get() = client.getDatabase("LilyBot") @@ -24,6 +31,8 @@ class Database { /** The database for storing per guild configuration data. */ val configDatabase get() = client.getDatabase("LilyBotConfig") + val tempKordExDatabase get() = client.getDatabase("kordex-data") + /** * Runs the migrations for both databases. * diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/_DatabaseUtils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/_DatabaseUtils.kt new file mode 100644 index 00000000..188d7453 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/_DatabaseUtils.kt @@ -0,0 +1,18 @@ +package org.hyacinthbots.lilybot.database + +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Filters.and +import com.mongodb.kotlin.client.coroutine.FindFlow +import com.mongodb.kotlin.client.coroutine.MongoCollection +import kotlinx.coroutines.flow.firstOrNull +import org.bson.conversions.Bson + +private fun MongoCollection.find(vararg filters: Bson?): FindFlow = find(and(*filters)) + +suspend fun MongoCollection.findOne(filter: Bson): T? = find(filter).firstOrNull() + +suspend fun MongoCollection.findOne(vararg filters: Bson?): T? = find(*filters).firstOrNull() + +suspend fun MongoCollection.deleteOne() = deleteOne(Filters.empty()) + +// TODO Make more cool useful functions diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt index db1c6104..071204e1 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt @@ -1,11 +1,13 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.AutoThreadingData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [AutoThreading Database][AutoThreadingData]. This @@ -22,7 +24,7 @@ class AutoThreadingCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(AutoThreadingData.name) /** * Gets all auto threads for a given [inputGuildId]. @@ -33,7 +35,7 @@ class AutoThreadingCollection : KordExKoinComponent { * @since 4.6.0 */ suspend inline fun getAllAutoThreads(inputGuildId: Snowflake): List = - collection.find(AutoThreadingData::guildId eq inputGuildId).toList() + collection.find(eq(AutoThreadingData::guildId.name, inputGuildId)).toList() /** * Gets a single auto thread based off the channel ID. @@ -44,7 +46,7 @@ class AutoThreadingCollection : KordExKoinComponent { * @since 4.6.0 */ suspend inline fun getSingleAutoThread(inputChannelId: Snowflake): AutoThreadingData? = - collection.findOne(AutoThreadingData::channelId eq inputChannelId) + collection.findOne(eq(AutoThreadingData::channelId.name, inputChannelId)) /** * Sets a new auto thread. @@ -54,7 +56,7 @@ class AutoThreadingCollection : KordExKoinComponent { * @since 4.6.0 */ suspend inline fun setAutoThread(inputAutoThreadData: AutoThreadingData) { - collection.deleteOne(AutoThreadingData::channelId eq inputAutoThreadData.channelId) + collection.deleteOne(eq(AutoThreadingData::channelId.name, inputAutoThreadData.channelId)) collection.insertOne(inputAutoThreadData) } @@ -66,7 +68,7 @@ class AutoThreadingCollection : KordExKoinComponent { * @since 4.6.0 */ suspend inline fun deleteAutoThread(inputChannelId: Snowflake) = - collection.deleteOne(AutoThreadingData::channelId eq inputChannelId) + collection.deleteOne(eq(AutoThreadingData::channelId.name, inputChannelId)) /** * Deletes auto threads for a given guild. @@ -76,5 +78,5 @@ class AutoThreadingCollection : KordExKoinComponent { * @since 4.6.0 */ suspend inline fun deleteGuildAutoThreads(inputGuildId: Snowflake) = - collection.deleteMany(AutoThreadingData::guildId eq inputGuildId) + collection.deleteMany(eq(AutoThreadingData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt index 61752bca..df428d1a 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt @@ -1,13 +1,14 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.LoggingConfigData import org.hyacinthbots.lilybot.database.entities.ModerationConfigData import org.hyacinthbots.lilybot.database.entities.UtilityConfigData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Logging Config Database][LoggingConfigData]. This class @@ -22,7 +23,7 @@ class LoggingConfigCollection : KordExKoinComponent { private val configDb: Database by inject() @PublishedApi - internal val collection = configDb.configDatabase.getCollection() + internal val collection = configDb.configDatabase.getCollection(LoggingConfigData.name) /** * Gets the logging config for the given guild using the [guildId][inputGuildId]. @@ -33,7 +34,7 @@ class LoggingConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun getConfig(inputGuildId: Snowflake): LoggingConfigData? = - collection.findOne(LoggingConfigData::guildId eq inputGuildId) + collection.findOne(eq(LoggingConfigData::guildId.name, inputGuildId)) /** * Adds the given [loggingConfig] to the database. @@ -43,7 +44,7 @@ class LoggingConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun setConfig(loggingConfig: LoggingConfigData) { - collection.deleteOne(LoggingConfigData::guildId eq loggingConfig.guildId) + collection.deleteOne(eq(LoggingConfigData::guildId.name, loggingConfig.guildId)) collection.insertOne(loggingConfig) } @@ -55,7 +56,7 @@ class LoggingConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun clearConfig(inputGuildId: Snowflake) = - collection.deleteOne(LoggingConfigData::guildId eq inputGuildId) + collection.deleteOne(eq(LoggingConfigData::guildId.name, inputGuildId)) } /** @@ -71,7 +72,7 @@ class ModerationConfigCollection : KordExKoinComponent { private val configDb: Database by inject() @PublishedApi - internal val collection = configDb.configDatabase.getCollection() + internal val collection = configDb.configDatabase.getCollection(ModerationConfigData.name) /** * Gets the Moderation config for the given guild using the [guildId][inputGuildId]. @@ -82,7 +83,7 @@ class ModerationConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun getConfig(inputGuildId: Snowflake): ModerationConfigData? = - collection.findOne(ModerationConfigData::guildId eq inputGuildId) + collection.findOne(eq(ModerationConfigData::guildId.name, inputGuildId)) /** * Adds the given [moderationConfig] to the database. @@ -92,7 +93,7 @@ class ModerationConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun setConfig(moderationConfig: ModerationConfigData) { - collection.deleteOne(ModerationConfigData::guildId eq moderationConfig.guildId) + collection.deleteOne(eq(ModerationConfigData::guildId.name, moderationConfig.guildId)) collection.insertOne(moderationConfig) } @@ -104,7 +105,7 @@ class ModerationConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun clearConfig(inputGuildId: Snowflake) = - collection.deleteOne(ModerationConfigData::guildId eq inputGuildId) + collection.deleteOne(eq(ModerationConfigData::guildId.name, inputGuildId)) } /** @@ -120,7 +121,7 @@ class UtilityConfigCollection : KordExKoinComponent { private val configDb: Database by inject() @PublishedApi - internal val collection = configDb.configDatabase.getCollection() + internal val collection = configDb.configDatabase.getCollection(UtilityConfigData.name) /** * Gets the Utility config for the given guild using the [guildId][inputGuildId]. @@ -131,7 +132,7 @@ class UtilityConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun getConfig(inputGuildId: Snowflake): UtilityConfigData? = - collection.findOne(UtilityConfigData::guildId eq inputGuildId) + collection.findOne(eq(UtilityConfigData::guildId.name, inputGuildId)) /** * Adds the given [utilityConfig] to the database. @@ -141,7 +142,7 @@ class UtilityConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun setConfig(utilityConfig: UtilityConfigData) { - collection.deleteOne(UtilityConfigData::guildId eq utilityConfig.guildId) + collection.deleteOne(eq(UtilityConfigData::guildId.name, utilityConfig.guildId)) collection.insertOne(utilityConfig) } @@ -153,5 +154,5 @@ class UtilityConfigCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun clearConfig(inputGuildId: Snowflake) = - collection.deleteOne(UtilityConfigData::guildId eq inputGuildId) + collection.deleteOne(eq(UtilityConfigData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt index c9b75e79..d647eba5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt @@ -1,12 +1,13 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GalleryChannelData import org.koin.core.component.inject -import org.litote.kmongo.coroutine.CoroutineCollection -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Gallery Channel Database][GalleryChannelData]. This @@ -22,17 +23,17 @@ class GalleryChannelCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(GalleryChannelData.name) /** * Collects every gallery channel in the database into a [List]. * - * @return The [CoroutineCollection] of [GalleryChannelData] for all the gallery channels in the database + * @return The [MongoCollection] of [GalleryChannelData] for all the gallery channels in the database * @author NoComment1105 * @since 3.3.0 */ suspend inline fun getChannels(inputGuildId: Snowflake): List = - collection.find(GalleryChannelData::guildId eq inputGuildId).toList() + collection.find(eq(GalleryChannelData::guildId.name, inputGuildId)).toList() /** * Stores a channel ID as input by the user, in the database, with it's corresponding guild, allowing us to find @@ -56,8 +57,10 @@ class GalleryChannelCollection : KordExKoinComponent { */ suspend inline fun removeChannel(inputGuildId: Snowflake, inputChannelId: Snowflake) = collection.deleteOne( - GalleryChannelData::channelId eq inputChannelId, - GalleryChannelData::guildId eq inputGuildId + and( + eq(GalleryChannelData::channelId.name, inputChannelId), + eq(GalleryChannelData::guildId.name, inputGuildId) + ) ) /** @@ -68,5 +71,5 @@ class GalleryChannelCollection : KordExKoinComponent { * @since 4.1.0 */ suspend inline fun removeAll(inputGuildId: Snowflake) = - collection.deleteMany(GalleryChannelData::guildId eq inputGuildId) + collection.deleteMany(eq(GalleryChannelData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt index 98732e2f..2a95ea9b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt @@ -1,11 +1,12 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GithubData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [GitHub database][GithubData]. This class contains @@ -20,7 +21,7 @@ class GithubCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(GithubData.name) /** * Gets the default repo for GitHub commands. @@ -31,7 +32,7 @@ class GithubCollection : KordExKoinComponent { * @since 4.3.0 */ suspend inline fun getDefaultRepo(inputGuildId: Snowflake): String? = - collection.findOne(GithubData::guildId eq inputGuildId)?.defaultRepo + collection.findOne(eq(GithubData::guildId.name, inputGuildId))?.defaultRepo /** * Sets the default repo for GitHub commands. @@ -42,7 +43,7 @@ class GithubCollection : KordExKoinComponent { * @since 4.3.0 */ suspend inline fun setDefaultRepo(inputGuildId: Snowflake, url: String) { - collection.deleteOne(GithubData::guildId eq inputGuildId) + collection.deleteOne(eq(GithubData::guildId.name, inputGuildId)) collection.insertOne(GithubData(inputGuildId, url)) } @@ -54,5 +55,5 @@ class GithubCollection : KordExKoinComponent { * @since 4.3.0 */ suspend inline fun removeDefaultRepo(inputGuildId: Snowflake) = - collection.deleteOne(GithubData::guildId eq inputGuildId) + collection.deleteOne(eq(GithubData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt index 60b21c6b..6b1a35ec 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt @@ -1,12 +1,12 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import kotlinx.datetime.Instant import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GuildLeaveTimeData import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Guild Leave Time Database][GuildLeaveTimeData]. This @@ -20,7 +20,7 @@ class GuildLeaveTimeCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(GuildLeaveTimeData.name) /** * Adds the time Lily bot left a guild with a config. @@ -43,5 +43,5 @@ class GuildLeaveTimeCollection : KordExKoinComponent { * @since 3.2.0 */ suspend inline fun removeLeaveTime(inputGuildId: Snowflake) = - collection.deleteOne(GuildLeaveTimeData::guildId eq inputGuildId) + collection.deleteOne(eq(GuildLeaveTimeData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt index 01ae94aa..3eddab80 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt @@ -1,11 +1,12 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.ConfigMetaData import org.hyacinthbots.lilybot.database.entities.MainMetaData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [main meta database][MainMetaData]. This class @@ -20,7 +21,7 @@ class MainMetaCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(MainMetaData.name) /** * Gets the main metadata from the database. @@ -30,7 +31,7 @@ class MainMetaCollection : KordExKoinComponent { * @since 4.0.0 */ suspend fun get(): MainMetaData? = - collection.findOne() + collection.findOne(eq(MainMetaData::id.name, "mainMeta")) /** * Sets the metadata when the table is first created. @@ -49,7 +50,7 @@ class MainMetaCollection : KordExKoinComponent { */ suspend fun update(meta: MainMetaData) = collection.findOneAndReplace( - MainMetaData::id eq "mainMeta", + eq(MainMetaData::id.name, "mainMeta"), meta ) } @@ -67,7 +68,7 @@ class ConfigMetaCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.configDatabase.getCollection() + internal val collection = db.configDatabase.getCollection(ConfigMetaData.name) /** * Gets the config metadata from the database. @@ -77,7 +78,7 @@ class ConfigMetaCollection : KordExKoinComponent { * @since 4.0.0 */ suspend fun get(): ConfigMetaData? = - collection.findOne() + collection.findOne(eq(ConfigMetaData::id.name, "configMeta")) /** * Sets the metadata when the table is first created. @@ -96,7 +97,7 @@ class ConfigMetaCollection : KordExKoinComponent { */ suspend fun update(meta: ConfigMetaData) = collection.findOneAndReplace( - ConfigMetaData::id eq "configMeta", + eq(ConfigMetaData::id.name, "configMeta"), meta ) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt index 2851da56..1bf10b8c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt @@ -1,11 +1,14 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.NewsChannelPublishingData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains functions for interacting with the [news channel publishing database][NewsChannelPublishingData]. @@ -22,7 +25,7 @@ class NewsChannelPublishingCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(NewsChannelPublishingData.name) /** * Adds a channel for auto-publishing. @@ -47,8 +50,10 @@ class NewsChannelPublishingCollection : KordExKoinComponent { */ suspend inline fun removeAutoPublishingChannel(inputGuildId: Snowflake, inputChannelId: Snowflake) = collection.deleteOne( - NewsChannelPublishingData::guildId eq inputGuildId, - NewsChannelPublishingData::channelId eq inputChannelId + and( + eq(NewsChannelPublishingData::guildId.name, inputGuildId), + eq(NewsChannelPublishingData::channelId.name, inputChannelId) + ) ) /** @@ -66,8 +71,8 @@ class NewsChannelPublishingCollection : KordExKoinComponent { inputChannelId: Snowflake ): NewsChannelPublishingData? = collection.findOne( - NewsChannelPublishingData::guildId eq inputGuildId, - NewsChannelPublishingData::channelId eq inputChannelId + eq(NewsChannelPublishingData::guildId.name, inputGuildId), + eq(NewsChannelPublishingData::channelId.name, inputChannelId) ) /** @@ -80,7 +85,7 @@ class NewsChannelPublishingCollection : KordExKoinComponent { * @since 4.7.0 */ suspend inline fun getAutoPublishingChannels(inputGuildId: Snowflake): List = - collection.find(NewsChannelPublishingData::guildId eq inputGuildId).toList() + collection.find(eq(NewsChannelPublishingData::guildId.name, inputGuildId)).toList() /** * Clears all the auto-publishing channels from a guild. @@ -91,6 +96,6 @@ class NewsChannelPublishingCollection : KordExKoinComponent { * @since 4.7.0 */ suspend inline fun clearAutoPublishingForGuild(inputGuildId: Snowflake) { - collection.deleteMany(NewsChannelPublishingData::guildId eq inputGuildId) + collection.deleteMany(eq(NewsChannelPublishingData::guildId.name, inputGuildId)) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt index 90e719d2..f4a066d9 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt @@ -1,14 +1,16 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone import kotlinx.datetime.plus import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.ReminderData import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with []the reminder database][ReminderData]. This @@ -28,7 +30,7 @@ class ReminderCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(ReminderData.name) /** * Gets all the reminders currently in the database. @@ -48,7 +50,7 @@ class ReminderCollection : KordExKoinComponent { * @since 4.2.0 */ suspend fun getRemindersForUser(userId: Snowflake): List = - collection.find(ReminderData::userId eq userId).toList() + collection.find(eq(ReminderData::userId.name, userId)).toList() /** * Gets all the reminders in the database for a specific user, in a specific guild. @@ -80,7 +82,7 @@ class ReminderCollection : KordExKoinComponent { * @since 4.2.0 */ suspend fun removeReminder(userId: Snowflake, number: Long) = - collection.deleteOne(ReminderData::userId eq userId, ReminderData::id eq number) + collection.deleteOne(and(eq(ReminderData::userId.name, userId), eq(ReminderData::id.name, number))) /** * Removes all the reminders for a given guild. @@ -89,7 +91,7 @@ class ReminderCollection : KordExKoinComponent { * @author NoComment1105 * @since 4.2.0 */ - suspend fun removeGuildReminders(guildId: Snowflake) = collection.deleteMany(ReminderData::guildId eq guildId) + suspend fun removeGuildReminders(guildId: Snowflake) = collection.deleteMany(eq(ReminderData::guildId.name, guildId)) /** * Updates a repeating reminder to be extended by the given [repeatingInterval]. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt index f8319ee5..1999ab21 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt @@ -1,11 +1,12 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.RoleMenuData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Role Menu Database][RoleMenuData]. This class contains @@ -21,7 +22,7 @@ class RoleMenuCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(RoleMenuData.name) /** * Using the provided [inputMessageId] the associated [RoleMenuData] will be returned from the database. @@ -32,7 +33,7 @@ class RoleMenuCollection : KordExKoinComponent { * @since 3.4.0 */ suspend inline fun getRoleData(inputMessageId: Snowflake): RoleMenuData? = - collection.findOne(RoleMenuData::messageId eq inputMessageId) + collection.findOne(eq(RoleMenuData::messageId.name, inputMessageId)) /** * Add the given [inputRoles] to the database entry for the role menu for the provided [inputMessageId], @@ -52,7 +53,7 @@ class RoleMenuCollection : KordExKoinComponent { inputRoles: MutableList ) { val newRoleMenu = RoleMenuData(inputMessageId, inputChannelId, inputGuildId, inputRoles) - collection.deleteOne(RoleMenuData::messageId eq inputMessageId) + collection.deleteOne(eq(RoleMenuData::messageId.name, inputMessageId)) collection.insertOne(newRoleMenu) } @@ -65,11 +66,11 @@ class RoleMenuCollection : KordExKoinComponent { * @since 3.4.0 */ suspend inline fun removeRoleFromMenu(inputMessageId: Snowflake, inputRoleId: Snowflake) { - val roleMenu = collection.findOne(RoleMenuData::messageId eq inputMessageId) ?: return + val roleMenu = collection.findOne(eq(RoleMenuData::messageId.name, inputMessageId)) ?: return roleMenu.roles.remove(inputRoleId) - collection.deleteOne(RoleMenuData::messageId eq inputMessageId) + collection.deleteOne(eq(RoleMenuData::messageId.name, inputMessageId)) collection.insertOne(roleMenu) } @@ -81,5 +82,5 @@ class RoleMenuCollection : KordExKoinComponent { * @since 4.0.0 */ suspend inline fun removeAllRoleMenus(inputGuildId: Snowflake) = - collection.deleteMany(RoleMenuData::guildId eq inputGuildId) + collection.deleteMany(eq(RoleMenuData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt index 027e5272..17523a39 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt @@ -1,11 +1,13 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq +import com.mongodb.client.model.Updates import dev.kord.common.entity.Snowflake import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.RoleSubscriptionData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Role Subscription database][RoleSubscriptionData]. This @@ -22,7 +24,7 @@ class RoleSubscriptionCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(RoleSubscriptionData.name) /** * Gets the roles that are subscribable for a given guild. @@ -34,7 +36,7 @@ class RoleSubscriptionCollection : KordExKoinComponent { * @since 4.9.0 */ suspend inline fun getSubscribableRoles(inputGuildId: Snowflake): RoleSubscriptionData? = - collection.findOne(RoleSubscriptionData::guildId eq inputGuildId) + collection.findOne(eq(RoleSubscriptionData::guildId.name, inputGuildId)) /** * Creates a subscribable role record in the database. This should only be used if a record does not already exist. @@ -58,12 +60,12 @@ class RoleSubscriptionCollection : KordExKoinComponent { * @since 4.9.0 */ suspend inline fun addSubscribableRole(inputGuildId: Snowflake, inputRoleId: Snowflake): Boolean? { - val col = collection.findOne(RoleSubscriptionData::guildId eq inputGuildId) ?: return null + val col = collection.findOne(eq(RoleSubscriptionData::guildId.name, inputGuildId)) ?: return null val newRoleList = col.subscribableRoles if (newRoleList.contains(inputRoleId)) return false else newRoleList.add(inputRoleId) collection.updateOne( - RoleSubscriptionData::guildId eq inputGuildId, - RoleSubscriptionData(inputGuildId, newRoleList) + eq(RoleSubscriptionData::guildId.name, inputGuildId), + Updates.set(RoleSubscriptionData::guildId.name, newRoleList) ) return true } @@ -79,7 +81,7 @@ class RoleSubscriptionCollection : KordExKoinComponent { * @since 4.9.0 */ suspend inline fun removeSubscribableRole(inputGuildId: Snowflake, inputRoleId: Snowflake): Boolean? { - val col = collection.findOne(RoleSubscriptionData::guildId eq inputGuildId) ?: return null + val col = collection.findOne(eq(RoleSubscriptionData::guildId.name, inputGuildId)) ?: return null val newRoleList = col.subscribableRoles if (!newRoleList.contains(inputRoleId)) { return false @@ -88,8 +90,8 @@ class RoleSubscriptionCollection : KordExKoinComponent { if (!removal) return false } collection.updateOne( - RoleSubscriptionData::guildId eq inputGuildId, - RoleSubscriptionData(inputGuildId, newRoleList) + eq(RoleSubscriptionData::guildId.name, inputGuildId), + Updates.set(RoleSubscriptionData::guildId.name, newRoleList) ) return true } @@ -103,5 +105,5 @@ class RoleSubscriptionCollection : KordExKoinComponent { * @since 4.9.0 */ suspend inline fun removeAllSubscribableRoles(inputGuildId: Snowflake) = - collection.deleteOne(RoleSubscriptionData::guildId eq inputGuildId) + collection.deleteOne(eq(RoleSubscriptionData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt index aaccbba8..4a961feb 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt @@ -2,7 +2,9 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database +import org.hyacinthbots.lilybot.database.deleteOne import org.hyacinthbots.lilybot.database.entities.StatusData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject /** @@ -17,7 +19,7 @@ class StatusCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(StatusData.name) /** * Gets Lily's status from the database. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt index 89584046..21476ee5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt @@ -1,11 +1,14 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.TagsData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class contains the functions for interacting with the [Tags Database][TagsData]. This class has functions for @@ -22,7 +25,7 @@ class TagsCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(TagsData.name) /** * Gets the given tag using it's [name] and returns its [TagsData]. If the tag does not exist. @@ -35,7 +38,7 @@ class TagsCollection : KordExKoinComponent { * @since 3.1.0 */ suspend inline fun getTag(inputGuildId: Snowflake, name: String): TagsData? = - collection.findOne(TagsData::guildId eq inputGuildId, TagsData::name eq name) + collection.findOne(and(eq(TagsData::guildId.name, inputGuildId), eq(TagsData::name.name, name))) /** * Gets all tags in the given [inputGuildId]. @@ -46,7 +49,7 @@ class TagsCollection : KordExKoinComponent { * @since 3.1.0 */ suspend inline fun getAllTags(inputGuildId: Snowflake): List = - collection.find(TagsData::guildId eq inputGuildId).toList() + collection.find(eq(TagsData::guildId.name, inputGuildId)).toList() /** * Adds a tag to the database, using the provided parameters. @@ -59,12 +62,12 @@ class TagsCollection : KordExKoinComponent { * @since 3.1.0 */ suspend inline fun setTag( - inputGuildId: Snowflake, - name: String, - tagTitle: String, - tagValue: String, - tagAppearance: String - ) = + inputGuildId: Snowflake, + name: String, + tagTitle: String, + tagValue: String, + tagAppearance: String + ) = collection.insertOne(TagsData(inputGuildId, name, tagTitle, tagValue, tagAppearance)) /** @@ -76,7 +79,7 @@ class TagsCollection : KordExKoinComponent { * @since 3.1.0 */ suspend inline fun removeTag(inputGuildId: Snowflake, name: String) = - collection.deleteOne(TagsData::guildId eq inputGuildId, TagsData::name eq name) + collection.deleteOne(and(eq(TagsData::guildId.name, inputGuildId), eq(TagsData::name.name, name))) /** * Clears all tags for the provided [inputGuildId]. @@ -86,5 +89,5 @@ class TagsCollection : KordExKoinComponent { * @since 3.1.0 */ suspend inline fun clearTags(inputGuildId: Snowflake) = - collection.deleteMany(TagsData::guildId eq inputGuildId) + collection.deleteMany(eq(TagsData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt index c65ad4ee..0b86be07 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt @@ -1,11 +1,13 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.ThreadData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class stores all the functions for interacting with the [Threads Database][ThreadData]. This class contains @@ -23,7 +25,7 @@ class ThreadsCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(ThreadData.name) /** * Using the provided [inputThreadId] the thread is returned. @@ -35,7 +37,7 @@ class ThreadsCollection : KordExKoinComponent { * @since 3.2.0 */ suspend inline fun getThread(inputThreadId: Snowflake): ThreadData? = - collection.findOne(ThreadData::threadId eq inputThreadId) + collection.findOne(eq(ThreadData::threadId.name, inputThreadId)) /** * Gets all threads into a list and return them to the user. @@ -56,7 +58,7 @@ class ThreadsCollection : KordExKoinComponent { * @since 3.2.0 */ suspend inline fun getOwnerThreads(inputOwnerId: Snowflake): List = - collection.find(ThreadData::ownerId eq inputOwnerId).toList() + collection.find(eq(ThreadData::ownerId.name, inputOwnerId)).toList() /** * Add or update the ownership of the given [inputThreadId] to the given [newOwnerId]. @@ -78,7 +80,7 @@ class ThreadsCollection : KordExKoinComponent { parentChannelId: Snowflake?, preventArchiving: Boolean = false ) { - collection.deleteOne(ThreadData::threadId eq inputThreadId) + collection.deleteOne(eq(ThreadData::threadId.name, inputThreadId)) collection.insertOne(ThreadData(inputGuildId, inputThreadId, newOwnerId, parentChannelId, preventArchiving)) } @@ -91,7 +93,7 @@ class ThreadsCollection : KordExKoinComponent { * @since 3.2.2 */ suspend inline fun removeThread(inputThreadId: Snowflake) = - collection.deleteOne(ThreadData::threadId eq inputThreadId) + collection.deleteOne(eq(ThreadData::threadId.name, inputThreadId)) /** * This function deletes the ownership data stored in database for the given [inputGuildId]. @@ -102,5 +104,5 @@ class ThreadsCollection : KordExKoinComponent { * @since 4.1.0 */ suspend inline fun removeGuildThreads(inputGuildId: Snowflake) = - collection.deleteMany(ThreadData::guildId eq inputGuildId) + collection.deleteMany(eq(ThreadData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt index f7f4057e..18d1a1e0 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt @@ -4,6 +4,7 @@ import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import kotlinx.datetime.Instant import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.UptimeData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject /** @@ -18,7 +19,7 @@ class UptimeCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(UptimeData.name) /** * Gets the uptime data from the database. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt index cb64da4d..2f8da8bc 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt @@ -1,11 +1,13 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters.and +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.WarnData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq /** * This class stores all the functions for interacting with the [Warn Database][WarnData]. The class contains the @@ -20,7 +22,7 @@ class WarnCollection : KordExKoinComponent { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(WarnData.name) /** * Gets the number of points the provided [inputUserId] has in the provided [inputGuildId] from the database. @@ -33,8 +35,10 @@ class WarnCollection : KordExKoinComponent { */ suspend inline fun getWarn(inputUserId: Snowflake, inputGuildId: Snowflake): WarnData? = collection.findOne( - WarnData::userId eq inputUserId, - WarnData::guildId eq inputGuildId + and( + eq(WarnData::userId.name, inputUserId), + eq(WarnData::guildId.name, inputGuildId) + ) ) /** @@ -48,7 +52,7 @@ class WarnCollection : KordExKoinComponent { */ suspend inline fun setWarn(inputUserId: Snowflake, inputGuildId: Snowflake, remove: Boolean) { val currentStrikes = getWarn(inputUserId, inputGuildId)?.strikes ?: 0 - collection.deleteOne(WarnData::userId eq inputUserId, WarnData::guildId eq inputGuildId) + collection.deleteOne(and(eq(WarnData::userId.name, inputUserId), eq(WarnData::guildId.name, inputGuildId))) collection.insertOne( WarnData( inputUserId, @@ -66,5 +70,5 @@ class WarnCollection : KordExKoinComponent { * @since 3.0.0 */ suspend inline fun clearWarns(inputGuildId: Snowflake) = - collection.deleteMany(WarnData::guildId eq inputGuildId) + collection.deleteMany(eq(WarnData::guildId.name, inputGuildId)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt index adc9e613..463bff04 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt @@ -8,12 +8,15 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Filters.eq import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.WelcomeChannelData +import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject -import org.litote.kmongo.eq import org.quiltmc.community.cozy.modules.welcome.data.WelcomeChannelData as CozyWelcomeChannelData /** @@ -30,25 +33,25 @@ class WelcomeChannelCollection : KordExKoinComponent, CozyWelcomeChannelData { private val db: Database by inject() @PublishedApi - internal val collection = db.mainDatabase.getCollection() + internal val collection = db.mainDatabase.getCollection(WelcomeChannelData.name) override suspend fun getChannelURLs(): Map = collection.find() .toList().associate { it.channelId to it.url } override suspend fun getUrlForChannel(channelId: Snowflake): String? = - collection.findOne(WelcomeChannelData::channelId eq channelId) + collection.findOne(eq(WelcomeChannelData::channelId.name, channelId)) ?.url override suspend fun setUrlForChannel(channelId: Snowflake, url: String) { - collection.save(WelcomeChannelData(channelId, url)) + collection.replaceOne(Filters.empty(), WelcomeChannelData(channelId, url)) } override suspend fun removeChannel(channelId: Snowflake): String? { val url = getUrlForChannel(channelId) ?: return null - collection.deleteOne(WelcomeChannelData::channelId eq channelId) + collection.deleteOne(eq(WelcomeChannelData::channelId.name, channelId)) return url } @@ -56,7 +59,7 @@ class WelcomeChannelCollection : KordExKoinComponent, CozyWelcomeChannelData { suspend fun removeWelcomeChannelsForGuild(guildId: Snowflake, kord: Kord) { val guild = kord.getGuildOrNull(guildId) ?: return guild.channels.collect { - collection.deleteOne(WelcomeChannelData::channelId eq it.id) + collection.deleteOne(eq(WelcomeChannelData::channelId.name, it.id)) } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt index c41a4716..4eda9635 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt @@ -10,8 +10,9 @@ import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable @Serializable -@Suppress("DataClassShouldBeImmutable") +@Suppress("DataClassShouldBeImmutable", "PropertyName", "ConstructorParameterNaming") data class AdaptedData( + val _id: String, val identifier: String, val type: StorageType? = null, diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt index c736f76a..546b7743 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for auto-threaded channels. @@ -29,4 +30,6 @@ data class AutoThreadingData( val mention: Boolean, val creationMessage: String?, val addModsAndRole: Boolean -) +) { + companion object : Collection("autoThreadingData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt index 84a256af..66ce1941 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt @@ -3,6 +3,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.datetime.DateTimePeriod import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for moderation configuration. The logging config stores where logs are sent to, and whether to enable or @@ -30,7 +31,9 @@ data class LoggingConfigData( val enablePublicMemberLogs: Boolean, val publicMemberLog: Snowflake?, val publicMemberLogData: PublicMemberLogData? -) +) { + companion object : Collection("loggingConfigData") +} /** * The data for moderation configuration. The moderation config is what stores the data for moderation actions. The @@ -56,7 +59,9 @@ data class ModerationConfigData( val autoPunishOnWarn: Boolean?, val publicLogging: Boolean?, val banDmMessage: String?, -) +) { + companion object : Collection("moderationConfigData") +} /** * The data for miscellaneous configuration. The miscellaneous config stores the data for enabling or disabling log @@ -70,4 +75,6 @@ data class ModerationConfigData( data class UtilityConfigData( val guildId: Snowflake, val utilityLogChannel: Snowflake? -) +) { + companion object : Collection("utilityConfigData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt index 5c7944c8..1c7433da 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for image channels in a guild. @@ -14,4 +15,6 @@ import kotlinx.serialization.Serializable data class GalleryChannelData( val guildId: Snowflake, val channelId: Snowflake -) +) { + companion object : Collection("galleryChannelData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GithubData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GithubData.kt index c1c3d011..4438ee49 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GithubData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GithubData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for GitHub commands. @@ -15,4 +16,6 @@ import kotlinx.serialization.Serializable data class GithubData( val guildId: Snowflake, val defaultRepo: String -) +) { + companion object : Collection("githubData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt index 53cf35c2..cdf1a197 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt @@ -3,6 +3,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for when Lily leaves a guild. @@ -15,4 +16,6 @@ import kotlinx.serialization.Serializable data class GuildLeaveTimeData( val guildId: Snowflake, val guildLeaveTime: Instant -) +) { + companion object : Collection("guildLeaveTimeData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/MetaData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/MetaData.kt index c7769389..53db865e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/MetaData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/MetaData.kt @@ -1,6 +1,7 @@ package org.hyacinthbots.lilybot.database.entities import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for the metadata of the main database. @@ -14,7 +15,9 @@ import kotlinx.serialization.Serializable data class MainMetaData( val version: Int, val id: String = "mainMeta" -) +) { + companion object : Collection("mainMetaData") +} /** * The data for the metadata of the config database. @@ -28,4 +31,6 @@ data class MainMetaData( data class ConfigMetaData( val version: Int, val id: String = "configMeta" -) +) { + companion object : Collection("configMetaData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/NewsChannelPublishingData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/NewsChannelPublishingData.kt index 0bf01b37..258245ef 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/NewsChannelPublishingData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/NewsChannelPublishingData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for the news channel publishing database. @@ -15,4 +16,6 @@ import kotlinx.serialization.Serializable data class NewsChannelPublishingData( val guildId: Snowflake, val channelId: Snowflake -) +) { + companion object : Collection("newsChannelPublishingData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/PublicMemberLogData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/PublicMemberLogData.kt index 573d6b61..f3d6f1a1 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/PublicMemberLogData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/PublicMemberLogData.kt @@ -1,6 +1,7 @@ package org.hyacinthbots.lilybot.database.entities import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for public member logging. @@ -14,4 +15,6 @@ data class PublicMemberLogData( val pingNewUsers: Boolean, val joinMessage: String?, val leaveMessage: String? -) +) { + companion object : Collection("publicMemberLogData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ReminderData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ReminderData.kt index b7ca91c2..3bd03f56 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ReminderData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ReminderData.kt @@ -4,6 +4,7 @@ import dev.kord.common.entity.Snowflake import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * THe data for reminders in a guild. @@ -35,4 +36,6 @@ data class ReminderData( val repeating: Boolean, val repeatingInterval: DateTimePeriod?, val id: Long -) +) { + companion object : Collection("reminderData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt index 0673bd2e..de5a944b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for role menus. @@ -18,4 +19,6 @@ data class RoleMenuData( val channelId: Snowflake, val guildId: Snowflake, val roles: MutableList -) +) { + companion object : Collection("roleMenuData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleSubscriptionData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleSubscriptionData.kt index 59290937..ed1e47b1 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleSubscriptionData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleSubscriptionData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for role subscriptions. @@ -14,4 +15,6 @@ import kotlinx.serialization.Serializable data class RoleSubscriptionData( val guildId: Snowflake, val subscribableRoles: MutableList -) +) { + companion object : Collection("roleSubscriptionData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt index 090244d0..a199aa0d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt @@ -1,6 +1,7 @@ package org.hyacinthbots.lilybot.database.entities import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for the bot status. @@ -11,4 +12,6 @@ import kotlinx.serialization.Serializable @Serializable data class StatusData( val status: String? -) +) { + companion object : Collection("statusData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt index 78b00ffb..4ee92e9c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data of guild tags, which are stored in the database. @@ -20,4 +21,6 @@ data class TagsData( val tagTitle: String, val tagValue: String, val tagAppearance: String -) +) { + companion object : Collection("tagsData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt index 1e60fc9e..2bd5fd1c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for threads. @@ -21,4 +22,6 @@ data class ThreadData( val ownerId: Snowflake, val parentChannelId: Snowflake?, var preventArchiving: Boolean = false -) +) { + companion object : Collection("threadData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/UptimeData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/UptimeData.kt index 45503f83..93c6d945 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/UptimeData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/UptimeData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import kotlinx.datetime.Instant import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data to help calculate bot uptime. @@ -12,4 +13,6 @@ import kotlinx.serialization.Serializable @Serializable data class UptimeData( val onTime: Instant -) +) { + companion object : Collection("uptimeData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt index db5a79be..bac600c9 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for warnings in guilds. @@ -16,4 +17,6 @@ data class WarnData( val userId: Snowflake, val guildId: Snowflake, val strikes: Int -) +) { + companion object : Collection("warnData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WelcomeChannelData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WelcomeChannelData.kt index 796c75ec..383c5d2c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WelcomeChannelData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WelcomeChannelData.kt @@ -2,6 +2,7 @@ package org.hyacinthbots.lilybot.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.database.Collection /** * The data for Welcome channels. @@ -14,4 +15,6 @@ import kotlinx.serialization.Serializable data class WelcomeChannelData( val channelId: Snowflake, val url: String -) +) { + companion object : Collection("welcomeChannelData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt index 3c8af692..8497b07d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt @@ -24,6 +24,7 @@ import org.hyacinthbots.lilybot.database.migrations.config.configV3 import org.hyacinthbots.lilybot.database.migrations.config.configV4 import org.hyacinthbots.lilybot.database.migrations.config.configV5 import org.hyacinthbots.lilybot.database.migrations.config.configV6 +import org.hyacinthbots.lilybot.database.migrations.config.configV7 import org.hyacinthbots.lilybot.database.migrations.main.mainV1 import org.hyacinthbots.lilybot.database.migrations.main.mainV2 import org.hyacinthbots.lilybot.database.migrations.main.mainV3 @@ -121,6 +122,7 @@ object Migrator : KordExKoinComponent { 4 -> ::configV4 5 -> ::configV5 6 -> ::configV6 + 7 -> ::configV7 else -> break }(db.configDatabase) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV1.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV1.kt index d63e0460..87f42e59 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV1.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV1.kt @@ -1,20 +1,20 @@ package org.hyacinthbots.lilybot.database.migrations.config +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.LoggingConfigData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun configV1(configDb: CoroutineDatabase) { - with(configDb.getCollection("loggingConfigData")) { +suspend fun configV1(configDb: MongoDatabase) { + with(configDb.getCollection(LoggingConfigData.name)) { updateMany( - LoggingConfigData::enableMessageEditLogs exists false, - setValue(LoggingConfigData::enableMessageEditLogs, false) + Filters.exists(LoggingConfigData::enableMessageEditLogs.name, false), + Updates.set(LoggingConfigData::enableMessageEditLogs.name, false) ) } - configDb.getCollection("loggingConfigData").updateMany( - "{}", - "{\$rename: {enableMessageLogs: \"enableMessageDeleteLogs\"}}" + configDb.getCollection(LoggingConfigData.name).updateMany( + Filters.empty(), + Updates.rename("enableMessageLogs", "enableMessageDeleteLogs") ) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV2.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV2.kt index fa6afca6..7fb69f2a 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV2.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV2.kt @@ -1,20 +1,20 @@ package org.hyacinthbots.lilybot.database.migrations.config +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.ModerationConfigData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun configV2(db: CoroutineDatabase) { - with(db.getCollection("moderationConfigData")) { +suspend fun configV2(db: MongoDatabase) { + with(db.getCollection(ModerationConfigData.name)) { updateMany( - ModerationConfigData::quickTimeoutLength exists false, - setValue(ModerationConfigData::quickTimeoutLength, null) + Filters.exists(ModerationConfigData::quickTimeoutLength.name, false), + Updates.set(ModerationConfigData::quickTimeoutLength.name, null) ) updateMany( - ModerationConfigData::autoPunishOnWarn exists false, - setValue(ModerationConfigData::autoPunishOnWarn, null) + Filters.exists(ModerationConfigData::autoPunishOnWarn.name, false), + Updates.set(ModerationConfigData::autoPunishOnWarn.name, null) ) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV3.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV3.kt index 857d8022..5881fa76 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV3.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV3.kt @@ -1,23 +1,23 @@ package org.hyacinthbots.lilybot.database.migrations.config +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.LoggingConfigData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun configV3(db: CoroutineDatabase) { - with(db.getCollection("loggingConfigData")) { +suspend fun configV3(db: MongoDatabase) { + with(db.getCollection(LoggingConfigData.name)) { updateMany( - LoggingConfigData::enablePublicMemberLogs exists false, - setValue(LoggingConfigData::enablePublicMemberLogs, false) + Filters.exists(LoggingConfigData::enablePublicMemberLogs.name, false), + Updates.set(LoggingConfigData::enablePublicMemberLogs.name, false) ) updateMany( - LoggingConfigData::publicMemberLog exists false, - setValue(LoggingConfigData::publicMemberLog, null) + Filters.exists(LoggingConfigData::publicMemberLog.name, false), + Updates.set(LoggingConfigData::publicMemberLog.name, null) ) updateMany( - LoggingConfigData::publicMemberLogData exists false, - setValue(LoggingConfigData::publicMemberLogData, null) + Filters.exists(LoggingConfigData::publicMemberLogData.name, false), + Updates.set(LoggingConfigData::publicMemberLogData.name, null) ) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV4.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV4.kt index 3c214b91..99ef32ce 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV4.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV4.kt @@ -1,9 +1,9 @@ package org.hyacinthbots.lilybot.database.migrations.config -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase @Suppress("UnusedPrivateMember", "UNUSED_PARAMETER", "RedundantSuspendModifier") -suspend fun configV4(db: CoroutineDatabase) { +suspend fun configV4(db: MongoDatabase) { // Support config has been removed. // if (db.getCollection().find().toList().isEmpty()) { // db.dropCollection("supportConfigData") diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt index 1a264ed1..edd5e203 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV5.kt @@ -2,9 +2,9 @@ package org.hyacinthbots.lilybot.database.migrations.config -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase -suspend fun configV5(db: CoroutineDatabase) { +suspend fun configV5(db: MongoDatabase) { // val collection = db.getCollection("utilityConfigData") // val oldConfigs = collection.find().toList() // val newConfigs = mutableListOf() diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt index da9c7cd0..073accdb 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV6.kt @@ -1,15 +1,15 @@ package org.hyacinthbots.lilybot.database.migrations.config +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.ModerationConfigData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun configV6(db: CoroutineDatabase) { - with(db.getCollection("moderationConfigData")) { +suspend fun configV6(db: MongoDatabase) { + with(db.getCollection(ModerationConfigData.name)) { updateMany( - ModerationConfigData::banDmMessage exists false, - setValue(ModerationConfigData::banDmMessage, null) + Filters.exists(ModerationConfigData::banDmMessage.name, false), + Updates.set(ModerationConfigData::banDmMessage.name, null) ) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt new file mode 100644 index 00000000..ea4c3eeb --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt @@ -0,0 +1,9 @@ +package org.hyacinthbots.lilybot.database.migrations.config + +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.hyacinthbots.lilybot.database.entities.AdaptedData + +suspend fun configV7(db: MongoDatabase) { + db.getCollection("ext-pluralkit").drop() + db.getCollection("data-ext-pluralkit").drop() +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV1.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV1.kt index cbeb8940..bf774eb7 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV1.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV1.kt @@ -1,10 +1,10 @@ package org.hyacinthbots.lilybot.database.migrations.main +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.StatusData -import org.litote.kmongo.coroutine.CoroutineDatabase // This was commented out due to the remindme data class being removed -suspend fun mainV1(db: CoroutineDatabase) { +suspend fun mainV1(db: MongoDatabase) { // val reminders = db.getCollection("remindMeData") // // val repeating = mutableListOf>() @@ -63,7 +63,7 @@ suspend fun mainV1(db: CoroutineDatabase) { // reminders.bulkWrite(requests = nonRepeating, BulkWriteOptions().ordered(true)) // } - db.dropCollection("statusData") - db.createCollection("statusData") - db.getCollection("statusData").insertOne(StatusData(null)) + db.getCollection(StatusData.name).drop() + db.createCollection(StatusData.name) + db.getCollection(StatusData.name).insertOne(StatusData(null)) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV2.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV2.kt index cb8682fc..bfbf954d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV2.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV2.kt @@ -1,12 +1,12 @@ package org.hyacinthbots.lilybot.database.migrations.main +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.ThreadData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun mainV2(db: CoroutineDatabase) { - with(db.getCollection()) { - updateMany(ThreadData::guildId exists false, setValue(ThreadData::guildId, null)) +suspend fun mainV2(db: MongoDatabase) { + with(db.getCollection(ThreadData.name)) { + updateMany(Filters.exists(ThreadData::guildId.name, false), Updates.set(ThreadData::guildId.name, null)) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV3.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV3.kt index 3b775ed7..28b4815b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV3.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV3.kt @@ -1,8 +1,9 @@ package org.hyacinthbots.lilybot.database.migrations.main -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.hyacinthbots.lilybot.database.entities.ReminderData -suspend fun mainV3(db: CoroutineDatabase) { - db.dropCollection("remindMeData") - db.createCollection("reminderData") +suspend fun mainV3(db: MongoDatabase) { + // db.getCollection("remindMeData") + db.createCollection(ReminderData.name) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV4.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV4.kt index 4c5dc577..16b1c1a4 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV4.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV4.kt @@ -1,8 +1,10 @@ package org.hyacinthbots.lilybot.database.migrations.main -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.hyacinthbots.lilybot.database.entities.GithubData +import org.hyacinthbots.lilybot.database.entities.WelcomeChannelData -suspend fun mainV4(db: CoroutineDatabase) { - db.createCollection("welcomeChannelData") - db.createCollection("githubData") +suspend fun mainV4(db: MongoDatabase) { + db.createCollection(WelcomeChannelData.name) + db.createCollection(GithubData.name) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV5.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV5.kt index 9d1f5d87..e27f2bf0 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV5.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV5.kt @@ -1,15 +1,18 @@ package org.hyacinthbots.lilybot.database.migrations.main +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.ThreadData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun mainV5(db: CoroutineDatabase) { +suspend fun mainV5(db: MongoDatabase) { // db.createCollection("autoThreadingData") - with(db.getCollection()) { - updateMany(ThreadData::parentChannelId exists false, setValue(ThreadData::parentChannelId, null)) + with(db.getCollection(ThreadData.name)) { + updateMany( + Filters.exists(ThreadData::parentChannelId.name, false), + Updates.set(ThreadData::parentChannelId.name, null) + ) } // with(configDb.getCollection()) { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV6.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV6.kt index ac8b0e21..08b85186 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV6.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV6.kt @@ -1,12 +1,15 @@ package org.hyacinthbots.lilybot.database.migrations.main +import com.mongodb.client.model.Filters +import com.mongodb.client.model.Updates +import com.mongodb.kotlin.client.coroutine.MongoDatabase import org.hyacinthbots.lilybot.database.entities.AutoThreadingData -import org.litote.kmongo.coroutine.CoroutineDatabase -import org.litote.kmongo.exists -import org.litote.kmongo.setValue -suspend fun mainV6(db: CoroutineDatabase) { - with(db.getCollection()) { - updateMany(AutoThreadingData::addModsAndRole exists false, setValue(AutoThreadingData::addModsAndRole, false)) +suspend fun mainV6(db: MongoDatabase) { + with(db.getCollection(AutoThreadingData.name)) { + updateMany( + Filters.exists(AutoThreadingData::addModsAndRole.name, false), + Updates.set(AutoThreadingData::addModsAndRole.name, false) + ) } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV7.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV7.kt index 6c14d1de..9bc111fe 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV7.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV7.kt @@ -1,7 +1,8 @@ package org.hyacinthbots.lilybot.database.migrations.main -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.hyacinthbots.lilybot.database.entities.NewsChannelPublishingData -suspend fun mainV7(db: CoroutineDatabase) { - db.createCollection("newsChannelPublishingData") +suspend fun mainV7(db: MongoDatabase) { + db.createCollection(NewsChannelPublishingData.name) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV8.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV8.kt index 0995230d..311db03d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV8.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV8.kt @@ -1,7 +1,8 @@ package org.hyacinthbots.lilybot.database.migrations.main -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase +import org.hyacinthbots.lilybot.database.entities.RoleSubscriptionData -suspend fun mainV8(db: CoroutineDatabase) { - db.createCollection("roleSubscriptionData") +suspend fun mainV8(db: MongoDatabase) { + db.createCollection(RoleSubscriptionData.name) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV9.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV9.kt index 0ac485e1..d2e99172 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV9.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV9.kt @@ -1,7 +1,8 @@ package org.hyacinthbots.lilybot.database.migrations.main -import org.litote.kmongo.coroutine.CoroutineDatabase +import com.mongodb.kotlin.client.coroutine.MongoDatabase -suspend fun mainV9(db: CoroutineDatabase) { - db.dropCollection("logUploadingBlacklistData") +@Suppress("UnusedPrivateMember", "UNUSED_PARAMETER", "RedundantSuspendModifier") +suspend fun mainV9(db: MongoDatabase) { + // db.getCollection<>("logUploadingBlacklistData") } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt deleted file mode 100644 index 6790aa07..00000000 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* -* This code was utilized from [cozy](https://github.com/QuiltMC/cozy-discord) by QuiltMC -* and hence is subject to the terms of the Mozilla Public License V. 2.0 -* A copy of this license can be found at https://mozilla.org/MPL/2.0/. -*/ - -package org.hyacinthbots.lilybot.database.storage - -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import com.kotlindiscord.kord.extensions.storage.Data -import com.kotlindiscord.kord.extensions.storage.DataAdapter -import com.kotlindiscord.kord.extensions.storage.StorageUnit -import com.mongodb.client.model.Filters.and -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.serializer -import org.bson.conversions.Bson -import org.hyacinthbots.lilybot.database.Database -import org.hyacinthbots.lilybot.database.entities.AdaptedData -import org.koin.core.component.inject -import org.litote.kmongo.coroutine.CoroutineCollection -import org.litote.kmongo.eq - -@OptIn(InternalSerializationApi::class) -class MongoDBDataAdapter : DataAdapter(), KordExKoinComponent { - private val database: Database by inject() - private val collectionCache: MutableMap> = mutableMapOf() - - private fun StorageUnit<*>.getIdentifier(): String = - buildString { - append("${storageType.type}/") - - if (guild != null) append("guild-$guild/") - if (channel != null) append("channel-$channel/") - if (user != null) append("user-$user/") - if (message != null) append("message-$message/") - - append(identifier) - } - - private fun getCollection(namespace: String): CoroutineCollection { - var collection = collectionCache[namespace] - - if (collection == null) { - collection = database.configDatabase.getCollection(namespace) - - collectionCache[namespace] = collection - } - - return collection - } - - private fun constructQuery(unit: StorageUnit<*>): Bson { - var query = AdaptedData::identifier eq unit.identifier - - query = and(query, AdaptedData::type eq unit.storageType) - - query = and(query, AdaptedData::channel eq unit.channel) - query = and(query, AdaptedData::guild eq unit.guild) - query = and(query, AdaptedData::message eq unit.message) - query = and(query, AdaptedData::user eq unit.user) - - return query - } - - override suspend fun delete(unit: StorageUnit): Boolean { - removeFromCache(unit) - - val result = getCollection(unit.namespace) - .deleteOne(constructQuery(unit)) - - return result.deletedCount > 0 - } - - override suspend fun get(unit: StorageUnit): R? { - val dataId = unitCache[unit] - - if (dataId != null) { - val data = dataCache[dataId] - - if (data != null) { - return data as R - } - } - - return reload(unit) - } - - override suspend fun reload(unit: StorageUnit): R? { - val dataId = unit.getIdentifier() - val result = getCollection(unit.namespace) - .findOne(constructQuery(unit))?.data - - if (result != null) { - dataCache[dataId] = Json.decodeFromString(unit.dataType.serializer(), result) - unitCache[unit] = dataId - } - - return dataCache[dataId] as R? - } - - override suspend fun save(unit: StorageUnit): R? { - val data = get(unit) ?: return null - - getCollection(unit.namespace).save( - AdaptedData( - identifier = unit.identifier, - - type = unit.storageType, - - channel = unit.channel, - guild = unit.guild, - message = unit.message, - user = unit.user, - - data = Json.encodeToString(unit.dataType.serializer(), data) - ) - ) - - return data - } - - override suspend fun save(unit: StorageUnit, data: R): R { - val dataId = unit.getIdentifier() - - dataCache[dataId] = data - unitCache[unit] = dataId - - getCollection(unit.namespace).save( - AdaptedData( - identifier = unit.identifier, - - type = unit.storageType, - - channel = unit.channel, - guild = unit.guild, - message = unit.message, - user = unit.user, - - data = Json.encodeToString(unit.dataType.serializer(), data) - ) - ) - - return data - } -} From 94634e89b7680e0d1cbb526be5b191eeb798b573 Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 12 Sep 2023 16:17:09 +0100 Subject: [PATCH 10/13] Fix getting some elements from the database --- .../lilybot/database/collections/StatusCollection.kt | 4 ++-- .../lilybot/database/collections/UptimeCollection.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt index 4a961feb..5ae84085 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt @@ -1,10 +1,10 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import kotlinx.coroutines.flow.firstOrNull import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.deleteOne import org.hyacinthbots.lilybot.database.entities.StatusData -import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject /** @@ -29,7 +29,7 @@ class StatusCollection : KordExKoinComponent { * @since 3.0.0 */ suspend inline fun getStatus(): String? = - collection.findOne()?.status + collection.find().firstOrNull()?.status /** * Add the given [newStatus] to the database. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt index 18d1a1e0..f96babc2 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt @@ -1,10 +1,10 @@ package org.hyacinthbots.lilybot.database.collections import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import kotlinx.coroutines.flow.firstOrNull import kotlinx.datetime.Instant import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.UptimeData -import org.hyacinthbots.lilybot.database.findOne import org.koin.core.component.inject /** @@ -29,7 +29,7 @@ class UptimeCollection : KordExKoinComponent { * @since 4.2.0 */ suspend fun get(): UptimeData? = - collection.findOne() + collection.find().firstOrNull() /** * Sets the on time. From f2d7deece3ca3c6d3540e85a3211d9be529f52d5 Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 12 Sep 2023 17:11:08 +0100 Subject: [PATCH 11/13] Update dependencies --- build.gradle.kts | 12 +++++++++--- gradle/libs.versions.toml | 8 ++++---- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 ++- .../lilybot/internal/BuildInfo.kt | 12 ++++++++++++ .../lilybot/extensions/util/InfoCommands.kt | 6 +++--- .../hyacinthbots/lilybot/utils/_Constants.kt | 4 ---- 8 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin-templates/org/hyacinthbots/lilybot/internal/BuildInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index f55c55f0..402c7317 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -113,7 +113,13 @@ detekt { autoCorrect = true } -blossom { - replaceToken("@build_id@", grgit.head().abbreviatedId) - replaceToken("@version@", project.version.toString()) +sourceSets { + main { + blossom { + kotlinSources { + property("build_id", grgit.head().abbreviatedId) + property("version", project.version.toString()) + } + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68e9ef53..0284f908 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,15 +5,15 @@ shadow = "8.1.1" detekt = "1.23.1" git-hooks = "0.0.2" grgit = "5.2.0" -blossom = "1.3.1" +blossom = "2.0.1" # Libraries #kord-extensions = "1.5.9-20230820.204324-8" kord-extensions = "1.5.9-SNAPSHOT" logging = "5.1.0" -logback = "1.4.9" -github-api = "1.315" -mongo-driver = "4.10.1" +logback = "1.4.11" +github-api = "1.316" +mongo-driver = "4.10.2" cozy-welcome = "1.0.1-SNAPSHOT" dma = "v0.2.1" docgenerator = "0.1.2-SNAPSHOT" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 zcmZ6yQ*@x+6TO*^ZQHip9ox2TJ8x{;wr$&H$LgqKv*-KI%$l`+bAK-CVxOv0&)z5g z2JHL}tl@+Jd?b>@B>9{`5um}}z@(_WbP841wh56Q*(#D!%+_WFn zxTW!hkY%qR9|LgnC$UfeVp69yjV8RF>YD%YeVEatr**mzN7 z%~mf;`MId9ttnTP(NBpBu_T!aR9RPfUey|B+hCTWWUp*Wy%dWP;fVVjO?KDc*VJ^iSto8gEBp#a5qRnMR zR-GrMr4};1AUK^Wl4El^I$-(Vox98wN~VNm(oL!Se73~FCH0%|9`4hgXt)VkY;&YA zxyNzaSx28JDZ@IjQQ-r%=U60hdM!;;Y1B&M`-jR5wo|dL0PfRJBs={0-i#sk@ffUT z&!L4AR}OfxIMF;CysW-jf@GxJRaJf6F$^KwJk-s_L0t?_fJ4k67RHAk3M+heW>EqQ>mh(Ebmt5gvhew5D{oe# zo`>K30R3ukH;X#Wq!&s zh<7!d$VmuwoQfFr&7EXB^fHQhPSUeX-@m@70<^Z-3rtpi;hOA_$6iw7N*XT>pwkm9^O|F` zV$|!O7HK<&%rdLqo6c5A>AL}T)rY)mCX9IQZdUUafh2CzC~-ixktzMIU(ZZ}?tK;b zJk9Wwx!+Ej!fTgInh8by&<<;Q+>(gN(w-wO{3c($ua2PiC10N6MH6zHuCrIMQL^<_ zJbok&IZ1f&2hF8#E}+@2;m7z@mRJbXJZAMDrA>>?YCn~dS;HOKzymOhHng2>Vqt^| zqR71FIPY1`Y_tsTs>9k)&f%JOVl9oUZ$3ufI0`kM#_d@%1~~NYRSbgq>`8HS@YCTP zN1lIW7odKxwcu71yGi#68$K_+c ziEt@@hyTm6*U^3V^=kEYm`?AR*^&DQz$%CV6-c-87CA>z6cAI!Vqdi|Jtw*PVTC)3 zlYI4yE!rS)gHla|DYjQ~Vea(In8~mqeIn7W;5?2$4lJ;wAqMcLS|AcWwN%&FK2(WL zCB@UE7+TPVkEN#q8zY_zi3x8BE+TsYo3s#nfJ3DnuABb|!28j#;A;27g+x)xLTX7; zFdUA=o26z`apjP!WJaK>P+gP2ijuSvm!WBq{8a4#OJrB?Ug=K7+zHCo#~{om5nhEs z9#&+qk>(sVESM`sJSaE)ybL7yTB^J;zDIu1m$&l!OE#yxvjF6c{p&|oM!+4^|7sVv zEAcZqfZP}eW}<;f4=Lg1u0_*M-Zd@kKx|7%JfW;#kT}yRVY^C5IX^Mr^9vW0=G!6T zF&u}?lsA7r)qVcE`SrY(kG$-uK` zy|vn}D^GBxhP+f%Y;>yBFh0^0Q5|u_)gQylO808C5xO_%+ih8?+Yv@4|M?vYB7is!1y@n%8fZ?IL%a@%Qe;9q@IC)BmfjA?Nu*COkU$PP%XoE%%B7dd0rf;*AuGIs%d zOMi)Jd9Gk%3W)sXCM{Upg&JbSh^G5j%l!y8;nw*n+WIK}OM-wt=d*R0>_L9r1Z`Z+ zc;l>^^y#C*RBicDoGdG^c-*Zr{)PYO-TL>cc2ra#H9P@ml{LnWdB+Cg@@z`F$Cg+) zG%M(!=}+i3o``uvsP4UI;}edQyyqZbhpD_!BTz{O#yrq`+%` zc`uT~qNjFFBRixfq)^)E7CBxi+tN7qW>|BPwlr(li({kN6O$wSLd~@Z?I;>xiv*V4 zNVM-0H#h?4NaQa%3c&yC zig%>pq3m7pKFUN(2zW>A1lJ+WSZAKAGYMiK8&pp)v01^a<6B_rE*}s1p0O(4zakbSt3e((EqbeC`uF1H|A;Kp%N@+b0~5;x6Sji?IUl||MmI_F~I2l;HWrhBF@A~cyW>#?3TOhsOX~T z(J+~?l^huJf-@6)ffBq5{}E(V#{dT0S-bwmxJdBun@ag@6#pTiE9Ezrr2eTc4o@dX z7^#jNNu1QkkCv-BX}AEd5UzX2tqN~X2OVPl&L0Ji(PJ5Iy^nx?^D%V!wnX-q2I;-) z60eT5kXD5n4_=;$XA%1n?+VR-OduZ$j7f}>l5G`pHDp*bY%p$(?FY8OO;Quk$1iAZ zsH$={((`g1fW)?#-qm}Z7ooqMF{7%3NJzC`sqBIK+w16yQ{=>80lt}l2ilW=>G0*7 zeU>_{?`68NS8DJ>H1#HgY!!{EG)+Cvvb{7~_tlQnzU!^l+JP7RmY4hKA zbNYsg5Imd)jj?9-HRiDIvpga&yhaS2y6}aAS?|gA9y$}Z2w%N?Hi;14$6Qt9Fc(zl zSClM66;E1hxh^>PDv1XMq3yzJ#jIQ2n+?hwjw)8hFcXDQ$PiWf{s&^_>jbGGeg0{e zx4b5kIhB2gIgyS27y+;DfV`%)h1F!WTP!76o?^QsSBR~nBXnz|IYr*$k${m-u>9Mj z>09A!u0*q9wSQ>0WDmmm6hKju+`dxYkybvA=1jG|1`G$ikS^okbnAN=Wz*xojmwWtY zZq{@FnLJg|h&Ci78w-ZXi=9I>WkRlD1d>c0=b9iXFguf*jq8UF(aM^HPO6~l!aXXi zc4bhK;mEsobxUit``hThf!0qvU3#~h%+C7bA-UJ%beFlm%?79KFM=Q2ALm>*ejo)1 zN33ZFKX8=zsg25G0Ab*X= zdcI5{@`irEC^Vn3q59Jucz{N6{KZY%y!;&|6(=B*Qp4*X@6+qsstjw|K^Wnh^m zw8Uv>6;*bKq>4?Gx3QFDLt`0UxmmN7Xiq<$s>g!~1}N!FL8j3aRyuwusB^Rr5ctV|o-cP?J#Un1>4_;4aB&7@B;k zdZy2^x1cZ-*IQTd25OC9?`_p0K$U0DHZIt8<7E+h=)E^Rp0gzu`UVffNxwLzG zX*D_UAl34>+%*J+r|O0;FZ>F4(Wc?6+cR=BtS-N0cj2Yp2q1d6l?d$Iytr<#v-_FO z?eHZv2-Ip;7yMv=O)FL_oCZRJQZX}2v%EkS681es?4j-kL}8;X|j8CJgydxjyLn~K)YXxg3=u&4MoB$FGPl~zhg3Z zt9ULN>|(KD1PZU)Y&rZfmS<5B={#}jsn5pr0NC%Kj3BZIDQ?<^F6!SqVMmILZ*Rg9 zh;>0;5a)j%SOPWU-3a2Uio^ISC|#-S@d({=CDa}9snC0(l2PSpUg_lNxPwJt^@lHE zzsH2EZ{#WTf~S~FR+S{&bn+>G!R`)dK>!wpyCXVYKkn$H26^H}y?Pi92!6C`>d|xr z04#wV>t1@WEpp8Z4ox^;Kfbf?SOf8A+gRb-FV zo*K})Vl88rX(Cy{n7WTpuH!!Cg7%u|7ebCsC3o@cBYL-WRS+Ei#Eqz-Kus=L zHm{IVReCv-q^w<(1uL|t!n?OI9^C>u04UcQmT0+f^tju& z)>4-ifqvfZeaFYITS2-g=cs6(oOxE+d0EAHd3=(PzjT#uzKm@ zgrDe|sc}|ch_f*s3u~u-E>%w54`pHmYs8;Y6D8+zZv{~2!v$2Rn;zl9<~J?1z{;(A z@UoM9-m`u#g!u`Iq<$7d5R2hKH24np5$k`9nQM%%90Hu&6MGS8YIgT?UIB{>&e~~QN=3Dxs}jp=o+ZtT+@i3B z08fM@&s=^0OlDN8C7NrIV)tHN@k(btrvS=hU;f^XtyY9ut0iGguY>N^z5G-_QRcbC zY1in&LcJK1Gy{kQR-+*eQxf|JW=##h%gG)PkfBE#!`!l9VMx=a#}oEB`ankvFMAzGI$+YZtR5 z1#tsKLDn{?6SAY-0$IOK4t{yC)-@xeTjmW*n{|re;5Zj0I?(*cntWv<9!m=Xzc)thU&Kd>|ZN$$^G_#)x z2%^6f(ME|_JBHgD=EEJIc0R()U=&0+!(7cWHJKxMo1=D#X9X^ zrn{#b5-y<<3@jpQxz(mDBys9EFS5&gC%No+d9<9`I(p|yOCN8U|MWIe?<88JU1}F$ z65mW}YpxpK(06$&)134EYp_b9?A<36n^XgK?+NsqIxAAw_@(Tp-w?v6(>YT23bWyZ zk~QuSf%CmhEgzU-si-Le?l zi<Y8De#UBk7GH}6lp7u4ZWWW(HWvk6HGK98r>$Lhc4g>ap&DIbg26pN+IKTkJ zj5m%j@9m+o$P$$I!#9sR5R0^V@L^NNGv^d6!c6ZN5bxwax7k%OpKLd_i@oS9R%8#E zOguV^hwbW1dDkx{my`)5g+*i`=fWpHXS6_nmBZR1B?{kB6?K=0PvDypQp`g_ZXmio zBbJ}pvNMlcCGE?=PM>)|nvl5CgjfTi#%PTW40+-&gMw{NEtnF+S~(9qEfgfDG^6G4 z%$l!(mS|w3m6R10{XU%-Ur0t>CjI)`_R)dXqz;6O(d3<7PL>M_R%b8%6DaTC^J;#i1tIdy>{u!xr>XSQX51%i%eA(F-EG&?U3Y(n$kgTebw z*5Ia#73$3pSKF2>3>E&PR7fw#DEU;bDP7H_=iDgSbb#c^bgLQP$1EJqp!V1){_wra zF59?uP;Z@lTi7ryb657UZjutvVVOkT6$~??*6|%Rc<>G0dh(q_OVcx$60m@FQA&sL zfT*O1>pj?j0>2}h+`SRQ%DG!)|FBZo@t$e_g0-S3r>OdqMG>pIeoj+aK^9mNx16!O z7_Y)>4;X8X_QdIEDmGS_z)Zut1ZLLs+{!kZ!>rS_()wo@HKglQ?U-lq6Q26_Rs?#N z)9_e6|54ab35x_OYoog1O$J@^GOgyFR-BQ#au9KSFL3Ku3489qnI6QaKc`JoyDPg^ zDi3~ zFkumPkT5n=3>cI$4y%}(Ae_H+!eb+hL;0W01;%>Oq(0LM7ssp8>O+%V zmDC^L*Fu(}l%Hx*h_ZlbpuhcNVU~)(u3aW~F4l`abNHXu3G!^0jg}1t0wVPvqviVl z*4n&FOdwTl$9Y*C{d+BqOpJPzJ5pqch&V)B+BgSX+A^mM=Ffbslck)9h)zaqElW|< zaiVEi?-|}Ls9(^o<1${kiaD?DOCUBc1Hqg$t(*zUGLFyu_2$jzb$j*Rzwak55Sb3D zBQOlKj)KDu?6F4rqoOEyb=8zc+9NUu8(MTSv6hmf)&w1EUDX6k zGk)E41#Er(#H*^f+!#Vwq1tp~5Jy;xy)BC*M!Oj+eyvuV*3I>G#x6sjNiwB|OZN8e zVIIX=qcZHZj-ZHpGn!_dijxQ5_EF#^i>2B)OK;Sy-yZo$XVzt_j9q-YZSzV?Evk`6 zC$NlaWbZuB)tebCI0f&_rmIw7^GY_1hNtO%zBgBo2-wfycBB z*db(hOg4Om(MRI;=R3R|BOH9z#LTn%#zCSy?Qf!75wuqvVD=eiaCi7r+H5i;9$?zr zyrOR5UhmUEienla;e|Z~zNvROs1xkD`qDKJW_?BGV+Sla;(8$2nW%OS%ret|12;a; z`E{Z#hS)NP5PF$|Ib`}Rv&68%SpPEY{~l=$!$)u*edKO&Lc}y!b&0L0^rp4s%dR#p z&Rb0lAa!89w%6_piY4(I@-_px7>I)K?vD>PO6o&HRX)65xFFC@m1IrI+!QDQ%A{a# zmbl4N{^INwcVhl<1YIW2ERZ#wL3d6g*(vTMETNjPZ5Dw40)3-NdH2n?7Nh+W=A#IV zR8ny_^+GY|#y{SwBT2Yu;d*mFqm>x@DMuwPv#=^Z3b7?G!HP{rQWuX(0hQs6<0%Tf zH6%>VCi5&)-@gLCq!dOCUITlfZFq@J2-eBXEpGiaPsz|N(}t+~!V!agF$|5<%u)YX z0`N<4D`wP>I_3S1LL%z=*o`9$hB_7V#%Yq4Q~rTp<&_YN{g|gU9i(1B_d7l}iL6Zj z-<#a0p5CAQ&F2b+?uXUv#vk+p0=i(Xqbm7R;1_TukEVny;PKIT)s&(PE~Qc3$Q8 z{{+A?Mw{8ajV#H_*i98t&3Qtt5V(x0G8PMp$VJ5>HqoymH+V3RRQXLKocae7bawv$ z`JLyE?M8K>eOH`+aFX=tS_INlAhueE#lj|qEp*GvJLZt|wee$As&+4;0i-1=(S<8g$m3Xb=#BWA0>4=j}1$3D)zaX}Q=oUvOk^ z*G8i{bP{R$f13(&Bv@%4!0}n~d|tu=4$8T7p~mgvKI_8zACF<}1^ z2T!5zg82qwbK-BTWdGH#74|81kL~SQYYrjQ$I2ygzB)uvzS!zyH@kIbvnHcMZ&U$h zq+N1$CZR5Y2qw(GxEM~)!j$edV-jfeN`L)8uvMwk7gw&i;sjR=9}`q>qB;toio7ZJ z;57Za)8J~a)%KinL+9}ShCi>x8hLFcKK94Ew2zwm>sf=WmwJu5!=CvcEMU%wSWcDY{lffr`Ln!Vqu*WB* zm|=gzA%I%wGdVshI$arMJQ*i1FBvfIIxcK?A|vEFs}|1mtY0ERL%Sg*HC&n?!hgiIDq|(#Y)g^T%xRON`#>J+>-SyaWjZJ#@}e8@R;yVcl)vqza?DVx4(E%~O$55{&N zT{2{U;6Y@lG5sg#RM|zLWsf&$9N)6ORZp{rCCAYJIlkI}9_WLpLn|}+b}1IN-Cuz7 ze(Ao9VI*_Wa7V>iyWl>Pe`x1A-zQc2*tLF-w`QUfmv(O5PK<=ZoWR-;gMko_-RA9F z6ERTL6?g*aZkeyS!)4qACG4KV$_#|Ti@ba6!rT1w3amqq9yP}9m1hV$-~9)!hdS<@ zeIWE`dsZg*#2YN;?ZJx;d6rtWudEpbNy9qH+7#Idck6NN2)~$>A|)8W{w5ATfDn^p zrkpo-Ft13BWQ#RlSm97m=}<_U{m?I7ZT*b?p5Yw^?qD%r;u96}`y1p5q8s>CBzb0< z9Yw8l1oLhiP|iF7m3ShOabR`)#w_g%KJ80S+Jee;g`Bi2w;d&Ef5hpPGr?ej?@?in z$+JzNK!N1SYh~M5&#c*Vac+leQN%Wfdw|hY*?CB1`S8dmVer9}RbmWlg`?mWRg-)| zAhh`uWNth_@elmkDC-$xJD&5Fhd<&ky!b?%N*@sfd@>i!!MR{oSpex+KiL0j*K?W) z4*WmucKqiVu>OCKD~>A^AXP=rVaX8PU!DdX&Lx0#=hJwC6B}=J2PcLSRZe!oJZN+D zTED*HJ8`{wvt0(%3_rZIe(CyVblz{zJ}bPW#u_=_wNkl;x&mu{Bw+ zHKu~yN`slvxNvTQ*SQpvx0vKA-Z*$O8ob_+^?LI4!Dz=#ReaG6;8M1N06Fv%b87jH z+)BJ$Uvk0^nbuW}2^EFv;ilA8Z5+$!?0#CEOOec?WMsi3H}Hlh*N`96xq^?}t+n!= zvyd6n;GI!|mX|la=NIbK({<)6IljR};&OBfmBiH;49R6^dP0gKS*D$lF;sKX_VfeVlea2Qyc&L^)p8C zgNS|b8Uo9DzwhC(vVPW3+dGS&-V{dt%WY%BfrEklVMAnbNYKb3bJMd0*y6d!?+lJ` zZ20^QvpPDgXOo5xG0%*-xUUNIri#IvhXS?mk7k1lbRY)+rUasnarW-lk0U%jNLzn% z*QBY5#(V`3Ta6#dsRh_*sT-8!c6F@mZp|t0h!2+tSx*_}41whAjUG@QLb94;Um2bR zcsW%39m?x5CVdXHTRF<&FlIt3f?4Q&hBmTeSu~6a=TZjeQb#O#BW9`C{gGR?TnUF< zTbe9(bsJ;20&PefJqcfM|Erf9&5@pDUhxo^UOWRhF8l2>sOE9;N>BvkXI|V`R1gqa zS`ZM*|5rzl$puo-fR&-nYU+0!!};VqQ#KkEiYba##FZyZV8)16E(G(4`~bK6JzDMuJ)vrJ`JvjUZ&7PE{@R+(v8qop6hX>Zql zN%WhroL_|=H{CBeF7pD@9`kmBgA zeSC`r*~jk4O$2q93WFvgdwft4XhI2j7TuV-`o^qUMpO?bfG(NxfR#+oagb#A@0IM6RYV$cSzvH=jYYHm^E2ky!Yg z;J3EoqNPuCR(a%Uq|t({W+_um%W5&6`ka8$ilj^S($F0X*Vm{fSHpKo8vbXdxw|S+ zBS&wt3{IF`-5HYW62(IfGenbS{{~z9#gEESBE;;kL~OnuV&cw?83V=C?1Kgq#=Cv) zTMbbRFu}Knl4TFi9pC?AHX~h74l`fcBbZ53h?^aTWn3f}zwsx~tsCk6f;P zu&HY5B_812M#a5$B4Eq&;Fc3U=^1^{Zm|c?xncA)Q&yq?<->-oJKf*)Qs*obH+2x(FnH|-x(lQb`R5Gdl?o!$nCx`d<3|6ed7R3raL>;n7=qV4|byO!fh5x{2#Vtq7Z0D+qio4lT zZtn~8C9PmHYw1`~*xzKHu02^SWG?I?(k(4=fz*>Ymd$>U+QAU-qN zClRs5z}Z&%9MUWZW$JT{S8Z=+bI??tHG;snJWo$H^+& zUNV$D&)zckKt*O$0hwAu9522A{34ez&5Mr61!_7-37jyZwKz=e@8~y6NCZ?yv?h&~ z;O7*xraDDhV79j90vUoLd#^G$lBk}3FThNgTWpDQR?JTc6#pY5h07ZBUGbebfCf-#PPfMIelyFl*xiiV+z<%58 zfOFgaKz_9w>IJpXJB^zPK(;wy4FhM`q_)Gn9%l^f|G9BR7HnlACCTXo0aGm@s(30Aqqu%!C zu=BD^+qu+L+c{O&Zjz&EHp#|}udvwCzlK|grM+h)>GIfH?2$nRuus5)iTBo*tJd;` z@@O=aib<`dV=~$<|Dn-@tb-aWUX-?7l0vx3#Sm0TnaVQcw?p5q>0G^SK6y2Tyq9*B zwoT%p?VP@CIl0rZo^&%IkhWbd`t+=mui19oeJ`-4sAZ@;IyTSt*+pu-^;o^%@oZ3D-?IU6-_yavDEcK3xqhA;t&txcIA7Lpf(m5p5b3-cSM zzxkM?Qw~IiFzp6T+m(ed>g}kuEngzy=hEN3UpC{@K}NvgBg0F6ZR*|S63w4@H`|EK zbobi^WwJmyPCJYTDC2KQ?v?X+C}X?7;%-zFLrHq~1tdQkfZMvyg(L}Ynk-&SdM{Oo zHXCPKXKu1Sf|^#-cH6dNiF<4hb}gvkqnP!Ky?Si=w?^qdiJMBR2~_A`$u$B?Q4B@q zGQ=ZYEhcDODOH(TqCDcy3YqxXhe*yqVFiKZ#Ut09D$Lg_V>Iplw)Y7(A)%k&BnThg0n6dv?&X8j#*hafajC7Z=HEJI3)^OAw&F;{~^Y zq+Vq4H6h1GTCfRJ^synHxe^VI{T@^Iu2ABOU_8+7()wBYX`?a>!zPl~Tp~lmT4s6m zS!=UZUxBD}oob`p+w^oP9mTLo_hGr>Uz|4j733cYy!S58UucX(*8P{4tNEJ_3_d#e zpWr}m=kE^>#sn6+=ifksiN)<2pn;d}9h0&rm{2^(h}v^2Q)YM@*U`ghE`TAuOPBQi zq%LMOyUVSGoFiUN;N@;slp~cvl5BE+05_i7K8~rPRyxLbVb~SuvZXpbD>_75_3J}Z z&AlK5SZF_DbJ*;_sH5Nep`U?H0l9kh1r4|~wZW8G33FSfb2v8v8-$UIzYI=alOa#J zbTtOz=ol7sN#XXeuJ(#tH{ zRjBq2r!@tEi){HTj3x|iFJbo%iruQ=6v&DAkW12o60mUVsbkJG>Mv&<^p>0~hUX># z!kuy60#ZSSeQB|ewqlJ&a^CyNOn7uNUAzu0Y_`V@>%6kf&60I;Q+P>~ za$iUy6P8UTgB3d|UA2|qH~S%r6K5;ySM`(U^#9oR(OU`$1E8oXf2a2*JEGYGVf&cR zE{=3SPw~Uo*83OYx2N9vSGO9UYfG2by&tlbXZYzuw{Ld1?lZSu6INZ4eFxt2&;!16 z-dfJy(XuJrOaPqP#$evbf(g~NNq6k}7nEe7>8x3`<%4wDb?_p@jS3A3;jC*LCi4=B zG_+zb)E)9Ek@?=}^T+2-yq+o$BkZylg!hJibRn)U!Zj0?BrvfV?>nfk>BCadh8K({ zEp5gWwj#F^U)ZD3;am5GO}RnhP^BNZPXS-=oc^}0hutWW_t*&s+s*6@73OZD8f;9U z*RDgj-%t-nbu}PW^4KZm>x?y~>gAiq7(+3rjvBKJej@m?(5Z)QaP9<9!$}=zw1myy z-p#s2{t*b3wMe!KGUpXr?%IY?j(X}8py|4sH$0R_Px3~s^dRlWOFoZMF(8MFtm3!c z5}fy!oh(F=pw-G7iPGllNl(x-vy>(i>a4B76GKVarn-lpUDbuYT-&^oU z<}-6qO-a1cx`Q=MP{1M?p2x4yMm|oGQ)($ zjq!wIrfG%WBmT3@uV+b(@t%$P$%MDJy9XOvVI7{0y{}ffn!r-)wxvA^yBAucD|OHE z^iOEy{v4n4m4(L9hbsypf5Zny((kaUAa&`^u$d0+Os)e^>ePMVF!DUO>e{F z{k2%oVQ}-q5mBQMmP7il&BS_>#}GAlIvArt-u!m_gEPh#dwz96gJI>v)R|(rTa>$eL1bgJ0%k?(9B22W?pKIl4Jg~Nmz z8XfqPUPnT9wp!Nqmb86!!hdVpKB-0UHT*rKhH%la=coFZ>F{!;XHQfGIH?e!(trd$ zwK=?;#WRz|F?d9Q(VxHOfByE$c7|tgKw*aiM9kOz^Sk3Q4GIo7)h9X;$EC54iar3|MN{zd%afpw5w%VeU+5Z*&v( zKE!zed9qHQM$jCr+<}>6q5nQTb$>FO1JsWkt5jE_o$e8};a8nInzIdBDwkPYPi~&D zb9&lML^jKp)Uxs`N@~}Qe2E%U3EJ&ds=2dR)%w>xJLAAKw)S4I)d?*9t>BldVm(hr zHR6$#P82}d=O^m>p+P^;Z$$Dv@de}zwJWQK_m2~;;EXewN z2BCeYmQUDbO6su=>uX{KCD>T}=}zlLHDd0__&?%N{o+`F`0^fR(AxJDCl~jGIWo5? ze92r^DAe+qtH;u*_Tx-r{9p|tatXyj5CQ-jtv}#{8rF@SjhqVc>F_6Tn;)6n6;$h- z!|HU6)_V=hwlrtS^(|8?`{(DuyjF&bw*h+-8<6B?hBGh~)ALVWFB9_&XFy|NEfg6E za^1eeIe&B{NbUpKA9L34MqcDR$)dFb-zL!U7GR$=SeScuUh_wxNT5}3cJ58l=%(Jn z-rBT1vgO;*7kA3uv^QekntXOnkEGkMKlz|;(`f3Ax>`-)&$!~SZEx&dOAWrVttb0> zvh6QTyeIZQpZoy+5ARAwxW-LZwLnh(Ws2M^qDz2=prk!IDD)pE#rcnu3ML!b;3r2q zPyu%TrK*wr+n989;<2WqNl8l!+5!Ydn8t9?g0eEu*>hHIoqY7B4jVl>?P1=lZ{f(3 zUROu{DYF_s*brO70dS zl0ut8DZ&a*m8HIdNVI6zag_0dRG4GdN&r-y+~Kf@-G?xRJYR;}4ujJ~cK7+rrH`iB z+Zs$!hH{L%GNzokv_7&_%*4aK2a-c0>Z0_fTCz=IdPTm(ev}Hb|MI`7MpKu#>%!RT zGOb|#BLw-?X-BAK+N*UEkaITY(bk1srnEBHN0d z&I;Z)o}v&~(i-WU9lx}pR*>9uyWHiNhLN6Wk&Qv1>PNJpjA)e1IPF>^==Mq{^kq)jyWrOeTwu>=5YaU_P0AsAr8k=$ zH$EAcZu%hpV9l3Kf0$tpiao4EAV5HB;F9kOag&*Iox6mQH(o|Qbrtr2AA=h~9xwSdLLZ%y*>x!`>`{N{p@S5P zO)8giI0iU=Oie+P8D8e6NmW%{UFw%@Qyq!zl-88UPM^)ixCT*b61_Yg&otyQbkyZ` z<)vuFZK)-yHFTcERO+0cZH}mAK1xdXZAtpoqGGh_0~wK@t$pEYQVz z#6e%6dbg5tl^B8egc=QYo2%R$ZK;BpY%?jY;B`jo`@Htl71vD`;QGcra7=JLLD``7 zte&w}^+yPSTz6>$Tb>f5-JmxIet}50g;DX~f@4&m`K&J%uezgHpazF@813MF=I0K# zwZMQ!N2TFM6P*dqG#jfk&690L3;!75jc%<~g_ims{lPl536&Iqfu>X&EiHF52AM2&|KTUo zuzLyuZ<989r#NL(!cnRx*~oRM&HFnJ9Y%*pISgAxDl;6m%KUcK3v^mXJL#;YWMFz1 z-`HX8`;%UP`^3V=%imqqkg&mmVR@}`RZXLxbeteKFT=5O@;SA>m3s8t+soac=O-qe zyFbg)Fuv6(F6q;awd0e-F@5raumN$c;zC%~n0Ve2NbLtK-K;fG>U34lK6M^kmF2G& zk)+CXHCGJV+R`TaJTDUII#W!$1n|UPNV-@O7D~Fz@>`R_ReWW7RxOA$q>%^ycxMJ{ zLya|cLJt1{jB}#Dmv>5Amjm9yYkc2}!AC;SsYi8?8D_P_j=IC8pE1`VHx7x9&Y7UbCs-fNix$IE)f& z%*I|(DN7W-`;E?;@=zqLbyD}lxSixcliB3HZ@vw-QAo^%`||vsb3-uf$oM7rKjjQ! z%UMFO54nTku*E^iB#-cWEu6NC;DLCj&j^^$5UEdT{OFEj3#K6C$*Tbr{HF)c_Jna} z{{fb&LgA&I(B&i1y_gF?-bpC5s_4bR_7$qQg+$?(H#-03hJ+SCJJDreP^ThC9v|+Y zL7xYW4J)3$g8cX4O`&Md0LpRdCtisn(qdhtr4P#I6Y3L;<-h;i^-Lak#BEluXaz-J zc-7zd!~p@3=L7*EPB!wwOlGV`0-!u~Rxt!mt@yS4aoUc^r&NVy@#p^{^N@45iQwB( zZD`3;6K~D8{Yr}=r($U~Lm#3IRmQc{BCvuBEn#r4$Sj4B{;$qbpT%CTt*?1Mg=ux+ zrF!2xpO+n{>&$;VFHxtvZ%ZbkEvkIeGNZaw@!nqSo|U;=XTDv*uP0PJ!0}7sgW`((})@6D|;$_@JOtNV?UQinTx ztIFKH;{TG~f)b}LZiwDij1ISs;XQmOizh}ZyF2<>!valh>%$~o`Bbj+=@OcRe!LQ{ zao&|tAHAxRSQBKF@f~w801}d?7t+nstsoQ9eJEkygv|7-@#Z^fF4NPknecHhp?`k5 zb9s$SLH7Lm-P65OFu(odEmY4VQJ>T)l6R%p zt7oi3TAoe`M*3QKk1rjtA%oHKnr=3A%1$+qP}nwvCBx=fw7jZDW#& zHL<8*T@Mb*)MG`MPC(T3( zzWE>nM5Vr;lnDjO5Q!V*&kXVrCqE7v;q5S=3hb2ym<356yjKczdIU~QCf=dndN0Ul zTn`g{G({HN-fBP9_`GollfMB3&UPEdUwMBXobdq$wlQy{_|puf6l?z9-dn{(MMl1t>#!4^PHQI=tS9oW1h>2^zPK8$$1QZm<7w zE?^uWHKk+7gOix!LS-B<7_sJ{s6SifWWT<))*iUNGBVA0Y+tq6nOp_-sp<0A3YmXcOt$_R|N!Dpy$8Tl&!JK4!$X+Rv=N{;O^eH`e(TxB0T7Ey@=`!}*?MXO7ij4(cC6BffqHIw#0fzIOcp zV`&|l+1VBo`6B{`Y|~4?83OWVI;{pV;K?wFp@Qr)Mha=Q!eF_ zql$279;UB4mF6P7ZNmc!=#00h?5aI=EvV{n17v0aBLaDVu*>qsO@+yA%^diVx&fq4 z7FFVyGA`vw%gSl5@Rvh;zEI)J_a=lF#uF~|yq=!~_RQ1eNsLpOjr%J+0w!WZ99?@4 zRUo^DPwc~EF;uMpWNl-dUky+-v_$;?m-4`M-_WSJ)?lG_M=unHpaddzRwf#jB1Y76 zf$zMl4c#)w#Ak2lVN*P$?3KALZ$?1Imtup;J;nQn3XY2iH&0m|CFME;;kiwRk*Rtu zPO&R99xaa>T^kK#KVOF667{h4L_q#cy}v4Kd6|7KxUzEc#-0a2y6G%wRB{W| z`DMLFX{dseQ=02*$FgEh#o(Z)UxEMJH%(N|#@#7h1MhVWz! z{ak$Kg90_`mq?;TKB(JFo*Z#$4kW?A0?a>S^Zik)5Ek3_o6@QDV_B@xFPRT>Jt63v z#9*dw|5?~c!ahmoHNIN773Vb~_Ku~%)0N8Z&BzD9FA1>Brd@}NkugZ^Ep`{cznY+$ z%EeAZ>SM&HKFWE0nVt#zSvHl4eXf82F<4#qsB0T3HHd`}!U}NYxALu%XNax>dRi$j z{|rT36BA4}F(ZL$iro%h;c1YX8l9FH6nc^r12c`qJ%bLnaQsx{ZWpa`^}g>isl1g zP;_fFXphQc!Tu8|CcfULKs347U5jEwryPV$y6>RAWB!^Y*dSMqYd@EW@B$aGT*!T* z7)o@o9rOW4_gb+5X+JxI=#ip8R_%S80k8SW9|BX0Mk*I;Z_PwZG813N- zHbUGm(7C8w1NSZB>kG+un`?ctG9ygwtgW54XTnhFBL4U#jCfH>FWd+*Qgu^+7Ik`5 zH1QILxLZ)j5e7Q;VdYBF*Rx{qU8d`d>l(GiZTz^$7uC5Zk7)~QM@48k?bGbhx!Whj zKJ3;gX>!o-MLwe0$Fb?Lu1j{6whN`00%o$kFu(4pi|3MJH=%HHO{~#P#T-(&aKnB< zrWIM8a72XR#v_^?G2|m!*Zo2UjG#qm^|705mj1S=uE!hzZy^)UAq$JKXw8kJm&{tz zaL`*wXiZ^5nV2iL6B5rU`XpiMuGt&rm|MGXvhXSAAm7iJp5*!2}6rEiTKfDF#SJm5pZi6uDl)Hw5wqjheZIM&S6Yz`R}%7Pi*j?SUB zs%f-Hp1u=x_H%~_4bsYG3gw3hLaoJ9sl65Rqt|G0z~{0c7Ya7Hj)iF&%+V}E@Ovc& z_(zJjEXC(pGj9X)~rpsbY+w;T?^&b)D_ zFclEt83QqG>rmA%@%183yfvlyKede_-+60fa`U6VWQiAddCu=K zg=SoKEkpTaxPFCzm76Z34$J^fZF%CR`aK$?0hF~|*Vgc3FI$v$(7z?p zjen`&!$VhVlseS9!#Q4^+DO&?iWTQ}&cJSoF{GgGs@eEUBv@=xb8WQ}>49g;>degb zw7AjB=EG}|c9ECb75z!runjX|SA#HEZL0igt2;BJ6PfQu?};YuCVFY$vM>OmX4;3j zkRf~tyldY*9Z*>hPQS!Nkkj)$X67qBs%?d0ZJ`o&5xQ&Ip%I0p$9+ok zr%pnEbk9MC_?PBU*PllR0WlI^9H2GWl2{lKeZ**|GWD{3kW+@xc=#;2Sp#xy1P7vBw!rp(x~(G;ODqCAiC(A7kY4-Js!=t_6!t zM96+;YwCG1RIG^KMD%_P6>fyooYx0_;7EHu-h|01zGQZ*C5%@bEiK&`L-Xtx!52|L zF9|Dcq@KE2v^>mPgRP>SJ4q34r1!~6E^*6NUjWK?L?FU-?bTV*J#SgtTyQJxV!z1^ z=?XgjzKPxAViu9bAr2*wRlJ;#^YWN?#`&Z#8t2olG~PMbB-D%wbX0Db7z$(cd5y#* z5y$+XPQ;wE_zEA$gNs)OFI9}H@oq|wSCM|yuBcAS$@GFg!oFP4i?{R$B_554HjJ*B z`2}!rV1sMJ@Y?I^dx=l?(`g#kXS;oJCQb~eEHBR{(8@e&nLY-A((cE(t1rrN zm=HWf>#8(*IWUp_N9j`|0@bN8lUZ9!S)kkuPNgd77RF}m0X{~h(q%F)^)XTYK{Wbx z{sV2-kN0$ZY0_*+Bm zl55$t3`?zTVI6BOy!lNbCNf%F#1}l=rl#DkEB`ZX5aTuW5kqw?D>{lZu6ygiqcwOQ zE*m0Db$-;-gOaWjN3%|7W4z7St3)gRjJ;R%`|+j6ib@s7r8%ZldCrI4#7pf@Rw)47 z8{70U)E#Da@X43CV=VeHq{-AZJwBdyM;)bbJUr6f?=dGjYMk7M4iWmS&Zh@uvLMA9tsyBdMlkQwrm41CFa)p9eB3-#H z?h|txb4$vWJ=rVsY^`8jMNk|KN)5;df-$-K`q!goZx|i9J?CN`4r;JSge$Ae7h(9R zlVZ&42`HCDYrtdu2tD*2UemJ+#jvA4fe}QYGHA~1l^`!^sRTj&{ z|#4F)+%Y6_z=e+^ss17tLZ!#Uutbq1{W-^8m+Nb>uV^=CsAFgo5(M;_!O1Hm{atl3I-N>kDXv{2KE1 zyAW1C=G~lKv1yFNjiCj(+q+|WL8X73=45tc3tY`Xvw#^Dk$b)rur@!2bgC;KD3J^ID zG~T7G7$BLYNn3~GxC1O)uQapRl|&obXFf@n#34FXK-e?XkK$h!#djuE7S>mqPLtqZ z*Dmz;%#o4C!DH<)*(bKOTZs=pOs4~D+Y`{fUKw=;L!C->h6;hKZIK9yM>hSUTaapOtgn6Y zUr0)4q#usk#t%=<%^F;wPxlY+buu5jBcWQq)KJCZk+Ew1LgyHdNmCIsy|Slj+Ll;v z$qGn#>hLoFfGI-Jj-qY4^BMhb>AhLeqxh6`iNLq|7dc*K8((y8r zs^(cPW>x_Qp$MoVOKg_Pv)vj>DIHufIf=X{$8Y}*$`<09GZ6$|!Kp2v(4xSYhKx>k z1Kx}l&j;00Y(HAvwt2MF+`LzX$d8mDwg>OEuP8-| zZoYLdOg>C{VX1q;?bD+pT*Oa^+7;&pgKuuqQ8y_myutFC(np zj48I}aRV+jtfk$>O&3vZ9r23NJt_94rxRKrfv2d-eZ2ZzvHqB5O^kL{+q^G{t_6#% zeo-?5JTLm*j%T85U`#eo28rUOtyub~pa*!`jWxH8epQ`8QuMKglT3nQ`ivlJN8LHM z0W;&Vk=CzB1?rtgSM3YK(9*_9@p4GP9kM1Ig@8h{cwc?nwS?-hLKtog7T6;FpeaE@ zQ9*pu9uPR1aJY0*kNOaNh-)FlE54^ksVD%|!l5I@lo3S~JjiLN4APbO_Oi2u>V@w0 zGg#%-BZv=lSm z06?zxL%4AzSn$W(_mk~HvJoAz7aEu@4A(d5iXTCQ4d@@!t02~*Vp(xcc}D|Z;FEZb zq-Vwzu$<;{JkR4pAWe()hw~vekzhM%!};?P)%?0jiZ5U;_{6%9O%E8BzIvIS2%1L{ zATR#R#w-##M&&!kRp9fQqQHeAk{do8rvpg#fD{>rwKJ2h_aY>|A?+Pw@)3fx zWc#`Mg2si`URmQGksFEXPe`*ol*orX)+V8Eno)m1=Va#vx7FIxMYq1TDO53r>kN=3 zB&WSS7*$Wug8E9~ybpoQWFjs!X9{Olhm*_>&eVhwVU+M_i^FHQyj)gVC%*PwUsm7h zlmE3icMMXez8aj4Uej}~;Sqt@QQu~b#!z76`J6S6q@|$3GEXPt%6}?7CJ<)n=-;UMiS0-)lp@hEd;A=(J>5nrC$F0wycd;J*UVVf+A4*rv?bhOr%L zx;&>^tM|H0S~kC`Qi%o1269k4BKv*-~Ovy@|sg~O>oTk7AdWR-jt>XAVaV1yM({;bW7~c4Fx<=L8(lPu0K`~^k zP(3R=N~7&YS@x?+39JUR3>~cprCU|AtQ=7L=Uk&FX%^O%8w@X~b=TX}duLQd5U^U;)cl4m3@{4 zkuz^_&g;|WWbSz;$6`lEQ3?Bz=-P0o>#b4!6Ea81u;%&C=+H-xZcdLrnj$VCSk+xI zPSr_Dm2!N8>0RJ1GoPATro2z`?cJHW-1q#+a|$oP40?d@Yzcik*ofkOUQ5$NJ*=%P zK%WKheP-Edk(O^0<~z~wQC1O2=t>mQc9PqeUFsv0O||`4?d)NsIzM9|Lcm@*C8QFD zE92qZMf&fw8GdUs$+8k07WdKqdEtIseNX}Dh44zc9v|oqA8gEP$LwJ%@WjSbsay5W%R?173^hLb2{`BOgV(k75`JR|e7U4|~L+mJ71xtz^|yj6N3 zKI$4hwADr`Esk*A&YWlEeUo;}ilTI?=CdCD*^Eq5eIrC|OIEpl!tk~mRqq?W1MxO= zT-SX&)w2eJ!3|hzPbJY>KKw9{-f#}zvA{2mr@0p4ZU9kAxWU&av&W7Lk z_y=En#~H{N@J2F5+Q;kt6uv?=KD_!dfHU;N=P4q}DaKnU%qg5T%qjAkQ0s#UdD~oi z+v*e&l{w-X91DOmAWzy&Fp#M8XOzqc^|~+4C}|Q{ZG&sO)v95L4j{4MRAgnd_{o8( z-nScjhYn;{uaSpWzpGhv>!?}|AAUYRmjq4DI=fZm)l6?uvkfM&E^`6R!!=}Q)cuxz z*i;8|(kUS9WkdIE_3JM>T-U~0hO8LYI&GankCIhh_zv~DwoiRY#PXWkzcKUI7#8DHu=(ozVr z=i}8TB-1-B#+IwiN|`2CULcZHNEJh!Ju)!txHW4UwLFzOjmgXu8GlAhb?%d2;qM;! z{SG;0IKL+=EXzp;g$%oGs+yXZa;cPYG;AE4^C(}*i+&5W%m=tj*1=`Q_IQ~KOXM@g zh&9LGHrv+&B?vkfs<2e`@VvAz7E|RXO7+wfrX^O4dFgivBT9voC_V{AsK%{$Slj0|Cp3j9aSbF58I#jRL*ABYnEJ*gK!3GYv6?2a4$L2mDIA>!D9y1ZJ z-PdVox@E$9YidVU#Rhl+>2}e*B?fo}$o4d0ZQc|HGzBPkWvApaN6_7Wdv#`9yLD5E zO67O<8PVA2Gh$0Q-XFOrD0#mN-^5gfp(E=wIt^n8BLF~l6w?9XHP`_tf^L>!) zC8B){UAkss?o2A?W8PT70{V?9-w<=qw)(aq@A**Z4|vkFhC3JTIVOs2!;L;z>oV zX9Utkz}N*H?VA-lpVN+$(7a=ka>8)N28yoeqX^Jt(*Tv$C;ml6yfDN2fFfU@Gxp`% zI#1$T0o5T_QmvaZ7R=7+`{`=iWO%z~d;APB{;n2wbB*LrGOys(Wey+;gYSGuV{Ml! zOS(gc;f)sI_l~A^$CI{pPQDG#xyhhD?6mj}PS2lU{5SKCYtI)SzBK6$gc(lY4IHUf z4jlmd%bR1Z`=_zAfIWtN9>H{_MfB-JA%VDWDA%mnEu^A%iC3A4WCNRt2Qb_sFERIt z*$DB83-;me{`VINKS+nrz2>o$x5BRwN1sB>k1B3x;z#EaXgX=`sck5KW$&^ofFul= zLP+n4I8an1-wbrefi8w>5*)A=MravTd$w0s91g#l`tsvc7N#2a>uGtC(QO zpoDD%&4$RrxXaq`#@G!K6{{p}%VN%h3t2~et-S%oxO6M#g0Q@Rg$%zu0>mf(L7oBt zDGRK}O@s$pPMtdEg1lVqsvt(5c{{ge#li!Y!necl%bBlHAO$b_V!Isit|JI(LdaQF zA|6RB3A`QrBfUY4sQFt7V(&M_0SRD4S&C}S!Hfv?Pq0h#djQIg2M`y_ zQesg4c^DMN5E4np@bI=_ev8xDcE^0w(o0q~a6xOzL%X3TBh} zam(7^Km>WD7mJiolv}c4n|=B<@qj#rjssux2^-!ddxx>66mt#klHjU*pI>|rPLVTk-OVxlPO=%sq@V`D4YP(Rq&x0 z0v%Zd_r^7*rMT}X76=opBG0m^rpSjFMFiPh%iAJzi4`{p!!SD}T6tzEC(f)`1)*hx z0{~Q1m-yW|{h`o1fezEX8EP^JnrAq%8}9kmtf)9H%U;DT&W2nva}6ma#j@7KLGi~& zkY2g|{Nf$u#ZRGOe9vi6|1qNYMG$|Y@DV7~hNl$|>_SI`|;@ZpB z)Yq&{gsAUtY}=1LkG+5RdmpzRFU*w%pHPB0#j2vTquLh}wdH6AY9zY##9$KuGAPd2 z>PF;yErH!iLuZr(Blr}lyYXmPJ5f>GvN}=Z78E|*fUT*5lI|O#kM3}tf0 zbFRIHCg)nrXojcfY8D%Gt0b7kl~&4IO2Jkg)F}{@@LMJWp0wcSHqquOz>Mir%-6Fu zv0k?=kb`ZNd?zN^`HwZl8uy%L)X5&kz=Nlx*CXONUVMaK=L=K`lh%cbpO?3vU$b5F zoIa@9#GHDysjaP^Nc@G%$P${vJ1?J)AuDx@xO~z&W@~AA+f6owoVl;7K@Q5?QXM|J z19}9Sa;3v!L`rdhL)S$kU@>JJC#LFDc1?q`9>3J80gt`S4l2N7zc8pJ{&^=u?3}M~ zgsnNg&p*#MmqCBEj&gZxYAMrJB8|0`bFOYQbtuWqy4y4Aysad|Oxlwt=p8a4U0Q*% zwLw~z_f@XVR(5)W%ETf#ZL7!*4~=B5)mEFygD|R!mKsdRO|7I4z-^Epdl*qY)MjV1 zI0qdc7Bn2MXvC|RJeTJE{mkH9FD0{@EsZ^_7KvINcah2o^@bAFxV-YfUOx5-4$@7G zlQCdT=QHhwWvG&+G2Pl9%u=N2Ntcl>P5 z1E`>-CJ6Uhhf{6~(1G4nkAsboN{d8d6Z=LAxnwLy3K=j3{)f!x$_6g{C)RqEa`G%Z zjsJ|P>TQE{u2b$Y>7ZqyHk<20t>nUK- z;wQ_VP1v@I)07Hw6gH=O|UjlM7b=-Xxv+vWN0S)A15A(e4L z_mkd8P+uzT0d@#3xZC|+lK#pgpQ{&fcTb=;ab0*KkttdhZ%LHMdsMi>W-UHw?=ifz z`=bmu=$2YtS;?~DOdT?oawEzParzc-al;4VdURsa#cOzhGaJSStoA#`Z2Q_%m4!$g zb@;Ev7|Md;E>E0+gHha*PmF=m+LUF{A22 z2L&?6;rw+Q=e7Mzgn$XYa;=0v1(k*)@S21}q_}PSC|Ub69NJfhb%696>^IGkZ5}7I zOtc#>+&_K7l5g@O-)~Ce{_N1ADo<)yfiZ@WsnVoF7O0RF_GlyPL89lbOpWgdJrw5g zo~Gh00!BDFiI!6GM~ufBSKv{{zN6pnq2+Ph+q{D10x#So?Nm)=;oH~lLZ;57mVmMN z&-%7yUTb=4y$g2E7d)Gw5N2(fi*a`3(a;yUM16lmRy~`#^@Xw zW#jp)D3~YC2dZlI`~ z7qW~=huPW8cIp`zV@I|bI;XKs6lz&QYnfvcK6Iet}7TPqK4(mv?v3g~ndHVx`L*`GOOUA9Oi*X1kLkkytv zDE;V6{}`x$P}AGq(Sx?>nQU<^^k}o|0i>)5)_X*)^wfLMgZcL?2=sB+axUb_n?t^b z5e}iqUY2W8%h^CJ<%h8N!$}SniMU|(s?*@k6m!7ev_n1`ysU*N;*>YoI}JoZ8b%26 z_Q6JBHBfSZ{}I%2g|iq09rwb6kBAjd)*aJLEiknx@+TZlPk_S<)(o4E@vZed1=xN{ zwdPaOFD;576X;htV>?`<9{SV7!hspd^u;O_vn{!z1*_c2YH$KMrEi?wCK<3IiAa>N zmL+PkhB4W7%v8Zz1f~j^Vy&hMx5^n?Y_#>7t=5_g6}w`}GRGyh6PptQtq6 ze;~To_HiD(!7&W!F|?vN2+BGPx!Mmv*_U&yg{azxN87nTx9%DlMDDleJM+O-5gyM4 zQ`6}3u8@lHMdGCZiagMci%bx{S`q;Ivt7(Eb*WWDiz{GDGiMAWlB3Xw06$RDh~1Q= z5Efz{my%J~We_=4Iw;_Z-P? zo|y&16$jm$bNsStJM~WhXRID6Hcyb8?Lt-a;u`(tqyjUCEjvq<)V(6}+~D zbGD8iwr$_&i=cIW`#$~Cc;FSDJF$Z+&eUy>NJ?*WsI!rdyp8)Q`L| z(x0O&O04-Jl)Qscb{B>nVK99nYYS+FOA~WS`4^)c7inYX;212%OaKtOC}k(r(cn4> z`X;bBhNsFHxPVnFo7zSTSG;%ca3-W^x4z-Vy)SZe1;$PHZ>fdJe-W{)5zkD#j( z%mO6tB9NArhn#?xUVyZ!-WmVaEsdOB0<&OD6Usv_;%In>nZDFks552Ek(d}_Qa|UH zbF_iFQHLSnbH3+@Tt-A*eZ1V0n{%$F80B6h=5I>jlVV~wK$s{V12rkNw&R)a1#pR8 z%lZM1e$k7^5dmKS%i;3HBurkNuEj!D@;&CUK^gkDUT@ec^1#6Zyl>C@fe`<e1f=9shLYzW(7eF^jtF~B`agPh%;%V3GeZCCm^+68dYofH{?!QsCVe``MgKo1 z6~R9uO#ckuDe)J`c|l6>ALX6R&%3hw%r*)C145Gi3$l_T`g=$JNb&pwl#%-cl6|W3 zKmo^oqX4ll@xX8mfusgBK>bTPFe-~rlMJZx1px?si~=0~^vYQScP}l$h-`tfR~BG5 zcEGP!0$`-}z{@L1FungY1i(N$T%heW3c)`Fsefj*bOt&)i2(DDP=L=aCm z0p|lTfdsAue@M&@Z zzuwY;^@IZZL&$-DK25I7&t5{H%$*1rRo1782`spi17j=%vKBA{@$TusZi<1T4_H8h zdm@7WN4Wt3A^Yz|eYT~+>m{Ec0$|fU8<k~{XdsT@Xx;Se`3gMKYLNpE|Wq{rB@`RXuCYxyBgl z><%p92CU(j0Q~gDra$G3KpD{EZeUQZBHl%z6J<&bf!0?3ajZ)Xo&2Z2)ZjvNlVVH4 zA0mH9Yd}0y*7T$NE-Th$&M|mRwGA8f``7f$FQ+~pJ~qF=udjOyVWM<$c2Z3xvHCE| z5%Q766A7Vf7kKAwtZWh({9$|~Zb@?QJLQltDf|SUF>KpeEnC5j=>;HZCC;ASZX)X! zs@%!SMp$1fgc(SkVTOiMiZ|4 z5jHQL1+#xl5IU+B z6H#S>cAV^J_19u!WRL+*$Hm3M`|;R)I!_uSJe_tz@%^bS4mz=?gzMzk;X=)s-(-V7 zgWfrw!_gx8LZKe}!1UA%TGK6FM0d?AwuQAa`q74=`3%MDSPTHc^1m(4I;=!W$vnt> zGJ$M{zf#m1X1TIh#>;4V%x}Yg@JglLQHu9GyiGW~6BgmI6L%XOo~(_08hU^g6Yf;N2|X_dj6K;D8&9t0{p%lPCJP$?BYe>z z<1D`Nuc^95(GVaDu0E$TYJN(8ja~T|>j{(z#UUiQa=ITnO_b>ibW5=1gUXPo` zzh2wLK<+&!nXf!ZeQW3M3sX`n5edG}g`Cs%`H#TGI_u*IId`T7r6kYg7O&+?xNxB% z3|OhB{Xiu@EM04RbY9LFTuvw^xuP`l+7dE9{UMA2T@_%D1ZUXe-m9%HN-y#a8lM6F@&_ZPxMV8lEOia670ShaHsp1a=mL+Ti*p9DT48nWVl*TWE>a#m&x|)f^OFr zqqreScC}o{i3#;wiWm(oU1I(8GmCl7lDJ3kdbX~({nYHiDXRBlkJphO51Ku?iX87JRU^YGBHCrydn4*4YhczR9Nz7~sIA+IgYF`h~6ZAji%Tqp2MsCx0_bE0> zvAv4JkHR4*i7a}jx$w{JH)_`MXZ$QnDs*aj%5c~kXmYKIF#2B2+ZL^8xI_&q66kt0v7lFvQ^T~kcQUa)|oFNh>dGRbZWn$ zHInpr6%DTg;ZpvN{LXgN(|_~#Y4!D*&ghxhQSi&hDu@LY$guGhJ3~XMS3_7<|$Hyir zfk89c-k5)AK^H!bo(gmfL@_cJswK3D?3rNFO5%YHm3FvJ$uH>QN5g`$L{?v zyHIrfHD55Fs0Z1uDN$ebaA0XZj{_|;FQh;}uIlWrvSbbB~ zi`G}R8oRPpx3wypk7s!0rc%?Oy{V+vJTszq#@TL3@6!W8s%N<RpP?gS`!f@4AxMZbGib$tfc2}#W%7sVn z%2FP2F<^k8QX+Dt+zQ8&+sF*RG80m(>-iPsup%FyfCIVHdJ%)@(9|lBQ=ul$<-S!3NM zK43(ntb$6&5dkru$Qci9-SHmWAUA6I)sGQr2-3-@l~1)1w=4*e@ zAq$TupiyE-lvZP#ZCEe0%=Xy9`0qBaT;B*`tD>X=`{&RCWkHqZnnOfPE%T1Nk4L+P z`%hyPV(c4;K~AVU9DB3pEytRk;H72V2Egx_{gD@y_9Qi1Bh6apGUQ?ZPM#q3x{%Q; zykDqC#_k)=JLCO3rfWo|hE%k78M#%T9vyWwM>Ft6oB?WhtEF4PPiR(_{)^1N(c2X1 z>&E70n2$XV)5@MO!2X9w`dBwPUK!icIQ3>kbCIqrYXp*Wqs>1i=f}mGYcbj}G{7Dy zAg7V&k6-ZDh@3M~pcpY(oOHk08b%aT^!jadPefl$)N95VB{%6Agsj_EE7Vn zsn&8&A}v&jjcV?O&XqXA&QVH31xWAhO}I+q2RD--2RF|uKa|id&JbL0ka&F#F?Szu z$9K{~#q+cdoZye+XW&1LoU_((8(Hl(HU>T07)k{78Al8~kjOrCkiQ+lAFLqGL#q{n zi0Ah}E<#v2V-@Ak{UMu-oVWQBP5y@X-v)5&aEmGj3IYjo0}cWrnPP%LkP;*dnF2<` z1bk{&=v6{g6+x5A_L~f#7qE<&?*?Bkok&k} zcN7pXYom~I`P@#n-EMetKLhWM>4I==aWXgNj76Ae_*bUM(D--_*i|@HSX3;exk~6l zDaDGkdCjHUdV-C$&!x3`2=gDqc>f4Q0<5p`>nC$0TB`Yn=B(aS0TFSS&k|ez!Y`(U z^P(LKO8D%3sL1NP|Ik2IUv-JL;$Odqz#6*qbF@T8BjKAo6WE|Vg>{4N{A1ASQ{Hl; zzJRwB;$Ot(8=YejI&K@@DI_4dXwFj2vF%YI7Vt8<$oe5)Z&zYZoDh$Vy=vb51Gwo2 zMx`20<#u)-<0XVD<}GC%&=SOM^()^!u6piF5=`EW7T{wHc-(!M*ADQ2Y)gFU@vmcT zGfn4|3RVNBnzw_}l_glVD^HK4aQHf%jc^AOBu=qwFIu>1Z5EL}!S_Aj3DuAMr^zv` z1iaqEj;VJ1-emAPVOJh%m(cJzfZ-(BpEydBZQ@2K&}p)SC8_Z^OJQQ2e`>xsSvEmk zHkEJUUlbQiUu%5G&UuXQ>YUpql2PnF#iYGV}A1iLX0^|}&^0i>drOvAE76fd%*kVw zX-Nv3lNzX}%wvC0EWp_QG8V^)z9ywPRUfT72mduX7%+yjjsvbPF5x_gvH}h!wf{?H zTt^`APUsf@8xl#Xr@hKo4wrX7#c0>hV{d2oX7~O2;_Dg7N)Tcp!Ubo#K|vC|KfS>~ zlBUHKD7ySZGA9-Sl^dBm!%J+!3@SFnh_i0i9t%tE!+{>G^8;>p<}oOicjMzsT6(f# z%o^M;vqMXgj4<^M?<2h(pgLsy$m1f6{(~gHsTFLR#QRt}DCx4}W*yxxkCg8vSu!g->6+C0q;cyzN>^2A?5w~WyH6<7?cq0019=-7~0nNf2?ZnPI7UBUo2X#NKq9DZi(W3B0P-)!sXICls6_)zo zdgYO=8L#aSg}Ql*DAfF?rZyNI#O-7{C7UQLxf!q0o^ip-{+8LR_Lwg{>3;K7W`QvP zgPmJCJG#T{+n&M2|JcN9xm8Dlvo`lL{=tOt)`I6cA~rvkM0lP)?fi}>SE(}9)R%j* zX&c=8!E%I%3$F2xav7H+p#FZrNNqcKs3`20eHOu!u&p$gL9pIM`B1lgSz(+tPJo8m zD$ES&*vqw}12^}MeSElOx4;`=hCYfmU?^mk(+uVA75dj)NmaN1((uNaoafgHPAMzX zF|`|mmvTE7RA~{s-@ZJcD3edKh}a}L#D1=>F1x-WgK^r$K*0|N z*z{tJ!f7BpB&|baka7eZm+?xG7iR4y>Ow?a3w%pK=C{_To@#Bi$N5TFDPNUMXI1sp zn#Qd9^5mAhmKvuI*Ud)h_+)ecfz#z~AOzDv(7VrAlWq-I4slDNx=)5CCS9Wt{yCBny z#;S_r&)WnQg3xfsUaI)dGj? z@H{H^c92>dNv;UtL-{EKhd(w!gZZy%5psUBWx;jsoARh25EB%%i^2 z#nnCv!IaG$oSkbGH|VDX4{#jRnt3a;KfD&2S0%29zZZqg8Im%|b2-HvilV!uq*!g@ zEODVd^d_Cx+-!_EYd_pz0sCA}xQ=AKtnRHY`%f5s4I|`SSO&s%0xOw|sblvzuelZm zj1`{OTQ%0GT|00`-uyNUXyrRkuF^fDs*5GP2^K>09B>(<+prqh;-vSVHIpOk0WilS zoTlcky}U}?24E$^xGVU9$%!({Irkz+OOYZ<n%HBptG>=$c;rjV14YBBe%*DsL+45wzFIEma4SXR|AGy;;9Yxzy;w2NYTu2WO#| zr3o^ruf%=Q1I5!8d)R3ei^+X4OFzp|aK&_5OyKve53x(Em$69~A;js0j?Z2w;$nz@ z9AKnIWhm1in)P{O02~L?;o>q~>+0TP?`Z^tX{yfDZ7A%x1uH@WNXFt@~{mW}CUBduKaZ{-&j7k9XW?KXp7 zTRIf~@YmhgSmTZ-A7b@Ctga|3$2R$EmA{_*ZjhMP3I*Qj>84xlJCMN>&zaw8nd1C|}Y!i{;(DhwG3aHmzL9Q^pd&Pf2(VbirC@PKuF~A+EXi8f`@g1z~b&+`y zTx?ZOpZpM8-u1JNQWmjN6Ji-eUMD)JsEKes4PS514ecrLC_3hs{e-dwu!pR}Vkmzb zNj#h*(|y10A85Yy<*aH+QtueV27Md3+?^zTkp1uAtQPojP?B=ZDgziOEgPece_P@0 ztYP5L{;Zc5--K%lhK9B+dODXSr=^TCteKyw+BR z?GaB1ROf)&i^1mg8Rp^D5G0&K)O54bMG$PtxpZ@bd1u{p_;1RxhLzfe-B4>PApzxw z7iKx%w-W`e4f5+8%Z0N{F=T{&$!C{>N9W>l*A_8Cj2h2Kd;>t@`C#CN9_96%h1f>=)L6v09Cmluf&8dZe&(31MBhp=EM;G&&IS)pT+P^yaLR3Aj7SFg zx6$|yDI-ot=psOl3FFqwfMRk_{z)di_ut5VCA+7a(i{D^xb$IBWNI4EvG`!W zbux^*!(}@jXAZAIa}b@PM7#Mv^apggmNQ8&u7g;GMUXJU#gTuSE3L1E3&R7eaqT31}tObr!fms}D< zk8B0U_2_g5)>upemHAbOdX5?WR+HmA*Zu6)RiR9Zh@a0(uFJ24r-=IR1&OB?(``L` z@JLi4`-Ar>7LXRJl`2gzXB*ZWbYkd$h;X`}3Rj)XQ zAMd!IFC-9F_!K5Znz?|XJXZNnIR}kx3v8skhevzA_~LZGh2x}x!ScF0-K#-7rCU~~ zmYIHe&CZ-Exm?`2YK>)&WjCL$(JZrVIi5zn@8d7RcFqd}TY%~W7h#Ns?6Gs@ObmCZ z;Fl9|Rw|lO9y2;_(GTWdB-PSCnQLXpy5TGv>Y;Jex}kyl`H(r)Uls+8EaV&95fd3j z*tv!O_!o9%;*ebo2O8#kq}#+LVlT0%i4b2&(V?b2Z^aRPNIQPYp<8vtqU2ja1vsb= zzQi)C{9ByrBXPP%tQ4roSxQEk;(sHI5*XnOPY(U*XX;~RP@Oo`gg%`gbwl4^N2R4*d7&#i6agknUz&v6k!GgWH z#7<@l1&9y|V+#C17Pa5pKVFd^d(wuW$VtO!Fh3nI=XNb{@)-E}?-edcB9+3NnXE9s z|Bac>R51iZV+d516jOp;M%s-pj*3*1+h1cu4aJUh4ab*L9@u*1!byg(ND!gsgMu8c zt+K)6tNq)z-?#Y8a1XDU+vRw5RyTPyLGyAWpFq;>ca#%v;F&GeRs9}6O{`_Vwu>a6FN={o#)u-E1Wi~x4(^x zS$?FDBxdkT*p!D=V=jmArQd{~{fL;J@g^O57uL~-;~~21%pc4!0Wn|@r4I165%mUs z>51VcB?A2xi+Q45;z^#se4f}Qy6{=0bUHn;oY5v5@%G!i`#5eBlR1*3Dg9*OTv6+M%@_3bKR*{SqOA z6bcYxUBkjcnpuGT;bg;feCxZuO(01$N_A@_4UVed4?;A>-OT{qB2y@1Wo2pA_iAam zB?JIpkj#-*0oXy6DVb|YqAHoCasp02i1Q!JX0uoMg(q7lv z?a%#xop0B(_4HQ7{#h7B^dtCU*Ze;4pFO&*!^~QF`K6DtUm?q&-BC^2z ze^wj%m!;=c=`<#-s76bOc46s+sxUMSN#cJRWmV=%;;935PE*Ha@(#nDQE&H_>vz`jQ?qT6W;0)JIz|F->;Oo;DS&&4{skDh?BqJ6A1VS^f`po2UVT4bo z!rDqhLE(S)S-Sz>wy`qoC;?>a`4yl8KkTv9n%9Qp#qiy^;X%!&`kXzqiPFb#=%|YD zd=*5}9f1BjZwoqL%R!@em~200;Q=Q$`$9Kx6-C4t#j*DKm7)1KMqr#ZC*A?|Nx8$X zX_IXqDm}lyOEp}?P7;M9mu3ZNq>-6mzikFv=WG_;&V4MVDvjcuaA5R_Gzvhz^b3^c ze!7H*$$=jjdMxgE3dNa@S;Xd&Pm<^bm_J3Ewq?u{F3c4m6PutNr z@~LsvkBst-*nC_D%xr=cFb_PLZFtMaI#q4drjJ;xUNOx)|5jR{aG`IBgk;50Tf-#K(u+^81DSJcS8sk~@+(8yQjpemR)cu*+-Q7S%l@hIHA(s{@i zkO*&Bo;tH^q@sak>IV|~J9%+y9>?Dl4ENkgdPCffYP0zF9b$R1gs1LH z8|FqP4c@D4dhByM*WA@%S`%efa`^?bi#PCKx&7A3@igY<{F@9-lIdO$7FuxGaX+v= z&^jV%erq`k4V~Q45jQP&D0=?7r$J{C-3<$~g0#*imBs!>{9j&c;K%SGQf9?v0sjt# zlW}C1&_#@C%iw4{shhFnc-!2h(X*D5~|36vc)0+fY`^!yhGrvESYUjKft@ z7CvAd=Ou3$X3UHvvP(==D~Hwz4c6?g^v1QMs5l`BOL|DR*N;&UW*p1)=#lhzQl;BP zcEWd`f}CPSy8723iY6$}sAZuDHRTt_PPtq5j7_)qFC53UM7SdpVy4kPAd72$$q)7j z{iqgScZ1?`1?z#|>7tlZP>5{h3reBEZ!jFU^NfExxh5vXr|O&U($DDwgaUdG~qA36Crxh1TwmnUc-TN(rA6x3tl6m2jvIo0qAJM^V}!ymq( zmSkl*O2jY$^5W1pzsuNntU-NI~R50T|8fP2Ajab$pD~S3AE0CTF%M zXCXw12dJkfNH;^NQHF3aIb=a`!G}o|lXJ``n9(dLMYk(LJSs=mYC}9|YRlSeAvl6m z&h0K#?W)@ZYx^{fwx0dvv}zqNbl&)$=j1JuW1>FIu6dq+-T0sA0VjN3hJs&@CLnCb zmG~`(fYSM$)xVdRcwhg5eK7(@|ANE%7wMDRJ@yZSVIkK$O2M_lLo@;&?xKA)f?*eS ztZ`?4tas-Sq+rS-vq*Cv3cYb^7n_4M7EOM`#g%R?0ax_!x?(xkUek&slXDjRxY%1+ zLW`s%!^w5?)OeehAiim91z30V1F-s76FRe1!0eaqzFLABdZ-%4-rYHi$fQkePG-z7 zYZMax`bd4Ts^YSFQ~V~YL`r40{4$G{;<^gOGKNJVr35eL60B-XvF@z8Y!qcFZ#r#+ z(LRUboh5A#tJsxmgqCI1lf1!PvQCv&<>Y3kHcfLct5gc@YHqb>?n&CK>?4FB zpi{AnWusba#^5t;if^Tqz5plN+{&t$QfjDErp_ldZsA&Y{$DY!MZtqdr*Qg(DxHU+ zj)=)As!ru}xNDNu`RWm^0wX3i$9@Bj0V?c>sii!#rGykeHq82X@u2fX^2FbGVRqyM zaSk1Z%ocKFHoGAfHhj3T(2ShVC~zO(>HN{d4*ZZ2u|1MZZ}{nGN|@bJ^5QVKqjHjB z`z|D9h67rX7rq_?eFf5t#nEA2Q%bLv=3I3Lm8 z&7q&p!#5v@05MdH!5P{)O}4ley=Gm&W3I^_9)bb0lMXdp#&Ed}am2%l3@g#L2HBo9 z3*!cpY9Xa_i1T$YQ&CCFTeJpjEg91CpOOREvL@FF8rJ&zR7?P8LjOy-l+IoQKqTq_FWW(XbgJ_0ZuCP62qIg+oW1|m7OUL-dQIV_$HNpdQde1nsndQV+ znjniOCzZjU6Ze6`)NwB2=;O&;<`O95OY&6?QJ~((jcY9W#d% z*OFqT{zZR{d_Wr%nWUq}r#7HlHE9uYEM_Q3PNjG*haxIY8f3b<-xrpp%N>-Y_HvF{ zj4{)nUO3i(mXoCL$@U5~FHL6DjddH$$|8G+0HwjbUL-Fd4aFU0 ziiglWQ!?t3s^a6tUhqUkVT_fAbdQf0&zZGmwYpTH(3e`VZ`4o3pOiy$^kFVLnswyr z{)w6aC7Qdv;t+AD@~>~k5ssC_t%{>YQ-b%97L$O&eCRG{!+sxdr;Kq+9xlPjBViAB zi?l{-+spym0#|$6T4YHse^NUoH+RcjaUKH3SDPV)xbW9(mMUaYD8c>K%cK*3aMd%% zEhbA-n{(>?_=CQTNPJ9rPUlokwh=w1U|w`PmmOQ`zXTw?kz1C@A}EN4O?#%i0uoiL@5-dMp6++qi)*2x@sOkrM`Rh1x73yb75TNx&OFSFA;} zY1&L|5QjfYWQY)#Adv-5a8NT8al8HtS4~?~7uYWlEW;_aqBI-P(dl`eeIQUoxXYB2 zXicO==u>FnxyIR3xuY}2Vo*^3&A`IDhv?KqF|e9I+?4Td`McVZJ*w3ZqaklvV=v~z zawv$mxPdIN}_w>feJLX(DN#CZMmuH&z`TbHfQVz~E4L({LU`o-XRU2xGm>4+jiun0!`525&!$i#1e6tE`U>|E>#Q!GltK=N2&G)8yz@^T_@#$Gap^J z))%Z+Er_uIJ+qGw(05Y0A8{?7J@nX5REm49-<|2qfz|HOuV%S%EN*gCNOT;i8}>_@ zECBJ}gfKCKFK^@5o6xjp>?5#sAki^x#_X4hMv4>NTcnO(35K5d?3(b;QQH$s+Em&S z9q~=cC#8JMoNFZ2e&rQ-cCXhQpQ^~&zpfOcUa4aJb`xZ@XI1IoL;KR(MAnXq6%O^K zCZIBUZ#nka+Wg3I@9mI>4qs;$%hL$kL3jX%&r0I>kzY1{9ja4|@eVT2?+B;pu)`m| z49Mr!aAB2->>Ec;w#AXz^iYcw+taq3icH@#D-FZ)DFG3eS|PDa`u(?6{|K}+BPX8E zJt_@1#}Gy(BKS#^mMTIe8DicgLQxTXRr1-WV^VfDBa?OJxO@j^<^d#J*zNoyy8)o4 zu<$7;0ZdFH{wp6EyfpuWls(mq;^9Gba`KEom8l;IyJkA^_}K&pgJ#;X{G2Ov26TBp zi^3LF?d?yJ^&!m2Wv30!KjoqxI$Z5GznYL-x^WE5+?s=j+>%{&uAhx_SnhKzNQK0> zAF$jntxxcF?H|Fa4F#}e_JWjRy(IwC%4iJ(ay47~Xe|?U&85D{g@wCGlA6!2cAkaR zitFt~@B23`{BBxqeGs(m9me_;<*;_8cg&xZp`Un zb?)-YhBc9J;5g*+1;WDHl+D8YLT)OSWP9U1pk^Ut-_k9otE;<0HO|#4t{JfHf)Lci zg~jCS{QGd7o5LMvid6wuM`dh5?J}J7EHfq0bT>v;Y3Es3d^)T*%S~46)jLcF!y(I=8sLBBro3@_^ROR znNEG5Oa*t2ptmX&X%mq(xe_2?H#a<6B~~~uj9C_`2%+lrmV|R=2au>d>DrEE7Y!a+ zwITjvF=-2(5@Qc3-??l;_VL~`cM!%Iu04peeAeCLpvPruH*x^3ZX4{RB0qbJZld$9 z_eDT>K6A#r%SWzaD7@q<*w)hdx!-USsQw^}vAKxkKXjVU#_CAj76XwU)%3BONvWPf z6EBZ>A+;4A0oP_NVWoz>8W~(!IGjxx>%U|E@;cWk+~XyUDSXz7PFQoA4OVRa>ME}U zzc~t98#!%Z{GFe)j0oWWVQ(oW48kj~sLJT2_rQz%Bd7U|`Q^>h{?=Z_>GZ2h>^=b7 z##`^?!LyG+nA7hUqaXmH<-)X$0QJWQR_DDY&Fi+Z8NzZfe6u4(V7P4D;01Tf&Zlut z0d~|*P){O9P2Uw+7pW(qJkz^IVwxV(%)SU5Y;`NtkNex>$-w^R_{MQtYH))6-AbJ$ z!(P94!sax5SNVgy36Vt08D#7SeD&4nZNz~pPY{X+MP%YQUKlWa!W)(pvU4AOehim4 zTtVxVHNO+O*nO;$&(~i7W#&m%k7b6pvgG2i~R=eKMD`7b=rRn9~%59w<@$%1*SWpP^%?bXerpY2DO%${w?JteBWwJAWm! zsPH?1#!p%Jyb>tc4c#`BFQ!xc7R*Sjm?~a*@-byt^m&Y$+MWgW1){mZ+ql zu4lNAAi=>n#(FLgN6C0BP;Wh~?h$lCn(`#uJ5i{TQ*my_WvqA8`ip)b!^J#^y!s4;QX4`F0C=38UMSYx?fI~1`WNa;ZTj)?O{ z$k^8^@kfe#fy#CUon?hDil$fDZ1GDHtHiC^vA?`{+iZ>oakvyd0X1IXnzbv!pL{NX< z1VREE_pLFd&{eHR>&g=iKD>p{e@pB;DTt9U6h=6&{1?zNcHz_6-XA#72^Ouk3XcNqusnb+X1vcB3r_o zPuU|6Z8U*HYS5a~UJY*UQ0+2Z#~e>SqFQ4yIj|;maD_Th1bC5{nIQ!9ruS*x=SfUb zkqYh4!oBhZg&v9UsA+fQg;3M~V@1o8WCA!8-xdgcBFJn{XqP+dQKpaVv*?gt028Jz~~escDay5(iNj7EK{TDK}}3Ln6}LdGz9nst;&Z z8-i|mgbQNSK{0Qhcz~9RaYxQ{u~a&B8UJ~ViuB+8a6>xazZONYMc=|ow7c5{WBB$* z?C|Fi{6uD)(0pX`ulor3IDVol7R%*ql?5m&r6eLK&cs*cq^mGGFeWtc#SKbx8jI3v zusce~TFpzFCP?(H8QQ^lTG_uz*Ma5=rwL88YVdyo9hp+`r+Jwudt9H!`Bf?S9I_R=WQDAvmUl!Uj+lTT(osusoB^`0q@)cgNtk3Az1c zF1{rgTdT)0xH;7MNFtNM<{iHSTf7rHIDa@8j$tKank45JHUyFgUMjak zwT?Y{7@hu{+{=9oMgKFvR{WBSS``<#eq#MN;^JaRuZWRC8Ozz1`J_1fgxcwrHoM-;t$w!alwNy;C;jw&xSD|h`-QZg4!8}tg z!;hR;EI=t*SG2r2>4;0Qty3g3AQ(#(Ch6SK+TXwSglJX_A85<$CEYF-{~J}fg-=d3t?1>syx z*JaKOOqHjX`w=yrJgt#EQuJJNPQBF>ND<@zM+rMl=)wIJ4uE?`vgzz^qI|>Cz4g)` z?Yy{!x$+A0`J!1op)P*Xo`Nf0w9I97oI`BBm(FF4R4bp^AE9ZE=~I7A=T~bvyw!!8 zR8eOZrXmuNmje>d2uSM3sBW+(1=%~oC_@3GceKojdL~jU6I@Q0^9+J zG0ksA?7y(Sf&Rle*05Y0pME8SEKD7?Ag2CaC=x>WI>(Nt{DIVuStyi1PzJCYMIZOc zL(Fb^vn1zRB+N;o#la`owLp~7L{iOW*PS6cgH(suEB!W?wp@EAs_t6*_Qoqyzi_$n zH2eC4ckMQ<=H7@aPglaZCpi0h3%^`CIKGW*^3Q+vu>IB~$2s1UDGy4`I0kxXFp}8m z)dK&SsZc2a&QgHh|0}_lVWqDflPY7N&_J{>Opx|r+sQ-QimF!Gltzr7v8E4Nc(Uc9 zK5Fg5kte^{9yqa%vFU{sk&`<%oy>FwoUmF2e!RUQ4AAD8CymyGiekdd=&;@x58gxR zl-w;O7lkH=vJMZpRhIY+Ceo*8!&m-umST=oFGX#=1_I?yy?QVbEo*S!_^n+TYW>UP zvkW#(yfqO#w(RWs(4gz>%>T$(glY2M?%EMbi1w!v6kEjD7ye!v^sPV)qs)L6`yHmI z%UXk8?e`Jn$NFeEEv)XVI-s#-r(9#JB`c7II<{5iq+GGQ+C&%;Ve;Zi&(YwNozGnNhTF68iv*ywu?MfEka)$l4-o|Y+giU^}duk$J zF_l23z)m(iVmuLE?UU^&>Cv{Z$|Ka6AsGXU>kn(kCxz}#a*UMrml?O+Zg`}Hoq@|8 zb~U`x_p>XuB$MP*Su2%)_M-yk>EqRElrhK;?_s>N*F>3~RaH;q zcC(Z2Pa`b>(;O7Px&xWAdl~*a!{}+h}?f?I`{dSoLG}zJ@&U&C5hyQ+!CgKci@w=rDi34W*_KhSFE{EihuCUZmrLL z3iTwj++&Y|u!W^ijqnt~xup9e!JtiyT3|ZEwbQskrgVq_pk6Y3&`)SSktHm%$#6Gl8Gf78(nthd*4k-&5>K*Q4EiE zg?5_%o!VE4da~^E%+U3LEX>N2-%kC_^}5s7+s(5O2>yVV$41ODJS5I9lUw*u5{!4| z8e{SBkY-p(jTMv3B)1-b&nSkx-b^0Hih0mDc@P2vEK_wcGzOk=bzg^nynC89Zyau> zh)qs5Jh%mRQWw%W9ElaSOye@RG8st=V}`l`eFk>LXt@@1n#KL1D2srZfu_Oav?@?R zDN`}zt{C(plghz2u>TB}ozbK&YwESkETMa?DUsoGvkTfl<`9{Te_nas+F2n>3&LlS4mc*htNr~^i3~3NqE(TVVVfM1Ma~_eIeSfFI75Re}2Y>+Ed$P+^xA^Gg+Ft$#wX3Hkrd7!P4by#ru$l zx!y9v(;b!j7?Aa>R~$Wc`v^V%B|dv<{}3SD90(xX9D+d**}gy%*}a5y3XNL93a;Nm z^r_#bMbzH`aS=`~YQ}zxF%LXjTvo@fYnzlb-m$qmox1(X`8D$019ch?j0SDubT}r;*iBQI06^U{F&3CK{LGBnYm)$vpw{KW)X zh{u*qaQsH^__HiJtx`y9A6hc_(d(r9@Eg;GamFzyECdv|dqT2*P;@y&2}ehjiIoQHVMj zIk`8W>2#Ll$?}S6{$5Wluq{2qN($m{pw(O(ey*;;-6NgrHpiJqR9cR`-m9`*sW(g0 zFuu+>E-Bo#rT41T5q`>oJQ3bI@j}S?n=j!6NNsI++L&v@k~yMg_V33l^g<&lRPt4c zZWi^zh_$~jUp_y*-}$Q!2p)cp6=`PxWM^Z!!kCPBF1tOn0^dlkr!0%973tzODptsopDYsZBgHB^b?5fHv-QMi-E zUzqWi^JdEo?r0*+Ed18m;)l-fq?~)A3=DdX-yyXvj?;%E2Ts}a&RUC1x`|bWBTuLR z#iGRJgqf9!5*txdox~+6K{u7ycs3>2r&ohjGy;9W>pU^=D;#Y@+BwMegFS#aZwwhS zX#_`qfLRq=1oGr`Rd#8ME#ihHo`@wlpE=4X$_ynV z5aR!@y&?d$x-kCgtE)mMv-gxKQ06294T#d@<`z<@;$o=enc(u;@Y)v1J>hGm6vTlWQSZDb6svJn(mC?gX z;w3=TxqoA%nPI%!&~T{X?jWB)&$L{Ok2GhW_=%i=e-?7*_OOA;P?=Axom$X}PtAm%p+#-3jIjU6cwsCMQ6dub!A6gc1fypG0~DjtnRGdiTc?-Y$UvhS^NsKCFPs z$@me^WvK|^;%h;MXVe?gPF0N z?fU{H?>qkc4G#1Fsp>3%;)u3&4THP8LvVL@_uvxTo!}N2+xjoqEAu|GaRZ3S*u)8K`bnzKOgKa862W#|sM2Q0hn3Uq(C z7{7lVSDFZyOBmrQpvLD}g@x<*x%3?Zc1S4cT+GIe95=G~>l5Aqy2cQ$p0HF=_n#97vv{Xsl z_2dJ(%qCcxw3dRGAGwYO--`BYey*EqI45c$>gz+W3huI!;iiUn#%7$aLb*9v3G&xolLap0>4GK z@j$GN*WvycKkw6JW7nLG9*(YC!9V3pH6s3o+0WsC5syk!7ej!bs5H$TI*cO+opCL; zzCse^fGk@H7edh&Ga)+vWG(O;l5oTHd+;~O%yOp$DNMvEe)n{GqlsZF*}3*idhI@H z^AH)%brK|*YW%HJHIqwy_XQc)pFl2+798xPHadUXWnG?ika7k;D=7gqlcwA_ub1@r zdFXP{&kVdn6=Yb6V?(mKIn=oDDt!3wukB|!QTpk+m>RSWW8jL$coczP|1B{yHrNKF z^^gU8&4Gg*t3q46&q?UAOD5l8gRk0fT)6u}1;K|=$TaGkADb4W%%Fm#B!JSe*6@0m zpd!Oa6M~gx^ccA}6$wB_EC)_P?#Fajk@;0(*ySY??B_9LxE-b&ZYfw;fGNaEZ?W9Z z@cIeS2-4sy<~}w%Lbfxy?1aFx_`y|x*|`v7T6qp9jju@|DVb(7?CH!eG*5Gy&l+8h zRbM^8F!tpT5oH7_gW>9GoIpm};Yf!1O{25~qK{^yWgpO~+jaA%S(nwyE0EdwL!30c zKldt?xJ0aM&=1ycCR-5a38i5O*0PK$+gT3P>!y1@WKHxy>~~O27sP(<)ig}wRNBRr z%aKHq$VG*rl$FywL80@QG^{g$)G(eHOk>J}B_@)*1Pdw21lI-z;E;-&jIZWa_0rpSSA7mp= zY4%6fSDnyAb5@>5=Tji(VLG&@QJBH2*IT9d#Z0;Q1}$-PDQPDU=b^MOJ-_5unLk?& zJZi>Qg3o#87MvE77KLnnubDpISzVT$FGU~oW?sqGR>)#s1~C4_i_tCZz~R{`G{gU{ zE$-s^yxBhQl6sEv)_Qo3lC-ZDfTii0Zc2yEfn()i7M1a+7BB|f{1XW1VWwf3P^+de z<&}b!6y9Xr(kUtJ5k~uysJ}ev!@ZJgTX43?N(3|OzqhI_ zsE`L~Z(%4Bo2itEVg!ZfoN{oLg?~rEvg_D~ERcyBo#J#Sl8d<@Xys_0V6>-ceP)`5dl2>|jwH~b+=fqshaPwn^QIdTGV^Ti z8BzI7>A~8Nw6PZUN=A6is)VG6;#e}?*nJ}5PPBsTSPCo{pUH1sUePRlAORuxUGTL; zKEk~Tq9QxSdq&rcb2q7smlm$PdEqm_b)ERpIu%W>VLYrJ7aua2XM*1h2BvVi7cSXjq-L*w5-) zq9A6ft4bIGNCMU02vz_tSz-F^eHzfm>oq1zs4eB@ z@mighTiklDogFW5lyrl{W9cm1P0|dWwlOGh#Ja$N$km}-j? zY``YYW?#ckjy5RzMFrfp_H13V40I@GOpetB-1a9QVGpY6k-=rTjyBAN>)HrTAXhx? zjs+{5lV)GZRr2S&0QY?3JgpBZBe52ll7*daQZZ++teaus3k5iw5W=xmxQO%El^)7a`2Q7ALgm-8h!U^Y(ne^KbVI#U}z#)(&OI zJDMZDDt*AHcv3>&{(4=K_-i*KDFP6MMhTKL1F6)&UtMqCUz!7YI1}H)F1sD+?HsvM zwnbTk?(?UESMwaPnd@-|!F3FkpxHG`X_-S6%)#&Q8Y130A{gi2agh>GlFZi|_=nIj zwOXpd3C|nC_-6?4odNmsLdj^GmJ30Dm3 zp^Rl(mgvZ7rg?OPuqj8wp}kBq5<%s(y*A39AfzGg1#VM{I=3eH zr#^4k3i-u(AteXe|4|m>-P1 zBXT7m&IZ-{Z`Ubnyz&hjqacZm48@VyU>ux?>kb!B8u`*$ z6tcI(Z7o)f{5l1?jg>WYf1To^3 z-<_=Hk8jxi0(ZX&7?QJDyYNQ#(tSnb(7qlF+`@y0 zGG6G;Wc?tFFKF@juW~+#NK9N0>>e|@;?1~G6^qJ%ucLp^)ph}|*{{=dgk_%K=1}uw z1yk2-(#`kOv*gNxB5=4sc1PG1MXV;pYlZU0#XlnFvM&dZmD^_C%RR9Rwzz!R@(o#^ z=+} zr7EYu@;hHinSeF0V{y^VS_`oB3u!ar0?;%DO@ZA~5#pvo<3+5q7lQov3dG(!cl(yT?b(xcB+F_-Ld` zm66hh_Bn0T?$LPQU z{0+si%bDJMog9=Z86uvtvJ#wP9>-<@Hv-={&B;l}tM8!u__j-Xf#2KA)XS_#9;<=1OL|`w zg{mpfY;ju3s^xvMcEcN6EJj35M--uDj)8VE zyH~>{jkyBn+K>r{rG;rBb1SYHD*{O|i>(6MIJi^k!p#!|E5f^#*dRw;?j7LyG*I&~ zC!S!yeWH7M1JHiqalYa&v7bn@H|TP{rCu&~7tP3qkg?Y)*Zm4k%i<|wqoC_Yfl(4WW|6uE z1IoaVykI1l6mgiCB;j-@SYWd^ILaF8@*D1UUPx>^3V$OR|F)Ub9mQ@0TKKHO3SztkrL_O9a;xo~2 zlCE0m`)9ZXfw}{QXWHLn<&o^T$s&mTEI9mcC9^#kg6rhIpwb#~8{qp}-QHG}Mw5ni zIZ|iJGmHHg-XrGK2bsQLw&}_*syR+Ee7^<@-EtE&tjmfTcE}xt56B4WX_1~RfCnQ$3*fB;!?xeos|dU_fV?S1>I_e5iuA8g zp@Hcs)BHLeXt!xJHCZ;RJCKc4`R(*$NjQnCq4O-XuE^}^bxi(QRYrclRHsz3puDKu zen8iKi?)cpKXIuDpE2-LNycrIr8<0Co1($PtV3So;5T?5W3tjsBaVtM&lDXWi<;=xuTdL#5h;7fAWS}>n zliW&C-J|?)fwu(b5K7nAgCl2JIri-qLuphbM=~#o^*Un*u z4?aO(8`voaX8h1Vz?(8-Db{BR2FG9^)695+rSPsSI+Fd}nO}~4!7{v;?j0}}tyjn$ zxz;m=LNVt%%eS^*N#m{d(KI#P_voO;g3;Uq`GV@jC%)` z{s5K^NVk%P&ogIrM{Y~TGjp@_#6s0;*<0-|?NaSPNd#d4>P2()x)kY>pJGSo_ntZx zC;?TOy^^8@I4P?_Rmwb0H_U0f6#5hQjxRZ6HW>hyYJ49a9*kN>mX2d`!{0s~Rv9&p zU+JDV*$ipn)K9ARQ|X1!V7_D~2P8KS?ym->l`-%x>@Ip{UxE^~Bt992U6)9E8*J!5 zA&+|jtFqLhzVLP$Y}L4ar-VQ&8RxK$x>0fEC++wSY5bB|{3k-)MMhe)W>7}Uq%aGy z4YsBwaQ{XE-xPzn_kqJG$+ht*gCA;S4B;T7GC2v#A?-#fLtVF4@oSfgmTc9WU_9}~ z$E1k>@D)v@&GjGJCH6gfj|qwuw+v4&%Ir0AAoqA&@S0?kY;rWcGp{_oSEH0dj_@G8 zhvsXwo#9Vj(7Nh*1Mp-yB42@A)2S{z5Hc_I>ISQ|^73E#Ii zDV+JdPl>)k39i$JNrAf_uRm@H1l<_1v%D1^XGS!xYk3<xs<)1$j0{6LQ zVMvWe#~e27`Wg6h506iG<%}!Z=5gnvVS2d3(pQ-dzhqUrlYoOq0Uzw!Cl&^LJgawM zMi}_*ZQxwho1t$?%Y8L8zvbH*;(Gg(`0H)L9PT!drU=SMrv!D81RxJJY8U}%*5trkJ(cV#X{ zR0s%~zpsi&$8do_qIn!)b7rcs9hf2cx_Yc3gnFhCTzP~PzGA7CC>$oiJDFUF2|2xt0UNN=D}EKk*CbYB`l@Q|utEPBoL zH8<&klmS{1(FXF)r$GI|)+w&C{+GM1+_MjVu z5ZQN#0Q~-hrKk6geOFA>>V%fk2yx4j#~5L29^D9O%i|s>IhYM_%AUD#wKd>omKUVV+)3u}*B-W$n09lTz9b+CG_3LKuZe5%M{7}00v zmW6EEE)TqCH{@j2YsB44u7*G46BTrGGIQwet}L<{4ohw@VfbEbWQE2XTTw=;sfZYM zSb_g+N$nh02^-hpVkmZ*Qt@@c781^U^;_#?I4%(8@y9Jd`YcDC+j52F0NdPXA{D!I ztes^veALZ(+PS(SWw$rQ30s4uagJNEMiZOL!>C1jG7;YLnk!PrTCKiCv6|hoIAJ_8ic?D`fKpOrtVOfH zB+W^({5z{CP3#z+U}mZkT4w-~6-&8Z9SPW&Y52j!2QOCr+dA(zdhf7NvB6J(er#Ul zh<)PW-g5wVH;!l?yJOC*BUSAsCC+n81K}14rp#4KXzjKL0l}=yy8No$*L-};fC-VFURL?clu+XR7EJEll&uXnW1^x;X#RVt`pGOIrWl)r(CzIRGxcu?=y!2HJ;XZd9~s6t$n<} zpTb`#`<(nv8LMggUEB9VZH%Y^eHZBxgW;aIhhUO8*0VVSuPWPu3-|pLdbIEvL_m1Y zl=X!c9xuD%#?Rf)v+F&~Q-v=mYD8}QzF6r4B+6X)wET)4N`q1wMrydoTD`!a{S7xs zG~1J$?YF#u-TUa+8^xbk1?HV)J@%4FE;^t6vP5|X4Vi6p5F4bo0QE7pDgwHfQ^EDI zoejKcw!T7FR^#95IeP347u%2o^joH>1BdZanlo`wmqP{jHtbf~$F)0H(`@6%;x-sz z_FO)(WD0J#;|K}3o8sk26Bh#grrA5yad0zD*5t{$(kFZdWv?iR9bi_;p# zUURB8U3pfDyE{eJ)?Kg^;I^nV?`xVb7lPTUf~&7wr1@9m`WVu1;=nlV!gC&>K+ZsO z_Sj8b~rcPhN}w>rfhab6|WO%{Og{!~n->G3Tr2}7_s zyIQH2U@5UL^Xud#e3$Ht_kmpT0j_T&wD%A9<{pTXq-Sk)knt<(~InierO=! z2p`()B!L$UCcaa=5mbrcsL4Vs7M`-q7^R%epvuJ^1oYi+z~zsU_uv zU!W}l-V*VwsYk8mmq(M+mjQ9C5px7Q_>qC%Xe&o8gF29C4+twG?0)iPx;!JYZny5D zL9~mY-*1Xq$lSoG2et3{#84@DQUsoADj1^$F8bd*V83}|Ct%1x_|>0cgQUpt+^+Zy z^eJBPFfh_HPz?oz1SU1`anCg=B|?*(DX{-QFrP#XfA-)1bf9rFO3xu-xjUz6cjMM} z0wM`z#ayC-exoCqHg`8kC+>eS$Pw7m7+yq+?nfM8st$qy_9DR_v{Q~TzI-N$ zP_qtp(mHb8?P_-M!H%TL(?XclnIIAq_vPiE6VWSN%Al-LTYKNK(xX(;d$~^zR7)St zXG`s7UlcBu-W}Vhl&}3c2RJ%o!`~j+FZ_SJ0Dt&xJgkd6?}ng3+Tcb@btw$yLU!p( zKpIhPH)Fm6`Dny@4S)LNMlQl#!eTh5e8zT8{us-vs2gZbxlU@8~ zLS%I3$0H|3uRN*fL`UA{G8AOawo5XhsAH@?Ywqr^)eq0vTGxkt)w?A~-3&9g`;bK#`3Z}oCI2V%~u zFJfM*I$obtt5n76{CiwK+A7eEB$bxi+KePI0~GY{ELJp=_erUf)L`D-s~nu8TH4WF z!+tT>0}WZWl8H^-b;iVQI_{vR*HIyLZe=^*3hUpU=)Op$e;})AWNvA#w0;m{nwegh zCvuCbxNmBb^=ukkfxRxmAumA|E+H%}Erros!LU|ho}SCy)0iu1)E8`q4l}f~xAVoC zEmq?yrj2OEfb=-)V4vYKqq_=S;c}v**I#T}1d@JY&W$a|$O0Ej?+tW_d)`+{?xT+9 z*E$j7*0u29y}Cv^M$8o;GgGk{SCZ0B;&XtE$Z@2yJKp1B z7-L*%jVdg(HbvH|amZ@UHk6@QWiXmd$Bq=+@!Z`@4X;tEk1p#$-ZlT3WJlLxlv0@O zUh#K>x|WFkj6s75ZaC|3N*+_Fklbp+0S;)Q*i(IpW|vr|d#DpvvEeBW%o-yoE=Kd+ zG~QnG>yWT*nfE+0$G!n57ulC*tXmn{F&y-5MB zSk5qX!e#K&lJTOd#PbFhE7`MfEB%ZI+_{*k9z&MnFoq16zIzF zOGLGQy6=pTy^0JrJAvV0+Lh4lF!1B@;>FerM>sm(6%>K!;0_1NwyXvFxgEr6Y7@iG zkH|5;*ldf}(D8j6cgFql*t~}Cle)TFxH7Uh9lM2@>;$5%>`tjyNZOzTo3C_^QFfmm zsTF~#RCPhX@!*ZR{1kzyHYegpHIX~yy{*qq`n?CbciClsXJxoIH5+MMR zIoEfXA!Dk|Dn1;wJmL%l0;+tKT&XMlE~!5=`;^JKzy}Ii6QrPJtyhyIYh~@#`^BQu zg1eXA6j&+DI-KJqCEQ+@)+4=erSjzVx>$!P zmmu=QyfY|7tcyQ1Wa)^0qh#@=pXO~lM4#?7ymc*HHN0gg1PU6sXB?{F{fZ>tDCI)C z4zr7MADYos=+X77kKlU1oR6l=g4CKte=b#ElHKZeT~3lB?)`o-C`a){PK( z9=)f${WLYSlnz52WHUn84}xC{p`N8XM^fnK)Sc47j|Ybfg(WvSFy+`6O*N<~P}OCz z5vql7vwT8P0phdPxrY%F9txWi;hY!3h-@1ms}`gL;$dDEYS1C^=18y^01@}@cE??W z3^qO!#tfk4#~vc8*9gTi($t6YZ<*krfy%-CjWlZJH)$(fjLhqejz+`#hSE{`JW-X7 z`>xsT{ptp`H`>cx`Y}4zH~l=d0f;CdUB??jN26J6;DXXNKkdg~ww7mvg7$Yg&GQ<% ze)k{3i2AAc60B&A-|y)Fiyto;>(TA&mjrB1w+Vj}|(ZfOGKn(V>no5cP;4~?a|MM9qai$5$YH}In)H_N|kJ%wEE zdx$Z6Fc7ko*OZyo|CG!w&B?BIv=@OJI>X*t!GUulJ9dnILly;;_GbzLJoz@!^eyTP z3FJ6(Fmdx-3yB*J!WKSFbNv27JBI|e?BPdEz|QNBeLkBXBJuZxY^0Y|Imm3u@`1iG z`~1gsxuzr*Sya zJh;m-lFd&fn=g^uzqV+wix*k~8f!T zn3ir71+XJq3a*|ATML^!$z&d9uh&(qV~yQRUJXAQSBDwbpX|E&S8!O65W-Z+>9)&z zGMbzw&w;!+q_q|G&ugeXvj@*#c7abnsgu&v1r4nWX-*X5c47i`^q;+i-j&%PL5+I^ zjT(Ca(EpQqY5vF(`frjLkz+&XzZp03j;)~oqr4A7IQb0oR}&o+aAHOLSLF3Qz~=T{ ztx)Jax6J=;#X-v)pe;Ho5FsZKNaPfq_&;)*74P8SJ1G3W)O%SRw8#yDJf{bNPHBk$ z(LVeKTI2f*y`7R1|DzoD4|FQ{7s3_B0Og;f6aUqZdmpmpJz9hFAMi-{9b^Sfp5YSz z73g}0yx*aJ=d~mD4yh9VRYZCR+TODbaQxHDtmNM-OgN_?{*Oe?uXo7)eK|_>ABaxo zFLZIvLj3>ra^Bag{(;Qo-yurSrwcX!i~(rtf)Z5wZem)zo4NoVYmnfj6#&r|Bw!~9 zV!K8M_3j~qo-a`WzwAJWS3&?3d(h<-5yX8zN~@GT(#HRJE;r&|R8PTpVB zD4!67cZ3cKy(0uH7l88bxQPD=xcT2f-^=2lfkM#boeF@j93*xxO8k%K_&?n5ig%6} z)Oybbz#aNK%-cN=p#R5TlXUF;SNMUB_@C9pf0~z${1?RfJMp;(LcsYH=<>k;@HP+n syvPdje?%w#=c($S<~7S8@>K@hkBTtwU;THn!}mQ03j*TT&VOqE4-{M+YybcN diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f4197d5..ac72c34e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca1..0adc8e1a 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/src/main/kotlin-templates/org/hyacinthbots/lilybot/internal/BuildInfo.kt b/src/main/kotlin-templates/org/hyacinthbots/lilybot/internal/BuildInfo.kt new file mode 100644 index 00000000..838c9b20 --- /dev/null +++ b/src/main/kotlin-templates/org/hyacinthbots/lilybot/internal/BuildInfo.kt @@ -0,0 +1,12 @@ +package org.hyacinthbots.lilybot.internal + +/** + * This object stores the constants for the Build ID and version of Lily in her current state + */ +object BuildInfo { + /** The short commit hash of this build of Lily. */ + const val BUILD_ID: String = "{{ build_id }}" + + /** The current version of LilyBot. */ + const val LILY_VERSION: String = "{{ version }}" +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt index 6f47de43..b01d2f2c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt @@ -13,9 +13,8 @@ import dev.kord.rest.builder.message.create.embed import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import org.hyacinthbots.lilybot.database.collections.UptimeCollection -import org.hyacinthbots.lilybot.utils.BUILD_ID +import org.hyacinthbots.lilybot.internal.BuildInfo import org.hyacinthbots.lilybot.utils.HYACINTH_GITHUB -import org.hyacinthbots.lilybot.utils.LILY_VERSION /** * This class contains the info commands that allow users to get a better idea of how to use the bot. @@ -127,8 +126,9 @@ class InfoCommands : Extension() { } field { name = "Version" + // To avoid IntelliJ shouting about build errors, use https://plugins.jetbrains.com/plugin/9407-pebble value = - "$LILY_VERSION ($BUILD_ID)" + "${BuildInfo.LILY_VERSION} (${BuildInfo.BUILD_ID})" inline = true } field { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt index 4bebc87d..0bcc99a4 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt @@ -24,8 +24,4 @@ val ENVIRONMENT = env("ENVIRONMENT") val ENV = envOrNull("STATUS_URL") -const val BUILD_ID: String = "@build_id@" - -const val LILY_VERSION: String = "@version@" - const val HYACINTH_GITHUB: String = "https://github.com/HyacinthBots" From a9b8c8252aa1404c7a0287f880d54ff6edf350ed Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 12 Sep 2023 17:20:06 +0100 Subject: [PATCH 12/13] Bump to version 4.9.0 --- build.gradle.kts | 2 +- docs/changelogs/4.x.x/4.9.0.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 docs/changelogs/4.x.x/4.9.0.md diff --git a/build.gradle.kts b/build.gradle.kts index 402c7317..0ab16aad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { } group = "org.hyacinthbots.lilybot" -version = "4.8.5" +version = "4.9.0" repositories { mavenCentral() diff --git a/docs/changelogs/4.x.x/4.9.0.md b/docs/changelogs/4.x.x/4.9.0.md new file mode 100644 index 00000000..dd0c5a38 --- /dev/null +++ b/docs/changelogs/4.x.x/4.9.0.md @@ -0,0 +1,19 @@ +# LilyBot 4.9.0 + +This release removes th deprecated log uploading, expands the capabilities of the clear command and fixes many bugs +You can find the full changelog below + +New: +* Allow custom messages to be sent with bans, intended for adding Appeals Servers. This is part of the moderation config + +Change: +* The clear command can now clear different ranges of messages based on certain parameters. #348 +* Lily now uses the official Kotlin MongoDB driver, rather than KMongo. #351 +* Upgradle to 8.3 and update other dependencies + +Fix: +* Remove usage of deprecated username tags, making use of the new username system +* Fix bulk delete now working properly +* The permission checks on Auto-threading roles + +You can find a list of all the commits in this update [here](https://github.com/hyacinthbots/LilyBot/compare/v4.8.5...v4.9.0) From 0bd8543c174594edcb2d8902355cc31299d488d7 Mon Sep 17 00:00:00 2001 From: NoComment1105 Date: Tue, 12 Sep 2023 17:20:06 +0100 Subject: [PATCH 13/13] Bump to version 4.9.0 --- build.gradle.kts | 2 +- docs/changelogs/4.x.x/4.9.0.md | 19 +++++++++++++++++++ gradle/libs.versions.toml | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 docs/changelogs/4.x.x/4.9.0.md diff --git a/build.gradle.kts b/build.gradle.kts index 402c7317..0ab16aad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ plugins { } group = "org.hyacinthbots.lilybot" -version = "4.8.5" +version = "4.9.0" repositories { mavenCentral() diff --git a/docs/changelogs/4.x.x/4.9.0.md b/docs/changelogs/4.x.x/4.9.0.md new file mode 100644 index 00000000..dd0c5a38 --- /dev/null +++ b/docs/changelogs/4.x.x/4.9.0.md @@ -0,0 +1,19 @@ +# LilyBot 4.9.0 + +This release removes th deprecated log uploading, expands the capabilities of the clear command and fixes many bugs +You can find the full changelog below + +New: +* Allow custom messages to be sent with bans, intended for adding Appeals Servers. This is part of the moderation config + +Change: +* The clear command can now clear different ranges of messages based on certain parameters. #348 +* Lily now uses the official Kotlin MongoDB driver, rather than KMongo. #351 +* Upgradle to 8.3 and update other dependencies + +Fix: +* Remove usage of deprecated username tags, making use of the new username system +* Fix bulk delete now working properly +* The permission checks on Auto-threading roles + +You can find a list of all the commits in this update [here](https://github.com/hyacinthbots/LilyBot/compare/v4.8.5...v4.9.0) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0284f908..c1bce581 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # Plugins -kotlin = "1.9.0" +kotlin = "1.9.10" shadow = "8.1.1" detekt = "1.23.1" git-hooks = "0.0.2"