From a9d3161d2e291bf5afa6d4b85188693657e9052b Mon Sep 17 00:00:00 2001 From: sschr15 Date: Sun, 4 Feb 2024 17:43:33 -0600 Subject: [PATCH] Change log parsing & add auto archival of threads As is tradition, completely untested. When will this strategy start backfiring again? --- src/main/kotlin/org/quiltmc/community/App.kt | 67 ++++----------- .../org/quiltmc/community/_Constants.kt | 7 ++ .../collections/OwnedThreadCollection.kt | 5 ++ .../database/entities/OwnedThread.kt | 4 + .../database/entities/ServerSettings.kt | 13 ++- .../database/migrations/AllMigrations.kt | 14 +++ .../logs/WrongLocationMessageSender.kt | 35 ++++++++ .../quilt/extensions/UtilityExtension.kt | 85 +++++++++++++++++++ 8 files changed, 177 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/org/quiltmc/community/logs/WrongLocationMessageSender.kt diff --git a/src/main/kotlin/org/quiltmc/community/App.kt b/src/main/kotlin/org/quiltmc/community/App.kt index 15473432..636db6de 100644 --- a/src/main/kotlin/org/quiltmc/community/App.kt +++ b/src/main/kotlin/org/quiltmc/community/App.kt @@ -14,6 +14,7 @@ package org.quiltmc.community import com.kotlindiscord.kord.extensions.ExtensibleBot +import com.kotlindiscord.kord.extensions.checks.channelFor import com.kotlindiscord.kord.extensions.checks.guildFor import com.kotlindiscord.kord.extensions.checks.types.Check import com.kotlindiscord.kord.extensions.modules.extra.mappings.extMappings @@ -32,6 +33,7 @@ import dev.kord.rest.builder.message.embed import org.quiltmc.community.cozy.modules.logs.extLogParser import org.quiltmc.community.cozy.modules.logs.processors.PiracyProcessor import org.quiltmc.community.cozy.modules.logs.processors.ProblematicLauncherProcessor +import org.quiltmc.community.cozy.modules.logs.types.LogRetriever import org.quiltmc.community.cozy.modules.tags.TagFormatter import org.quiltmc.community.cozy.modules.tags.config.TagsConfig import org.quiltmc.community.cozy.modules.tags.tags @@ -41,8 +43,7 @@ import org.quiltmc.community.database.collections.TagsCollection import org.quiltmc.community.database.collections.WelcomeChannelCollection import org.quiltmc.community.database.entities.InvalidMention import org.quiltmc.community.database.getSettings -import org.quiltmc.community.logs.NonQuiltLoaderProcessor -import org.quiltmc.community.logs.RuleBreakingModProcessor +import org.quiltmc.community.logs.WrongLocationMessageSender import org.quiltmc.community.modes.quilt.extensions.* import org.quiltmc.community.modes.quilt.extensions.filtering.FilterExtension import org.quiltmc.community.modes.quilt.extensions.github.GithubExtension @@ -106,57 +107,19 @@ suspend fun setupLadysnake() = ExtensibleBot(DISCORD_TOKEN) { processor(PiracyProcessor()) processor(ProblematicLauncherProcessor()) - // Quilt-specific processors - processor(NonQuiltLoaderProcessor()) - processor(RuleBreakingModProcessor()) + parser(WrongLocationMessageSender()) -// @Suppress("TooGenericExceptionCaught") -// suspend fun predicate(handler: BaseLogHandler, event: Event): Boolean = with(handler) { -// val predicateLogger = KotlinLogging.logger( -// "org.quiltmc.community.AppKt.setupQuilt.extLogParser.predicate" -// ) -// -// val kord: Kord = getKoin().get() -// val channelId = channelSnowflakeFor(event) -// val guild = guildFor(event) -// -// try { -// val skippableChannelIds = SKIPPABLE_HANDLER_CATEGORIES.mapNotNull { -// kord.getChannelOf(it) -// ?.channels -// ?.map { ch -> ch.id } -// ?.toList() -// }.flatten() -// -// val isSkippable = identifier in SKIPPABLE_HANDLER_IDS -// -// if (guild?.id == TOOLCHAIN_GUILD && isSkippable) { -// predicateLogger.info { -// "Skipping handler '$identifier' in <#$channelId>: Skippable handler, and on Toolchain" -// } -// -// return false -// } -// -// if (channelId in skippableChannelIds && isSkippable) { -// predicateLogger.info { -// "Skipping handler '$identifier' in <#$channelId>: Skippable handler, and in a dev category" -// } -// -// return false -// } -// -// predicateLogger.debug { "Passing handler '$identifier' in <#$channelId>" } -// -// return true -// } catch (e: Exception) { -// predicateLogger.warn(e) { "Skipping processor '$identifier' in <#$channelId> due to an error." } -// -// return true -// } -// } -// -// globalPredicate(::predicate) + globalPredicate { event -> + // If any condition is unmet, always let the normal parsers handle it and don't try to send a message + val skip = this !is WrongLocationMessageSender + + val channel = channelFor(event) ?: return@globalPredicate skip + val guild = guildFor(event) ?: return@globalPredicate skip + val allowedChannels = LOG_PARSING_CONFINEMENT[guild.id] ?: return@globalPredicate skip + if (channel.id in allowedChannels) return@globalPredicate skip + + this is WrongLocationMessageSender || this is LogRetriever + } } help { diff --git a/src/main/kotlin/org/quiltmc/community/_Constants.kt b/src/main/kotlin/org/quiltmc/community/_Constants.kt index 88c604fc..5f9bcc03 100644 --- a/src/main/kotlin/org/quiltmc/community/_Constants.kt +++ b/src/main/kotlin/org/quiltmc/community/_Constants.kt @@ -150,3 +150,10 @@ internal val EXEMPTED_USERS = envOrNull("EXEMPTED_USERS") ?.split(",") ?.map { Snowflake(it) } ?: emptyList() + +internal val LOG_PARSING_CONFINEMENT = envOrNull("LOG_PARSING_CONFINEMENT") + ?.split(';') + ?.associate { it.substringBefore(':') to it.substringAfter(':').split(',') } + ?.mapKeys { (k) -> Snowflake(k) } + ?.mapValues { (_, v) -> v.map { Snowflake(it) } } + ?: emptyMap() diff --git a/src/main/kotlin/org/quiltmc/community/database/collections/OwnedThreadCollection.kt b/src/main/kotlin/org/quiltmc/community/database/collections/OwnedThreadCollection.kt index e6fc339d..a7ff15e3 100644 --- a/src/main/kotlin/org/quiltmc/community/database/collections/OwnedThreadCollection.kt +++ b/src/main/kotlin/org/quiltmc/community/database/collections/OwnedThreadCollection.kt @@ -13,6 +13,8 @@ import dev.kord.core.behavior.UserBehavior import dev.kord.core.behavior.channel.threads.ThreadChannelBehavior import org.koin.core.component.inject import org.litote.kmongo.eq +import org.litote.kmongo.ne +import org.litote.kmongo.or import org.quiltmc.community.database.Collection import org.quiltmc.community.database.Database import org.quiltmc.community.database.entities.OwnedThread @@ -32,6 +34,9 @@ class OwnedThreadCollection : KordExKoinComponent { suspend fun set(thread: OwnedThread) = col.save(thread) + suspend fun getAllWithDuration() = + col.find(or(OwnedThread::maxThreadDuration ne null, OwnedThread::maxThreadAfterIdle ne null)) + // endregion // region: Get by owner diff --git a/src/main/kotlin/org/quiltmc/community/database/entities/OwnedThread.kt b/src/main/kotlin/org/quiltmc/community/database/entities/OwnedThread.kt index d39cad6d..82f2651b 100644 --- a/src/main/kotlin/org/quiltmc/community/database/entities/OwnedThread.kt +++ b/src/main/kotlin/org/quiltmc/community/database/entities/OwnedThread.kt @@ -11,6 +11,7 @@ package org.quiltmc.community.database.entities import dev.kord.common.entity.Snowflake import kotlinx.serialization.Serializable import org.quiltmc.community.database.Entity +import kotlin.time.Duration @Serializable @Suppress("ConstructorParameterNaming") // MongoDB calls it that... @@ -20,4 +21,7 @@ data class OwnedThread( var owner: Snowflake, val guild: Snowflake, var preventArchiving: Boolean = false, + + var maxThreadDuration: Duration? = null, + var maxThreadAfterIdle: Duration? = null, ) : Entity diff --git a/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt b/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt index a852b13a..d05d3498 100644 --- a/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt +++ b/src/main/kotlin/org/quiltmc/community/database/entities/ServerSettings.kt @@ -23,6 +23,7 @@ import org.quiltmc.community.* import org.quiltmc.community.database.Entity import org.quiltmc.community.database.collections.ServerSettingsCollection import org.quiltmc.community.database.enums.LadysnakeServerType +import kotlin.time.Duration @Serializable @Suppress("ConstructorParameterNaming") // MongoDB calls it that... @@ -44,6 +45,8 @@ data class ServerSettings( var leaveServer: Boolean = false, val threadOnlyChannels: MutableSet = mutableSetOf(), var defaultThreadLength: ArchiveDuration? = null, + var defaultTotalMaxThreadLength: Duration? = null, + var defaultIdleMaxThreadLength: Duration? = null, val pingTimeoutBlacklist: MutableSet = mutableSetOf(), @@ -218,7 +221,7 @@ data class ServerSettings( if (defaultThreadLength != null) { val readableName = when (val length = defaultThreadLength!!) { - is ArchiveDuration.Unknown -> "Unknown (${length.duration} minutes)" + is ArchiveDuration.Unknown -> "Unknown (${length.duration})" ArchiveDuration.Hour -> "1 hour" ArchiveDuration.Day -> "1 day" ArchiveDuration.ThreeDays -> "3 days" @@ -229,6 +232,14 @@ data class ServerSettings( builder.append(":x: Not configured (using longest server / channel setting)") } + builder.append("\n") + builder.append("Default Total Max Thread Length:".bold() + ' ') + builder.append(defaultTotalMaxThreadLength ?: ":x: Not configured") + + builder.append("\n") + builder.append("Default Idle Max Thread Length:".bold() + ' ') + builder.append(defaultIdleMaxThreadLength ?: ":x: Not configured") + with(embedBuilder) { color = DISCORD_BLURPLE description = builder.toString() diff --git a/src/main/kotlin/org/quiltmc/community/database/migrations/AllMigrations.kt b/src/main/kotlin/org/quiltmc/community/database/migrations/AllMigrations.kt index 9455448a..7d1c99b7 100644 --- a/src/main/kotlin/org/quiltmc/community/database/migrations/AllMigrations.kt +++ b/src/main/kotlin/org/quiltmc/community/database/migrations/AllMigrations.kt @@ -354,4 +354,18 @@ object AllMigrations { setValue(ServerSettings::vcMuteRole, null) ) } + + suspend fun v29(db: CoroutineDatabase) { + with(db.getCollection(OwnedThreadCollection.name)) { + updateMany( + OwnedThread::maxThreadDuration exists false, + setValue(OwnedThread::maxThreadDuration, null) + ) + + updateMany( + OwnedThread::maxThreadAfterIdle exists false, + setValue(OwnedThread::maxThreadAfterIdle, null) + ) + } + } } diff --git a/src/main/kotlin/org/quiltmc/community/logs/WrongLocationMessageSender.kt b/src/main/kotlin/org/quiltmc/community/logs/WrongLocationMessageSender.kt new file mode 100644 index 00000000..5598dff9 --- /dev/null +++ b/src/main/kotlin/org/quiltmc/community/logs/WrongLocationMessageSender.kt @@ -0,0 +1,35 @@ +package org.quiltmc.community.logs + +import com.kotlindiscord.kord.extensions.checks.channelFor +import com.kotlindiscord.kord.extensions.checks.guildFor +import dev.kord.core.behavior.channel.asChannelOfOrNull +import dev.kord.core.entity.channel.TextChannel +import dev.kord.core.event.Event +import org.quiltmc.community.LOG_PARSING_CONFINEMENT +import org.quiltmc.community.cozy.modules.logs.data.Log +import org.quiltmc.community.cozy.modules.logs.data.Order +import org.quiltmc.community.cozy.modules.logs.types.LogParser + +class WrongLocationMessageSender : LogParser() { + override val identifier: String = "wrong-location-message-sender" + override val order = Order(Int.MAX_VALUE) // be the last parser to run (to destroy the log if necessary) + + override suspend fun predicate(log: Log, event: Event): Boolean { + val channel = channelFor(event)?.asChannelOfOrNull() ?: return false + val guild = guildFor(event) ?: return false + val allowedChannels = LOG_PARSING_CONFINEMENT[guild.id] ?: return false + if (channel.id in allowedChannels) return false + + channel.createMessage( + "This log was sent in wrong location. No parsing will be done.\n" + + if (allowedChannels.size > 1) { + "Please use one of ${allowedChannels.joinToString(", ") { "<#$it>" }} to parse logs." + } else { + "Please use <#${allowedChannels.single()}> to parse logs." + } + ) + return false + } + + override suspend fun process(log: Log) = Unit +} diff --git a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/UtilityExtension.kt b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/UtilityExtension.kt index 01e20d95..136ebdca 100644 --- a/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/UtilityExtension.kt +++ b/src/main/kotlin/org/quiltmc/community/modes/quilt/extensions/UtilityExtension.kt @@ -69,6 +69,7 @@ import org.quiltmc.community.database.getSettings import org.quiltmc.community.modes.quilt.extensions.suggestions.SuggestionStatus import org.quiltmc.community.modes.quilt.extensions.suggestions.SuggestionsExtension import java.time.format.DateTimeFormatter +import kotlin.time.Duration.Companion.INFINITE import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.ExperimentalTime @@ -776,6 +777,48 @@ class UtilityExtension : Extension() { } } + ephemeralSubCommand(::AutoLockArguments) { + name = "set-auto-lock" + description = "Set the auto-lock options for the current thread, if you have permission" + + check { hasBaseModeratorRole() } + check { isInThread() } + + action { + val channel = channel.asChannelOf() + val member = user.asMember(guild!!.id) + val ownedThread = threads.get(channel) + + if (ownedThread != null) { + if (ownedThread.preventArchiving) { + throw DiscordRelayedException( + "This thread is set to prevent archiving, and cannot be auto-locked." + ) + } + + // Update, but don't remove existing values + ownedThread.maxThreadDuration = arguments.maxTotalTime?.toDuration(TimeZone.UTC) + ?: ownedThread.maxThreadDuration + ownedThread.maxThreadAfterIdle = arguments.maxIdleTime?.toDuration(TimeZone.UTC) + ?: ownedThread.maxThreadAfterIdle + threads.set(ownedThread) + } else { + threads.set( + OwnedThread( + channel.id, + channel.owner.id, + channel.guild.id, + false, + arguments.maxTotalTime?.toDuration(TimeZone.UTC), + arguments.maxIdleTime?.toDuration(TimeZone.UTC) + ) + ) + } + + edit { content = "Auto-lock settings updated." } + } + } + ephemeralSubCommand(::PinMessageArguments) { name = "pin" description = "Pin a message in this thread, if you have permission" @@ -1879,6 +1922,36 @@ class UtilityExtension : Extension() { it.delete("Temp role for force verify (>=10 minutes old)") } } + + threads.getAllWithDuration().consumeEach { + val openTime = it._id.timestamp + val channel = kord.getChannelOf(it._id)!! + val archiveTime = channel.archiveTimestamp.takeIf { channel.isArchived } + val guildSettings = getKoin().get().get(channel.guildId) + val now = Clock.System.now() + + val maxThreadDuration = it.maxThreadDuration ?: guildSettings?.defaultTotalMaxThreadLength + if (maxThreadDuration != null && now > openTime + maxThreadDuration) { + channel.edit { + archived = true + locked = true + reason = "Thread automatically archived and locked after reaching max duration" + } + + // Prevent automatic re-archiving if a moderator has manually unarchived the thread + it.maxThreadDuration = INFINITE + threads.set(it) + } + + val maxThreadAfterIdle = it.maxThreadAfterIdle ?: guildSettings?.defaultIdleMaxThreadLength + if (maxThreadAfterIdle != null && archiveTime != null && now > archiveTime + maxThreadAfterIdle) { + channel.edit { + archived = true + locked = true + reason = "Thread automatically locked after reaching max idle duration" + } + } + } } } @@ -1920,6 +1993,18 @@ class UtilityExtension : Extension() { } } + inner class AutoLockArguments : Arguments() { + val maxTotalTime by optionalDuration { + name = "max-total-time" + description = "Maximum total time to keep the thread unlocked from creation." + } + + val maxIdleTime by optionalDuration { + name = "max-idle-time" + description = "Maximum time to keep the thread unlocked after archival." + } + } + inner class SetOwnerArguments : Arguments() { val user by user { name = "user"