Skip to content

Commit

Permalink
Change log parsing & add auto archival of threads
Browse files Browse the repository at this point in the history
As is tradition, completely untested. When will this strategy start backfiring again?
  • Loading branch information
sschr15 committed Feb 4, 2024
1 parent 1169525 commit a9d3161
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 53 deletions.
67 changes: 15 additions & 52 deletions src/main/kotlin/org/quiltmc/community/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<Category>(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 {
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/org/quiltmc/community/_Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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<Snowflake>
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -44,6 +45,8 @@ data class ServerSettings(
var leaveServer: Boolean = false,
val threadOnlyChannels: MutableSet<Snowflake> = mutableSetOf(),
var defaultThreadLength: ArchiveDuration? = null,
var defaultTotalMaxThreadLength: Duration? = null,
var defaultIdleMaxThreadLength: Duration? = null,

val pingTimeoutBlacklist: MutableSet<Snowflake> = mutableSetOf(),

Expand Down Expand Up @@ -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"
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,18 @@ object AllMigrations {
setValue(ServerSettings::vcMuteRole, null)
)
}

suspend fun v29(db: CoroutineDatabase) {
with(db.getCollection<OwnedThread>(OwnedThreadCollection.name)) {
updateMany(
OwnedThread::maxThreadDuration exists false,
setValue(OwnedThread::maxThreadDuration, null)
)

updateMany(
OwnedThread::maxThreadAfterIdle exists false,
setValue(OwnedThread::maxThreadAfterIdle, null)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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<TextChannel>() ?: 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ThreadChannel>()
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"
Expand Down Expand Up @@ -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<ThreadChannel>(it._id)!!
val archiveTime = channel.archiveTimestamp.takeIf { channel.isArchived }
val guildSettings = getKoin().get<ServerSettingsCollection>().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"
}
}
}
}
}

Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit a9d3161

Please sign in to comment.