diff --git a/build.gradle.kts b/build.gradle.kts index 65327001..b51ade74 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "org.hyacinthbots.lilybot" -version = "4.0.1" +version = "4.1.0" repositories { mavenCentral() diff --git a/docs/changelogs/4.1.0.md b/docs/changelogs/4.1.0.md new file mode 100644 index 00000000..eeb18104 --- /dev/null +++ b/docs/changelogs/4.1.0.md @@ -0,0 +1,29 @@ +# LilyBot 4.1.0 + +This release tracks down many old bugs, fixes a lot of errors, and adds a nice new feature or two. +You can find the full changelog below. + +New: +* message edit logging +* add an announcements system to allow distribution of messages across every guild Lily is in +* command to completely reset the database for a guild +* command to view set configs +* log when message tags are sent + +Change: +* require only the bare minimum config for each feature, + with additional functionality coming with additional configs +* re-add member counts to member logging +* images can now be attached to commands directly rather than providing a link +* check that Lily can view and send messages in configured channels +* check that roles can be pinged before adding them to the database + +Fix: +* "required content missing" errors in log uploading +* broken reminder interval field +* missing parameter on log uploading command +* log channel name instead of enabled or disabled when setting a utility log +* inconsistent timestamps when editing messages sent through Lily +* pk;e being logged as a deleted message + +You can find a list of all the commits in this update [here](https://github.com/hyacinthbots/LilyBot/compare/v4.0.1...v4.1.0) diff --git a/docs/changelogs/changelog-template.md b/docs/changelogs/changelog-template.md new file mode 100644 index 00000000..aa412992 --- /dev/null +++ b/docs/changelogs/changelog-template.md @@ -0,0 +1,46 @@ +# LilyBot X.X.X + +Throughout this file, examples are placed in code blocks. + +X.X.X above should be replaced with the version number where +* breaking versions are something that requires a significant change in the environment to run Lily +* major versions have a fair few features, changes, and fixes +* minor versions are simply a patch + +Next should be a description of the features of the update. +``` +This long-awaited update rewrites Lily in Rust. This will be the last update, as the bot is now perfect. +``` + +It's important that this end with the following statement. +``` +You can find the full changelog below. +``` + +The changelog should then be split into three categories: New, Change, & Fix. +These are fairly self-explanatory buckets, with new features going in the first, +changes to existing functionality going in the second, and restorations of intended functionality in the third. + +``` +New: +* very memory safe +* lots of crabs + +Change: +* use rust instead of Kotlin +* auto-ban anyone who says the word Kotlin + +Fix: +* literally every bug, Rust is perfect +* we've even fixed bugs Discord hasn't thought of +``` + +The changelog should then end with the following +``` +You can find a list of all the commits in this update [here](https://github.com/hyacinthbots/LilyBot/compare/vP.P.P...vX.X.X) +``` +where P.P.P is replaced with the previous version number and X.X.X is the new version number. + +The changelog should be copied and pasted into the GitHub release, excepting the header. +This changelog can then be trimmed or adjusted if necessary for publication on Discord. +If need be, a more concise version can be sent out via the Lily's announcement system. diff --git a/docs/commanddocs.toml b/docs/commanddocs.toml index 91b732ad..e94a2655 100644 --- a/docs/commanddocs.toml +++ b/docs/commanddocs.toml @@ -37,6 +37,12 @@ name = "config clear" args = "* `config-type` - The type of config to clear, 'support', 'moderation', 'logging', 'miscellaneous', 'all' - String Choice" result = "Clears the config of the specified type." permissions = "Manage Guild" + +[[command]] +category = "Administration commands" +name = "announcement" +result = "Produces a modal for inputting the announcement content, then sends it to every guild the bot is in. Only works in the bots `TEST_GUILD_ID`" +permissions = "Administrator" # End administration commands # Moderation commands @@ -54,35 +60,35 @@ permissions = "Manage Messages" [[command]] category = "Moderation commands" name = "ban" -args = "* `user` – Person to ban - User\n* `messages` - Number of days of messages to delete - Integer\n* `reason` - Reason for the ban - Optional String\n* `image` - The URL to an image to provide extra context for the action - Optional String\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" +args = "* `user` – Person to ban - User\n* `messages` - Number of days of messages to delete - Integer\n* `reason` - Reason for the ban - Optional String\n* `image` - An image to provide extra context for the action - Optional Attachment\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" result = "Bans `banUser` from the server with reason `reason` and deletes any messages they sent in the last `messages` day(s)." permissions = "Ban Members" [[command]] category = "Moderation commands" name = "unban" -args = "* `user ` - The Discord ID of the person to unban - User ID" +args = "* `user ` - The Discord ID (Snowflake) of the person to unban - User ID" result = "The user with the ID `unbanUserId` is unbanned." permissions = "Ban Members" [[command]] category = "Moderation commands" name = "soft-ban" -args = "* `user` - Person to soft ban - User\n* `messages` - Number of days of messages to delete - Integer (default 3)\n* `reason` - Reason for the ban - Optional String\n* `image` - The URL to an image to provide extra context for the action - Optional String\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" +args = "* `user` - Person to soft ban - User\n* `messages` - Number of days of messages to delete - Integer (default 3)\n* `reason` - Reason for the ban - Optional String\n* `image` - An image to provide extra context for the action - Optional Attachment\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" result = "Bans `softBanUser`, deletes the last `messages` days of messages from them, and unbans them." permissions = "Ban Members" [[command]] category = "Moderation commands" name = "warn" -args = "* `user` - Person to warn - User\n* `reason` - Reason for warn - Optional String\n* `image` - The URL to an image to provide extra context for the action - Optional String\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" +args = "* `user` - Person to warn - User\n* `reason` - Reason for warn - Optional String\n* `image` - An image to provide extra context for the action - Optional Attachment\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" result = "Warns `warnUser` with a DM and adds a strike to their points total. Depending on their new points total, action is taken based on the below table.\n\n| Points | Sanction |\n|:------:|:----------------:|\n| 1 | None. |\n| 2 | 3 hour timeout. |\n| 3 | 12 hour timeout. |\n| 3+ | 3 day timeout. |" permissions = "Moderate Members" [[command]] category = "Moderation commands" name = "timeout" -args = "* `user` - Person to timeout - User\n* `duration` - Duration of timeout - Duration [e.g. 6h or 30s] (default 6h)\n* `reason` - Reason for timeout - Optional String\n* `image` - The URL to an image to provide extra context for the action - Optional String\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" +args = "* `user` - Person to timeout - User\n* `duration` - Duration of timeout - Duration [e.g. 6h or 30s] (default 6h)\n* `reason` - Reason for timeout - Optional String\n* `image` - An image to provide extra context for the action - Optional Attachment\n* `dm` - Whether to DM the user or not. Default: True - Optional Boolean" result = "Times `timeoutUser` out for `duration`. A timeout is Discord's built-in mute function." permissions = "Moderate Members" @@ -156,7 +162,7 @@ permissions = "Moderate Members" [[command]] category = "Utility commands" name = "edit-say" -args = "(Moderators only)\n* `message-to-edit` - The ID of the message contain the embed you'd like to edit - Snowflake\n* `new-content` - The new content for the message - Optional String\n* `new-color` - The new color for the embed - Optional Color (default: Blurple)\n* `channel-of-message` - The channel the embed was originally sent in - Optional channel (default: Channel command was executed in)\n* `timestamp` - Whether to add the timestamp of when the message was originally sent or not - Optional boolean (default: true)" +args = "(Moderators only)\n* `message-to-edit` - The ID of the message contain the embed you'd like to edit - Snowflake\n* `new-content` - The new content for the message - Optional String\n* `new-color` - The new color for the embed - Optional Color (default: Blurple)\n* `channel-of-message` - The channel the embed was originally sent in - Optional channel (default: Channel command was executed in)\n* `timestamp` - Whether to add the timestamp of when the message was originally sent or not - Optional boolean" result = "Edited message/embed" permissions = "Moderate Members" diff --git a/docs/commands.md b/docs/commands.md index c2bfb272..ed2300b6 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -78,6 +78,18 @@ These are commands for the maintenance of LilyBot. The can only be run by Server --- +### Name: `announcement` +**Arguments**: +None + +**Result**: Produces a modal for inputting the announcement content, then sends it to every guild the bot is in. Only works in the bots `TEST_GUILD_ID` + +**Required Permissions**: `Administrator` + +**Command category**: `Administration commands` + +--- + ## Moderation commands These commands are for use by moderators. They utilize built-in permission checks. All moderation commands are logged to the modActionLog established in the config. A Direct Message is sent to the target user containing the sanction they received and the provided reason. If Lily fails to DM them, this failure will be noted in the logging embed. @@ -99,7 +111,7 @@ These commands are for use by moderators. They utilize built-in permission check * `user` – Person to ban - User * `messages` - Number of days of messages to delete - Integer * `reason` - Reason for the ban - Optional String -* `image` - The URL to an image to provide extra context for the action - Optional String +* `image` - An image to provide extra context for the action - Optional Attachment * `dm` - Whether to DM the user or not. Default: True - Optional Boolean **Result**: Bans `banUser` from the server with reason `reason` and deletes any messages they sent in the last `messages` day(s). @@ -112,7 +124,7 @@ These commands are for use by moderators. They utilize built-in permission check ### Name: `unban` **Arguments**: -* `user ` - The Discord ID of the person to unban - User ID +* `user ` - The Discord ID (Snowflake) of the person to unban - User ID **Result**: The user with the ID `unbanUserId` is unbanned. @@ -127,7 +139,7 @@ These commands are for use by moderators. They utilize built-in permission check * `user` - Person to soft ban - User * `messages` - Number of days of messages to delete - Integer (default 3) * `reason` - Reason for the ban - Optional String -* `image` - The URL to an image to provide extra context for the action - Optional String +* `image` - An image to provide extra context for the action - Optional Attachment * `dm` - Whether to DM the user or not. Default: True - Optional Boolean **Result**: Bans `softBanUser`, deletes the last `messages` days of messages from them, and unbans them. @@ -142,7 +154,7 @@ These commands are for use by moderators. They utilize built-in permission check **Arguments**: * `user` - Person to warn - User * `reason` - Reason for warn - Optional String -* `image` - The URL to an image to provide extra context for the action - Optional String +* `image` - An image to provide extra context for the action - Optional Attachment * `dm` - Whether to DM the user or not. Default: True - Optional Boolean **Result**: Warns `warnUser` with a DM and adds a strike to their points total. Depending on their new points total, action is taken based on the below table. @@ -165,7 +177,7 @@ These commands are for use by moderators. They utilize built-in permission check * `user` - Person to timeout - User * `duration` - Duration of timeout - Duration [e.g. 6h or 30s] (default 6h) * `reason` - Reason for timeout - Optional String -* `image` - The URL to an image to provide extra context for the action - Optional String +* `image` - An image to provide extra context for the action - Optional Attachment * `dm` - Whether to DM the user or not. Default: True - Optional Boolean **Result**: Times `timeoutUser` out for `duration`. A timeout is Discord's built-in mute function. @@ -306,7 +318,7 @@ None * `new-content` - The new content for the message - Optional String * `new-color` - The new color for the embed - Optional Color (default: Blurple) * `channel-of-message` - The channel the embed was originally sent in - Optional channel (default: Channel command was executed in) -* `timestamp` - Whether to add the timestamp of when the message was originally sent or not - Optional boolean (default: true) +* `timestamp` - Whether to add the timestamp of when the message was originally sent or not - Optional boolean **Result**: Edited message/embed diff --git a/libs.versions.toml b/libs.versions.toml index 85241182..c63d5b72 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -1,12 +1,12 @@ [versions] kotlin = "1.7.10" # Note: Plugin versions must be updated in the settings.gradle.kts too -groovy = "3.0.12" -kord-extensions = "1.5.5-20220831.110202-26" -logging = "2.1.23" +groovy = "3.0.13" +kord-extensions = "1.5.5-20220925.092000-32" +logging = "2.1.23" # Cannot be updated to 3.0.0 because we need newer logback, which requires an XML file, which throws errors I cannot fix logback = "1.2.8" -github-api = "1.308" -kmongo = "4.7.0" +github-api = "1.313" +kmongo = "4.7.1" detekt = "1.21.0" koma = "1.1.0" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt index 8cfd87b9..7c096bf0 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt @@ -18,12 +18,14 @@ import org.hyacinthbots.lilybot.extensions.config.GuildLogging import org.hyacinthbots.lilybot.extensions.events.LogUploading import org.hyacinthbots.lilybot.extensions.events.MemberLogging import org.hyacinthbots.lilybot.extensions.events.MessageDelete +import org.hyacinthbots.lilybot.extensions.events.MessageEdit import org.hyacinthbots.lilybot.extensions.events.ThreadInviter import org.hyacinthbots.lilybot.extensions.moderation.Report import org.hyacinthbots.lilybot.extensions.moderation.TemporaryModeration import org.hyacinthbots.lilybot.extensions.moderation.TerminalModeration import org.hyacinthbots.lilybot.extensions.util.GalleryChannel import org.hyacinthbots.lilybot.extensions.util.Github +import org.hyacinthbots.lilybot.extensions.util.GuildAnnouncements import org.hyacinthbots.lilybot.extensions.util.InfoCommands import org.hyacinthbots.lilybot.extensions.util.ModUtilities import org.hyacinthbots.lilybot.extensions.util.PublicUtilities @@ -76,10 +78,12 @@ suspend fun main() { add(::Github) add(::GalleryChannel) add(::InfoCommands) + add(::GuildAnnouncements) add(::GuildLogging) add(::LogUploading) add(::MemberLogging) add(::MessageDelete) + add(::MessageEdit) add(::ModUtilities) add(::PublicUtilities) add(::Reminders) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt index 12540107..5ed9a97f 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt @@ -2,15 +2,19 @@ package org.hyacinthbots.lilybot.database import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.core.Kord +import dev.kord.core.behavior.getChannelOf import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.rest.request.KtorRequestException import kotlinx.datetime.Clock import mu.KotlinLogging +import org.hyacinthbots.lilybot.database.Cleanups.cleanupGuildData +import org.hyacinthbots.lilybot.database.Cleanups.cleanupThreadData import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.RoleMenuCollection import org.hyacinthbots.lilybot.database.collections.SupportConfigCollection import org.hyacinthbots.lilybot.database.collections.TagsCollection +import org.hyacinthbots.lilybot.database.collections.ThreadsCollection import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection import org.hyacinthbots.lilybot.database.collections.WarnCollection import org.hyacinthbots.lilybot.database.entities.GuildLeaveTimeData @@ -82,15 +86,15 @@ object Cleanups : KordExKoinComponent { var deletedThreads = 0 for (it in threads) { try { - val thread = kordInstance.getChannelOf(it.threadId) ?: continue + val thread = kordInstance.getGuild(it.guildId!!)?.getChannelOf(it.threadId) ?: continue val latestMessage = thread.getLastMessage() ?: continue val timeSinceLatestMessage = Clock.System.now() - latestMessage.id.timestamp if (timeSinceLatestMessage.inWholeDays > 7) { - threadDataCollection.deleteOne(ThreadData::threadId eq thread.id) + ThreadsCollection().removeThread(thread.id) deletedThreads++ } } catch (e: KtorRequestException) { - threadDataCollection.deleteOne(ThreadData::threadId eq it.threadId) + ThreadsCollection().removeThread(it.threadId) deletedThreads++ continue } 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 41f369f1..63b5475e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt @@ -58,4 +58,14 @@ class GalleryChannelCollection : KordExKoinComponent { GalleryChannelData::channelId eq inputChannelId, GalleryChannelData::guildId eq inputGuildId ) + + /** + * Removes all gallery channels from this guild. + * + * @param inputGuildId The guild to clear the gallery channels from + * @author NoComment1105 + * @since 4.1.0 + */ + suspend inline fun removeAll(inputGuildId: Snowflake) = + collection.deleteMany(GalleryChannelData::guildId eq inputGuildId) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LogUploadingBlacklistCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LogUploadingBlacklistCollection.kt index 826c98b5..fb92c58c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LogUploadingBlacklistCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LogUploadingBlacklistCollection.kt @@ -69,4 +69,14 @@ class LogUploadingBlacklistCollection : KordExKoinComponent { */ suspend inline fun getLogUploadingBlacklist(inputGuildId: Snowflake): List = collection.find(LogUploadingBlacklistData::guildId eq inputGuildId).toList() + + /** + * Removes all data of the log upload blacklist for a given guild. + * + * @param inputGuildId The guild to clear the data from + * @author NoComment1105 + * @since 4.1.0 + */ + suspend inline fun clearBlacklist(inputGuildId: Snowflake) = + collection.deleteMany(LogUploadingBlacklistData::guildId eq inputGuildId) } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RemindMeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RemindMeCollection.kt index 3e3678bb..158aec7c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RemindMeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RemindMeCollection.kt @@ -106,4 +106,15 @@ class RemindMeCollection : KordExKoinComponent { RemindMeData::userId eq inputUserId, RemindMeData::id eq id ) + + /** + * Removes all reminders associated with a guild. + * + * @param inputGuildId The guild to remove reminders for + * + * @author NoComment1105 + * @since 4.1.0 + */ + suspend inline fun removeGuildReminders(inputGuildId: Snowflake) = + collection.deleteMany(RemindMeData::guildId eq 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 070f7b2b..c4f3adb8 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt @@ -70,12 +70,13 @@ class ThreadsCollection : KordExKoinComponent { * @since 3.2.0 */ suspend inline fun setThreadOwner( + inputGuildId: Snowflake?, inputThreadId: Snowflake, newOwnerId: Snowflake, preventArchiving: Boolean = false ) { collection.deleteOne(ThreadData::threadId eq inputThreadId) - collection.insertOne(ThreadData(inputThreadId, newOwnerId, preventArchiving)) + collection.insertOne(ThreadData(inputGuildId, inputThreadId, newOwnerId, preventArchiving)) } /** @@ -88,4 +89,15 @@ class ThreadsCollection : KordExKoinComponent { */ suspend inline fun removeThread(inputThreadId: Snowflake) = collection.deleteOne(ThreadData::threadId eq inputThreadId) + + /** + * This function deletes the ownership data stored in database for the given [inputGuildId]. + * + * @param inputGuildId The ID of the guild whose threads to delete + * + * @author NoComment1105 + * @since 4.1.0 + */ + suspend inline fun removeGuildThreads(inputGuildId: Snowflake) = + collection.deleteMany(ThreadData::guildId eq inputGuildId) } 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 96471b2e..9df1e8da 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt @@ -7,17 +7,19 @@ import kotlinx.serialization.Serializable * The data for moderation configuration. The logging config stores where logs are sent to, and whether to enable or * disable certain configurations. * - * @param guildId The ID of the guild the config is for - * @param enableMessageLogs If edited and deleted messages should be logged - * @param messageChannel The channel to send message logs to - * @param enableMemberLogs If users joining or leaving the guild should be logged - * @param memberLog The channel to send member logs to + * @property guildId The ID of the guild the config is for + * @property enableMessageDeleteLogs If deleted messages should be logged + * @property enableMessageEditLogs If edited messages should be logged + * @property messageChannel The channel to send message logs to + * @property enableMemberLogs If users joining or leaving the guild should be logged + * @property memberLog The channel to send member logs to * @since 4.0.0 */ @Serializable data class LoggingConfigData( val guildId: Snowflake, - val enableMessageLogs: Boolean, + val enableMessageDeleteLogs: Boolean, + val enableMessageEditLogs: Boolean, val messageChannel: Snowflake?, val enableMemberLogs: Boolean, val memberLog: Snowflake?, @@ -27,10 +29,10 @@ data class LoggingConfigData( * The data for moderation configuration. The moderation config is what stores the data for moderation actions. The * channel for logging and the team for pinging. * - * @param guildId The ID of the guild the config is for - * @param enabled If the support module is enabled or not - * @param channel The ID of the action log for the guild - * @param role The ID of the moderation role for the guild + * @property guildId The ID of the guild the config is for + * @property enabled If the support module is enabled or not + * @property channel The ID of the action log for the guild + * @property role The ID of the moderation role for the guild * @since 4.0.0 */ @Serializable @@ -46,11 +48,11 @@ data class ModerationConfigData( * The data for support configuration. The support config stores the data for support functionality. Channel for the * place to create threads to and team for pinging into support threads. * - * @param guildId The ID of the guild the config is for - * @param enabled If the support module is enabled or not - * @param channel The ID of the support channel for the guild - * @param role The ID of the support team for the guild - * @param message The support message as a string, nullable + * @property guildId The ID of the guild the config is for + * @property enabled If the support module is enabled or not + * @property channel The ID of the support channel for the guild + * @property role The ID of the support team for the guild + * @property message The support message as a string, nullable * @since 4.0.0 */ @Serializable @@ -66,9 +68,9 @@ data class SupportConfigData( * The data for miscellaneous configuration. The miscellaneous config stores the data for enabling or disabling log * uploading. * - * @param guildId The ID of the guild the config is for - * @param disableLogUploading If log uploading is enabled or not - * @param utilityLogChannel The channel to log various utility actions too + * @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 */ @Serializable 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 6a79d23d..5c7944c8 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GalleryChannelData.kt @@ -6,8 +6,8 @@ import kotlinx.serialization.Serializable /** * The data for image channels in a guild. * - * @param guildId The ID of the guild the image channel is for - * @param channelId The ID of the image channel being set + * @property guildId The ID of the guild the image channel is for + * @property channelId The ID of the image channel being set * @since 3.3.0 */ @Serializable 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 14819679..53cf35c2 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/GuildLeaveTimeData.kt @@ -7,8 +7,8 @@ import kotlinx.serialization.Serializable /** * The data for when Lily leaves a guild. * - * @param guildId The ID of the guild Lily left - * @param guildLeaveTime The [Instant] that Lily left the guild + * @property guildId The ID of the guild Lily left + * @property guildLeaveTime The [Instant] that Lily left the guild * @since 3.2.0 */ @Serializable diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RemindMeData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RemindMeData.kt index e2b7e7c6..65bf89f5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RemindMeData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RemindMeData.kt @@ -8,15 +8,15 @@ import kotlinx.serialization.Serializable /** * The data for reminders set by users. * - * @param initialSetTime The time the reminder was set - * @param guildId The ID of the guild the reminder was set in - * @param userId The ID of the user that would like to be reminded - * @param channelId The ID of the channel the reminder was set in - * @param remindTime The time the user would like to be reminded at - * @param originalMessageUrl The URL to the original message that set the reminder - * @param customMessage A custom message to attach to the reminder - * @param repeating Whether the reminder should repeat - * @param id The numerical ID of the reminder + * @property initialSetTime The time the reminder was set + * @property guildId The ID of the guild the reminder was set in + * @property userId The ID of the user that would like to be reminded + * @property channelId The ID of the channel the reminder was set in + * @property remindTime The time the user would like to be reminded at + * @property originalMessageUrl The URL to the original message that set the reminder + * @property customMessage A custom message to attach to the reminder + * @property repeating Whether the reminder should repeat + * @property id The numerical ID of the reminder * * @since 3.3.2 */ 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 bd423f9d..0673bd2e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/RoleMenuData.kt @@ -6,10 +6,10 @@ import kotlinx.serialization.Serializable /** * The data for role menus. * - * @param messageId The ID of the message of the role menu - * @param channelId The ID of the channel the role menu is in - * @param guildId The ID of the guild the role menu is in - * @param roles A [MutableList] of the role IDs associated with this role menu. + * @property messageId The ID of the message of the role menu + * @property channelId The ID of the channel the role menu is in + * @property guildId The ID of the guild the role menu is in + * @property roles A [MutableList] of the role IDs associated with this role menu. * @since 3.4.0 */ @Serializable 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 0d0aac95..090244d0 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/StatusData.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable /** * The data for the bot status. * - * @param status The string value that will be seen in the bots presence + * @property status The string value that will be seen in the bots presence * @since 3.0.0 */ @Serializable 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 9a113c05..78b00ffb 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TagsData.kt @@ -6,11 +6,11 @@ import kotlinx.serialization.Serializable /** * The data of guild tags, which are stored in the database. * - * @param guildId The ID of the guild the tag will be saved for - * @param name The named identifier of the tag - * @param tagTitle The title of the created tag - * @param tagValue The value of the created tag - * @param tagAppearance The appearance of the created tag + * @property guildId The ID of the guild the tag will be saved for + * @property name The named identifier of the tag + * @property tagTitle The title of the created tag + * @property tagValue The value of the created tag + * @property tagAppearance The appearance of the created tag * @since 3.1.0 */ @Serializable 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 16860ebb..6baa6a86 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ThreadData.kt @@ -6,14 +6,16 @@ import kotlinx.serialization.Serializable /** * The data for threads. * - * @param threadId The ID of the thread - * @param ownerId The ID of the thread's owner - * @param preventArchiving Whether to stop the thread from being archived or not + * @property guildId The ID of the guild this thread is in + * @property threadId The ID of the thread + * @property ownerId The ID of the thread's owner + * @property preventArchiving Whether to stop the thread from being archived or not * @since 3.2.0 */ @Suppress("DataClassShouldBeImmutable") @Serializable data class ThreadData( + val guildId: Snowflake?, // TODO make not nullable after migration val threadId: Snowflake, val ownerId: Snowflake, var preventArchiving: Boolean = false 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 5491161e..db5a79be 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/WarnData.kt @@ -6,9 +6,9 @@ import kotlinx.serialization.Serializable /** * The data for warnings in guilds. *. - * @param userId The ID of the user with warnings - * @param guildId The ID of the guild they received the warning in - * @param strikes The amount of strikes they have received + * @property userId The ID of the user with warnings + * @property guildId The ID of the guild they received the warning in + * @property strikes The amount of strikes they have received * @since 3.0.0 */ @Serializable 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 c07e440e..0de1bb9c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt @@ -18,6 +18,7 @@ import org.hyacinthbots.lilybot.database.entities.ConfigMetaData import org.hyacinthbots.lilybot.database.entities.MainMetaData import org.hyacinthbots.lilybot.database.migrations.config.configV1 import org.hyacinthbots.lilybot.database.migrations.main.mainV1 +import org.hyacinthbots.lilybot.database.migrations.main.mainV2 import org.koin.core.component.inject object Migrator : KordExKoinComponent { @@ -50,6 +51,7 @@ object Migrator : KordExKoinComponent { @Suppress("UseIfInsteadOfWhen") when (nextVersion) { 1 -> ::mainV1 + 2 -> ::mainV2 else -> break }(db.mainDatabase) @@ -75,6 +77,9 @@ object Migrator : KordExKoinComponent { suspend fun migrateConfig() { logger.info { "Starting config database migration" } + // TODO Remove this line once the migration is done because nc is an absolute clown and got versions out of sync + db.configDatabase.dropCollection("configMetaData") + // ^ var meta = configMetaCollection.get() if (meta == null) { 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 8ac635f1..d63e0460 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,8 +1,20 @@ package org.hyacinthbots.lilybot.database.migrations.config +import org.hyacinthbots.lilybot.database.entities.LoggingConfigData import org.litote.kmongo.coroutine.CoroutineDatabase +import org.litote.kmongo.exists +import org.litote.kmongo.setValue -@Suppress("UnusedPrivateMember") suspend fun configV1(configDb: CoroutineDatabase) { - // Empty until required + with(configDb.getCollection("loggingConfigData")) { + updateMany( + LoggingConfigData::enableMessageEditLogs exists false, + setValue(LoggingConfigData::enableMessageEditLogs, false) + ) + } + + configDb.getCollection("loggingConfigData").updateMany( + "{}", + "{\$rename: {enableMessageLogs: \"enableMessageDeleteLogs\"}}" + ) } 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 new file mode 100644 index 00000000..cb8682fc --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV2.kt @@ -0,0 +1,12 @@ +package org.hyacinthbots.lilybot.database.migrations.main + +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)) + } +} 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 c64c1b9d..2adaf3f6 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt @@ -17,22 +17,22 @@ import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashComman import com.kotlindiscord.kord.extensions.modules.unsafe.types.ackEphemeral import com.kotlindiscord.kord.extensions.modules.unsafe.types.respondEphemeral import com.kotlindiscord.kord.extensions.types.respond +import com.kotlindiscord.kord.extensions.utils.botHasPermissions import com.kotlindiscord.kord.extensions.utils.waitFor import dev.kord.common.entity.Permission -import dev.kord.common.entity.Snowflake import dev.kord.common.entity.TextInputStyle -import dev.kord.core.behavior.GuildBehavior import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.getChannelOfOrNull import dev.kord.core.behavior.interaction.modal import dev.kord.core.behavior.interaction.respondEphemeral -import dev.kord.core.behavior.interaction.response.FollowupPermittingInteractionResponseBehavior import dev.kord.core.behavior.interaction.response.createEphemeralFollowup import dev.kord.core.behavior.interaction.response.respond -import dev.kord.core.entity.channel.GuildMessageChannel +import dev.kord.core.entity.channel.TextChannel import dev.kord.core.event.interaction.ModalSubmitInteractionCreateEvent import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.builder.message.create.embed import dev.kord.rest.builder.message.modify.embed +import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.SupportConfigCollection @@ -41,7 +41,7 @@ import org.hyacinthbots.lilybot.database.entities.LoggingConfigData import org.hyacinthbots.lilybot.database.entities.ModerationConfigData import org.hyacinthbots.lilybot.database.entities.SupportConfigData import org.hyacinthbots.lilybot.database.entities.UtilityConfigData -import org.hyacinthbots.lilybot.utils.getFirstUsableChannel +import org.hyacinthbots.lilybot.utils.canPingRole import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms import kotlin.time.Duration.Companion.seconds @@ -75,12 +75,36 @@ suspend fun Config.configCommand() = unsafeSlashCommand { if (supportConfig != null) { ackEphemeral() respondEphemeral { - content = "You already have a moderation configuration set. " + + content = "You already have a support configuration set. " + "Please clear it before attempting to set a new one." } return@action } + if (canPingRole(arguments.role)) { + ackEphemeral() + respondEphemeral { + content = + "I cannot use the role: ${arguments.role!!.mention}, because it is not mentionable by" + + "regular users. Please enable this in the role settings, or use a different role." + } + return@action + } + + var supportChannel: TextChannel? = null + if (arguments.enable && arguments.channel != null) { + supportChannel = guild!!.getChannelOfOrNull(arguments.channel!!.id) + if (supportChannel?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + ackEphemeral() + respondEphemeral { + content = "The mod action log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + supportChannel ?: return@action + suspend fun EmbedBuilder.supportEmbed() { title = "Configuration: Support" field { @@ -163,31 +187,23 @@ suspend fun Config.configCommand() = unsafeSlashCommand { ) } - if (ModerationConfigCollection().getConfig(guild!!.id) == null) { - getLoggingChannelWithPerms( - guild!!.asGuild(), - guild!!.asGuild().getSystemChannel()?.id ?: getFirstUsableChannel(guild!!.asGuild())!!.id, - ConfigType.MODERATION, - interactionResponse - ) - } else { - getLoggingChannelWithPerms( - guild!!.asGuild(), - ModerationConfigCollection().getConfig(guild!!.id)!!.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - }?.createMessage { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + + if (utilityLog == null) { + ackEphemeral() + respondEphemeral { + content = "Consider setting a utility config to log changes to configurations." + } + return@action + } + + utilityLog.createMessage { embed { supportEmbed() field { name = "Message" value = SupportConfigCollection().getConfig(guild!!.id)?.message ?: "default" } - ModerationConfigCollection().getConfig(guild!!.id) ?: run { - description = "Consider setting the moderation configuration to receive configuration " + - "updates where you want them!" - } } } } @@ -223,6 +239,28 @@ suspend fun Config.configCommand() = unsafeSlashCommand { return@action } + if (canPingRole(arguments.moderatorRole)) { + respond { + content = + "I cannot use the role: ${arguments.moderatorRole!!.mention}, because it is not mentionable by" + + "regular users. Please enable this in the role settings, or use a different role." + } + return@action + } + + var modActionLog: TextChannel? = null + if (arguments.enabled && arguments.modActionLog != null) { + modActionLog = guild!!.getChannelOfOrNull(arguments.modActionLog!!.id) + if (modActionLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + respond { + content = "The mod action log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + modActionLog ?: return@action + suspend fun EmbedBuilder.moderationEmbed() { title = "Configuration: Moderation" field { @@ -252,15 +290,6 @@ suspend fun Config.configCommand() = unsafeSlashCommand { } } - if (getLoggingChannelWithPerms( - guild!!.asGuild(), - arguments.modActionLog?.id, - ConfigType.MODERATION - )?.id != arguments.modActionLog?.id - ) { - return@action - } - ModerationConfigCollection().setConfig( ModerationConfigData( guild!!.id, @@ -271,11 +300,16 @@ suspend fun Config.configCommand() = unsafeSlashCommand { ) ) - checkChannel( - guild, - arguments.modActionLog?.id, - interactionResponse - )?.createMessage { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + + if (utilityLog == null) { + respond { + content = "Consider setting a utility config to log changes to configurations." + } + return@action + } + + utilityLog.createMessage { embed { moderationEmbed() } @@ -305,16 +339,50 @@ suspend fun Config.configCommand() = unsafeSlashCommand { if (arguments.enableMemberLogging && arguments.memberLog == null) { respond { content = "You must specify a channel to log members joining and leaving to!" } return@action - } else if (arguments.enableMessageLogs && arguments.messageLogs == null) { - respond { content = "You must specify a channel to log deleted messages to!" } + } else if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && arguments.messageLogs == null) { + respond { content = "You must specify a channel to log deleted/edited messages to!" } return@action } + var memberLog: TextChannel? = null + if (arguments.enableMemberLogging && arguments.memberLog != null) { + memberLog = guild!!.getChannelOfOrNull(arguments.memberLog!!.id) + if (memberLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + respond { + content = "The member log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + memberLog ?: return@action + + var messageLog: TextChannel? = null + if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && arguments.memberLog != null) { + messageLog = guild!!.getChannelOfOrNull(arguments.messageLogs!!.id) + if (messageLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + respond { + content = "The message log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + messageLog ?: return@action + suspend fun EmbedBuilder.loggingEmbed() { title = "Configuration: Logging" field { - name = "Message Logs" - value = if (arguments.enableMessageLogs && arguments.messageLogs?.mention != null) { + name = "Message Delete Logs" + value = if (arguments.enableMessageDeleteLogs && arguments.messageLogs?.mention != null) { + arguments.messageLogs!!.mention + } else { + "Disabled" + } + } + field { + name = "Message Edit Logs" + value = if (arguments.enableMessageEditLogs && arguments.messageLogs?.mention != null) { arguments.messageLogs!!.mention } else { "Disabled" @@ -343,24 +411,26 @@ suspend fun Config.configCommand() = unsafeSlashCommand { LoggingConfigCollection().setConfig( LoggingConfigData( guild!!.id, - arguments.enableMessageLogs, + arguments.enableMessageDeleteLogs, + arguments.enableMessageEditLogs, arguments.messageLogs?.id, arguments.enableMemberLogging, arguments.memberLog?.id ) ) - checkChannel( - guild, - ModerationConfigCollection().getConfig(guild!!.id)?.channel, - interactionResponse - )?.createMessage { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + + if (utilityLog == null) { + respond { + content = "Consider setting a utility config to log changes to configurations." + } + return@action + } + + utilityLog.createMessage { embed { loggingEmbed() - ModerationConfigCollection().getConfig(guild!!.id) ?: run { - description = "Consider setting the moderation configuration to receive configuration " + - "updates where you want them!" - } } } } @@ -386,6 +456,19 @@ suspend fun Config.configCommand() = unsafeSlashCommand { return@action } + var utilityLog: TextChannel? = null + if (arguments.utilityLogChannel != null) { + utilityLog = guild!!.getChannelOfOrNull(arguments.utilityLogChannel!!.id) + if (utilityLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + respond { + content = "The utility log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + utilityLog ?: return@action + suspend fun EmbedBuilder.utilityEmbed() { title = "Configuration: Utility" field { @@ -399,7 +482,7 @@ suspend fun Config.configCommand() = unsafeSlashCommand { field { name = "Utility Log" value = if (arguments.utilityLogChannel != null) { - "Enabled" + "${arguments.utilityLogChannel!!.mention} ${arguments.utilityLogChannel!!.data.name.value}" } else { "Disabled" } @@ -425,17 +508,9 @@ suspend fun Config.configCommand() = unsafeSlashCommand { ) ) - checkChannel( - guild, - ModerationConfigCollection().getConfig(guild!!.id)?.channel, - interactionResponse - )?.createMessage { + utilityLog.createMessage { embed { utilityEmbed() - ModerationConfigCollection().getConfig(guild!!.id) ?: run { - description = "Consider setting the moderation configuration to receive configuration " + - "updates where you want them!" - } } } } @@ -452,17 +527,20 @@ suspend fun Config.configCommand() = unsafeSlashCommand { action { suspend fun logClear() { - checkChannel( - guild, - ModerationConfigCollection().getConfig(guild!!.id)?.channel, - interactionResponse - )?.createMessage { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + + if (utilityLog == null) { + respond { + content = "Consider setting a utility config to log changes to configurations." + } + return + } + + utilityLog.createMessage { embed { - title = "Configuration Cleared: ${arguments.config}" - ModerationConfigCollection().getConfig(guild!!.id) ?: run { - description = "Consider setting the moderation configuration to receive configuration " + - "updates where you want them!" - } + title = "Configuration Cleared: ${arguments.config[0]}${ + arguments.config.substring(1, arguments.config.length).lowercase() + }" footer { text = "Config cleared by ${user.asUser().tag}" icon = user.asUser().avatar?.url @@ -578,6 +656,172 @@ suspend fun Config.configCommand() = unsafeSlashCommand { } } } + + ephemeralSubCommand(::ViewArgs) { + name = "view" + description = "View the current config that you have set" + + check { + anyGuild() + hasPermission(Permission.ManageGuild) + } + + action { + when (arguments.config) { + ConfigType.MODERATION.name -> { + val config = ModerationConfigCollection().getConfig(guild!!.id) + if (config == null) { + respond { + content = "There is no moderation config for this guild" + } + return@action + } + + respond { + embed { + title = "Current moderation config" + description = "This is the current moderation config for this guild" + field { + name = "Enabled/Disabled" + value = if (config.enabled) "Enabled" else "Disabled" + } + field { + name = "Moderators" + value = config.role?.let { guild!!.getRoleOrNull(it)?.mention } ?: "Disabled" + } + field { + name = "Action log" + value = config.channel?.let { guild!!.getChannelOrNull(it)?.mention } ?: "Disabled" + } + field { + name = "Log publicly" + value = when (config.publicLogging) { + true -> "True" + false -> "Disabled" + null -> "Disabled" + } + } + timestamp = Clock.System.now() + } + } + } + + ConfigType.LOGGING.name -> { + val config = LoggingConfigCollection().getConfig(guild!!.id) + if (config == null) { + respond { + content = "There is no logging config for this guild" + } + return@action + } + + respond { + embed { + title = "Current logging config" + description = "This is the current logging config for this guild" + field { + name = "Message delete logs" + value = if (config.enableMessageDeleteLogs) { + "Enabled\n" + + "${guild!!.getChannel(config.messageChannel!!).mention} " + + "${guild!!.getChannel(config.messageChannel).name }}" + } else { + "Disabled" + } + } + field { + name = "Message edit logs" + value = if (config.enableMessageEditLogs) { + "Enabled\n" + + "${guild!!.getChannel(config.messageChannel!!).mention }} " + + "${guild!!.getChannel(config.messageChannel).name }}" + } else { + "Disabled" + } + } + field { + name = "Member logs" + value = if (config.enableMemberLogs) { + "Enabled\n" + + "${guild!!.getChannel(config.memberLog!!).mention }} " + + "${guild!!.getChannel(config.memberLog).name }} " + } else { + "Disabled" + } + } + timestamp = Clock.System.now() + } + } + } + + ConfigType.SUPPORT.name -> { + val config = SupportConfigCollection().getConfig(guild!!.id) + if (config == null) { + respond { + content = "There is no support config for this guild" + } + return@action + } + + respond { + embed { + title = "Current support config" + description = "This is the current support config for this guild" + field { + name = "Enabled/Disabled" + value = if (config.enabled) "Enabled" else "Disabled" + } + field { + name = "Channel" + value = "${config.channel?.let { guild!!.getChannelOrNull(it)?.mention }} " + + "${config.channel?.let { guild!!.getChannelOrNull(it)?.name }}" + } + field { + name = "Role" + value = "${config.role?.let { guild!!.getRoleOrNull(it)?.mention }} " + + "${config.role?.let { guild!!.getRoleOrNull(it)?.name }}" + } + field { + name = "Custom message" + value = + if (config.message != null) "${config.message.substring(0, 500)} ..." else "Default" + } + timestamp = Clock.System.now() + } + } + } + + ConfigType.UTILITY.name -> { + val config = UtilityConfigCollection().getConfig(guild!!.id) + if (config == null) { + respond { + content = "There is no utility config for this guild" + } + return@action + } + + respond { + embed { + title = "Current utility config" + description = "This is the current utility config for this guild" + field { + name = "Log uploading" + value = if (config.disableLogUploading) "Disabled" else "Enabled" + } + field { + name = "Channel" + value = + "${ + config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.mention } ?: "None" + } ${config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.name } ?: ""}" + } + timestamp = Clock.System.now() + } + } + } + } + } + } } class SupportArgs : Arguments() { @@ -625,11 +869,16 @@ class ModerationArgs : Arguments() { } class LoggingArgs : Arguments() { - val enableMessageLogs by boolean { - name = "enable-message-logs" + val enableMessageDeleteLogs by boolean { + name = "enable-delete-logs" description = "Enable logging of message deletions" } + val enableMessageEditLogs by boolean { + name = "enable-edit-logs" + description = "Enable logging of message edits" + } + val enableMemberLogging by boolean { name = "enable-member-logging" description = "Enable logging of members joining and leaving the guild" @@ -671,39 +920,15 @@ class ClearArgs : Arguments() { } } -/** - * Checks the moderation config and returns where the message needs to be sent. - * - * @param guild The guild the event is in - * @param channelIdToCheck The id of the channel to check - * @param interactionResponse The response for the interaction - * @return the channel to send the message to - * @since 4.0.0 - * @author NoComment - */ -suspend inline fun checkChannel( - guild: GuildBehavior?, - channelIdToCheck: Snowflake?, - interactionResponse: FollowupPermittingInteractionResponseBehavior -): GuildMessageChannel? { - val toReturn: GuildMessageChannel? - if (ModerationConfigCollection().getConfig(guild!!.id) == null || - !ModerationConfigCollection().getConfig(guild.id)!!.enabled || - channelIdToCheck == null - ) { - toReturn = getLoggingChannelWithPerms( - guild.asGuild(), - guild.asGuild().getSystemChannel()?.id ?: getFirstUsableChannel(guild.asGuild())!!.id, - ConfigType.MODERATION, - interactionResponse - ) - } else { - toReturn = getLoggingChannelWithPerms( - guild.asGuild(), - channelIdToCheck, - ConfigType.MODERATION, - interactionResponse +class ViewArgs : Arguments() { + val config by stringChoice { + name = "config-type" + description = "The type of config to clear" + choices = mutableMapOf( + "support" to ConfigType.SUPPORT.name, + "moderation" to ConfigType.MODERATION.name, + "logging" to ConfigType.LOGGING.name, + "utility" to ConfigType.UTILITY.name, ) } - return toReturn } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigOptions.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigOptions.kt index b4584f2d..c2ca7044 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigOptions.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigOptions.kt @@ -27,8 +27,11 @@ enum class ConfigOptions { /** The option that stores whether to log a moderation action publicly. */ LOG_PUBLICLY, - /** The option that stores whether the logging config is enabled or not. */ - MESSAGE_LOGGING_ENABLED, + /** The option that stores whether message delete logging is enabled. */ + MESSAGE_DELETE_LOGGING_ENABLED, + + /** The option that stores whether message edit logging is enabled. */ + MESSAGE_EDIT_LOGGING_ENABLED, /** The options that stores the message logging channel. */ MESSAGE_LOG, diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/LogUploading.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/LogUploading.kt index c15d2263..ffcf7bd5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/LogUploading.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/LogUploading.kt @@ -44,12 +44,14 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.hyacinthbots.lilybot.database.collections.LogUploadingBlacklistCollection -import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.SupportConfigCollection import org.hyacinthbots.lilybot.database.collections.ThreadsCollection +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection +import org.hyacinthbots.lilybot.database.entities.SupportConfigData import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.botHasChannelPerms -import org.hyacinthbots.lilybot.utils.configPresent +import org.hyacinthbots.lilybot.utils.configIsUsable +import org.hyacinthbots.lilybot.utils.requiredConfigs import java.io.ByteArrayInputStream import java.io.IOException import java.util.zip.GZIPInputStream @@ -81,11 +83,7 @@ class LogUploading : Extension() { event.message.author.isNullOrBot() event.message.getChannelOrNull() !is MessageChannel } - configPresent( - ConfigOptions.SUPPORT_ENABLED, - ConfigOptions.SUPPORT_CHANNEL, - ConfigOptions.LOG_UPLOADS_ENABLED - ) + requiredConfigs(ConfigOptions.LOG_UPLOADS_ENABLED) // I hate NullPointerExceptions. This is to prevent a null pointer exception if the message is a Pk one. if (channelFor(event) == null) return@check @@ -100,10 +98,15 @@ class LogUploading : Extension() { return@action } - val supportConfig = SupportConfigCollection().getConfig(guildFor(event)!!.id)!! var deferUploadUntilThread = false - if (supportConfig.enabled && event.message.channel.id == supportConfig.channel) { - deferUploadUntilThread = true + var supportConfig: SupportConfigData? = null + if (configIsUsable(ConfigOptions.SUPPORT_ENABLED, event.guildId!!) && + configIsUsable(ConfigOptions.SUPPORT_CHANNEL, event.guildId!!) + ) { + supportConfig = SupportConfigCollection().getConfig(guildFor(event)!!.id)!! + if (supportConfig.enabled && event.message.channel.id == supportConfig.channel) { + deferUploadUntilThread = true + } } val eventMessage = event.message.asMessageOrNull() // Get the message @@ -114,7 +117,7 @@ class LogUploading : Extension() { delay(4.seconds) // Delay to allow for thread creation ThreadsCollection().getOwnerThreads(eventMember!!.id).forEach { if (event.getGuild().getChannelOf(it.threadId).parentId == - supportConfig.channel + supportConfig?.channel ) { uploadChannel = event.getGuild().getChannelOf(it.threadId) return@forEach @@ -131,23 +134,21 @@ class LogUploading : Extension() { if (attachmentFileExtension in logFileExtensions) { val logBytes = attachment.download() - val builder = StringBuilder() - - if (attachmentFileExtension != "gz") { + val logContent: String = if (attachmentFileExtension != "gz") { // If the file is not a gz log, we just decode it - builder.append(logBytes.decodeToString()) + logBytes.decodeToString() } else { // If the file is a gz log, we convert it to a byte array, // and unzip it val bis = ByteArrayInputStream(logBytes) val gis = GZIPInputStream(bis) - builder.append(String(gis.readAllBytes())) + gis.readAllBytes().decodeToString() } // Ask the user to remove NEC to ease the debugging on the support team val necText = "at Not Enough Crashes" - val indexOfNECText = builder.indexOf(necText) + val indexOfNECText = logContent.indexOf(necText) if (indexOfNECText != -1) { uploadChannel.createEmbed { title = "Not Enough Crashes detected in logs" @@ -203,7 +204,7 @@ class LogUploading : Extension() { } try { - val response = postToMCLogs(builder.toString()) + val response = postToMCLogs(logContent) uploadMessage.edit { embed { @@ -281,7 +282,6 @@ class LogUploading : Extension() { check { anyGuild() - configPresent(ConfigOptions.ACTION_LOG) hasPermission(Permission.ModerateMembers) } @@ -291,7 +291,7 @@ class LogUploading : Extension() { action { val blacklist = LogUploadingBlacklistCollection().isChannelInUploadBlacklist(guild!!.id, channel.id) - val config = ModerationConfigCollection().getConfig(guild!!.id)!! + val utilityConfig = UtilityConfigCollection().getConfig(guild!!.id)!! if (blacklist != null) { respond { @@ -306,15 +306,15 @@ class LogUploading : Extension() { content = "Log uploading is now blocked in this channel!" } - guild!!.getChannelOf(config.channel!!).createMessage { - embed { - title = "Log uploading disabled" - description = "Log uploading was disabled in ${channel.mention}" - color = DISCORD_RED - footer { - text = "Disabled by ${user.asUser().tag}" - icon = user.asUser().avatar?.url - } + if (!configIsUsable(ConfigOptions.UTILITY_LOG, guild!!.id)) return@action + + guild!!.getChannelOf(utilityConfig.utilityLogChannel!!).createEmbed { + title = "Log uploading disabled" + description = "Log uploading was disabled in ${channel.mention}" + color = DISCORD_RED + footer { + text = "Disabled by ${user.asUser().tag}" + icon = user.asUser().avatar?.url } } } @@ -332,7 +332,7 @@ class LogUploading : Extension() { return@action } - val config = ModerationConfigCollection().getConfig(guild!!.id)!! + val utilityConfig = UtilityConfigCollection().getConfig(guild!!.id)!! LogUploadingBlacklistCollection().removeLogUploadingBlacklist(guild!!.id, channel.id) @@ -340,15 +340,15 @@ class LogUploading : Extension() { content = "Log uploading is no longer blocked in this channel!" } - guild!!.getChannelOf(config.channel!!).createMessage { - embed { - title = "Log uploading re-enabled" - description = "Log uploading was re-enabled in ${channel.mention}" - color = DISCORD_GREEN - footer { - text = "Enabled by ${user.asUser().tag}" - icon = user.asUser().avatar?.url - } + if (!configIsUsable(ConfigOptions.UTILITY_LOG, guild!!.id)) return@action + + guild!!.getChannelOf(utilityConfig.utilityLogChannel!!).createEmbed { + title = "Log uploading re-enabled" + description = "Log uploading was re-enabled in ${channel.mention}" + color = DISCORD_GREEN + footer { + text = "Enabled by ${user.asUser().tag}" + icon = user.asUser().avatar?.url } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt index ad6a902c..d4785c0e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt @@ -9,11 +9,10 @@ import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.event.guild.MemberJoinEvent import dev.kord.core.event.guild.MemberLeaveEvent import kotlinx.datetime.Clock -import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.getMemberCount +import org.hyacinthbots.lilybot.utils.requiredConfigs /** * Logs members joining and leaving a guild to the member log channel designated in the config for that guild. @@ -29,13 +28,11 @@ class MemberLogging : Extension() { event { check { anyGuild() - configPresent(ConfigOptions.MEMBER_LOGGING_ENABLED, ConfigOptions.MEMBER_LOG) + requiredConfigs(ConfigOptions.MEMBER_LOGGING_ENABLED, ConfigOptions.MEMBER_LOG) failIf { event.member.id == kord.selfId } } action { - val config = LoggingConfigCollection().getConfig(event.guildId)!! - val memberLog = getLoggingChannelWithPerms(event.getGuild(), config.memberLog!!, ConfigType.LOGGING) - ?: return@action + val memberLog = getLoggingChannelWithPerms(ConfigOptions.MEMBER_LOG, event.guild) ?: return@action memberLog.createEmbed { author { @@ -52,6 +49,9 @@ class MemberLogging : Extension() { value = event.member.id.toString() inline = false } + footer { + text = "Member Count: ${getMemberCount(event.guildId)}" + } timestamp = Clock.System.now() color = DISCORD_GREEN } @@ -62,13 +62,11 @@ class MemberLogging : Extension() { event { check { anyGuild() - configPresent(ConfigOptions.MEMBER_LOGGING_ENABLED, ConfigOptions.MEMBER_LOG) + requiredConfigs(ConfigOptions.MEMBER_LOGGING_ENABLED, ConfigOptions.MEMBER_LOG) failIf { event.user.id == kord.selfId } } action { - val config = LoggingConfigCollection().getConfig(event.guildId)!! - val memberLog = getLoggingChannelWithPerms(event.getGuild(), config.memberLog!!, ConfigType.LOGGING) - ?: return@action + val memberLog = getLoggingChannelWithPerms(ConfigOptions.MEMBER_LOG, event.guild) ?: return@action memberLog.createEmbed { author { @@ -83,7 +81,9 @@ class MemberLogging : Extension() { field { name = "ID:" value = event.user.id.toString() - inline = false + } + footer { + text = "Member Count: ${getMemberCount(event.guildId)}" } timestamp = Clock.System.now() color = DISCORD_RED diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt index 82f3f1d5..1167a6e2 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt @@ -4,21 +4,20 @@ import com.kotlindiscord.kord.extensions.DISCORD_PINK import com.kotlindiscord.kord.extensions.checks.anyGuild import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.event +import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api.PKMessage import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.ProxiedMessageDeleteEvent import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.UnProxiedMessageDeleteEvent import dev.kord.core.behavior.channel.asChannelOf -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.behavior.getChannelOf -import dev.kord.core.entity.Attachment +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.entity.Message import dev.kord.core.entity.channel.GuildMessageChannel -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.create.embed import kotlinx.datetime.Clock -import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType -import org.hyacinthbots.lilybot.utils.configPresent +import org.hyacinthbots.lilybot.utils.attachmentsAndProxiedMessageInfo import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.ifNullOrEmpty +import org.hyacinthbots.lilybot.utils.requiredConfigs +import org.hyacinthbots.lilybot.utils.trimmedContents /** * The class for logging deletion of messages to the guild message log. @@ -30,76 +29,33 @@ class MessageDelete : Extension() { override suspend fun setup() { /** - * Logs deleted messages in a guild to the message log channel designated in the config for that guild + * Logs proxied deleted messages in a guild to the message log channel designated in the config for that guild * @author NoComment1105 - * @since 2.0 + * @see onMessageDelete */ event { check { anyGuild() - configPresent(ConfigOptions.MESSAGE_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) + requiredConfigs(ConfigOptions.MESSAGE_DELETE_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) failIf { event.message?.author?.id == kord.selfId } } action { - val config = LoggingConfigCollection().getConfig(event.getGuild().id) ?: return@action - val messageLog = - getLoggingChannelWithPerms(event.getGuild(), config.messageChannel!!, ConfigType.LOGGING) - ?: return@action - - val originalMessage = event.message - val proxiedMessage = event.pkMessage - val messageContent = if (originalMessage?.asMessageOrNull() != null) { - if (originalMessage.asMessageOrNull().content.length > 1024) { - originalMessage.asMessageOrNull().content.substring(0, 1024) + "..." - } else { - originalMessage.asMessageOrNull().content - } - } else { - null - } - val messageLocation = event.pkMessage.channel - val attachments = event.message?.attachments - val images: MutableSet = mutableSetOf() - attachments?.forEach { if (it.isImage) images += it } - - messageLog.createMessage { - embed { - color = DISCORD_PINK - author { - name = "Message deleted" - icon = proxiedMessage.member.avatarUrl - } - description = - "Location: ${event.getGuild().getChannelOf(messageLocation).mention}" + - "(${event.getGuild().getChannelOf(messageLocation).name})" - timestamp = Clock.System.now() - - fields(messageContent, attachments) - - field { - name = "Message Author:" - value = "System Member: ${proxiedMessage.member.name}\n" + - "Account: ${event.getGuild().getMember(proxiedMessage.sender).tag} " + - event.getGuild().getMember(proxiedMessage.sender).mention - inline = true - } - - field { - name = "Author ID:" - value = proxiedMessage.sender.toString() - } - } - } + onMessageDelete(event.getMessage(), event.pkMessage) } } + /** + * Logs unproxied deleted messages in a guild to the message log channel designated in the config for that guild. + * @author NoComment1105 + * @see onMessageDelete + */ event { check { anyGuild() - configPresent(ConfigOptions.MESSAGE_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) + requiredConfigs(ConfigOptions.MESSAGE_DELETE_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) failIf { event.message?.author?.id == kord.selfId || event.message?.author?.isBot == true @@ -107,94 +63,44 @@ class MessageDelete : Extension() { } action { - val config = LoggingConfigCollection().getConfig(event.getGuild().id) ?: return@action - - val message = event.message - - val messageLog = - getLoggingChannelWithPerms(event.getGuild(), config.messageChannel!!, ConfigType.LOGGING) - ?: return@action - - val messageContent = if (message?.asMessageOrNull() != null) { - if (message.asMessageOrNull().content.length > 1024) { - message.asMessageOrNull().content.substring(0, 1024) + "..." - } else { - message.asMessageOrNull().content - } - } else { - null - } - - message ?: return@action - - val messageLocation = event.channel.asChannelOf() - val attachments = event.message?.attachments - val images: MutableSet = mutableSetOf() - attachments?.forEach { if (it.isImage) images += it } - - messageLog.createMessage { - embed { - color = DISCORD_PINK - author { - name = "Message deleted" - icon = message.author?.avatar?.url - } - description = - "Location: ${messageLocation.mention}" + - "(${messageLocation.name})" - timestamp = Clock.System.now() - - fields(messageContent, attachments) - - field { - name = "Message Author:" - value = - "${message.author?.tag ?: "Failed to get author of message"} ${message.author?.mention ?: ""}" - inline = true - } - - field { - name = "Author ID:" - value = message.author?.id.toString() - } - } - } + onMessageDelete(event.getMessage(), null) } } } -} -/** - * Adds the common fields to a deleted message embed. - * - * @param messageContent The content of the message. - * @param attachments The attachments of the message. - * - * @author NoComment1105 - * @since 3.6.0 - */ -private fun EmbedBuilder.fields(messageContent: String?, attachments: Set?) { - field { - name = "Message contents" - value = - if (messageContent.isNullOrEmpty()) { - "Failed to retrieve message contents" - } else { - messageContent - } - inline = false - } - if (!attachments.isNullOrEmpty()) { - val attachmentUrls = StringBuilder() - attachments.forEach { - attachmentUrls.append( - it.url + "\n" - ) + /** + * If message logging is enabled, sends an embed describing the message deletion to the guild's message log channel. + * + * @param message The deleted message + * @param proxiedMessage Extra data for PluralKit proxied messages + * @author trainb0y + */ + private suspend fun onMessageDelete(message: Message, proxiedMessage: PKMessage?) { + val guild = message.getGuild() + + if (message.content.startsWith("pk;e", 0, true)) { + return } - field { - name = "Attachments" - value = attachmentUrls.trim().toString() - inline = false + + val messageLog = getLoggingChannelWithPerms(ConfigOptions.MESSAGE_LOG, guild) ?: return + + messageLog.createEmbed { + author { + name = "Message deleted" + icon = proxiedMessage?.member?.avatarUrl ?: message.author?.avatar?.url + } + description = + "Location: ${message.channel.mention} " + + "(${message.channel.asChannelOf().name})" + color = DISCORD_PINK + timestamp = Clock.System.now() + + field { + name = "Message contents" + value = message.trimmedContents().ifNullOrEmpty { "Failed to retrieve previous message contents" } + inline = false + } + attachmentsAndProxiedMessageInfo(guild, message, proxiedMessage) } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt new file mode 100644 index 00000000..24f97278 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt @@ -0,0 +1,104 @@ +package org.hyacinthbots.lilybot.extensions.events + +import com.kotlindiscord.kord.extensions.DISCORD_YELLOW +import com.kotlindiscord.kord.extensions.checks.anyGuild +import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.extensions.event +import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api.PKMessage +import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.ProxiedMessageUpdateEvent +import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.UnProxiedMessageUpdateEvent +import dev.kord.core.behavior.channel.asChannelOf +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.entity.Message +import dev.kord.core.entity.channel.GuildMessageChannel +import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.utils.attachmentsAndProxiedMessageInfo +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.ifNullOrEmpty +import org.hyacinthbots.lilybot.utils.requiredConfigs +import org.hyacinthbots.lilybot.utils.trimmedContents + +/** + * The class for logging editing of messages to the guild message log. + * @since 4.1.0 + */ +class MessageEdit : Extension() { + override val name = "message-edit" + + override suspend fun setup() { + /** + * Logs edited messages to the message log channel. + * @see onMessageEdit + * @author trainb0y + */ + event { + check { + anyGuild() + requiredConfigs(ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) + failIf { + event.message.asMessage().author?.id == kord.selfId + } + } + action { + onMessageEdit(event.getMessage(), event.old, null) + } + } + + /** + * Logs proxied edited messages to the message log channel. + * @see onMessageEdit + * @author trainb0y + */ + event { + check { + anyGuild() + requiredConfigs(ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) + failIf { + event.message.asMessage().author?.id == kord.selfId + } + } + action { + onMessageEdit(event.getMessage(), event.old, event.pkMessage) + } + } + } + + /** + * If message logging is enabled, sends an embed describing the message edit to the guild's message log channel. + * + * @param message The current message + * @param old The original message + * @param proxiedMessage Extra data for PluralKit proxied messages + * @author trainb0y + */ + private suspend fun onMessageEdit(message: Message, old: Message?, proxiedMessage: PKMessage?) { + val guild = message.getGuild() + + val messageLog = getLoggingChannelWithPerms(ConfigOptions.MEMBER_LOG, guild) ?: return + + messageLog.createEmbed { + color = DISCORD_YELLOW + author { + name = "Message Edited" + icon = proxiedMessage?.member?.avatarUrl ?: message.author?.avatar?.url + } + description = + "Location: ${message.channel.mention} " + + "(${message.channel.asChannelOf().name})" + timestamp = Clock.System.now() + + field { + name = "Previous contents" + value = old?.trimmedContents().ifNullOrEmpty { "Failed to retrieve previous message contents" } + inline = false + } + field { + name = "New contents" + value = message.trimmedContents().ifNullOrEmpty { "Failed to retrieve new message contents" } + inline = false + } + attachmentsAndProxiedMessageInfo(guild, message, proxiedMessage) + } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ThreadInviter.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ThreadInviter.kt index 40f13203..d1e81bc7 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ThreadInviter.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ThreadInviter.kt @@ -36,14 +36,17 @@ import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.SupportConfigCollection import org.hyacinthbots.lilybot.database.collections.ThreadsCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.requiredConfigs import kotlin.time.Duration.Companion.seconds +// todo This is rewritten in another branch, but said branch should take care of making sure that the target roles are +// ping able by Lily. Should/Could be done in a similar fashion to the method in extensions/config/Config.kt class ThreadInviter : Extension() { override val name = "thread-inviter" + // note: the requireConfigs checks in this file are not perfect, + // but will be fully replaced with the thread inviting rewrite so it's ok override suspend fun setup() { /** * Thread inviting system for Support Channels @@ -61,7 +64,7 @@ class ThreadInviter : Extension() { */ check { anyGuild() - configPresent(ConfigOptions.SUPPORT_ENABLED, ConfigOptions.SUPPORT_CHANNEL, ConfigOptions.SUPPORT_ROLE) + requiredConfigs(ConfigOptions.SUPPORT_ENABLED, ConfigOptions.SUPPORT_CHANNEL, ConfigOptions.SUPPORT_ROLE) failIf { event.message.type == MessageType.ChatInputCommand || event.message.type == MessageType.ThreadCreated || @@ -91,8 +94,7 @@ class ThreadInviter : Extension() { return@action } - val supportChannel = - getLoggingChannelWithPerms(event.getGuild(), config.channel, ConfigType.SUPPORT) ?: return@action + val supportChannel = getLoggingChannelWithPerms(ConfigOptions.SUPPORT_CHANNEL, guild) ?: return@action if (textChannel != supportChannel) return@action @@ -128,7 +130,7 @@ class ThreadInviter : Extension() { event.message.getChannel().data.defaultAutoArchiveDuration.value ?: ArchiveDuration.Day ) - ThreadsCollection().setThreadOwner(thread.id, userId) + ThreadsCollection().setThreadOwner(guild.id, thread.id, userId) val startMessage = thread.createMessage("Welcome to your support thread! Let me grab the support team...") @@ -169,7 +171,7 @@ class ThreadInviter : Extension() { */ check { anyGuild() - configPresent(ConfigOptions.SUPPORT_ENABLED, ConfigOptions.SUPPORT_CHANNEL, ConfigOptions.SUPPORT_ROLE) + requiredConfigs(ConfigOptions.SUPPORT_ENABLED, ConfigOptions.SUPPORT_CHANNEL, ConfigOptions.SUPPORT_ROLE) failIf { event.message.type == MessageType.ChatInputCommand || event.message.type == MessageType.ThreadCreated || @@ -193,9 +195,8 @@ class ThreadInviter : Extension() { var existingUserThread: TextChannelThread? = null val textChannel = event.message.getChannel().asChannelOf() val guild = event.getGuild() - val supportChannel = - getLoggingChannelWithPerms(event.getGuild(), config.channel!!, ConfigType.SUPPORT) - ?: return@action + + val supportChannel = getLoggingChannelWithPerms(ConfigOptions.SUPPORT_CHANNEL, guild) ?: return@action if (textChannel != supportChannel) return@action @@ -234,7 +235,7 @@ class ThreadInviter : Extension() { event.message.getChannel().data.defaultAutoArchiveDuration.value ?: ArchiveDuration.Day ) - ThreadsCollection().setThreadOwner(thread.id, userId) + ThreadsCollection().setThreadOwner(guild.id, thread.id, userId) val startMessage = thread.createMessage("Welcome to your support thread! Let me grab the support team...") @@ -279,7 +280,7 @@ class ThreadInviter : Extension() { event.channel.ownerId == kord.selfId || event.channel.member != null } - configPresent( + requiredConfigs( ConfigOptions.SUPPORT_ENABLED, ConfigOptions.SUPPORT_CHANNEL, ConfigOptions.SUPPORT_ROLE, @@ -293,7 +294,7 @@ class ThreadInviter : Extension() { val modRole = event.channel.guild.getRole(moderationConfig.role!!) val threadOwner = event.channel.owner.asUser() - ThreadsCollection().setThreadOwner(event.channel.id, threadOwner.id) + ThreadsCollection().setThreadOwner(event.channel.guildId, event.channel.id, threadOwner.id) if (supportConfig.enabled && event.channel.parentId == supportConfig.channel) { val supportRole = event.channel.guild.getRole(supportConfig.role!!) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt index 129c1b35..c21e30b7 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt @@ -27,7 +27,6 @@ import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.getChannelOf import dev.kord.core.behavior.interaction.ModalParentInteractionBehavior import dev.kord.core.behavior.interaction.modal -import dev.kord.core.behavior.interaction.response.DeferredEphemeralMessageInteractionResponseBehavior import dev.kord.core.behavior.interaction.response.createEphemeralFollowup import dev.kord.core.behavior.interaction.response.edit import dev.kord.core.behavior.interaction.response.respond @@ -41,9 +40,9 @@ import dev.kord.rest.builder.message.create.embed import dev.kord.rest.request.KtorRequestException import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection -import org.hyacinthbots.lilybot.database.entities.ModerationConfigData import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.utils.configPresent +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.requiredConfigs import kotlin.time.Duration.Companion.seconds /** @@ -74,12 +73,10 @@ suspend inline fun Report.reportMessageCommand() = unsafeMessageCommand { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) } action { - val moderationConfig = ModerationConfigCollection().getConfig(guild!!.id)!! - val modLog = guild?.getChannelOf(moderationConfig.channel!!) val reportedMessage: Message try { @@ -106,11 +103,13 @@ suspend inline fun Report.reportMessageCommand() = unsafeMessageCommand { return@action } + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action + val config = ModerationConfigCollection().getConfig(guild!!.id) ?: return@action createReportModal( event.interaction as ModalParentInteractionBehavior, user, - moderationConfig, - modLog, + config.role!!, + actionLog, reportedMessage, ) } @@ -132,7 +131,7 @@ suspend inline fun Report.reportSlashCommand() = unsafeSlashCommand(::ManualRepo check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) } action { @@ -180,7 +179,7 @@ suspend inline fun Report.reportSlashCommand() = unsafeSlashCommand(::ManualRepo createReportModal( event.interaction as ModalParentInteractionBehavior, user, - moderationConfig, + moderationConfig.role!!, modLog, reportedMessage, ) @@ -192,7 +191,7 @@ suspend inline fun Report.reportSlashCommand() = unsafeSlashCommand(::ManualRepo * * @param inputInteraction The interaction to create a modal in response to * @param user The user who created the [inputInteraction] - * @param config The configuration from the database for the guild + * @param moderatorRoleId The ID of the configured moderator role for the guild * @param modLog The channel for the guild that deleted messages are logged to * @param reportedMessage The message that was reported * @author tempest15 @@ -201,7 +200,7 @@ suspend inline fun Report.reportSlashCommand() = unsafeSlashCommand(::ManualRepo suspend fun createReportModal( inputInteraction: ModalParentInteractionBehavior, user: UserBehavior, - config: ModerationConfigData, + moderatorRoleId: Snowflake, modLog: GuildMessageChannel?, reportedMessage: Message, ) { @@ -229,36 +228,6 @@ suspend fun createReportModal( val reason = interaction.textInputs["reason"]!!.value!! val modalResponse = interaction.deferEphemeralResponse() - createReport( - user, - modLog, - reportedMessage, - config.role!!, - reason, - modalResponse - ) -} - -/** - * Create an embed in the [modLog] for moderators to respond to with appropriate action. - * - * @param user The user that reported the message - * @param modLog The channel to send the report embed to - * @param reportedMessage The message being reported - * @param moderatorRole The role to ping when a report is submitted - * @param reportReason The reason provided from the modal for the report - * @param modalResponse The modal interaction for the message - * @author MissCorruption - * @since 2.0 - */ -private suspend inline fun createReport( - user: UserBehavior, - modLog: GuildMessageChannel?, - reportedMessage: Message, - moderatorRole: Snowflake, - reportReason: String?, - modalResponse: DeferredEphemeralMessageInteractionResponseBehavior -) { var reportResponse: EphemeralMessageInteractionResponse? = null reportResponse = modalResponse.respond { @@ -274,7 +243,7 @@ private suspend inline fun createReport( content = "Message reported to staff" components { removeAll() } - modLog?.createMessage { content = "<@&$moderatorRole>" } + modLog?.createMessage { content = "<@&$moderatorRoleId>" } modLog?.createMessage { embed { @@ -295,7 +264,7 @@ private suspend inline fun createReport( } field { name = "Report reason:" - value = reportReason ?: "No reason provided" + value = reason } footer { text = "Reported by: ${user.asUser().tag}" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TemporaryModeration.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TemporaryModeration.kt index 7e7f30e9..16a9a833 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TemporaryModeration.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TemporaryModeration.kt @@ -11,8 +11,8 @@ import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingDefa 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.optionalChannel -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString import com.kotlindiscord.kord.extensions.commands.converters.impl.user import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand @@ -34,7 +34,6 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.channel.TextChannel import dev.kord.core.entity.channel.thread.TextChannelThread 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.map @@ -46,13 +45,12 @@ import kotlinx.datetime.plus import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.WarnCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType import org.hyacinthbots.lilybot.utils.baseModerationEmbed import org.hyacinthbots.lilybot.utils.botHasChannelPerms -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.dmNotificationStatusEmbedField import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms import org.hyacinthbots.lilybot.utils.isBotOrModerator +import org.hyacinthbots.lilybot.utils.requiredConfigs import java.lang.Integer.min import kotlin.time.Duration @@ -76,7 +74,7 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ManageMessages) requireBotPermissions(Permission.ManageMessages) botHasChannelPerms(Permissions(Permission.ManageMessages)) @@ -84,15 +82,6 @@ class TemporaryModeration : Extension() { action { val config = ModerationConfigCollection().getConfig(guild!!.id)!! - - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val messageAmount = arguments.messages val textChannel = channel.asChannelOf() @@ -107,6 +96,14 @@ class TemporaryModeration : Extension() { content = "Messages cleared." } + if (config.publicLogging != null && config.publicLogging == true) { + channel.createEmbed { + title = "$messageAmount messages have been cleared." + color = DISCORD_BLACK + } + } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "$messageAmount messages have been cleared." description = "Action occurred in ${textChannel.mention}" @@ -116,13 +113,6 @@ class TemporaryModeration : Extension() { } color = DISCORD_BLACK } - - if (config.publicLogging != null && config.publicLogging == true) { - channel.createEmbed { - title = "$messageAmount messages have been cleared." - color = DISCORD_BLACK - } - } } } @@ -137,21 +127,15 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + // todo this code doesn't actually hard require action log and needs a refactor to make it optional + requiredConfigs(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ModerateMembers) } action { val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action val userArg = arguments.userArgument isBotOrModerator(userArg, "warn") ?: return@action @@ -263,24 +247,18 @@ class TemporaryModeration : Extension() { } } - val embed = EmbedBuilder() - embed.color = DISCORD_BLACK - embed.title = "Warning" - embed.image = arguments.image - embed.baseModerationEmbed(arguments.reason, userArg, user) - embed.dmNotificationStatusEmbedField(arguments.dm, dm) - embed.timestamp = Clock.System.now() - embed.field { - name = "Total Strikes:" - value = newStrikes.toString() - inline = false - } - - try { - actionLog.createMessage { embeds.add(embed) } - } catch (e: KtorRequestException) { - embed.image = null - actionLog.createMessage { embeds.add(embed) } + actionLog.createMessage { + embed { + title = "Warning" + image = arguments.image?.url + baseModerationEmbed(arguments.reason, userArg, user) + dmNotificationStatusEmbedField(arguments.dm, dm) + timestamp = Clock.System.now() + field { + name = "Total strikes" + value = newStrikes.toString() + } + } } if (config.publicLogging != null && config.publicLogging == true) { @@ -305,23 +283,13 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ModerateMembers) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument - val targetUser = guild?.getMember(userArg.id) val userStrikes = WarnCollection().getWarn(targetUser!!.id, guild!!.id)?.strikes if (userStrikes == 0 || userStrikes == null) { @@ -349,6 +317,7 @@ class TemporaryModeration : Extension() { } } + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Warning Removal" color = DISCORD_BLACK @@ -377,21 +346,12 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ModerateMembers) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument val duration = Clock.System.now().plus(arguments.duration, TimeZone.UTC) @@ -428,27 +388,7 @@ class TemporaryModeration : Extension() { content = "Timed out ${userArg.id}" } - val embed = EmbedBuilder() - embed.color = DISCORD_BLACK - embed.title = "Timeout" - embed.image = arguments.image - embed.baseModerationEmbed(arguments.reason, userArg, user) - embed.dmNotificationStatusEmbedField(arguments.dm, dm) - embed.timestamp = Clock.System.now() - embed.field { - name = "Duration:" - value = duration.toDiscord(TimestampType.Default) + " (" + arguments.duration.toString() - .replace("PT", "") + ")" - inline = false - } - - try { - actionLog.createMessage { embeds.add(embed) } - } catch (e: KtorRequestException) { - embed.image = null - actionLog.createMessage { embeds.add(embed) } - } - + val config = ModerationConfigCollection().getConfig(guild!!.id)!! if (config.publicLogging != null && config.publicLogging == true) { channel.createEmbed { title = "Timeout" @@ -462,6 +402,21 @@ class TemporaryModeration : Extension() { } } } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action + actionLog.createEmbed { + title = "Timeout" + image = arguments.image?.url + baseModerationEmbed(arguments.reason, userArg, user) + dmNotificationStatusEmbedField(arguments.dm, dm) + timestamp = Clock.System.now() + field { + name = "Duration:" + value = duration.toDiscord(TimestampType.Default) + " (" + arguments.duration.toString() + .replace("PT", "") + ")" + inline = false + } + } } } @@ -477,21 +432,12 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ModerateMembers) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument // Set timeout to null, or no timeout @@ -503,6 +449,7 @@ class TemporaryModeration : Extension() { content = "Removed timeout on ${userArg.id}" } + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Timeout Removed" field { @@ -536,10 +483,8 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent( - ConfigOptions.MODERATION_ENABLED, - ConfigOptions.MODERATOR_ROLE, - ConfigOptions.ACTION_LOG + requiredConfigs( + ConfigOptions.MODERATION_ENABLED ) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ManageChannels) @@ -548,16 +493,6 @@ class TemporaryModeration : Extension() { @Suppress("DuplicatedCode") action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action - val channelArg = arguments.channel ?: event.interaction.getChannel() var channelParent: TextChannel? = null if (channelArg is TextChannelThread) { @@ -584,6 +519,9 @@ class TemporaryModeration : Extension() { denied += Permission.UseApplicationCommands } + respond { content = "${targetChannel.mention} has been locked." } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Channel Locked" description = "${targetChannel.mention} has been locked.\n\n**Reason:** ${arguments.reason}" @@ -594,8 +532,6 @@ class TemporaryModeration : Extension() { timestamp = Clock.System.now() color = DISCORD_RED } - - respond { content = "${targetChannel.mention} has been locked." } } } @@ -605,25 +541,14 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent( - ConfigOptions.MODERATION_ENABLED, - ConfigOptions.MODERATOR_ROLE, - ConfigOptions.ACTION_LOG + requiredConfigs( + ConfigOptions.MODERATION_ENABLED ) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ManageChannels) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val everyoneRole = guild!!.getRole(guild!!.id) if (!everyoneRole.permissions.contains(Permission.SendMessages)) { @@ -639,6 +564,9 @@ class TemporaryModeration : Extension() { .minus(Permission.UseApplicationCommands) } + respond { content = "Server locked." } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Server locked" description = "**Reason:** ${arguments.reason}" @@ -649,8 +577,6 @@ class TemporaryModeration : Extension() { timestamp = Clock.System.now() color = DISCORD_RED } - - respond { content = "Server locked." } } } } @@ -671,10 +597,8 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent( - ConfigOptions.MODERATION_ENABLED, - ConfigOptions.MODERATOR_ROLE, - ConfigOptions.ACTION_LOG + requiredConfigs( + ConfigOptions.MODERATION_ENABLED ) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ManageChannels) @@ -683,16 +607,6 @@ class TemporaryModeration : Extension() { @Suppress("DuplicatedCode") action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action - val channelArg = arguments.channel ?: event.interaction.getChannel() var channelParent: TextChannel? = null if (channelArg is TextChannelThread) { @@ -724,6 +638,9 @@ class TemporaryModeration : Extension() { color = DISCORD_GREEN } + respond { content = "${targetChannel.mention} has been unlocked." } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Channel Unlocked" description = "${targetChannel.mention} has been unlocked." @@ -734,8 +651,6 @@ class TemporaryModeration : Extension() { timestamp = Clock.System.now() color = DISCORD_GREEN } - - respond { content = "${targetChannel.mention} has been unlocked." } } } @@ -745,25 +660,14 @@ class TemporaryModeration : Extension() { check { anyGuild() - configPresent( - ConfigOptions.MODERATION_ENABLED, - ConfigOptions.MODERATOR_ROLE, - ConfigOptions.ACTION_LOG + requiredConfigs( + ConfigOptions.MODERATION_ENABLED ) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.ManageChannels) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val everyoneRole = guild!!.getRole(guild!!.id) if (everyoneRole.permissions.contains(Permission.SendMessages)) { @@ -779,6 +683,9 @@ class TemporaryModeration : Extension() { .plus(Permission.UseApplicationCommands) } + respond { content = "Server unlocked." } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Server unlocked" footer { @@ -788,8 +695,6 @@ class TemporaryModeration : Extension() { timestamp = Clock.System.now() color = DISCORD_GREEN } - - respond { content = "Server unlocked." } } } } @@ -825,9 +730,9 @@ class TemporaryModeration : Extension() { } /** An image that the user wishes to provide for context to the kick. */ - val image by optionalString { + val image by optionalAttachment { name = "image" - description = "The URL to an image you'd like to provide as extra context for the action" + description = "An image you'd like to provide as extra context for the action" } val dm by defaultingBoolean { @@ -860,9 +765,9 @@ class TemporaryModeration : Extension() { } /** An image that the user wishes to provide for context to the kick. */ - val image by optionalString { + val image by optionalAttachment { name = "image" - description = "The URL to an image you'd like to provide as extra context for the action" + description = "An image you'd like to provide as extra context for the action" } val dm by defaultingBoolean { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TerminalModeration.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TerminalModeration.kt index 165b3e36..22ddf025 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TerminalModeration.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/TerminalModeration.kt @@ -9,34 +9,33 @@ import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBool import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingInt 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.optionalString +import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalAttachment import com.kotlindiscord.kord.extensions.commands.converters.impl.user import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand import com.kotlindiscord.kord.extensions.types.respond import com.kotlindiscord.kord.extensions.utils.dm import com.kotlindiscord.kord.extensions.utils.timeoutUntil +import com.kotlindiscord.kord.extensions.utils.toDuration import dev.kord.common.entity.Permission import dev.kord.core.behavior.ban import dev.kord.core.behavior.channel.createEmbed -import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.entity.Message import dev.kord.core.exception.EntityNotFoundException -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.toList import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimePeriod +import kotlinx.datetime.TimeZone import mu.KotlinLogging import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType import org.hyacinthbots.lilybot.utils.baseModerationEmbed -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.dmNotificationStatusEmbedField import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms import org.hyacinthbots.lilybot.utils.isBotOrModerator +import org.hyacinthbots.lilybot.utils.requiredConfigs /** * The class for permanent moderation actions, such as ban and kick. @@ -60,21 +59,12 @@ class TerminalModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.BanMembers) requireBotPermissions(Permission.BanMembers, Permission.ManageMessages) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument // Clarify the user is not a bot or moderator @@ -105,36 +95,16 @@ class TerminalModeration : Extension() { } // Run the ban task - guild?.ban(userArg.id, builder = { - this.reason = arguments.reason - this.deleteMessagesDays = arguments.messages - }) + guild?.ban(userArg.id) { + reason = arguments.reason + deleteMessageDuration = DateTimePeriod(days = arguments.messages).toDuration(TimeZone.UTC) + } respond { content = "Banned a user" } - val embed = EmbedBuilder() - embed.color = DISCORD_BLACK - embed.title = "Banned a user" - embed.description = "${userArg.mention} has been banned!" - embed.image = arguments.image - embed.baseModerationEmbed(arguments.reason, userArg, user) - embed.dmNotificationStatusEmbedField(arguments.dm, dm) - embed.timestamp = Clock.System.now() - embed.field { - name = "Days of messages deleted:" - value = arguments.messages.toString() - inline = false - } - - try { - actionLog.createMessage { embeds.add(embed) } - } catch (e: KtorRequestException) { - embed.image = null - actionLog.createMessage { embeds.add(embed) } - } - + val config = ModerationConfigCollection().getConfig(guild!!.id) ?: return@action if (config.publicLogging != null && config.publicLogging == true) { channel.createEmbed { title = "Banned a user" @@ -142,6 +112,21 @@ class TerminalModeration : Extension() { color = DISCORD_BLACK } } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action + actionLog.createEmbed { + title = "Banned a user" + description = "${userArg.mention} has been banned!" + image = arguments.image?.url + baseModerationEmbed(arguments.reason, userArg, user) + dmNotificationStatusEmbedField(arguments.dm, dm) + timestamp = Clock.System.now() + field { + name = "Days of messages deleted:" + value = arguments.messages.toString() + inline = false + } + } } } @@ -156,21 +141,12 @@ class TerminalModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.BanMembers) requireBotPermissions(Permission.BanMembers) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument // Get all the bans into a list val bans = guild!!.bans.toList().map { it.userId } @@ -188,6 +164,7 @@ class TerminalModeration : Extension() { content = "Unbanned user" } + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action actionLog.createEmbed { title = "Unbanned a user" description = "${userArg.mention} has been unbanned!\n${userArg.id} (${userArg.tag})" @@ -216,21 +193,12 @@ class TerminalModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.BanMembers) requireBotPermissions(Permission.BanMembers, Permission.ManageMessages) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument isBotOrModerator(userArg, "soft-ban") ?: return@action @@ -261,36 +229,16 @@ class TerminalModeration : Extension() { } // Ban the user, mark it as a soft-ban clearly - guild?.ban(userArg.id, builder = { - this.reason = "${arguments.reason} + **SOFT-BAN**" - this.deleteMessagesDays = arguments.messages - }) + guild?.ban(userArg.id) { + reason = "${arguments.reason} + **SOFT-BAN**" + deleteMessageDuration = DateTimePeriod(days = arguments.messages).toDuration(TimeZone.UTC) + } respond { content = "Soft-Banned User" } - val embed = EmbedBuilder() - embed.color = DISCORD_BLACK - embed.title = "Soft-Banned a user" - embed.description = "${userArg.mention} has been soft-banned!" - embed.image = arguments.image - embed.baseModerationEmbed(arguments.reason, userArg, user) - embed.dmNotificationStatusEmbedField(arguments.dm, dm) - embed.timestamp = Clock.System.now() - embed.field { - name = "Days of messages deleted" - value = arguments.messages.toString() - inline = false - } - - try { - actionLog.createMessage { embeds.add(embed) } - } catch (e: KtorRequestException) { - embed.image = null - actionLog.createMessage { embeds.add(embed) } - } - + val config = ModerationConfigCollection().getConfig(guild!!.id) ?: return@action if (config.publicLogging != null && config.publicLogging == true) { channel.createEmbed { title = "Soft-Banned a user" @@ -300,6 +248,21 @@ class TerminalModeration : Extension() { // Unban the user, as you're supposed to in soft-ban guild?.unban(userArg.id) + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action + actionLog.createEmbed { + title = "Soft-Banned a user" + description = "${userArg.mention} has been soft-banned!" + image = arguments.image?.url + baseModerationEmbed(arguments.reason, userArg, user) + dmNotificationStatusEmbedField(arguments.dm, dm) + timestamp = Clock.System.now() + field { + name = "Days of messages deleted" + value = arguments.messages.toString() + inline = false + } + } } } @@ -314,21 +277,12 @@ class TerminalModeration : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.MODERATOR_ROLE, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.KickMembers) requireBotPermissions(Permission.KickMembers) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action val userArg = arguments.userArgument // Clarify the user isn't a bot or a moderator @@ -358,28 +312,23 @@ class TerminalModeration : Extension() { content = "Kicked User" } - val embed = EmbedBuilder() - embed.color = DISCORD_BLACK - embed.title = "Kicked a user" - embed.description = "${userArg.mention} has been kicked!" - embed.image = arguments.image - embed.baseModerationEmbed(arguments.reason, userArg, user) - embed.dmNotificationStatusEmbedField(arguments.dm, dm) - embed.timestamp = Clock.System.now() - - try { - actionLog.createMessage { embeds.add(embed) } - } catch (e: KtorRequestException) { - embed.image = null - actionLog.createMessage { embeds.add(embed) } - } - + val config = ModerationConfigCollection().getConfig(guild!!.id) ?: return@action if (config.publicLogging != null && config.publicLogging == true) { channel.createEmbed { title = "Kicked a user" description = "${userArg.mention} has been kicked!" } } + + val actionLog = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, this.getGuild()!!) ?: return@action + actionLog.createEmbed { + title = "Kicked a user" + description = "${userArg.mention} has been kicked!" + image = arguments.image?.url + baseModerationEmbed(arguments.reason, userArg, user) + dmNotificationStatusEmbedField(arguments.dm, dm) + timestamp = Clock.System.now() + } } } } @@ -405,9 +354,9 @@ class TerminalModeration : Extension() { } /** An image that the user wishes to provide for context to the kick. */ - val image by optionalString { + val image by optionalAttachment { name = "image" - description = "The URL to an image you'd like to provide as extra context for the action" + description = "An image you'd like to provide as extra context for the action" } } @@ -438,9 +387,9 @@ class TerminalModeration : Extension() { } /** An image that the user wishes to provide for context to the ban. */ - val image by optionalString { + val image by optionalAttachment { name = "image" - description = "The URL to an image you'd like to provide as extra context for the action" + description = "An image you'd like to provide as extra context for the action" } } @@ -487,9 +436,9 @@ class TerminalModeration : Extension() { } /** An image that the user wishes to provide for context to the soft-ban. */ - val image by optionalString { + val image by optionalAttachment { name = "image" - description = "The URL to an image you'd like to provide as extra context for the action" + description = "An image you'd like to provide as extra context for the action" } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt index 9ab96f32..b69508f5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt @@ -22,10 +22,9 @@ import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.message.create.embed import kotlinx.coroutines.delay import org.hyacinthbots.lilybot.database.collections.GalleryChannelCollection -import org.hyacinthbots.lilybot.extensions.config.ConfigType +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.botHasChannelPerms import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.getUtilityLogOrFirst /** * The class the holds the systems that allow a guild to set a channel as a gallery channel. @@ -60,15 +59,6 @@ class GalleryChannel : Extension() { } action { - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - GalleryChannelCollection().getChannels(guildFor(event)!!.id).forEach { if (channel.asChannel().id == it.channelId) { respond { @@ -84,6 +74,8 @@ class GalleryChannel : Extension() { content = "Set channel as gallery channel." } + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createEmbed { title = "New Gallery channel" description = "${channel.mention} was added as a Gallery channel" @@ -111,15 +103,6 @@ class GalleryChannel : Extension() { } action { - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - var channelFound = false GalleryChannelCollection().getChannels(guildFor(event)!!.id).forEach { @@ -134,6 +117,8 @@ class GalleryChannel : Extension() { content = "Unset channel as gallery channel." } + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createEmbed { title = "Removed Gallery channel" description = "${channel.mention} was removed as a Gallery channel" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt new file mode 100644 index 00000000..cbec7ca4 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt @@ -0,0 +1,147 @@ +package org.hyacinthbots.lilybot.extensions.util + +import com.kotlindiscord.kord.extensions.checks.hasPermission +import com.kotlindiscord.kord.extensions.components.components +import com.kotlindiscord.kord.extensions.components.ephemeralButton +import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI +import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSlashCommand +import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse +import com.kotlindiscord.kord.extensions.utils.waitFor +import dev.kord.common.Color +import dev.kord.common.entity.ButtonStyle +import dev.kord.common.entity.Permission +import dev.kord.common.entity.TextInputStyle +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.behavior.interaction.modal +import dev.kord.core.behavior.interaction.response.createEphemeralFollowup +import dev.kord.core.behavior.interaction.response.edit +import dev.kord.core.behavior.interaction.response.respond +import dev.kord.core.entity.interaction.response.EphemeralMessageInteractionResponse +import dev.kord.core.event.interaction.ModalSubmitInteractionCreateEvent +import dev.kord.rest.builder.message.create.embed +import kotlinx.coroutines.flow.toList +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.utils.TEST_GUILD_ID +import org.hyacinthbots.lilybot.utils.getFirstUsableChannel +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.getSystemChannelWithPerms +import kotlin.time.Duration.Companion.seconds + +@OptIn(UnsafeAPI::class) +class GuildAnnouncements : Extension() { + override val name = "guild-announcements" + + override suspend fun setup() { + unsafeSlashCommand { + name = "announcement" + description = "Send an announcement to all guilds Lily is in" + + initialResponse = InitialSlashCommandResponse.None + + guild(TEST_GUILD_ID) + + check { + hasPermission(Permission.Administrator) + } + + action { + val footer = "Sent by ${user.asUser().tag}" // Useless, just needed for length calculations + + val modal = event.interaction.modal("Send an announcement", "announcementModal") { + actionRow { + textInput(TextInputStyle.Short, "header", "Announcement Header") { + placeholder = "Version 7.6.5!" + allowedLength = IntRange(1, 250) + required = false + } + } + actionRow { + textInput(TextInputStyle.Paragraph, "body", "Announcement Body") { + placeholder = "We're happy to announce Lily is now written in Rust! " + + "It turns out we really like crabs." + allowedLength = IntRange(1, 1750 - footer.length) + required = false + } + } + } + + val interaction = + modal.kord.waitFor(300.seconds.inWholeMilliseconds) { + interaction.modalId == "announcementModal" + }?.interaction + + if (interaction == null) { + modal.createEphemeralFollowup { + embed { + description = "Announcement timed out" + } + } + return@action + } + + val body = interaction.textInputs["body"]!!.value + val header = interaction.textInputs["header"]!!.value + val modalResponse = interaction.deferEphemeralResponse() + + if (body.isNullOrEmpty() && header.isNullOrEmpty()) { + modalResponse.respond { + content = "Your announcement cannot be completely empty!" + } + return@action + } + + var response: EphemeralMessageInteractionResponse? = null + + response = modalResponse.respond { + content = "Would you like to send this message? It will be delivered to all servers this bot is in." + components { + ephemeralButton(0) { + label = "Yes" + style = ButtonStyle.Success + + action { + response?.edit { + content = "Message sent!" + components { removeAll() } + } + + event.kord.guilds.toList().chunked(15).forEach { chunk -> + for (it in chunk) { + val channel = + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, it) + ?: getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, it) + ?: getSystemChannelWithPerms(it) + ?: getFirstUsableChannel(it) + ?: return@forEach + + channel.createEmbed { + title = header + description = body + color = Color(0x7B52AE) + footer { + text = footer + } + } + } + } + } + } + + ephemeralButton(0) { + label = "No" + style = ButtonStyle.Danger + + action { + response?.edit { + content = "Message not sent." + components { removeAll() } + } + } + } + } + } + } + } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt index fe377d9b..597c3baf 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt @@ -10,47 +10,70 @@ import com.kotlindiscord.kord.extensions.commands.Arguments import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingColor +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.optionalColour import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString import com.kotlindiscord.kord.extensions.commands.converters.impl.snowflake import com.kotlindiscord.kord.extensions.commands.converters.impl.string import com.kotlindiscord.kord.extensions.components.components +import com.kotlindiscord.kord.extensions.components.ephemeralButton import com.kotlindiscord.kord.extensions.components.linkButton import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand import com.kotlindiscord.kord.extensions.extensions.event +import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI +import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSlashCommand +import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse import com.kotlindiscord.kord.extensions.types.respond import com.kotlindiscord.kord.extensions.utils.getJumpUrl +import com.kotlindiscord.kord.extensions.utils.waitFor +import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.common.entity.PresenceStatus +import dev.kord.common.entity.TextInputStyle import dev.kord.core.behavior.channel.MessageChannelBehavior import dev.kord.core.behavior.channel.asChannelOf import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.behavior.getChannelOf +import dev.kord.core.behavior.interaction.modal +import dev.kord.core.behavior.interaction.response.createEphemeralFollowup +import dev.kord.core.behavior.interaction.response.edit +import dev.kord.core.behavior.interaction.response.respond import dev.kord.core.entity.Message import dev.kord.core.entity.channel.GuildMessageChannel +import dev.kord.core.entity.interaction.response.EphemeralMessageInteractionResponse import dev.kord.core.event.guild.GuildCreateEvent import dev.kord.core.event.guild.GuildDeleteEvent +import dev.kord.core.event.interaction.ModalSubmitInteractionCreateEvent import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.message.create.embed import dev.kord.rest.builder.message.modify.embed import dev.kord.rest.request.KtorRequestException import kotlinx.coroutines.flow.toList import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.database.collections.GalleryChannelCollection +import org.hyacinthbots.lilybot.database.collections.LogUploadingBlacklistCollection +import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection +import org.hyacinthbots.lilybot.database.collections.RemindMeCollection +import org.hyacinthbots.lilybot.database.collections.RoleMenuCollection import org.hyacinthbots.lilybot.database.collections.StatusCollection +import org.hyacinthbots.lilybot.database.collections.SupportConfigCollection +import org.hyacinthbots.lilybot.database.collections.TagsCollection +import org.hyacinthbots.lilybot.database.collections.ThreadsCollection +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection +import org.hyacinthbots.lilybot.database.collections.WarnCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType import org.hyacinthbots.lilybot.utils.TEST_GUILD_ID import org.hyacinthbots.lilybot.utils.botHasChannelPerms -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.getUtilityLogOrFirst +import org.hyacinthbots.lilybot.utils.requiredConfigs import org.hyacinthbots.lilybot.utils.updateDefaultPresence +import kotlin.time.Duration.Companion.seconds /** * This class contains a few utility commands that can be used by moderators. They all require a guild to be run. @@ -60,6 +83,7 @@ import org.hyacinthbots.lilybot.utils.updateDefaultPresence class ModUtilities : Extension() { override val name = "mod-utilities" + @OptIn(UnsafeAPI::class) override suspend fun setup() { /** * Say Command @@ -72,22 +96,11 @@ class ModUtilities : Extension() { check { anyGuild() - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.SendMessages, Permission.EmbedLinks) botHasChannelPerms(Permissions(Permission.SendMessages, Permission.EmbedLinks)) } action { - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action - val targetChannel: GuildMessageChannel? try { targetChannel = @@ -123,7 +136,8 @@ class ModUtilities : Extension() { respond { content = "Message sent." } - actionLog.createMessage { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) ?: return@action + utilityLog.createMessage { embed { title = "Say command used" description = "```${arguments.message}```" @@ -186,16 +200,6 @@ class ModUtilities : Extension() { } else { channel } - - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - val message: Message try { @@ -235,6 +239,8 @@ class ModUtilities : Extension() { respond { content = "Message edited" } + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Say message edited" @@ -275,12 +281,18 @@ class ModUtilities : Extension() { embed { description = arguments.newContent ?: oldContent color = arguments.newColor ?: oldColor - timestamp = if (arguments.timestamp) message.timestamp else oldTimestamp + timestamp = when (arguments.timestamp) { + true -> message.timestamp + false -> null + null -> oldTimestamp + } } } respond { content = "Embed updated" } + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Say message edited" @@ -305,7 +317,11 @@ class ModUtilities : Extension() { } field { name = "Has Timestamp" - value = arguments.timestamp.toString() + value = when (arguments.timestamp) { + true -> "True" + false -> "False" + else -> "Original" + } } footer { text = "Edited by ${user.asUser().tag}" @@ -343,7 +359,7 @@ class ModUtilities : Extension() { check { hasPermission(Permission.Administrator) - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) } action { @@ -380,7 +396,7 @@ class ModUtilities : Extension() { check { hasPermission(Permission.Administrator) - configPresent(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) + requiredConfigs(ConfigOptions.MODERATION_ENABLED, ConfigOptions.ACTION_LOG) } action { @@ -390,19 +406,11 @@ class ModUtilities : Extension() { updateDefaultPresence() val guilds = this@ephemeralSlashCommand.kord.guilds.toList().size - val config = ModerationConfigCollection().getConfig(guild!!.id)!! - val actionLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - config.channel!!, - ConfigType.MODERATION, - interactionResponse - ) - ?: return@action - respond { content = "Presence set to default" } - actionLog.createEmbed { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action + utilityLog.createEmbed { title = "Presence changed" description = "Lily's presence has been set to default." field { @@ -418,6 +426,108 @@ class ModUtilities : Extension() { } } + unsafeSlashCommand { + name = "reset" + description = "'Resets' Lily for this guild by deleting all database information relating to this guild" + + initialResponse = InitialSlashCommandResponse.None + + requirePermission(Permission.Administrator) // Hide this command from non-administrators + + check { + anyGuild() + hasPermission(Permission.Administrator) + } + + action { + val modal = event.interaction.modal("Reset data for this guild", "resetModal") { + actionRow { + textInput(TextInputStyle.Short, "confirmation", "Confirm reset") { + placeholder = "Type 'yes' to confirm" + } + } + } + + val interaction = + modal.kord.waitFor(120.seconds.inWholeMilliseconds) { + interaction.modalId == "resetModal" + }?.interaction + + if (interaction == null) { + modal.createEphemeralFollowup { content = "Reset interaction timed out" } + return@action + } + + val confirmation = interaction.textInputs["confirmation"]!!.value!! + val modalResponse = interaction.deferEphemeralResponse() + + if (confirmation.lowercase() != "yes") { + modalResponse.respond { content = "Confirmation failure. Reset cancelled" } + return@action + } + + var response: EphemeralMessageInteractionResponse? = null + + response = modalResponse.respond { + content = + "Are you sure you want to reset the database? This will remove all data associated with " + + "this guild from Lily's database. This includes configs, user-set reminders, tags and more." + + "This action is **irreversible** and the data **cannot** be recovered." + + components { + ephemeralButton(0) { + label = "I'm sure" + style = ButtonStyle.Danger + + action { + response?.edit { + content = "Database reset!" + components { removeAll() } + } + + guild?.getChannelOf( + ModerationConfigCollection().getConfig(guild!!.id)?.channel ?: guild!!.asGuild() + .getSystemChannel()!!.id + )?.createMessage { + embed { + title = "Database Reset!" + description = "All data associated with this guild has been removed." + timestamp = Clock.System.now() + color = DISCORD_BLACK + } + } + + // Reset + LoggingConfigCollection().clearConfig(guild!!.id) + ModerationConfigCollection().clearConfig(guild!!.id) + SupportConfigCollection().clearConfig(guild!!.id) + UtilityConfigCollection().clearConfig(guild!!.id) + GalleryChannelCollection().removeAll(guild!!.id) + LogUploadingBlacklistCollection().clearBlacklist(guild!!.id) + RemindMeCollection().removeGuildReminders(guild!!.id) + RoleMenuCollection().removeAllRoleMenus(guild!!.id) + TagsCollection().clearTags(guild!!.id) + ThreadsCollection().removeGuildThreads(guild!!.id) + WarnCollection().clearWarns(guild!!.id) + } + } + + ephemeralButton(0) { + label = "Nevermind" + style = ButtonStyle.Secondary + + action { + response?.edit { + content = "Reset cancelled" + components { removeAll() } + } + } + } + } + } + } + } + /** * Update the presence to reflect the new number of guilds, if the presence is set to "default" * @@ -518,10 +628,9 @@ class ModUtilities : Extension() { } /** Whether to add the timestamp of when the message was originally sent or not. */ - val timestamp by defaultingBoolean { + val timestamp by optionalBoolean { name = "timestamp" description = "Whether to timestamp the embed or not. Embeds only" - defaultValue = true } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt index c82a4fed..ab00060e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt @@ -29,7 +29,7 @@ import dev.kord.rest.request.KtorRequestException import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.utils.configPresent +import org.hyacinthbots.lilybot.utils.requiredConfigs /** * This class contains a few utility commands that can be used by the public in guilds, or that are often seen by the @@ -85,7 +85,7 @@ class PublicUtilities : Extension() { check { anyGuild() - configPresent(ConfigOptions.UTILITY_LOG) + requiredConfigs(ConfigOptions.UTILITY_LOG) } action { @@ -293,7 +293,7 @@ class PublicUtilities : Extension() { check { anyGuild() - configPresent(ConfigOptions.UTILITY_LOG) + requiredConfigs(ConfigOptions.UTILITY_LOG) } action { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt index a434f142..a5ac59cb 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt @@ -73,7 +73,7 @@ class Reminders : Extension() { override suspend fun setup() { /** Set the task to run every 30 seconds. */ - task = scheduler.schedule(30, pollingSeconds = 30, repeat = true, callback = ::postReminders) + task = scheduler.schedule(30, pollingSeconds = 1, repeat = true, callback = ::postReminders) /** * The command for reminders diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt index ece8ab03..24ff003b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt @@ -38,10 +38,9 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.collections.RoleMenuCollection -import org.hyacinthbots.lilybot.extensions.config.ConfigType +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.botHasChannelPerms import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.getUtilityLogOrFirst import org.hyacinthbots.lilybot.utils.utilsLogger /** @@ -118,14 +117,8 @@ class RoleMenu : Extension() { mutableListOf(arguments.initialRole.id) ) - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { @@ -215,14 +208,8 @@ class RoleMenu : Extension() { data.roles ) - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Role Added to Role Menu" @@ -284,15 +271,9 @@ class RoleMenu : Extension() { } RoleMenuCollection().removeRoleFromMenu(menuMessage!!.id, arguments.role.id) - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Role Removed from Role Menu" @@ -391,15 +372,8 @@ class RoleMenu : Extension() { roles ) - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Pronoun Role Menu Created" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt index 150a9672..b945b303 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt @@ -5,17 +5,26 @@ import com.kotlindiscord.kord.extensions.extensions.Extension import com.kotlindiscord.kord.extensions.extensions.event import com.kotlindiscord.kord.extensions.time.TimestampType import com.kotlindiscord.kord.extensions.time.toDiscord +import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler +import com.kotlindiscord.kord.extensions.utils.scheduling.Task import dev.kord.common.entity.PresenceStatus import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.getChannelOf import dev.kord.core.entity.channel.NewsChannel +import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.event.gateway.ReadyEvent import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.Cleanups +import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.collections.StatusCollection +import org.hyacinthbots.lilybot.database.entities.ThreadData import org.hyacinthbots.lilybot.utils.ONLINE_STATUS_CHANNEL import org.hyacinthbots.lilybot.utils.TEST_GUILD_ID import org.hyacinthbots.lilybot.utils.updateDefaultPresence +import org.litote.kmongo.coroutine.toList +import org.litote.kmongo.eq +import org.litote.kmongo.setValue +import kotlin.time.Duration.Companion.days /** * This class serves as a place for all functions that get run on bot start and bot start alone. This *hypothetically* @@ -26,11 +35,26 @@ import org.hyacinthbots.lilybot.utils.updateDefaultPresence * @since 3.2.2 */ class StartupHooks : Extension() { - override val name = "startuphooks" + override val name = "startup-hooks" + + private val cleanupScheduler = Scheduler() + + private lateinit var cleanupTask: Task override suspend fun setup() { event { action { + // TODO Remove this once the migration is done, because of the fact we cannot access kord in the + // migration we need to do this to apply the guild IDs + with(Database().mainDatabase.getCollection("threadData")) { + collection.find(ThreadData::guildId eq null).toList().forEach { + updateOne( + ThreadData::threadId eq it.threadId, + setValue(ThreadData::guildId, kord.getChannelOf(it.threadId)!!.guildId) + ) + } + } + val now = Clock.System.now() /** @@ -47,36 +71,32 @@ class StartupHooks : Extension() { color = DISCORD_GREEN }?.publish() - /** - * This function is called to remove any threads in the database that haven't had a message sent in the last - * week. It only runs on startup. - * @author tempest15 - * @since 3.2.0 - */ - Cleanups.cleanupThreadData(kord) - - /** - * This function is called to remove any guilds in the database that haven't had Lily in them for more than - * a month. It only runs on startup - * - * @author NoComment1105 - * @since 3.2.0 - */ - Cleanups.cleanupGuildData() - /** * Check the status value in the database. If it is "default", set the status to watching over X guilds, * else the database value. */ - if (StatusCollection().getStatus() == null) { - updateDefaultPresence() - } else { - this@event.kord.editPresence { - status = PresenceStatus.Online - playing(StatusCollection().getStatus()!!) - } - } + if (StatusCollection().getStatus() == null) { + updateDefaultPresence() + } else { + this@event.kord.editPresence { + status = PresenceStatus.Online + playing(StatusCollection().getStatus()!!) + } + } } } + + cleanupTask = cleanupScheduler.schedule(1.days, callback = ::cleanup) + } + + /** + * This function is called to remove any threads in the database that haven't had a message sent in the last + * week. + * @author NoComment1105 + * @since 4.1.0 + */ + private suspend fun cleanup() { + Cleanups.cleanupThreadData(kord) + Cleanups.cleanupGuildData() } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt index a768a36c..bd22867e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt @@ -25,15 +25,15 @@ import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.getChannelOfOrNull +import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.rest.builder.message.create.embed import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.TagsCollection +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType import org.hyacinthbots.lilybot.utils.botHasChannelPerms -import org.hyacinthbots.lilybot.utils.configPresent import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.getUtilityLogOrFirst /** * The class that holds the commands to create tags commands. @@ -149,6 +149,33 @@ class Tags : Extension() { "${arguments.user?.mention ?: ""}\n**${tagFromDatabase.tagTitle}**\n${tagFromDatabase.tagValue}" } } + + // Log when a message tag is sent to allow identification of tag spammers + if (tagFromDatabase.tagAppearance == "message") { + val utilityLog = UtilityConfigCollection().getConfig(guild!!.id)?.utilityLogChannel ?: return@action + guild!!.getChannelOfOrNull(utilityLog)?.createMessage { + embed { + title = "Message Tag used" + field { + name = "User" + value = "${user.asUser().mention} (${user.asUser().tag})" + } + field { + name = "Tag name" + value = "`${arguments.tagName}`" + } + field { + name = "Location" + value = "${channel.mention} ${channel.asChannel().data.name.value}" + } + footer { + text = "User ID: ${user.asUser().id}" + icon = user.asUser().avatar?.url + } + timestamp = Clock.System.now() + } + } + } } } @@ -216,15 +243,6 @@ class Tags : Extension() { } action { - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - if (TagsCollection().getTag(guild!!.id, arguments.tagName) != null) { respond { content = "A tag with that name already exists in this guild." } return@action @@ -246,6 +264,7 @@ class Tags : Extension() { arguments.tagAppearance ) + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) ?: return@action utilityLog.createEmbed { title = "Tag created!" description = "The tag `${arguments.tagName}` has been created" @@ -303,17 +322,9 @@ class Tags : Extension() { return@action } - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - TagsCollection().removeTag(guild!!.id, arguments.tagName) + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) ?: return@action utilityLog.createEmbed { title = "Tag deleted!" description = "The tag ${arguments.tagName} was deleted" @@ -335,22 +346,12 @@ class Tags : Extension() { check { anyGuild() - configPresent(ConfigOptions.UTILITY_LOG) hasPermission(Permission.ModerateMembers) requireBotPermissions(Permission.SendMessages, Permission.EmbedLinks) botHasChannelPerms(Permissions(Permission.SendMessages, Permission.EmbedLinks)) } action { - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - if (TagsCollection().getTag(guild!!.id, arguments.tagName) == null) { respond { content = "Unable to find tag `${arguments.tagName}`! Does this tag exist?" } return@action @@ -380,53 +381,52 @@ class Tags : Extension() { arguments.newAppearance ?: originalAppearance ) - utilityLog.createMessage { - embed { - title = "Tag Edited" - description = "The tag `${arguments.tagName}` was edited" - field { - name = "Name" - value = if (arguments.newName.isNullOrEmpty()) { - originalName - } else { - "$originalName -> ${arguments.newName!!}" - } - } - field { - name = "Title" - value = if (arguments.newTitle.isNullOrEmpty()) { - originalTitle - } else { - "${arguments.newTitle} -> ${arguments.newTitle!!}" - } + respond { + content = "Tag edited!" + } + + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) ?: return@action + utilityLog.createEmbed { + title = "Tag Edited" + description = "The tag `${arguments.tagName}` was edited" + field { + name = "Name" + value = if (arguments.newName.isNullOrEmpty()) { + originalName + } else { + "$originalName -> ${arguments.newName!!}" } - field { - name = "Value" - value = if (arguments.newValue.isNullOrEmpty()) { - originalValue - } else { - "$originalValue -> ${arguments.newValue!!}" - } + } + field { + name = "Title" + value = if (arguments.newTitle.isNullOrEmpty()) { + originalTitle + } else { + "${arguments.newTitle} -> ${arguments.newTitle!!}" } - field { - name = "Tag appearance" - value = if (arguments.newAppearance.isNullOrEmpty()) { - originalAppearance - } else { - "$originalAppearance -> ${arguments.newAppearance}" - } + } + field { + name = "Value" + value = if (arguments.newValue.isNullOrEmpty()) { + originalValue + } else { + "$originalValue -> ${arguments.newValue!!}" } - footer { - text = "Edited by ${user.asUser().tag}" - icon = user.asUser().avatar?.url + } + field { + name = "Tag appearance" + value = if (arguments.newAppearance.isNullOrEmpty()) { + originalAppearance + } else { + "$originalAppearance -> ${arguments.newAppearance}" } - timestamp = Clock.System.now() - color = DISCORD_YELLOW } - } - - respond { - content = "Tag edited!" + footer { + text = "Edited by ${user.asUser().tag}" + icon = user.asUser().avatar?.url + } + timestamp = Clock.System.now() + color = DISCORD_YELLOW } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt index 3f1da501..6258e3ff 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt @@ -41,10 +41,9 @@ import dev.kord.rest.builder.message.create.embed import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.ThreadsCollection -import org.hyacinthbots.lilybot.extensions.config.ConfigType +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.botHasChannelPerms import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.getUtilityLogOrFirst class ThreadControl : Extension() { @@ -100,7 +99,7 @@ class ThreadControl : Extension() { if (it.threadId == threadChannel.id) { val preventingArchiving = ThreadsCollection().getThread(it.threadId)?.preventArchiving ThreadsCollection().removeThread(it.threadId) - ThreadsCollection().setThreadOwner(it.threadId, it.ownerId, false) + ThreadsCollection().setThreadOwner(it.guildId, it.threadId, it.ownerId, false) if (preventingArchiving == true) { guild!!.getChannelOf( ModerationConfigCollection().getConfig(guild!!.id)!!.channel!! @@ -158,14 +157,6 @@ class ThreadControl : Extension() { action { val threadChannel = channel.asChannelOf() val member = user.asMember(guild!!.id) - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action val oldOwnerId = ThreadsCollection().getThread(threadChannel.id)?.ownerId ?: threadChannel.ownerId val oldOwner = guild!!.getMember(oldOwnerId) @@ -182,7 +173,7 @@ class ThreadControl : Extension() { return@action } - ThreadsCollection().setThreadOwner(threadChannel.id, arguments.newOwner.id) + ThreadsCollection().setThreadOwner(guild!!.id, threadChannel.id, arguments.newOwner.id) respond { content = "Ownership transferred." } @@ -193,9 +184,11 @@ class ThreadControl : Extension() { threadChannel.createMessage(content) + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { - title = "Thread ownership transfered" + title = "Thread ownership transferred" field { name = "Previous owner" value = "${oldOwner.mention} ${oldOwner.tag}" @@ -228,15 +221,6 @@ class ThreadControl : Extension() { } action { - val utilityLog = - getLoggingChannelWithPerms( - guild!!.asGuild(), - getUtilityLogOrFirst(guild)?.id, - ConfigType.UTILITY, - interactionResponse - ) - ?: return@action - val threadChannel = channel.asChannelOf() val member = user.asMember(guild!!.id) if (!ownsThreadOrModerator(threadChannel, member)) return@action @@ -252,7 +236,7 @@ class ThreadControl : Extension() { var message: EphemeralMessageInteractionResponse? = null var thread = threads.firstOrNull { it.threadId == threadChannel.id } if (thread == null) { - ThreadsCollection().setThreadOwner(threadChannel.id, threadChannel.ownerId, false) + ThreadsCollection().setThreadOwner(threadChannel.guildId, threadChannel.id, threadChannel.ownerId, false) thread = threads.firstOrNull { it.threadId == threadChannel.id } } if (thread?.preventArchiving == true) { @@ -264,24 +248,28 @@ class ThreadControl : Extension() { label = "Yes" style = ButtonStyle.Primary - action { - ThreadsCollection().setThreadOwner(thread.threadId, thread.ownerId, false) + action button@{ + ThreadsCollection().setThreadOwner(thread.guildId, thread.threadId, thread.ownerId, false) edit { content = "Thread archiving will no longer be prevented" } + val utilityLog = getLoggingChannelWithPerms( + ConfigOptions.UTILITY_LOG, + this.getGuild()!! + ) ?: return@button utilityLog.createMessage { - embed { - title = "Thread archive prevention disabled" - color = DISCORD_FUCHSIA - - field { - name = "User" - value = user.asUser().tag - } - field { - name = "Thread" - value = threadChannel.mention - } + embed { + title = "Thread archive prevention disabled" + color = DISCORD_FUCHSIA + + field { + name = "User" + value = user.asUser().tag + } + field { + name = "Thread" + value = threadChannel.mention } } + } message!!.edit { components { removeAll() } } } } @@ -298,8 +286,10 @@ class ThreadControl : Extension() { } return@action } else if (thread?.preventArchiving == false) { - ThreadsCollection().setThreadOwner(thread.threadId, thread.ownerId, true) + ThreadsCollection().setThreadOwner(thread.guildId, thread.threadId, thread.ownerId, true) try { + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, this.getGuild()!!) + ?: return@action utilityLog.createMessage { embed { title = "Thread archive prevention enabled" diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt index cc82e665..3534943e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt @@ -6,6 +6,7 @@ import com.kotlindiscord.kord.extensions.checks.guildFor import com.kotlindiscord.kord.extensions.checks.types.CheckContext import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSlashCommandContext import com.kotlindiscord.kord.extensions.extensions.Extension +import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api.PKMessage import com.kotlindiscord.kord.extensions.types.respond import com.kotlindiscord.kord.extensions.utils.botHasPermissions import com.kotlindiscord.kord.extensions.utils.loadModule @@ -14,13 +15,12 @@ import dev.kord.common.entity.Permissions import dev.kord.common.entity.PresenceStatus import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.RoleBehavior import dev.kord.core.behavior.channel.asChannelOf import dev.kord.core.behavior.channel.asChannelOfOrNull -import dev.kord.core.behavior.getChannelOf import dev.kord.core.behavior.getChannelOfOrNull -import dev.kord.core.behavior.interaction.response.FollowupPermittingInteractionResponseBehavior -import dev.kord.core.behavior.interaction.response.createEphemeralFollowup import dev.kord.core.entity.Guild +import dev.kord.core.entity.Message import dev.kord.core.entity.User import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.channel.NewsChannel @@ -28,9 +28,10 @@ import dev.kord.core.entity.channel.TextChannel import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.exception.EntityNotFoundException import dev.kord.core.supplier.EntitySupplyStrategy -import kotlinx.coroutines.delay +import dev.kord.rest.builder.message.EmbedBuilder import kotlinx.coroutines.flow.count import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import mu.KotlinLogging @@ -50,7 +51,6 @@ import org.hyacinthbots.lilybot.database.collections.ThreadsCollection import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection import org.hyacinthbots.lilybot.database.collections.WarnCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.ConfigType import org.koin.dsl.bind @PublishedApi @@ -64,7 +64,7 @@ internal val utilsLogger = KotlinLogging.logger("Checks Logger") * @author NoComment1105 * @since 3.2.0 */ -suspend inline fun CheckContext<*>.configPresent(vararg configOptions: ConfigOptions) { +suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigOptions) { if (!passed) { return } @@ -170,13 +170,26 @@ suspend inline fun CheckContext<*>.configPresent(vararg configOptions: ConfigOpt } } - ConfigOptions.MESSAGE_LOGGING_ENABLED -> { + ConfigOptions.MESSAGE_DELETE_LOGGING_ENABLED -> { val loggingConfig = LoggingConfigCollection().getConfig(guildFor(event)!!.id) if (loggingConfig == null) { fail("Unable to access logging config for this guild! Please inform a member of staff.") break - } else if (!loggingConfig.enableMessageLogs) { - fail("Message logging is disabled for this guild!") + } else if (!loggingConfig.enableMessageDeleteLogs) { + fail("Message delete logging is disabled for this guild!") + break + } else { + pass() + } + } + + ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED -> { + val loggingConfig = LoggingConfigCollection().getConfig(guildFor(event)!!.id) + if (loggingConfig == null) { + fail("Unable to access logging config for this guild! Please inform a member of staff.") + break + } else if (!loggingConfig.enableMessageEditLogs) { + fail("Message edit logging is disabled for this guild!") break } else { pass() @@ -251,6 +264,159 @@ suspend inline fun CheckContext<*>.configPresent(vararg configOptions: ConfigOpt } } +/** + * This function checks if a single config exists and is valid. Returns true if it is or false otherwise. + * + * @param option The config option to check the database for. Only takes a single option. + * @return True if the selected [option] is valid and enabled and false if it isn't + * @author NoComment1105 + * @since 3.2.0 + */ +suspend inline fun configIsUsable(option: ConfigOptions, guildId: Snowflake): Boolean { + when (option) { + ConfigOptions.SUPPORT_ENABLED -> return SupportConfigCollection().getConfig(guildId)?.enabled ?: false + + ConfigOptions.SUPPORT_CHANNEL -> { + val supportConfig = SupportConfigCollection().getConfig(guildId) ?: return false + return supportConfig.channel != null + } + + ConfigOptions.SUPPORT_ROLE -> { + val supportConfig = SupportConfigCollection().getConfig(guildId) ?: return false + return supportConfig.role != null + } + + ConfigOptions.MODERATION_ENABLED -> return ModerationConfigCollection().getConfig(guildId)?.enabled ?: false + + ConfigOptions.MODERATOR_ROLE -> { + val moderationConfig = ModerationConfigCollection().getConfig(guildId) ?: return false + return moderationConfig.role != null + } + + ConfigOptions.ACTION_LOG -> { + val moderationConfig = ModerationConfigCollection().getConfig(guildId) ?: return false + return moderationConfig.channel != null + } + + ConfigOptions.LOG_PUBLICLY -> { + val moderationConfig = ModerationConfigCollection().getConfig(guildId) ?: return false + return moderationConfig.publicLogging != null + } + + ConfigOptions.MESSAGE_DELETE_LOGGING_ENABLED -> + return LoggingConfigCollection().getConfig(guildId)?.enableMessageDeleteLogs ?: false + + ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED -> + return LoggingConfigCollection().getConfig(guildId)?.enableMessageEditLogs ?: false + + ConfigOptions.MESSAGE_LOG -> { + val loggingConfig = LoggingConfigCollection().getConfig(guildId) ?: return false + return loggingConfig.messageChannel != null + } + + ConfigOptions.MEMBER_LOGGING_ENABLED -> return LoggingConfigCollection().getConfig(guildId)?.enableMemberLogs ?: false + + ConfigOptions.MEMBER_LOG -> { + val loggingConfig = LoggingConfigCollection().getConfig(guildId) ?: return false + return loggingConfig.memberLog != null + } + + ConfigOptions.LOG_UPLOADS_ENABLED -> { + val utilityConfig = UtilityConfigCollection().getConfig(guildId) ?: return false + return utilityConfig.disableLogUploading + } + + ConfigOptions.UTILITY_LOG -> { + val utilityConfig = UtilityConfigCollection().getConfig(guildId) ?: return false + return utilityConfig.utilityLogChannel != null + } + } +} + +/** + * This function checks if a single config exists and is valid. Returns true if it is or false otherwise. + * + * @param channelType The type of logging channel desired + * @param guild The guild the desired channel is in + * @param resetConfig If configured channels should be reset if invalid. + * Should only be passed as false, and defaults to true. + * @return The logging channel of [channelType] for the [guild] or null if it doesn't exist + * @author tempest15 + * @since 4.1.0 + */ +suspend inline fun getLoggingChannelWithPerms( + channelType: ConfigOptions, + guild: GuildBehavior, + resetConfig: Boolean? = null +): GuildMessageChannel? { + val guildId = guild.id + + if (!configIsUsable(channelType, guildId)) return null + + val channelId = when (channelType) { + ConfigOptions.SUPPORT_CHANNEL -> SupportConfigCollection().getConfig(guildId)?.channel ?: return null + ConfigOptions.ACTION_LOG -> ModerationConfigCollection().getConfig(guildId)?.channel ?: return null + ConfigOptions.UTILITY_LOG -> UtilityConfigCollection().getConfig(guildId)?.utilityLogChannel ?: return null + ConfigOptions.MESSAGE_LOG -> LoggingConfigCollection().getConfig(guildId)?.messageChannel ?: return null + ConfigOptions.MEMBER_LOG -> LoggingConfigCollection().getConfig(guildId)?.memberLog ?: return null + else -> throw IllegalArgumentException("$channelType does not point to a channel.") + } + val channel = guild.getChannelOfOrNull(channelId) ?: return null + + if (!channel.botHasPermissions(Permission.ViewChannel) || !channel.botHasPermissions(Permission.SendMessages)) { + if (resetConfig == true) { + when (channelType) { + ConfigOptions.SUPPORT_CHANNEL -> SupportConfigCollection().clearConfig(guildId) + ConfigOptions.ACTION_LOG -> ModerationConfigCollection().clearConfig(guildId) + ConfigOptions.UTILITY_LOG -> UtilityConfigCollection().clearConfig(guildId) + ConfigOptions.MESSAGE_LOG -> LoggingConfigCollection().clearConfig(guildId) + ConfigOptions.MEMBER_LOG -> LoggingConfigCollection().clearConfig(guildId) + else -> throw IllegalArgumentException("$channelType does not point to a channel.") + } + val informChannel = getSystemChannelWithPerms(guild as Guild) ?: getFirstUsableChannel(guild) + informChannel?.createMessage( + "Lily is unable to send messages in the configured " + + "${channelType.toString().lowercase()} for this guild. " + + "As a result, the corresponding config has been reset. \n\n" + + "*Note:* this channel has been used to send this message because it's the first channel " + + "in the guild Lily could use. Please inform this guild's staff about this message." + ) + } + return null + } + + return channel +} + +/** + * Get the first text channel the bot can send a message in. + * + * @param inputGuild The guild in which to get the channel. + * @return The first text channel the bot can send a message in or null if there isn't one. + * @author tempest15 + * @since 3.5.4 + */ +suspend inline fun getFirstUsableChannel(inputGuild: GuildBehavior): GuildMessageChannel? = + inputGuild.channels.first { + it.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) + }.asChannelOfOrNull() + +/** + * Gets a guild's system channel as designated by Discord, or null if said channel is invalid or doesn't exist. + * + * @param inputGuild The guild in which to get the channel. + * @return The guild's system channel or null if it's invalid + * @author tempest15 + * @since 4.1.0 + */ +suspend inline fun getSystemChannelWithPerms(inputGuild: Guild): GuildMessageChannel? { + val systemChannel = inputGuild.getSystemChannel() ?: return null + if (!systemChannel.botHasPermissions(Permission.ViewChannel) || + !systemChannel.botHasPermissions(Permission.SendMessages) + ) return null + return systemChannel +} + /** * Gets the channel of the event and checks that the bot has the required [permissions]. * @@ -388,6 +554,17 @@ suspend inline fun Extension.updateDefaultPresence() { */ suspend inline fun Extension.getGuildCount() = kord.with(EntitySupplyStrategy.cacheWithRestFallback).guilds.count() +/** + * Gets the member count for a given guild. + * + * @param guildId The target guild + * @return The number of members in that guild + * @author NoComment1105 + * @since 4.1.0 + */ +suspend inline fun Extension.getMemberCount(guildId: Snowflake) = + kord.with(EntitySupplyStrategy.rest).getGuild(guildId).members.map { }.count() + /** * This function loads the database and checks if it is up-to-date. If it isn't, it will update the database via * migrations. @@ -430,118 +607,87 @@ suspend inline fun ExtensibleBotBuilder.database(migrate: Boolean) { } /** - * Get the first text channel the bot can send a message in. + * Utility to get a string or a default value. + * Basically String.ifEmpty but works with nullable strings * - * @param inputGuild The guild in which to get the channel. - * @return The first text channel the bot can send a message in or null if there isn't one. - * @author tempest15 - * @since 3.5.4 + * @return This, or defaultValue if this is null or empty + * @author trainb0y + * @since 4.1.0 + * @see String.ifEmpty */ -suspend inline fun getFirstUsableChannel(inputGuild: Guild): GuildMessageChannel? = inputGuild.channels.first { - it.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) -}.fetchChannelOrNull()?.asChannelOfOrNull() +fun String?.ifNullOrEmpty(defaultValue: () -> String): String = + if (this.isNullOrEmpty()) { + defaultValue() + } else { + this + } /** - * Check if the bot can send messages in a guild's configured logging channel. - * If the bot can't, reset a config and send a message in the top usable channel saying that the config was reset or - * if this function is in a command, an [interactionResponse] is provided, allowing a response to be given on the - * command. - * If the bot can, return the channel. - * - * **DO NOT USE THIS FUNCTION ON NON-MODERATION CHANNELS!** Use the [botHasChannelPerms] check instead. - * - * @param inputGuild The guild to check in. - * @param targetChannel The channel to check permissions for - * @param configType The config the channel will be in - * @param interactionResponse The interactionResponse to respond to if this function is in a command. - * @return The channel or null if it does not have the correct permissions. + * Get this message's contents, trimmed to 1024 characters. + * If the message exceeds that length, it will be truncated and an ellipsis appended. + * @author trainb0y + * @since 4.1.0 + */ +fun Message?.trimmedContents(): String? { + this ?: return null + return if (this.content.length > 1024) { + this.content.substring(0, 1020) + " ..." + } else this.content +} + +/** + * This function removed duplicated code from MessageDelete and MessageEdit. + * It holds attachment and PluralKit info fields for the logging embeds. * @author tempest15 - * @since 3.5.4 + * @since 4.1.0 */ -suspend inline fun getLoggingChannelWithPerms( - inputGuild: Guild, - targetChannel: Snowflake?, - configType: ConfigType, - interactionResponse: T? = null -): GuildMessageChannel? { - val channel = targetChannel?.let { inputGuild.getChannelOfOrNull(it) } - - // Check each permission in a separate check because all in one expects all to be there or not. This allows for - // some permissions to be false and some to be true while still producing the correct result. - if (channel?.botHasPermissions(Permission.ViewChannel) != true || - !channel.botHasPermissions(Permission.SendMessages) || - !channel.botHasPermissions(Permission.EmbedLinks) - ) { - val usableChannel = getFirstUsableChannel(inputGuild) ?: return null - - if (interactionResponse == null) { - usableChannel.createMessage( - "Lily cannot send messages in ${channel?.mention}. " + - "As a result, your config has been reset. " + - "Please fix the permissions before setting a new config." - ) - } else { - interactionResponse.createEphemeralFollowup { - content = "Lily cannot send messages in ${channel?.mention}. " + - "As a result, your config has been reset. " + - "Please fix the permissions before setting a new config." - } +suspend fun EmbedBuilder.attachmentsAndProxiedMessageInfo( + guild: Guild, + message: Message, + proxiedMessage: PKMessage? +) { + if (message.attachments.isNotEmpty()) { + field { + name = "Attachments" + value = message.attachments.map { it.url }.joinToString { "\n" } + inline = false + } + } + if (proxiedMessage != null) { + field { + name = "Message Author:" + value = "System Member: ${proxiedMessage.member.name}\n" + + "Account: ${guild.getMember(proxiedMessage.sender).tag} " + + guild.getMember(proxiedMessage.sender).mention + inline = true } - delay(3000) // So that other events may finish firing - when (configType) { - ConfigType.MODERATION -> ModerationConfigCollection().clearConfig(usableChannel.guildId) - ConfigType.LOGGING -> LoggingConfigCollection().clearConfig(usableChannel.guildId) - ConfigType.SUPPORT -> SupportConfigCollection().clearConfig(usableChannel.guildId) - ConfigType.UTILITY -> UtilityConfigCollection().clearConfig(usableChannel.guildId) - ConfigType.ALL -> { - ModerationConfigCollection().clearConfig(usableChannel.guildId) - LoggingConfigCollection().clearConfig(usableChannel.guildId) - SupportConfigCollection().clearConfig(usableChannel.guildId) - UtilityConfigCollection().clearConfig(usableChannel.guildId) - } + field { + name = "Author ID:" + value = proxiedMessage.sender.toString() + } + } else { + field { + name = "Message Author:" + value = + "${message.author?.tag ?: "Failed to get author of message"} ${message.author?.mention ?: ""}" + inline = true } - return null + field { + name = "Author ID:" + value = message.author?.id.toString() + } } - - return channel } /** - * Overload function for [getLoggingChannelWithPerms] that does not take an interaction response allowing the type - * variable not be specified in the function. + * Check if a role is mentionable by Lily. * - * **DO NOT USE THIS FUNCTION ON NON-MODERATION CHANNELS!** Use the [botHasChannelPerms] check instead. - * - * @see getLoggingChannelWithPerms - * - * @param inputGuild The guild to check in. - * @param targetChannel The channel to check permissions for - * @param configType The config the channel will be in - * @return The channel or null if it does not have the correct permissions. - * @author NoComment1105 - */ -suspend inline fun getLoggingChannelWithPerms( - inputGuild: Guild, - targetChannel: Snowflake?, - configType: ConfigType -): GuildMessageChannel? = - getLoggingChannelWithPerms(inputGuild, targetChannel, configType, null) - -/** - * A small function to get the utility log of a guild or the first available channel. + * @param role The role to check + * @return A Boolean of whether it is pingable or not * - * @param guild The guild for the channel - * @return The utility log or the first usable channel * @author NoComment1105 - * @since 4.0.1 + * @since 4.1.0 */ -suspend inline fun getUtilityLogOrFirst(guild: GuildBehavior?): GuildMessageChannel? { - val config = UtilityConfigCollection().getConfig(guild!!.id) - return if (config?.utilityLogChannel != null) { - guild.getChannelOf(config.utilityLogChannel) - } else { - guild.asGuild().getSystemChannel() ?: getFirstUsableChannel(guild.asGuild()) - } -} +suspend fun canPingRole(role: RoleBehavior?) = role != null && role.guild.getRole(role.id).mentionable