From 1fd8256a50e61c4486d017725bfcba4c8a99d86b Mon Sep 17 00:00:00 2001 From: NoComment <67918617+NoComment1105@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:33:36 +0100 Subject: [PATCH] LilyBot 5.0.0 (#422) * Bump detekt from 1.23.1 to 1.23.3 (#362) Bumps `detekt` from 1.23.1 to 1.23.3. Updates `io.gitlab.arturbosch.detekt:detekt-formatting` from 1.23.1 to 1.23.3 - [Release notes](https://github.com/detekt/detekt/releases) - [Commits](https://github.com/detekt/detekt/compare/v1.23.1...v1.23.3) Updates `io.gitlab.arturbosch.detekt` from 1.23.1 to 1.23.3 - [Release notes](https://github.com/detekt/detekt/releases) - [Commits](https://github.com/detekt/detekt/compare/v1.23.1...v1.23.3) --- updated-dependencies: - dependency-name: io.gitlab.arturbosch.detekt:detekt-formatting dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.gitlab.arturbosch.detekt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump kotlin from 1.9.10 to 1.9.20 (#363) Bumps `kotlin` from 1.9.10 to 1.9.20. Updates `org.jetbrains.kotlin.jvm` from 1.9.10 to 1.9.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.10...v1.9.20) Updates `org.jetbrains.kotlin.plugin.serialization` from 1.9.10 to 1.9.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.10...v1.9.20) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin.jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump org.litote.kmongo:kmongo-coroutine-serialization (#364) Bumps [org.litote.kmongo:kmongo-coroutine-serialization](https://github.com/Litote/kmongo) from 4.10.0 to 4.11.0. - [Release notes](https://github.com/Litote/kmongo/releases) - [Commits](https://github.com/Litote/kmongo/compare/kmongo-4.10.0...kmongo-4.11.0) --- updated-dependencies: - dependency-name: org.litote.kmongo:kmongo-coroutine-serialization dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump io.github.oshai:kotlin-logging from 5.1.0 to 5.1.1 (#366) Bumps [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) from 5.1.0 to 5.1.1. - [Release notes](https://github.com/oshai/kotlin-logging/releases) - [Changelog](https://github.com/oshai/kotlin-logging/blob/master/ChangeLog-old.md) - [Commits](https://github.com/oshai/kotlin-logging/compare/5.1.0...5.1.1) --- updated-dependencies: - dependency-name: io.github.oshai:kotlin-logging dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump AButler/upload-release-assets from 2.0 to 3.0 (#367) Bumps [AButler/upload-release-assets](https://github.com/abutler/upload-release-assets) from 2.0 to 3.0. - [Release notes](https://github.com/abutler/upload-release-assets/releases) - [Commits](https://github.com/abutler/upload-release-assets/compare/v2.0...v3.0) --- updated-dependencies: - dependency-name: AButler/upload-release-assets dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump kotlin from 1.9.20 to 1.9.21 (#368) * Bump kotlin from 1.9.20 to 1.9.21 Bumps `kotlin` from 1.9.20 to 1.9.21. Updates `org.jetbrains.kotlin.jvm` from 1.9.20 to 1.9.21 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.21) Updates `org.jetbrains.kotlin.plugin.serialization` from 1.9.20 to 1.9.21 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.21) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin.jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update libs.versions.toml --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: NoComment <67918617+NoComment1105@users.noreply.github.com> * Bump detekt from 1.23.3 to 1.23.4 (#369) Bumps `detekt` from 1.23.3 to 1.23.4. Updates `io.gitlab.arturbosch.detekt:detekt-formatting` from 1.23.3 to 1.23.4 - [Release notes](https://github.com/detekt/detekt/releases) - [Commits](https://github.com/detekt/detekt/compare/v1.23.3...v1.23.4) Updates `io.gitlab.arturbosch.detekt` from 1.23.3 to 1.23.4 - [Release notes](https://github.com/detekt/detekt/releases) - [Commits](https://github.com/detekt/detekt/compare/v1.23.3...v1.23.4) --- updated-dependencies: - dependency-name: io.gitlab.arturbosch.detekt:detekt-formatting dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.gitlab.arturbosch.detekt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ch.qos.logback:logback-classic from 1.4.11 to 1.4.14 (#371) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.11 to 1.4.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.4.11...v_1.4.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/setup-java from 3 to 4 (#372) Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump actions/upload-artifact from 3 to 4 (#374) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update to Kordex 1.7.1-SNAPSHOT * Remove redundant codec * Update dependencies and upgradle (still hilarious) * Update dependencies * Fix cancelling reminders with moderation command. Turns out we were trying to cancel the reminder for the command executor haha very optimal * Add 'Auto-invite Moderator Role' option (#382) * Add 'Auto-invite Moderator Role' option * Fix v7 migration * Fix compilation * Fix unpinning/pinning of old messages causing edit log (#381) * Update dependencies * Bump org.kohsuke:github-api from 1.318 to 1.319 (#385) Bumps [org.kohsuke:github-api](https://github.com/hub4j/github-api) from 1.318 to 1.319. - [Release notes](https://github.com/hub4j/github-api/releases) - [Changelog](https://github.com/hub4j/github-api/blob/main/CHANGELOG.md) - [Commits](https://github.com/hub4j/github-api/compare/github-api-1.318...github-api-1.319) --- updated-dependencies: - dependency-name: org.kohsuke:github-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump kotlin from 1.9.22 to 1.9.23 (#390) Bumps `kotlin` from 1.9.22 to 1.9.23. Updates `org.jetbrains.kotlin.jvm` from 1.9.22 to 1.9.23 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.23/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v1.9.23) Updates `org.jetbrains.kotlin.plugin.serialization` from 1.9.22 to 1.9.23 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.23/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v1.9.23) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin.jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump ch.qos.logback:logback-classic from 1.5.0 to 1.5.3 (#388) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.0 to 1.5.3. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.0...v_1.5.3) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: NoComment <67918617+NoComment1105@users.noreply.github.com> * Create a moderation arguments class to shrink the arguments down * Revert "Create a moderation arguments class to shrink the arguments down" This reverts commit b82f7ef609b70763acab6debe893b7e05115fd4d. * Allow multiple roles to be pinged on auto-threads (#386) * Allow multiple roles to be pinged in auto-threaded channels * Add migration for new issue * Fix doc file * Update dependencies * Implement temporary bans (#391) * Revert "Create a moderation arguments class to shrink the arguments down" This reverts commit b82f7ef609b70763acab6debe893b7e05115fd4d. * Implement temporary bans * Apply review comments - Remove temp-ban remove, incorporate into unban - Remove soft-ban, incorporate into main ban command * Fix typo in command option description * Fix permissions being lost by locked channels when unlocked (#395) * Begin work on storing original permissions when locking channels * Complete work on storing original permission for locked channels * Bump ch.qos.logback:logback-classic from 1.5.3 to 1.5.6 (#398) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.3 to 1.5.6. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.3...v_1.5.6) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump io.github.oshai:kotlin-logging from 6.0.4 to 6.0.9 (#397) Bumps [io.github.oshai:kotlin-logging](https://github.com/oshai/kotlin-logging) from 6.0.4 to 6.0.9. - [Release notes](https://github.com/oshai/kotlin-logging/releases) - [Changelog](https://github.com/oshai/kotlin-logging/blob/master/ChangeLog-old.md) - [Commits](https://github.com/oshai/kotlin-logging/compare/6.0.4...6.0.9) --- updated-dependencies: - dependency-name: io.github.oshai:kotlin-logging dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: NoComment <67918617+NoComment1105@users.noreply.github.com> * Fix lily being unable to unlock channels after she locks them * Hopefully avoid funny embeds appearing when messages don't change properly * Bump org.kohsuke:github-api from 1.321 to 1.322 (#406) Bumps [org.kohsuke:github-api](https://github.com/hub4j/github-api) from 1.321 to 1.322. - [Release notes](https://github.com/hub4j/github-api/releases) - [Changelog](https://github.com/hub4j/github-api/blob/main/CHANGELOG.md) - [Commits](https://github.com/hub4j/github-api/compare/github-api-1.321...github-api-1.322) --- updated-dependencies: - dependency-name: org.kohsuke:github-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update to Kotlin 2 * Update to Java 21 :sunglasses: * Update dependencies and gradle * Update KordEx, Kotlin, Gradle, fix small K2 warnings (#413) * Update KordEx, Kotlin, Gradle, fix small K2 warnings * Yeet unnecessary Gradle task * Configure the about command * Address comments * Update KordEx to 2.1.0 Hopefully this is it! * Update to KordEx 2.2.0, other dependencies * Package restructure (#417) * Refactor package structure attempt 2 * Fix package references :D * Split out config view and clear * More event logging (#410) * Start work on audit-log event based logging * Refactor it all because yes * Create action transferring database to move important information over to the event action * Log Kicks, send quick actions to db, generalise channel creation logs * Start work on channel updates * Add the rest of the channel update fields * Fully set up channel edit logging * Add scheduled event logging * Invite events and Role events * Thread creates and deletes logging + updated kordex * Log timeouts created by lily * Fix timeout logging, yeehaw actual code is done now * Add docs * Fix package references * Remove the logging, however funny it was to me it is not needed * Complete merge * Add configuration, split down into two event categories * Update migrations for utility config * Reduce code duplication and rename some packages * Fix formatting of applied tags * Reduce some duplication here and there * Dm default false (#421) * Theoretically make the DM option default to a certain value. Needs testing :) * Forgot migration :sunglasses: * Well, lets do this I guess --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: TheKodeToad Co-authored-by: Gareth Coles --- .editorconfig | 2 - .github/workflows/gradle.yml | 14 +- .github/workflows/main.yml | 14 +- .github/workflows/release.yml | 14 +- .gitignore | 3 + Dockerfile | 2 +- build.gradle.kts | 20 +- detekt.yml | 3 + docs/commands.md | 87 +- gradle/libs.versions.toml | 27 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +- gradlew.bat | 2 + .../org/hyacinthbots/lilybot/LilyBot.kt | 174 +++- .../hyacinthbots/lilybot/database/Cleanups.kt | 19 +- .../collections/AutoThreadingCollection.kt | 18 +- .../database/collections/ConfigCollection.kt | 2 +- .../collections/GalleryChannelCollection.kt | 2 +- .../database/collections/GithubCollection.kt | 2 +- .../collections/GuildLeaveTimeCollection.kt | 2 +- .../collections/LockedChannelCollection.kt | 71 ++ .../database/collections/MetaCollection.kt | 2 +- .../collections/ModerationActionCollection.kt | 117 +++ .../NewsChannelPublishingCollection.kt | 2 +- .../collections/ReminderCollection.kt | 4 +- .../collections/RoleMenuCollection.kt | 2 +- .../collections/RoleSubscriptionCollection.kt | 2 +- .../database/collections/StatusCollection.kt | 2 +- .../database/collections/TagsCollection.kt | 2 +- .../collections/TemporaryBanCollection.kt | 79 ++ .../database/collections/ThreadsCollection.kt | 2 +- .../database/collections/UptimeCollection.kt | 2 +- .../database/collections/WarnCollection.kt | 2 +- .../collections/WelcomeChannelCollection.kt | 4 +- .../lilybot/database/entities/AdaptedData.kt | 6 +- .../database/entities/AutoThreadingData.kt | 3 +- .../lilybot/database/entities/Config.kt | 8 +- .../database/entities/LockedChannelData.kt | 21 + .../database/entities/ModerationActionData.kt | 66 ++ .../database/entities/TemporaryBanData.kt | 25 + .../lilybot/database/migrations/Migrator.kt | 8 +- .../database/migrations/config/configV7.kt | 38 + .../database/migrations/main/mainV10.kt | 15 + .../database/storage/MongoDBDataAdapter.kt | 8 +- .../lilybot/extensions/config/Config.kt | 860 ------------------ .../extensions/config/ConfigExtension.kt | 32 + .../config/commands/ConfigClearCommand.kt | 150 +++ .../config/commands/ConfigViewCommand.kt | 180 ++++ .../extensions/config/utils/ConfigEmbeds.kt | 160 ++++ .../extensions/logging/config/LoggingArgs.kt | 43 + .../logging/config/LoggingCommand.kt | 165 ++++ .../logging/config/PublicLoggingModal.kt | 24 + .../events}/GuildLogging.kt | 6 +- .../{ => logging}/events/MemberLogging.kt | 35 +- .../{ => logging}/events/MessageDelete.kt | 16 +- .../{ => logging}/events/MessageEdit.kt | 35 +- .../moderation/ModerationActions.kt | 9 - .../{ => commands}/ClearCommands.kt | 28 +- .../{ => commands}/LockingCommands.kt | 119 +-- .../commands}/ModUtilities.kt | 54 +- .../{ => commands}/ModerationCommands.kt | 636 +++++++------ .../moderation/{ => commands}/Report.kt | 28 +- .../moderation/config/ModerationArgs.kt | 56 ++ .../moderation/config/ModerationCommand.kt | 131 +++ .../moderation/events/ModerationEvents.kt | 304 +++++++ .../moderation/utils/ModerationAction.kt | 13 + .../{events => threads}/AutoThreading.kt | 176 +++- .../{events => threads}/ModThreadInviting.kt | 12 +- .../{util => threads}/ThreadControl.kt | 32 +- .../commands}/GalleryChannel.kt | 30 +- .../{util => utility/commands}/Github.kt | 77 +- .../commands}/GuildAnnouncements.kt | 20 +- .../commands}/InfoCommands.kt | 91 +- .../commands}/NewsChannelPublishing.kt | 32 +- .../commands}/PublicUtilities.kt | 38 +- .../{util => utility/commands}/Reminders.kt | 58 +- .../{util => utility/commands}/RoleMenu.kt | 58 +- .../commands}/StartupHooks.kt | 16 +- .../{util => utility/commands}/StatusPing.kt | 10 +- .../{util => utility/commands}/Tags.kt | 42 +- .../extensions/utility/config/UtilityArgs.kt | 29 + .../utility/config/UtilityCommand.kt | 74 ++ .../utility/events/UtilityEvents.kt | 677 ++++++++++++++ .../lilybot/utils/ResponseHelper.kt | 34 +- .../lilybot/utils/_ConfigUtils.kt | 91 +- .../hyacinthbots/lilybot/utils/_Constants.kt | 4 +- .../lilybot/utils/_PermissionUtils.kt | 54 +- .../org/hyacinthbots/lilybot/utils/_Utils.kt | 21 +- 89 files changed, 3818 insertions(+), 1849 deletions(-) create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LockedChannelCollection.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ModerationActionCollection.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TemporaryBanCollection.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/entities/LockedChannelData.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ModerationActionData.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TemporaryBanData.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV10.kt delete mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigExtension.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigClearCommand.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigViewCommand.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/utils/ConfigEmbeds.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingArgs.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingCommand.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/PublicLoggingModal.kt rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{config => logging/events}/GuildLogging.kt (88%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{ => logging}/events/MemberLogging.kt (75%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{ => logging}/events/MessageDelete.kt (89%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{ => logging}/events/MessageEdit.kt (75%) delete mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationActions.kt rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/{ => commands}/ClearCommands.kt (89%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/{ => commands}/LockingCommands.kt (76%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => moderation/commands}/ModUtilities.kt (91%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/{ => commands}/ModerationCommands.kt (66%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/{ => commands}/Report.kt (88%) create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationArgs.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationCommand.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/events/ModerationEvents.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/utils/ModerationAction.kt rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{events => threads}/AutoThreading.kt (77%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{events => threads}/ModThreadInviting.kt (81%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => threads}/ThreadControl.kt (92%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/GalleryChannel.kt (88%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/Github.kt (87%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/GuildAnnouncements.kt (87%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/InfoCommands.kt (50%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/NewsChannelPublishing.kt (88%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/PublicUtilities.kt (89%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/Reminders.kt (91%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/RoleMenu.kt (93%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/StartupHooks.kt (82%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/StatusPing.kt (70%) rename src/main/kotlin/org/hyacinthbots/lilybot/extensions/{util => utility/commands}/Tags.kt (93%) create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityArgs.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityCommand.kt create mode 100644 src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/events/UtilityEvents.kt diff --git a/.editorconfig b/.editorconfig index 6887b9a8..04a71793 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,8 +12,6 @@ indent_style = tab [*.kt] indent_style = tab -ij_kotlin_name_count_to_use_star_import = 99999 -ij_kotlin_name_count_to_use_star_import_for_members = 99999 [*.properties] indent_style = space diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 9408667c..4954de0d 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -14,21 +14,21 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 17 + java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Artifacts - uses: gradle/gradle-build-action@v3 - with: - arguments: build --stacktrace - gradle-home-cache-cleanup: true + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build Project + run: ./gradlew build --stacktrace - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 95758155..6b6b9837 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,21 +12,21 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 17 + java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Artifacts - uses: gradle/gradle-build-action@v3 - with: - arguments: build --stacktrace - gradle-home-cache-cleanup: true + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build Project + run: ./gradlew build --stacktrace - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d30bf7af..9dbf6e9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,21 +13,21 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: 17 + java-version: 21 - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Artifacts - uses: gradle/gradle-build-action@v3 - with: - arguments: build --stacktrace - gradle-home-cache-cleanup: true + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build Project + run: ./gradlew build --stacktrace - name: Upload artifacts GitHub uses: AButler/upload-release-assets@v3.0 diff --git a/.gitignore b/.gitignore index 16f36025..9d4af58b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ out/ classes/ +# kotlin +.kotlin/ + # eclipse *.launch diff --git a/Dockerfile b/Dockerfile index 09b9f635..6ac53d0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM openjdk:17-jdk-slim +FROM openjdk:21-jdk-slim RUN mkdir /bot RUN mkdir /data diff --git a/build.gradle.kts b/build.gradle.kts index 3bc9a704..40ba2bbd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,9 @@ plugins { } group = "org.hyacinthbots.lilybot" -version = "4.9.0" +version = "5.0.0" + +val className = "org.hyacinthbots.lilybot.LilyBotKt" repositories { mavenCentral() @@ -30,6 +32,11 @@ repositories { url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") } + maven { + name = "Kord Extensions (Snapshots)" + url = uri("https://snapshots-repo.kordex.dev") + } + maven { name = "JitPack" url = uri("https://jitpack.io") @@ -61,7 +68,7 @@ dependencies { } application { - mainClass.set("org.hyacinthbots.lilybot.LilyBotKt") + mainClass.set(className) } gitHooks { @@ -73,16 +80,21 @@ gitHooks { tasks { withType { compilerOptions { - jvmTarget.set(JvmTarget.fromTarget("17")) + jvmTarget.set(JvmTarget.fromTarget("21")) languageVersion.set(KotlinVersion.fromVersion(libs.plugins.kotlin.get().version.requiredVersion.substringBeforeLast("."))) incremental = true freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn") } } + java { // Should match the Kotlin compiler options ideally + sourceCompatibility = JavaVersion.toVersion("21") + targetCompatibility = JavaVersion.toVersion("21") + } + jar { manifest { - attributes("Main-Class" to "org.hyacinthbots.lilybot.LilyBotKt") + attributes("Main-Class" to className) } } diff --git a/detekt.yml b/detekt.yml index 4fa127fc..2f5db37a 100644 --- a/detekt.yml +++ b/detekt.yml @@ -175,6 +175,7 @@ formatting: active: true autoCorrect: true NoWildcardImports: + active: false packagesToUseImportOnDemandProperty: "" PackageName: active: true @@ -361,3 +362,5 @@ style: active: true UseRequireNotNull: active: true + WildcardImport: + active: false diff --git a/docs/commands.md b/docs/commands.md index b5e9e29d..37d4a9ce 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,5 +1,11 @@ ## Slash Commands +### Command name: `about` +**Description**: Learn about this bot + +* Arguments: +None +--- ### Command name: `auto-threading enable` **Description**: Automatically create a thread for each message sent in this channel. @@ -37,6 +43,28 @@ None * **Arguments**: * `channel` - The channel to view the auto-threading settings for. - Channel +--- +### Command name: `auto-threading add-roles` +**Description**: Add extra to threads in auto-threaded channels + +**Additional Information**: This command will add roles to be pinged alongside the default ping role for this auto-threaded channel + +**Required Member Permissions**: Manage Channels + +* **Arguments**: + * `role` - A role to invite to threads in this channel - Optional Role + +--- +### Command name: `auto-threading remove-roles` +**Description**: Remove extra from threads in auto-threaded channels + +**Additional Information**: This command will remove roles that have been added to be pinged alongside the default ping role for this auto-threaded channel + +**Required Member Permissions**: Manage Channels + +* **Arguments**: + * `role` - A role to invite to threads in this channel - Optional Role + --- ### Command name: `clear count` **Description**: Clear a specific count of messages @@ -80,21 +108,6 @@ None * `before` - The ID of the message to clear before - Snowflake * `author` - The author of the messages to clear - Optional User ---- -### Command name: `config moderation` -**Description**: Configure Lily's moderation system - -**Required Member Permissions**: Manage Server - -* **Arguments**: - * `enable-moderation` - Whether to enable the moderation system - Boolean - * `moderator-role` - The role of your moderators, used for pinging in message logs. - Optional Role - * `action-log` - The channel used to store moderator actions. - Optional Channel - * `quick-timeout-length` - The length of timeouts to use for quick timeouts - Coalescing Optional Duration - * `warn-auto-punishments` - Whether to automatically punish users for reach a certain threshold on warns - Optional Boolean - * `log-publicly` - Whether to log moderation publicly or not. - Optional Boolean - * `ban-dm-message` - A custom message to send to users when they are banned. - Optional String - --- ### Command name: `config logging` **Description**: Configure Lily's logging system @@ -110,6 +123,22 @@ None * `member-log` - The channel for logging members joining and leaving the guild - Optional Channel * `public-member-log` - The channel for the public logging of members joining and leaving the guild - Optional Channel +--- +### Command name: `config moderation` +**Description**: Configure Lily's moderation system + +**Required Member Permissions**: Manage Server + +* **Arguments**: + * `enable-moderation` - Whether to enable the moderation system - Boolean + * `moderator-role` - The role of your moderators, used for pinging in message logs. - Optional Role + * `action-log` - The channel used to store moderator actions. - Optional Channel + * `quick-timeout-length` - The length of timeouts to use for quick timeouts - Coalescing Optional Duration + * `warn-auto-punishments` - Whether to automatically punish users for reach a certain threshold on warns - Optional Boolean + * `log-publicly` - Whether to log moderation publicly or not. - Optional Boolean + * `ban-dm-message` - A custom message to send to users when they are banned. - Optional String + * `auto-invite-moderator-role` - Silently ping moderators to invite them to new threads. - Optional Boolean + --- ### Command name: `config utility` **Description**: Configure Lily's utility settings @@ -211,12 +240,6 @@ None ### Command name: `help` **Description**: Get help with using Lily! -* Arguments: -None ---- -### Command name: `info` -**Description**: Learn about Lily, and get uptime data! - * Arguments: None --- @@ -322,22 +345,32 @@ None * `user` - Person to ban - User * `delete-message-days` - The number of days worth of messages to delete - Int * `reason` - The reason for the ban - Defaulting String + * `soft-ban` - Weather to soft-ban this user (unban them once messages are deleted) - Defaulting Boolean * `dm` - Whether to send a direct message to the user about the ban - Defaulting Boolean * `image` - An image you'd like to provide as extra context for the action - Optional Attachment --- -### Command name: `soft-ban` -**Description**: Soft-bans a user. +### Command name: `temp-ban add` +**Description**: Temporarily bans a user **Required Member Permissions**: Ban Members -* Arguments: - * `user` - Person to Soft ban - User - * `delete-message-days` - The number of days worth of messages to delete - Optional Int/Long +* **Arguments**: + * `user` - Person to ban - User + * `delete-message-days` - The number of days worth of messages to delete - Int + * `duration` - The duration of the temporary ban. - Coalescing Duration * `reason` - The reason for the ban - Defaulting String - * `dm` - Whether to send a direct message to the user about the soft-ban - Defaulting Boolean + * `dm` - Whether to send a direct message to the user about the ban - Defaulting Boolean * `image` - An image you'd like to provide as extra context for the action - Optional Attachment +--- +### Command name: `temp-ban view-all` +**Description**: View all temporary bans for this guild + +**Required Member Permissions**: Ban Members + +* **Arguments**: +None --- ### Command name: `unban` **Description**: Unbans a user. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 852daae7..ed2ea567 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,27 @@ [versions] # Plugins -kotlin = "1.9.22" +kotlin = "2.0.10" shadow = "8.1.1" -detekt = "1.23.5" +detekt = "1.23.7" git-hooks = "0.0.2" grgit = "5.2.2" blossom = "2.1.0" # Libraries -kord-extensions = "1.8.0-20240319.115836-21" -logging = "6.0.3" -logback = "1.5.0" -github-api = "1.318" -kmongo = "4.11.0" -docgenerator = "0.1.5-SNAPSHOT" +kord-extensions = "2.2.0-20240824.203242-3" +logging = "7.0.0" +logback = "1.5.8" +github-api = "1.325" +kmongo = "5.1.0" +docgenerator = "0.2.2-SNAPSHOT" [libraries] -kord-extensions-core = { module = "com.kotlindiscord.kord.extensions:kord-extensions", version.ref = "kord-extensions" } -kord-extensions-phishing = { module = "com.kotlindiscord.kord.extensions:extra-phishing", version.ref = "kord-extensions" } -kord-extensions-pluralkit = { module = "com.kotlindiscord.kord.extensions:extra-pluralkit", version.ref = "kord-extensions"} -kord-extensions-unsafe = { module = "com.kotlindiscord.kord.extensions:unsafe", version.ref = "kord-extensions"} -kord-extensions-welcome = { module = "com.kotlindiscord.kord.extensions:extra-welcome", version.ref = "kord-extensions"} +kord-extensions-core = { module = "dev.kordex:kord-extensions", version.ref = "kord-extensions" } +kord-extensions-phishing = { module = "dev.kordex.modules:func-phishing", version.ref = "kord-extensions" } +kord-extensions-pluralkit = { module = "dev.kordex.modules:pluralkit", version.ref = "kord-extensions"} +kord-extensions-unsafe = { module = "dev.kordex.modules:dev-unsafe", version.ref = "kord-extensions"} +kord-extensions-welcome = { module = "dev.kordex.modules:func-welcome", version.ref = "kord-extensions"} + kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logging = { module = "io.github.oshai:kotlin-logging", version.ref = "logging" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 34592 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 delta 34555 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30db..9d21a218 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt index 6b2d0815..e1be17d7 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/LilyBot.kt @@ -2,48 +2,60 @@ package org.hyacinthbots.lilybot -import com.kotlindiscord.kord.extensions.ExtensibleBot -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.modules.extra.phishing.DetectionAction -import com.kotlindiscord.kord.extensions.modules.extra.phishing.extPhishing -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.extPluralKit -import com.kotlindiscord.kord.extensions.modules.extra.welcome.welcomeChannel import dev.kord.common.entity.Permission import dev.kord.gateway.Intent import dev.kord.gateway.PrivilegedIntent +import dev.kord.rest.builder.message.actionRow +import dev.kord.rest.builder.message.embed +import dev.kordex.core.ExtensibleBot +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import dev.kordex.data.api.DataCollection +import dev.kordex.modules.func.phishing.DetectionAction +import dev.kordex.modules.func.phishing.extPhishing +import dev.kordex.modules.func.welcome.welcomeChannel +import dev.kordex.modules.pluralkit.extPluralKit import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import org.hyacinthbots.docgenerator.docsGenerator import org.hyacinthbots.docgenerator.enums.CommandTypes import org.hyacinthbots.docgenerator.enums.SupportedFileFormat +import org.hyacinthbots.lilybot.database.collections.UptimeCollection import org.hyacinthbots.lilybot.database.collections.WelcomeChannelCollection import org.hyacinthbots.lilybot.database.storage.MongoDBDataAdapter -import org.hyacinthbots.lilybot.extensions.config.Config +import org.hyacinthbots.lilybot.extensions.config.ConfigExtension import org.hyacinthbots.lilybot.extensions.config.ConfigOptions -import org.hyacinthbots.lilybot.extensions.config.GuildLogging -import org.hyacinthbots.lilybot.extensions.events.AutoThreading -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.ModThreadInviting -import org.hyacinthbots.lilybot.extensions.moderation.ClearCommands -import org.hyacinthbots.lilybot.extensions.moderation.LockingCommands -import org.hyacinthbots.lilybot.extensions.moderation.ModerationCommands -import org.hyacinthbots.lilybot.extensions.moderation.Report -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.NewsChannelPublishing -import org.hyacinthbots.lilybot.extensions.util.PublicUtilities -import org.hyacinthbots.lilybot.extensions.util.Reminders -import org.hyacinthbots.lilybot.extensions.util.RoleMenu -import org.hyacinthbots.lilybot.extensions.util.StartupHooks -import org.hyacinthbots.lilybot.extensions.util.StatusPing -import org.hyacinthbots.lilybot.extensions.util.Tags -import org.hyacinthbots.lilybot.extensions.util.ThreadControl +import org.hyacinthbots.lilybot.extensions.logging.events.GuildLogging +import org.hyacinthbots.lilybot.extensions.logging.events.MemberLogging +import org.hyacinthbots.lilybot.extensions.logging.events.MessageDelete +import org.hyacinthbots.lilybot.extensions.logging.events.MessageEdit +import org.hyacinthbots.lilybot.extensions.moderation.commands.ClearCommands +import org.hyacinthbots.lilybot.extensions.moderation.commands.LockingCommands +import org.hyacinthbots.lilybot.extensions.moderation.commands.ModUtilities +import org.hyacinthbots.lilybot.extensions.moderation.commands.ModerationCommands +import org.hyacinthbots.lilybot.extensions.moderation.commands.Report +import org.hyacinthbots.lilybot.extensions.moderation.events.ModerationEvents +import org.hyacinthbots.lilybot.extensions.threads.AutoThreading +import org.hyacinthbots.lilybot.extensions.threads.ModThreadInviting +import org.hyacinthbots.lilybot.extensions.threads.ThreadControl +import org.hyacinthbots.lilybot.extensions.utility.commands.GalleryChannel +import org.hyacinthbots.lilybot.extensions.utility.commands.Github +import org.hyacinthbots.lilybot.extensions.utility.commands.GuildAnnouncements +import org.hyacinthbots.lilybot.extensions.utility.commands.InfoCommands +import org.hyacinthbots.lilybot.extensions.utility.commands.NewsChannelPublishing +import org.hyacinthbots.lilybot.extensions.utility.commands.PublicUtilities +import org.hyacinthbots.lilybot.extensions.utility.commands.Reminders +import org.hyacinthbots.lilybot.extensions.utility.commands.RoleMenu +import org.hyacinthbots.lilybot.extensions.utility.commands.StartupHooks +import org.hyacinthbots.lilybot.extensions.utility.commands.StatusPing +import org.hyacinthbots.lilybot.extensions.utility.commands.Tags +import org.hyacinthbots.lilybot.extensions.utility.events.UtilityEvents +import org.hyacinthbots.lilybot.internal.BuildInfo import org.hyacinthbots.lilybot.utils.BOT_TOKEN import org.hyacinthbots.lilybot.utils.ENVIRONMENT +import org.hyacinthbots.lilybot.utils.HYACINTH_GITHUB import org.hyacinthbots.lilybot.utils.database import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms import org.kohsuke.github.GitHub @@ -60,6 +72,8 @@ val docFile = Path("./docs/commands.md") suspend fun main() { val bot = ExtensibleBot(BOT_TOKEN) { + dataCollectionMode = DataCollection.None + database(true) dataAdapter(::MongoDBDataAdapter) @@ -73,11 +87,89 @@ suspend fun main() { +Intent.GuildMembers } + about { + ephemeral = false + general { + message { locale -> + embed { + title = "Info about LilyBot" + + thumbnail { + url = + "https://github.com/HyacinthBots/LilyBot/blob/main/docs/lily-logo-transparent.png?raw=true" + } + + description = + "Lily is a FOSS multi-purpose bot for Discord created by the HyacinthBots organization. " + + "Use `/help` for support or `/invite` to get an invite link." + field { + name = "How can I support the continued development of Lily?" + + value = "Lily is developed primarily by NoComment#6411 in their free time. Hyacinth " + + "doesn't have the resources to invest in hosting, so financial donations via " + + "[Buy Me a Coffee](https://buymeacoffee.com/Hyacinthbots) help keep Lily afloat. " + + "Currently, we run lily on a Hetzner cloud server, which we can afford in our " + + "current situation. We also have domain costs for our website.\n\nContributions of " + + "code & documentation are also incredibly appreciated, and you can read our " + + "[contributing guide]($HYACINTH_GITHUB/LilyBot/blob/main/CONTRIBUTING.md) or " + + "[development guide]($HYACINTH_GITHUB/LilyBot/blob/main/docs/development-guide.md) " + + "to get started." + } + + field { + name = "Version" + // To avoid IntelliJ shouting about build errors, use https://plugins.jetbrains.com/plugin/9407-pebble + value = "${BuildInfo.LILY_VERSION} (${BuildInfo.BUILD_ID})" + inline = true + } + + field { + name = "Up Since" + value = "${ + UptimeCollection().get()?.onTime?.toLocalDateTime(TimeZone.UTC) + ?.time.toString().split(".")[0] + } ${UptimeCollection().get()?.onTime?.toLocalDateTime(TimeZone.UTC)?.date} UTC\n " + + "(${UptimeCollection().get()?.onTime?.toDiscord(TimestampType.RelativeTime) ?: "??"})" + inline = true + } + + field { + name = "Useful links" + value = + "Website: Coming Soon™️\n" + + "GitHub: ${HYACINTH_GITHUB}\n" + + "Buy Me a Coffee: https://buymeacoffee.com/HyacinthBots\n" + + "Twitter: https://twitter.com/HyacinthBots\n" + + "Email: `hyacinthbots@outlook.com`\n" + + "Discord: https://discord.gg/hy2329fcTZ" + } + } + + actionRow { + linkButton( + "https://discord.com/api/oauth2/authorize?client_id=876278900836139008&" + + "permissions=1151990787078&scope=bot%20applications.commands" + ) { + label = "extensions.about.buttons.invite" + } + + linkButton("$HYACINTH_GITHUB/LilyBot/blob/main/docs/privacy-policy.md") { + label = "Privacy Policy" + } + + linkButton("$HYACINTH_GITHUB/.github/blob/main/terms-of-service.md") { + label = "Terms of Service" + } + } + } + } + } + // Add the extensions to the bot extensions { add(::AutoThreading) add(::ClearCommands) - add(::Config) + add(::ConfigExtension) add(::GalleryChannel) add(::Github) add(::GuildAnnouncements) @@ -87,6 +179,7 @@ suspend fun main() { add(::MemberLogging) add(::MessageDelete) add(::MessageEdit) + add(::ModerationEvents) add(::ModThreadInviting) add(::ModUtilities) add(::ModerationCommands) @@ -99,22 +192,23 @@ suspend fun main() { add(::StatusPing) add(::Tags) add(::ThreadControl) + add(::UtilityEvents) /* The welcome channel extension allows users to designate a YAML file to create a channel with a variety of pre-built blocks. */ - welcomeChannel(WelcomeChannelCollection()) { - staffCommandCheck { - hasPermission(Permission.BanMembers) - } + welcomeChannel(WelcomeChannelCollection()) { + staffCommandCheck { + hasPermission(Permission.BanMembers) + } - getLogChannel { _, guild -> - getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild) - } + getLogChannel { _, guild -> + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild) + } - refreshDuration = 5.minutes - } + refreshDuration = 5.minutes + } /* The anti-phishing extension automatically deletes and logs scam links. It also allows users to check links diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt index 7d5671ac..0d82cb81 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/Cleanups.kt @@ -1,15 +1,14 @@ package org.hyacinthbots.lilybot.database -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.core.Kord import dev.kord.core.behavior.getChannelOfOrNull import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.koin.KordExKoinComponent +import io.github.oshai.kotlinlogging.KotlinLogging 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.GithubCollection +import org.hyacinthbots.lilybot.database.collections.LockedChannelCollection import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.NewsChannelPublishingCollection @@ -53,17 +52,19 @@ object Cleanups : KordExKoinComponent { * @since 3.2.0 */ suspend fun cleanupGuildData(kord: Kord) { - cleanupsLogger.info("Starting guild cleanup...") + cleanupsLogger.info { "Starting guild cleanup..." } val leaveTimeData = guildLeaveTimeCollection.find().toList() var deletedGuildData = 0 + val now = Clock.System.now() leaveTimeData.forEach { // Calculate the time since Lily left the guild. - val leaveDuration = Clock.System.now() - it.guildLeaveTime + val leaveDuration = now - it.guildLeaveTime if (leaveDuration.inWholeDays > 30) { // If the bot has been out of the guild for more than 30 days, delete any related data. GithubCollection().removeDefaultRepo(it.guildId) + LockedChannelCollection().removeAllLockedChannels(it.guildId) LoggingConfigCollection().clearConfig(it.guildId) ModerationConfigCollection().clearConfig(it.guildId) NewsChannelPublishingCollection().clearAutoPublishingForGuild(it.guildId) @@ -80,7 +81,7 @@ object Cleanups : KordExKoinComponent { } } - cleanupsLogger.info("Deleted old data for $deletedGuildData guilds from the database") + cleanupsLogger.info { "Deleted old data for $deletedGuildData guilds from the database" } } /** @@ -90,7 +91,7 @@ object Cleanups : KordExKoinComponent { * @since 3.2.0 */ suspend fun cleanupThreadData(kordInstance: Kord) { - cleanupsLogger.info("Starting thread cleanup...") + cleanupsLogger.info { "Starting thread cleanup..." } val threads = threadDataCollection.find().toList() var deletedThreads = 0 for (it in threads) { @@ -114,6 +115,6 @@ object Cleanups : KordExKoinComponent { continue } } - cleanupsLogger.info("Deleted $deletedThreads old threads from the database") + cleanupsLogger.info { "Deleted $deletedThreads old threads from the database" } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt index db1c6104..c7d7489c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/AutoThreadingCollection.kt @@ -1,11 +1,12 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.AutoThreadingData import org.koin.core.component.inject import org.litote.kmongo.eq +import org.litote.kmongo.setValue /** * This class contains the functions for interacting with the [AutoThreading Database][AutoThreadingData]. This @@ -58,6 +59,21 @@ class AutoThreadingCollection : KordExKoinComponent { collection.insertOne(inputAutoThreadData) } + /** + * Updates the extra roles on an auto-threaded channel. + * + * @param inputChannelId The channel to update extra roles for + * @param newRoleIds The new list of role IDs to set + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun updateExtraRoles(inputChannelId: Snowflake, newRoleIds: List) { + collection.updateOne( + AutoThreadingData::channelId eq inputChannelId, + setValue(AutoThreadingData::extraRoleIds, newRoleIds) + ) + } + /** * Deletes an auto thread based off of the [inputChannelId]. * diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt index 61752bca..2702c26d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ConfigCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.LoggingConfigData import org.hyacinthbots.lilybot.database.entities.ModerationConfigData diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt index c9b75e79..ee27c237 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GalleryChannelCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GalleryChannelData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt index 98732e2f..0df3551c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GithubCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GithubData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt index 60b21c6b..0213c191 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/GuildLeaveTimeCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import kotlinx.datetime.Instant import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.GuildLeaveTimeData diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LockedChannelCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LockedChannelCollection.kt new file mode 100644 index 00000000..cf783ae5 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/LockedChannelCollection.kt @@ -0,0 +1,71 @@ +package org.hyacinthbots.lilybot.database.collections + +import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent +import org.hyacinthbots.lilybot.database.Database +import org.hyacinthbots.lilybot.database.entities.LockedChannelData +import org.koin.core.component.inject +import org.litote.kmongo.eq + +/** + * This class contains the function for interacting with the [Locked Channel Database][LockedChannelData]. This class + * contains functions for getting, setting and removing locked channels + * + * @since 5.0.0 + * @see addLockedChannel + * @see removeLockedChannel + * @see removeAllLockedChannels + * @see getLockedChannel + */ +class LockedChannelCollection : KordExKoinComponent { + private val db: Database by inject() + + @PublishedApi + internal val collection = db.mainDatabase.getCollection() + + /** + * Adds the data about a newly locked channel to the database. + * + * @param data The [LockedChannelData] for the locked channel + * + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun addLockedChannel(data: LockedChannelData) = collection.insertOne(data) + + /** + * Removes a locked channel from the database. This is usually called when a channel is unlocked. + * + * @param inputGuildId The ID of the guild the locked channel is in + * @param inputChannelId The ID of the locked channel itself + * + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun removeLockedChannel(inputGuildId: Snowflake, inputChannelId: Snowflake) = + collection.deleteOne(LockedChannelData::guildId eq inputGuildId, LockedChannelData::channelId eq inputChannelId) + + /** + * Removes all locked channels for a given guild from the database. Used in guild cleanups. + * + * @param inputGuildId The ID of the guild to remove the locked channels from + * + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun removeAllLockedChannels(inputGuildId: Snowflake) = + collection.deleteMany(LockedChannelData::guildId eq inputGuildId) + + /** + * Gets a locked channel based on the input parameters. + * + * @param inputGuildId The ID of the guild the locked channel is in + * @param inputChannelId The ID of the channel to get the locked data for + * @return A [LockedChannelData] object for the given channel + * + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun getLockedChannel(inputGuildId: Snowflake, inputChannelId: Snowflake): LockedChannelData? = + collection.findOne(LockedChannelData::guildId eq inputGuildId, LockedChannelData::channelId eq inputChannelId) +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt index 01ae94aa..79cc1733 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/MetaCollection.kt @@ -1,6 +1,6 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.ConfigMetaData import org.hyacinthbots.lilybot.database.entities.MainMetaData diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ModerationActionCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ModerationActionCollection.kt new file mode 100644 index 00000000..da0dba27 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ModerationActionCollection.kt @@ -0,0 +1,117 @@ +package org.hyacinthbots.lilybot.database.collections + +import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent +import org.hyacinthbots.lilybot.database.Database +import org.hyacinthbots.lilybot.database.entities.ActionData +import org.hyacinthbots.lilybot.database.entities.ModerationActionData +import org.hyacinthbots.lilybot.extensions.moderation.utils.ModerationAction +import org.koin.core.component.inject +import org.litote.kmongo.eq + +/** + * This class contains the function for interacting with the [Moderation Action Database][ModerationActionData]. This + * class contains functions for getting, setting, removing and ignoring actions + * + * @since 5.0.0 + * @see addAction + * @see removeAction + * @see getAction + * @see declareActionToIgnore + * @see shouldIgnoreAction + */ +class ModerationActionCollection : KordExKoinComponent { + private val db: Database by inject() + + @PublishedApi + internal val collection = db.mainDatabase.getCollection() + + /** + * Adds an action that occurred. + * + * @param action The type of action you're adding + * @param guildId The ID of the guild the action occurred in + * @param targetUserId The ID of the user this action happened to + * @param data The [ActionData] for the action + * @param ignore Whether to ignore the action or not. Defaults to false + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun addAction( + action: ModerationAction, + guildId: Snowflake, + targetUserId: Snowflake, + data: ActionData, + ignore: Boolean = false + ) = collection.insertOne(ModerationActionData(action, guildId, targetUserId, data, ignore)) + + /** + * Removes an action that occurred. + * + * @param type The type of action you're removing + * @param guildId The ID of the guild the action occurred in + * @param targetUserId The ID of the user this action happened to + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun removeAction(type: ModerationAction, guildId: Snowflake, targetUserId: Snowflake) = + collection.deleteOne( + ModerationActionData::actionType eq type, + ModerationActionData::guildId eq guildId, + ModerationActionData::targetUserId eq targetUserId + ) + + /** + * Gets an action that occurred. + * + * @param type The type of action you're looking for + * @param guildId The ID of the guild the action occurred in + * @param targetUserId The ID of the user this action happened to + * @return The [data][ModerationActionData] for the event. Can be null if there is no action. + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun getAction( + type: ModerationAction, + guildId: Snowflake, + targetUserId: Snowflake + ): ModerationActionData? = + collection.findOne( + ModerationActionData::actionType eq type, + ModerationActionData::guildId eq guildId, + ModerationActionData::targetUserId eq targetUserId + ) + + /** + * Sets an action as ignored. Convenience function more than anything + * + * @param type The type of action you're looking for + * @param guildId The ID of the guild the action occurred in + * @param targetUserId The ID of the user this action happened to + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun declareActionToIgnore(type: ModerationAction, guildId: Snowflake, targetUserId: Snowflake) = + addAction(type, guildId, targetUserId, ActionData(null, null, null, null, null, null, null), true) + + /** + * Checks if an action should be ignored or not. Convenience function more than anything. + * + * @param type The type of action you're looking for + * @param guildId The ID of the guild the action occurred in + * @param targetUserId The ID of the user this action happened to + * @return True if the action should be ignored, false if otherwise + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun shouldIgnoreAction( + type: ModerationAction, + guildId: Snowflake, + targetUserId: Snowflake + ): Boolean? = + collection.findOne( + ModerationActionData::actionType eq type, + ModerationActionData::guildId eq guildId, + ModerationActionData::targetUserId eq targetUserId + )?.ignore +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt index 2851da56..84ab4a51 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/NewsChannelPublishingCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.NewsChannelPublishingData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt index 90e719d2..e32aa144 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ReminderCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone import kotlinx.datetime.plus @@ -11,7 +11,7 @@ import org.koin.core.component.inject import org.litote.kmongo.eq /** - * This class contains the functions for interacting with []the reminder database][ReminderData]. This + * This class contains the functions for interacting with [the reminder database][ReminderData]. This * class contains functions for setting reminders, getting reminders based of various parameters, removing reminders and * repeating them. * diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt index f8319ee5..037c5d11 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleMenuCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.RoleMenuData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt index 027e5272..d103759e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/RoleSubscriptionCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.RoleSubscriptionData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt index aaccbba8..c6554147 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/StatusCollection.kt @@ -1,6 +1,6 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.StatusData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt index 89584046..a006389a 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TagsCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.TagsData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TemporaryBanCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TemporaryBanCollection.kt new file mode 100644 index 00000000..01fb1de6 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/TemporaryBanCollection.kt @@ -0,0 +1,79 @@ +package org.hyacinthbots.lilybot.database.collections + +import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent +import org.hyacinthbots.lilybot.database.Database +import org.hyacinthbots.lilybot.database.entities.TemporaryBanData +import org.koin.core.component.inject +import org.litote.kmongo.eq + +/** + * This class contains the functions for interacting with [the temporary ban database][TemporaryBanData]. This class + * contains functions for settings temporary bans, getting temporary bans based off of various parameters and removing + * them. + * + * @since 5.0.0 + * @see getAllTempBans + * @see getTempBansForGuild + * @see getUserTempBan + * @see setTempBan + * @see removeTempBan + */ +class TemporaryBanCollection : KordExKoinComponent { + private val db: Database by inject() + + @PublishedApi + internal val collection = db.mainDatabase.getCollection() + + /** + * Gets all the temporary bans currently in the database. + * + * @return A list of temporary bans in the database + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun getAllTempBans(): List = collection.find().toList() + + /** + * Gets all the temporary bans for a given guild. + * + * @param guildId The ID of the guild to get the bans in + * @return A list of Temporary bans for the given [guildId] + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun getTempBansForGuild(guildId: Snowflake): List = + collection.find(TemporaryBanData::guildId eq guildId).toList() + + /** + * Gets a temporary ban for a given user. + * + * @param guildId The ID of the guild the temporary ban occurred in + * @param bannedUserId The ID of the user that was temporarily banned + * @return The [TemporaryBanData] for the [bannedUserId] + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun getUserTempBan(guildId: Snowflake, bannedUserId: Snowflake): TemporaryBanData? = + collection.findOne(TemporaryBanData::guildId eq guildId, TemporaryBanData::bannedUserId eq bannedUserId) + + /** + * Sets a temporary ban. + * + * @param tempBanData The data for the temporary ban + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun setTempBan(tempBanData: TemporaryBanData) = collection.insertOne(tempBanData) + + /** + * Removes the temporary ban for a user in a given guild. This is called once a temporary ban is completed. + * + * @param guildId The guild the temporary ban is being removed from + * @param bannedUserId The ID of the user to remove the temporary ban from + * @author NoComment1105 + * @since 5.0.0 + */ + suspend inline fun removeTempBan(guildId: Snowflake, bannedUserId: Snowflake) = + collection.deleteOne(TemporaryBanData::guildId eq guildId, TemporaryBanData::bannedUserId eq bannedUserId) +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt index c65ad4ee..a66899a3 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/ThreadsCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.ThreadData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt index f7f4057e..3cae0c85 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/UptimeCollection.kt @@ -1,6 +1,6 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent +import dev.kordex.core.koin.KordExKoinComponent import kotlinx.datetime.Instant import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.UptimeData diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt index cb64da4d..602363f5 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WarnCollection.kt @@ -1,7 +1,7 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.WarnData import org.koin.core.component.inject diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt index 2b98e2e2..da0cd6c9 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/collections/WelcomeChannelCollection.kt @@ -7,14 +7,14 @@ package org.hyacinthbots.lilybot.database.collections -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent import dev.kord.common.entity.Snowflake import dev.kord.core.Kord +import dev.kordex.core.koin.KordExKoinComponent import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.entities.WelcomeChannelData import org.koin.core.component.inject import org.litote.kmongo.eq -import com.kotlindiscord.kord.extensions.modules.extra.welcome.data.WelcomeChannelData as KordExWelcomeChannelData +import dev.kordex.modules.func.welcome.data.WelcomeChannelData as KordExWelcomeChannelData /** * This class contains the functions for interacting with the [Welcome channel database][WelcomeChannelData]. This class diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt index c41a4716..34e14542 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AdaptedData.kt @@ -1,12 +1,12 @@ /* * This code was utilized from [cozy](https://github.com/QuiltMC/cozy-discord) by QuiltMC -* and hence is subject to the terms of the Mozilla Public License V. 2.0 -* A copy of this license can be found at https://mozilla.org/MPL/2.0/. +* and hence is subject to the terms of the Mozilla Public Licence V. 2.0 +* A copy of this licence can be found at https://mozilla.org/MPL/2.0/. */ package org.hyacinthbots.lilybot.database.entities -import com.kotlindiscord.kord.extensions.storage.StorageType import dev.kord.common.entity.Snowflake +import dev.kordex.core.storage.StorageType import kotlinx.serialization.Serializable @Serializable diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt index c736f76a..cd6f2e33 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/AutoThreadingData.kt @@ -28,5 +28,6 @@ data class AutoThreadingData( val contentAwareNaming: Boolean, val mention: Boolean, val creationMessage: String?, - val addModsAndRole: Boolean + val addModsAndRole: Boolean, + val extraRoleIds: MutableList ) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt index 84a256af..6ffe64af 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/Config.kt @@ -55,7 +55,9 @@ data class ModerationConfigData( val quickTimeoutLength: DateTimePeriod?, val autoPunishOnWarn: Boolean?, val publicLogging: Boolean?, + val dmDefault: Boolean?, val banDmMessage: String?, + val autoInviteModeratorRole: Boolean? ) /** @@ -69,5 +71,9 @@ data class ModerationConfigData( @Serializable data class UtilityConfigData( val guildId: Snowflake, - val utilityLogChannel: Snowflake? + val utilityLogChannel: Snowflake?, + val logChannelUpdates: Boolean, + val logEventUpdates: Boolean, + val logInviteUpdates: Boolean, + val logRoleUpdates: Boolean ) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/LockedChannelData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/LockedChannelData.kt new file mode 100644 index 00000000..a739485a --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/LockedChannelData.kt @@ -0,0 +1,21 @@ +package org.hyacinthbots.lilybot.database.entities + +import dev.kord.common.entity.Snowflake +import kotlinx.serialization.Serializable + +/** + * The data for locked channels. + * + * @property guildId The ID of the guild the locked channel is in + * @property channelId The ID of the channel that is locked + * @property allowed The Discord Bit Set code for the allowed permissions, formatted as a string + * @property denied The Discord Bit Set code for the denied permissions, formatted as a string + * @since 5.0.0 + */ +@Serializable +data class LockedChannelData( + val guildId: Snowflake, + val channelId: Snowflake, + val allowed: String, + val denied: String, +) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ModerationActionData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ModerationActionData.kt new file mode 100644 index 00000000..834b76ed --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/ModerationActionData.kt @@ -0,0 +1,66 @@ +package org.hyacinthbots.lilybot.database.entities + +import dev.kord.common.entity.Snowflake +import kotlinx.datetime.DateTimePeriod +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import org.hyacinthbots.lilybot.extensions.moderation.utils.ModerationAction + +/** + * The data for Moderation action. + * + * @property actionType The type of action you're adding + * @property guildId The ID of the guild the action occurred in + * @property targetUserId The ID of the user this action happened to + * @property data The [ActionData] for the action + * @property ignore Whether to ignore the action or not. Defaults to false + * @since 5.0.0 + */ +@Serializable +data class ModerationActionData( + val actionType: ModerationAction, + val guildId: Snowflake, + val targetUserId: Snowflake, + val data: ActionData, + val ignore: Boolean = false +) + +/** + * Further, more in-depth data about a [moderation action][ModerationActionData]. + * + * @property actioner The ID of the user that requested the action + * @property deletedMessages The amount of messages deleted in the action + * @property timeData The [TimeData] for the action + * @property reason The reason for the action + * @property dmOutcome The outcome of trying to send a DM to the user + * @property dmOverride Whether the DM sending function was override + * @property imageUrl The URL for the image attached to the action + * @since 5.0.0 + */ +@Serializable +data class ActionData( + val actioner: Snowflake?, + val deletedMessages: Int?, + val timeData: TimeData?, + val reason: String?, + val dmOutcome: Boolean?, + val dmOverride: Boolean?, + val imageUrl: String? +) + +/** + * Further, more in-depth data about the [time data for actions][ActionData.timeData]. + * + * @property durationDtp The Duration as a [DateTimePeriod] + * @property durationInst The Duration as an [Instant] + * @property start The start [Instant] of the action + * @property end The end [Instant] of the action + * @since 5.0.0 + */ +@Serializable +data class TimeData( + val durationDtp: DateTimePeriod?, + val durationInst: Instant?, + val start: Instant? = null, + val end: Instant? = null, +) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TemporaryBanData.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TemporaryBanData.kt new file mode 100644 index 00000000..e955ca9f --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/entities/TemporaryBanData.kt @@ -0,0 +1,25 @@ +package org.hyacinthbots.lilybot.database.entities + +import dev.kord.common.entity.Snowflake +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +/** + * The data for temporary bans in a guild. + * + * @property guildId The ID of the guild the ban occurred in + * @property bannedUserId The ID of the user that was banned + * @property moderatorUserId The ID of the moderator that applied the ban + * @property startTime The time the ban was applied + * @property endTime The time the ban will complete + * + * @since 5.0.0 + */ +@Serializable +data class TemporaryBanData( + val guildId: Snowflake, + val bannedUserId: Snowflake, + val moderatorUserId: Snowflake, + val startTime: Instant, + val endTime: Instant +) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt index 3c8af692..96f3cee6 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/Migrator.kt @@ -11,8 +11,8 @@ package org.hyacinthbots.lilybot.database.migrations -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import mu.KotlinLogging +import dev.kordex.core.koin.KordExKoinComponent +import io.github.oshai.kotlinlogging.KotlinLogging import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.collections.ConfigMetaCollection import org.hyacinthbots.lilybot.database.collections.MainMetaCollection @@ -24,7 +24,9 @@ import org.hyacinthbots.lilybot.database.migrations.config.configV3 import org.hyacinthbots.lilybot.database.migrations.config.configV4 import org.hyacinthbots.lilybot.database.migrations.config.configV5 import org.hyacinthbots.lilybot.database.migrations.config.configV6 +import org.hyacinthbots.lilybot.database.migrations.config.configV7 import org.hyacinthbots.lilybot.database.migrations.main.mainV1 +import org.hyacinthbots.lilybot.database.migrations.main.mainV10 import org.hyacinthbots.lilybot.database.migrations.main.mainV2 import org.hyacinthbots.lilybot.database.migrations.main.mainV3 import org.hyacinthbots.lilybot.database.migrations.main.mainV4 @@ -72,6 +74,7 @@ object Migrator : KordExKoinComponent { 7 -> ::mainV7 8 -> ::mainV8 9 -> ::mainV9 + 10 -> ::mainV10 else -> break }(db.mainDatabase) @@ -121,6 +124,7 @@ object Migrator : KordExKoinComponent { 4 -> ::configV4 5 -> ::configV5 6 -> ::configV6 + 7 -> ::configV7 else -> break }(db.configDatabase) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt new file mode 100644 index 00000000..7c25c303 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/config/configV7.kt @@ -0,0 +1,38 @@ +package org.hyacinthbots.lilybot.database.migrations.config + +import org.hyacinthbots.lilybot.database.entities.ModerationConfigData +import org.hyacinthbots.lilybot.database.entities.UtilityConfigData +import org.litote.kmongo.coroutine.CoroutineDatabase +import org.litote.kmongo.exists +import org.litote.kmongo.setValue + +suspend fun configV7(db: CoroutineDatabase) { + with(db.getCollection("moderationConfigData")) { + updateMany( + ModerationConfigData::autoInviteModeratorRole exists false, + setValue(ModerationConfigData::autoInviteModeratorRole, null) + ) + updateMany( + ModerationConfigData::dmDefault exists false, + setValue(ModerationConfigData::dmDefault, true) + ) + } + with(db.getCollection("utilityConfigData")) { + updateMany( + UtilityConfigData::logChannelUpdates exists false, + setValue(UtilityConfigData::logChannelUpdates, false) + ) + updateMany( + UtilityConfigData::logEventUpdates exists false, + setValue(UtilityConfigData::logEventUpdates, false) + ) + updateMany( + UtilityConfigData::logInviteUpdates exists false, + setValue(UtilityConfigData::logInviteUpdates, false) + ) + updateMany( + UtilityConfigData::logRoleUpdates exists false, + setValue(UtilityConfigData::logRoleUpdates, false) + ) + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV10.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV10.kt new file mode 100644 index 00000000..b5780198 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/migrations/main/mainV10.kt @@ -0,0 +1,15 @@ +package org.hyacinthbots.lilybot.database.migrations.main + +import org.hyacinthbots.lilybot.database.entities.AutoThreadingData +import org.litote.kmongo.coroutine.CoroutineDatabase +import org.litote.kmongo.exists +import org.litote.kmongo.setValue + +suspend fun mainV10(db: CoroutineDatabase) { + with(db.getCollection()) { + updateMany(AutoThreadingData::extraRoleIds exists false, setValue(AutoThreadingData::extraRoleIds, emptyList())) + } + db.createCollection("lockedChannelData") + db.createCollection("temporaryBanData") + db.createCollection("moderationActionCacheData") +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt b/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt index 6790aa07..d3b443bd 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/database/storage/MongoDBDataAdapter.kt @@ -6,11 +6,11 @@ package org.hyacinthbots.lilybot.database.storage -import com.kotlindiscord.kord.extensions.koin.KordExKoinComponent -import com.kotlindiscord.kord.extensions.storage.Data -import com.kotlindiscord.kord.extensions.storage.DataAdapter -import com.kotlindiscord.kord.extensions.storage.StorageUnit import com.mongodb.client.model.Filters.and +import dev.kordex.core.koin.KordExKoinComponent +import dev.kordex.core.storage.Data +import dev.kordex.core.storage.DataAdapter +import dev.kordex.core.storage.StorageUnit import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.serializer diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt deleted file mode 100644 index d8e42dc7..00000000 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/Config.kt +++ /dev/null @@ -1,860 +0,0 @@ -package org.hyacinthbots.lilybot.extensions.config - -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.stringChoice -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.boolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingOptionalDuration -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalChannel -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalRole -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString -import com.kotlindiscord.kord.extensions.components.forms.ModalForm -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI -import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSubCommand -import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse -import com.kotlindiscord.kord.extensions.modules.unsafe.types.ackEphemeral -import com.kotlindiscord.kord.extensions.modules.unsafe.types.respondEphemeral -import com.kotlindiscord.kord.extensions.utils.botHasPermissions -import dev.kord.common.entity.Permission -import dev.kord.core.behavior.channel.createMessage -import dev.kord.core.behavior.getChannelOfOrNull -import dev.kord.core.behavior.interaction.modal -import dev.kord.core.entity.channel.TextChannel -import dev.kord.rest.builder.message.EmbedBuilder -import dev.kord.rest.builder.message.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.UtilityConfigCollection -import org.hyacinthbots.lilybot.database.entities.LoggingConfigData -import org.hyacinthbots.lilybot.database.entities.ModerationConfigData -import org.hyacinthbots.lilybot.database.entities.PublicMemberLogData -import org.hyacinthbots.lilybot.database.entities.UtilityConfigData -import org.hyacinthbots.lilybot.utils.canPingRole -import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms -import org.hyacinthbots.lilybot.utils.interval -import org.hyacinthbots.lilybot.utils.trimmedContents - -class Config : Extension() { - override val name: String = "config" - - @OptIn(UnsafeAPI::class) - override suspend fun setup() { - ephemeralSlashCommand { - name = "config" - description = "Configure Lily's settings" - - ephemeralSubCommand(::ModerationArgs) { - name = "moderation" - description = "Configure Lily's moderation system" - - requirePermission(Permission.ManageGuild) - - check { - anyGuild() - hasPermission(Permission.ManageGuild) - } - - action { - val moderationConfig = ModerationConfigCollection().getConfig(guild!!.id) - if (moderationConfig != null) { - respond { - content = "You already have a moderation configuration set. " + - "Please clear it before attempting to set a new one." - } - return@action - } - - if (!arguments.enabled) { - ModerationConfigCollection().setConfig( - ModerationConfigData( - guild!!.id, - false, - null, - null, - null, - null, - null, - null - ) - ) - respond { - content = "Moderation system disabled." - } - return@action - } - - if ( - arguments.moderatorRole != null && arguments.modActionLog == null || - arguments.moderatorRole == null && arguments.modActionLog != null - ) { - respond { - content = - "You must set both the moderator role and the action log channel to use the moderation configuration." - } - return@action - } - - if (!canPingRole(arguments.moderatorRole, guild!!.id, this@ephemeralSubCommand.kord)) { - 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 - } - - val modActionLog: TextChannel? - 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 - } - } - - suspend fun EmbedBuilder.moderationEmbed() { - title = "Configuration: Moderation" - field { - name = "Moderators" - value = arguments.moderatorRole?.mention ?: "Disabled" - } - field { - name = "Action log" - value = arguments.modActionLog?.mention ?: "Disabled" - } - field { - name = "Log publicly" - value = when (arguments.logPublicly) { - true -> "True" - false -> "Disabled" - null -> "Disabled" - } - } - field { - name = "Quick timeout length" - value = arguments.quickTimeoutLength.interval() ?: "No quick timeout length set" - } - field { - name = "Warning Auto-punishments" - value = when (arguments.warnAutoPunishments) { - true -> "Enabled" - false -> "Disabled" - null -> "Disabled" - } - } - field { - name = "Ban DM Message" - value = arguments.banDmMessage ?: "No custom Ban DM message set" - } - footer { - text = "Configured by ${user.asUserOrNull()?.username}" - } - } - - respond { - embed { - moderationEmbed() - } - } - - ModerationConfigCollection().setConfig( - ModerationConfigData( - guild!!.id, - arguments.enabled, - arguments.modActionLog?.id, - arguments.moderatorRole?.id, - arguments.quickTimeoutLength, - arguments.warnAutoPunishments, - arguments.logPublicly, - arguments.banDmMessage - ) - ) - - 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() - } - } - } - } - - unsafeSubCommand(::LoggingArgs) { - name = "logging" - description = "Configure Lily's logging system" - - initialResponse = InitialSlashCommandResponse.None - - requirePermission(Permission.ManageGuild) - - check { - anyGuild() - hasPermission(Permission.ManageGuild) - } - - action { - val loggingConfig = LoggingConfigCollection().getConfig(guild!!.id) - if (loggingConfig != null) { - ackEphemeral() - respondEphemeral { - content = "You already have a logging configuration set. " + - "Please clear it before attempting to set a new one." - } - return@action - } - - if (arguments.enableMemberLogging && arguments.memberLog == null) { - ackEphemeral() - respondEphemeral { - content = "You must specify a channel to log members joining and leaving to!" - } - return@action - } else if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && - arguments.messageLogs == null - ) { - ackEphemeral() - respondEphemeral { content = "You must specify a channel to log deleted/edited messages to!" } - return@action - } else if (arguments.enablePublicMemberLogging && arguments.publicMemberLog == null) { - ackEphemeral() - respondEphemeral { - content = "You must specify a channel to publicly log members joining and leaving to!" - } - return@action - } - - val memberLog: TextChannel? - if (arguments.enableMemberLogging && arguments.memberLog != null) { - memberLog = guild!!.getChannelOfOrNull(arguments.memberLog!!.id) - if (memberLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { - ackEphemeral() - respondEphemeral { - 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 - } - } - - val messageLog: TextChannel? - if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && arguments.messageLogs != null) { - messageLog = guild!!.getChannelOfOrNull(arguments.messageLogs!!.id) - if (messageLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { - ackEphemeral() - respondEphemeral { - 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 - } - } - - val publicMemberLog: TextChannel? - if (arguments.enablePublicMemberLogging && arguments.publicMemberLog != null) { - publicMemberLog = guild!!.getChannelOfOrNull(arguments.publicMemberLog!!.id) - if (publicMemberLog?.botHasPermissions( - Permission.ViewChannel, - Permission.SendMessages - ) != true - ) { - ackEphemeral() - respondEphemeral { - content = "The public member log you've selected is invalid, or I can't view it. " + - "Please attempt to resolve this and try again." - } - return@action - } - } - - suspend fun EmbedBuilder.loggingEmbed() { - title = "Configuration: Logging" - field { - name = "Message Delete Logs" - value = if (arguments.enableMessageDeleteLogs && arguments.messageLogs != null) { - arguments.messageLogs!!.mention - } else { - "Disabled" - } - } - field { - name = "Message Edit Logs" - value = if (arguments.enableMessageEditLogs && arguments.messageLogs != null) { - arguments.messageLogs!!.mention - } else { - "Disabled" - } - } - field { - name = "Member Logs" - value = if (arguments.enableMemberLogging && arguments.memberLog != null) { - arguments.memberLog!!.mention - } else { - "Disabled" - } - } - - field { - name = "Public Member logs" - value = if (arguments.enablePublicMemberLogging && arguments.publicMemberLog != null) { - arguments.publicMemberLog!!.mention - } else { - "Disabled" - } - } - if (arguments.enableMemberLogging && arguments.publicMemberLog != null) { - val config = LoggingConfigCollection().getConfig(guild!!.id) - if (config != null) { - field { - name = "Join Message" - value = config.publicMemberLogData?.joinMessage.trimmedContents(256)!! - } - field { - name = "Leave Message" - value = config.publicMemberLogData?.leaveMessage.trimmedContents(256)!! - } - field { - name = "Ping on join" - value = config.publicMemberLogData?.pingNewUsers.toString() - } - } - } - - footer { - text = "Configured by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - - var publicMemberLogData: PublicMemberLogData? = null - if (arguments.enablePublicMemberLogging) { - val modalObj = LoggingModal() - - this@unsafeSubCommand.componentRegistry.register(modalObj) - - event.interaction.modal( - modalObj.title, - modalObj.id - ) { - modalObj.applyToBuilder(this, getLocale(), null) - } - - modalObj.awaitCompletion { modalSubmitInteraction -> - interactionResponse = modalSubmitInteraction?.deferEphemeralMessageUpdate() - } - - publicMemberLogData = PublicMemberLogData( - modalObj.ping.value == "yes", - modalObj.joinMessage.value, - modalObj.leaveMessage.value - ) - } - - LoggingConfigCollection().setConfig( - LoggingConfigData( - guild!!.id, - arguments.enableMessageDeleteLogs, - arguments.enableMessageEditLogs, - arguments.messageLogs?.id, - arguments.enableMemberLogging, - arguments.memberLog?.id, - arguments.enablePublicMemberLogging, - arguments.publicMemberLog?.id, - publicMemberLogData - ) - ) - - if (!arguments.enablePublicMemberLogging) { - ackEphemeral() - } - respondEphemeral { - embed { loggingEmbed() } - } - - val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild!!) - - if (utilityLog == null) { - respondEphemeral { - content = "Consider setting a utility config to log changes to configurations." - } - return@action - } - - utilityLog.createMessage { - embed { - loggingEmbed() - } - } - } - } - - ephemeralSubCommand(::UtilityArgs) { - name = "utility" - description = "Configure Lily's utility settings" - - requirePermission(Permission.ManageGuild) - - check { - anyGuild() - hasPermission(Permission.ManageGuild) - } - - action { - val utilityConfig = UtilityConfigCollection().getConfig(guild!!.id) - - if (utilityConfig != null) { - respond { - content = "You already have a utility configuration set. " + - "Please clear it before attempting to set a new one." - } - 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 - } - } - - suspend fun EmbedBuilder.utilityEmbed() { - title = "Configuration: Utility" - field { - name = "Utility Log" - value = if (arguments.utilityLogChannel != null) { - "${arguments.utilityLogChannel!!.mention} ${arguments.utilityLogChannel!!.data.name.value}" - } else { - "Disabled" - } - } - - footer { - text = "Configured by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - - respond { - embed { - utilityEmbed() - } - } - - UtilityConfigCollection().setConfig( - UtilityConfigData( - guild!!.id, - arguments.utilityLogChannel?.id - ) - ) - - utilityLog?.createMessage { - embed { - utilityEmbed() - } - } - } - } - - ephemeralSubCommand(::ClearArgs) { - name = "clear" - description = "Clear a config type" - - requirePermission(Permission.ManageGuild) - - check { - anyGuild() - hasPermission(Permission.ManageGuild) - } - - action { - suspend fun logClear() { - 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[0]}${ - arguments.config.substring(1, arguments.config.length).lowercase() - }" - footer { - text = "Config cleared by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - } - } - - when (arguments.config) { - ConfigType.MODERATION.name -> { - ModerationConfigCollection().getConfig(guild!!.id) ?: run { - respond { - content = "No moderation configuration exists to clear!" - } - return@action - } - - logClear() - - ModerationConfigCollection().clearConfig(guild!!.id) - respond { - embed { - title = "Config cleared: Moderation" - footer { - text = "Config cleared by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - } - } - - ConfigType.LOGGING.name -> { - LoggingConfigCollection().getConfig(guild!!.id) ?: run { - respond { - content = "No logging configuration exists to clear!" - } - return@action - } - - logClear() - - LoggingConfigCollection().clearConfig(guild!!.id) - respond { - embed { - title = "Config cleared: Logging" - footer { - text = "Config cleared by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - } - } - - ConfigType.UTILITY.name -> { - UtilityConfigCollection().getConfig(guild!!.id) ?: run { - respond { - content = "No utility configuration exists to clear" - } - return@action - } - - logClear() - - UtilityConfigCollection().clearConfig(guild!!.id) - respond { - embed { - title = "Config cleared: Utility" - footer { - text = "Config cleared by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - } - } - - ConfigType.ALL.name -> { - ModerationConfigCollection().clearConfig(guild!!.id) - LoggingConfigCollection().clearConfig(guild!!.id) - UtilityConfigCollection().clearConfig(guild!!.id) - respond { - embed { - title = "All configs cleared" - footer { - text = "Configs cleared by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - } - } - } - } - } - } - - ephemeralSubCommand(::ViewArgs) { - name = "view" - description = "View the current config that you have set" - - requirePermission(Permission.ManageGuild) - - 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" - } - } - field { - name = "Ban DM Message" - value = config.banDmMessage ?: "None" - } - 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!!.getChannelOrNull(config.messageChannel!!)?.mention ?: "Unable to get channel mention"} (" + - "${guild!!.getChannelOrNull(config.messageChannel)?.name ?: "Unable to get channel name"})" - } else { - "Disabled" - } - } - field { - name = "Message edit logs" - value = if (config.enableMessageEditLogs) { - "Enabled\n" + - "* ${guild!!.getChannelOrNull(config.messageChannel!!)?.mention ?: "Unable to get channel mention"} (" + - "${guild!!.getChannelOrNull(config.messageChannel)?.name ?: "Unable to get channel mention"})" - } else { - "Disabled" - } - } - field { - name = "Member logs" - value = if (config.enableMemberLogs) { - "Enabled\n" + - "* ${guild!!.getChannelOrNull(config.memberLog!!)?.mention ?: "Unable to get channel mention"} (" + - "${guild!!.getChannelOrNull(config.memberLog)?.name ?: "Unable to get channel mention."})" - } else { - "Disabled" - } - } - 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 = "Channel" - value = - "${ - config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.mention } ?: "None" - } ${config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.name } ?: ""}" - } - timestamp = Clock.System.now() - } - } - } - } - } - } - } - } - - inner class ModerationArgs : Arguments() { - val enabled by boolean { - name = "enable-moderation" - description = "Whether to enable the moderation system" - } - - val moderatorRole by optionalRole { - name = "moderator-role" - description = "The role of your moderators, used for pinging in message logs." - } - - val modActionLog by optionalChannel { - name = "action-log" - description = "The channel used to store moderator actions." - } - - val quickTimeoutLength by coalescingOptionalDuration { - name = "quick-timeout-length" - description = "The length of timeouts to use for quick timeouts" - } - - val warnAutoPunishments by optionalBoolean { - name = "warn-auto-punishments" - description = "Whether to automatically punish users for reach a certain threshold on warns" - } - - val logPublicly by optionalBoolean { - name = "log-publicly" - description = "Whether to log moderation publicly or not." - } - - val banDmMessage by optionalString { - name = "ban-dm-message" - description = "A custom message to send to users when they are banned." - } - } - - inner class LoggingArgs : Arguments() { - 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" - } - - val enablePublicMemberLogging by boolean { - name = "enable-public-member-logging" - description = - "Enable logging of members joining and leaving the guild with a public message and ping if enabled" - } - - val messageLogs by optionalChannel { - name = "message-logs" - description = "The channel for logging message deletions" - } - - val memberLog by optionalChannel { - name = "member-log" - description = "The channel for logging members joining and leaving the guild" - } - - val publicMemberLog by optionalChannel { - name = "public-member-log" - description = "The channel for the public logging of members joining and leaving the guild" - } - } - - inner class UtilityArgs : Arguments() { - val utilityLogChannel by optionalChannel { - name = "utility-log" - description = "The channel to log various utility actions too." - } - } - - inner class ClearArgs : Arguments() { - val config by stringChoice { - name = "config-type" - description = "The type of config to clear" - choices = mutableMapOf( - "moderation" to ConfigType.MODERATION.name, - "logging" to ConfigType.LOGGING.name, - "utility" to ConfigType.UTILITY.name, - "all" to ConfigType.ALL.name - ) - } - } - - inner class ViewArgs : Arguments() { - val config by stringChoice { - name = "config-type" - description = "The type of config to clear" - choices = mutableMapOf( - "moderation" to ConfigType.MODERATION.name, - "logging" to ConfigType.LOGGING.name, - "utility" to ConfigType.UTILITY.name, - ) - } - } - - inner class LoggingModal : ModalForm() { - override var title = "Public logging configuration" - - val joinMessage = paragraphText { - label = "What would you like sent when a user joins" - placeholder = "Welcome to the server!" - required = true - } - - val leaveMessage = paragraphText { - label = "What would you like sent when a user leaves" - placeholder = "Adiós amigo!" - required = true - } - - val ping = lineText { - label = "Type `yes` to ping new users when they join" - placeholder = "Defaults to false if input is invalid or not `yes`" - } - } -} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigExtension.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigExtension.kt new file mode 100644 index 00000000..454b113b --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/ConfigExtension.kt @@ -0,0 +1,32 @@ +package org.hyacinthbots.lilybot.extensions.config + +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.modules.dev.unsafe.annotations.UnsafeAPI +import org.hyacinthbots.lilybot.extensions.config.commands.configClearCommand +import org.hyacinthbots.lilybot.extensions.config.commands.configViewCommand +import org.hyacinthbots.lilybot.extensions.logging.config.loggingCommand +import org.hyacinthbots.lilybot.extensions.moderation.config.moderationCommand +import org.hyacinthbots.lilybot.extensions.utility.config.utilityCommand + +class ConfigExtension : Extension() { + override val name: String = "config" + + @OptIn(UnsafeAPI::class) + override suspend fun setup() { + ephemeralSlashCommand { + name = "config" + description = "Configure Lily's settings" + + loggingCommand() + + moderationCommand() + + utilityCommand() + + configClearCommand() + + configViewCommand() + } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigClearCommand.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigClearCommand.kt new file mode 100644 index 00000000..2e637d52 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigClearCommand.kt @@ -0,0 +1,150 @@ +package org.hyacinthbots.lilybot.extensions.config.commands + +import dev.kord.common.entity.Permission +import dev.kord.core.behavior.channel.createMessage +import dev.kord.rest.builder.message.embed +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.EphemeralSlashCommandContext +import dev.kordex.core.commands.application.slash.SlashCommand +import dev.kordex.core.commands.application.slash.converters.impl.stringChoice +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection +import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection +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.getLoggingChannelWithPerms + +suspend fun SlashCommand<*, *, *>.configClearCommand() = ephemeralSubCommand(::ClearArgs) { + name = "clear" + description = "Clear a config type" + + requirePermission(Permission.ManageGuild) + + check { + anyGuild() + hasPermission(Permission.ManageGuild) + } + + action { + when (arguments.config) { + ConfigType.MODERATION.name -> clearConfig(ConfigType.MODERATION, arguments) + + ConfigType.LOGGING.name -> clearConfig(ConfigType.LOGGING, arguments) + + ConfigType.UTILITY.name -> clearConfig(ConfigType.UTILITY, arguments) + + ConfigType.ALL.name -> clearConfig(ConfigType.ALL, arguments) + } + + respond { + embed { + title = if (arguments.config == ConfigType.ALL.name) { + "All configs cleared" + } else { + "Config cleared: ${arguments.config}" + } + footer { + text = "Config cleared by ${user.asUserOrNull()?.username}" + icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } + } + } + } +} + +/** + * Handles the clearing of a configuration(s) based on the [args] and [type]. + * + * @param type The type of config to clear + * @param args The [ClearArgs] for the clearing of the config + * @author NoComment1105 + * @since 5.0.0 + */ +private suspend fun EphemeralSlashCommandContext<*, *>.clearConfig(type: ConfigType, args: ClearArgs) { + when (type) { + ConfigType.MODERATION -> { + ModerationConfigCollection().getConfig(guild!!.id) ?: run { + respond { + content = "No moderation configuration exists to clear" + } + return + } + logClear(args) + ModerationConfigCollection().clearConfig(guild!!.id) + } + + ConfigType.LOGGING -> { + LoggingConfigCollection().getConfig(guild!!.id) ?: run { + respond { + content = "No logging configuration exists to clear" + } + } + logClear(args) + LoggingConfigCollection().clearConfig(guild!!.id) + } + + ConfigType.UTILITY -> { + UtilityConfigCollection().getConfig(guild!!.id) ?: run { + respond { + content = "No utility configuration exists to clear" + } + } + logClear(args) + UtilityConfigCollection().clearConfig(guild!!.id) + } + + ConfigType.ALL -> { + ModerationConfigCollection().clearConfig(guild!!.id) + LoggingConfigCollection().clearConfig(guild!!.id) + UtilityConfigCollection().clearConfig(guild!!.id) + } + } +} + +/** + * Log the clearing of a configuration to the utility log, should there still be one. + * + * @param arguments The [ClearArgs] for the clearing of the config + * @author NoComment1105 + * @since 5.0.0 + */ +suspend fun EphemeralSlashCommandContext<*, *>.logClear(arguments: ClearArgs) { + // Skip this if the utility config is cleared or all configs are cleared + if (arguments.config == ConfigType.UTILITY.name || arguments.config == ConfigType.ALL.name) return + 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[0]}${ + arguments.config.substring(1, arguments.config.length).lowercase() + }" + footer { + text = "Config cleared by ${user.asUserOrNull()?.username}" + icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } + } + } +} + +class ClearArgs : Arguments() { + val config by stringChoice { + name = "config-type" + description = "The type of config to clear" + choices = mutableMapOf( + "moderation" to ConfigType.MODERATION.name, + "logging" to ConfigType.LOGGING.name, + "utility" to ConfigType.UTILITY.name, + "all" to ConfigType.ALL.name + ) + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigViewCommand.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigViewCommand.kt new file mode 100644 index 00000000..79fa7ea0 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/commands/ConfigViewCommand.kt @@ -0,0 +1,180 @@ +package org.hyacinthbots.lilybot.extensions.config.commands + +import dev.kord.common.entity.Permission +import dev.kord.rest.builder.message.embed +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.SlashCommand +import dev.kordex.core.commands.application.slash.converters.impl.stringChoice +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection +import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection +import org.hyacinthbots.lilybot.extensions.config.ConfigType +import org.hyacinthbots.lilybot.utils.interval + +suspend fun SlashCommand<*, *, *>.configViewCommand() = ephemeralSubCommand(::ViewArgs) { + name = "view" + description = "View the current config that you have set" + + requirePermission(Permission.ManageGuild) + + 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 -> "Enabled" + false -> "Disabled" + null -> "Disabled" + } + } + field { + name = "Quick timeout length" + value = config.quickTimeoutLength.interval() ?: "No quick timeout length set" + } + field { + name = "Warning Auto-punishments" + value = when (config.autoPunishOnWarn) { + true -> "Enabled" + false -> "Disabled" + null -> "Disabled" + } + } + field { + name = "Ban DM Message" + value = config.banDmMessage ?: "No custom Ban DM message set" + } + field { + name = "Auto-invite Moderator Role" + value = when (config.autoInviteModeratorRole) { + true -> "Enabled" + 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!!.getChannelOrNull(config.messageChannel!!)?.mention ?: "Unable to get channel mention"} (" + + "${guild!!.getChannelOrNull(config.messageChannel)?.name ?: "Unable to get channel name"})" + } else { + "Disabled" + } + } + field { + name = "Message edit logs" + value = if (config.enableMessageEditLogs) { + "Enabled\n" + + "* ${guild!!.getChannelOrNull(config.messageChannel!!)?.mention ?: "Unable to get channel mention"} (" + + "${guild!!.getChannelOrNull(config.messageChannel)?.name ?: "Unable to get channel mention"})" + } else { + "Disabled" + } + } + field { + name = "Member logs" + value = if (config.enableMemberLogs) { + "Enabled\n" + + "* ${guild!!.getChannelOrNull(config.memberLog!!)?.mention ?: "Unable to get channel mention"} (" + + "${guild!!.getChannelOrNull(config.memberLog)?.name ?: "Unable to get channel mention."})" + } else { + "Disabled" + } + } + 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 = "Channel" + value = + "${ + config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.mention } ?: "None" + } ${config.utilityLogChannel?.let { guild!!.getChannelOrNull(it)?.name } ?: ""}" + } + timestamp = Clock.System.now() + } + } + } + } + } +} + +class ViewArgs : Arguments() { + val config by stringChoice { + name = "config-type" + description = "The type of config to clear" + choices = mutableMapOf( + "moderation" to ConfigType.MODERATION.name, + "logging" to ConfigType.LOGGING.name, + "utility" to ConfigType.UTILITY.name, + ) + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/utils/ConfigEmbeds.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/utils/ConfigEmbeds.kt new file mode 100644 index 00000000..2fc8d697 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/utils/ConfigEmbeds.kt @@ -0,0 +1,160 @@ +@file:Suppress("DuplicatedCode") + +package org.hyacinthbots.lilybot.extensions.config.utils + +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.UserBehavior +import dev.kord.rest.builder.message.EmbedBuilder +import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection +import org.hyacinthbots.lilybot.extensions.logging.config.LoggingArgs +import org.hyacinthbots.lilybot.extensions.moderation.config.ModerationArgs +import org.hyacinthbots.lilybot.extensions.utility.config.UtilityArgs +import org.hyacinthbots.lilybot.utils.interval +import org.hyacinthbots.lilybot.utils.trimmedContents + +suspend fun EmbedBuilder.utilityEmbed(arguments: UtilityArgs, user: UserBehavior) { + title = "Configuration: Utility" + field { + name = "Utility Log" + value = if (arguments.utilityLogChannel != null) { + "${arguments.utilityLogChannel!!.mention} ${arguments.utilityLogChannel!!.data.name.value}" + } else { + "Disabled" + } + } + field { + name = "Log Channel updates" + value = if (arguments.logChannelUpdates) "Yes" else "No" + } + field { + name = "Log Event updates" + value = if (arguments.logEventUpdates) "Yes" else "No" + } + field { + name = "Log Invite updates" + value = if (arguments.logInviteUpdates) "Yes" else "No" + } + field { + name = "Log Role updates" + value = if (arguments.logRoleUpdates) "Yes" else "No" + } + + footer { + text = "Configured by ${user.asUserOrNull()?.username}" + icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } +} + +suspend fun EmbedBuilder.moderationEmbed(arguments: ModerationArgs, user: UserBehavior) { + title = "Configuration: Moderation" + field { + name = "Moderators" + value = arguments.moderatorRole?.mention ?: "Disabled" + } + field { + name = "Action log" + value = arguments.modActionLog?.mention ?: "Disabled" + } + field { + name = "Log publicly" + value = when (arguments.logPublicly) { + true -> "Enabled" + false -> "Disabled" + null -> "Disabled" + } + } + field { + name = "Quick timeout length" + value = arguments.quickTimeoutLength.interval() ?: "No quick timeout length set" + } + field { + name = "Warning Auto-punishments" + value = when (arguments.warnAutoPunishments) { + true -> "Enabled" + false -> "Disabled" + null -> "Disabled" + } + } + field { + name = "DM Default" + value = when (arguments.dmDefault) { + true -> "DM argument will default to true" + false -> "DM argument will default to false" + null -> "DM argument will default to false" + } + } + field { + name = "Ban DM Message" + value = arguments.banDmMessage ?: "No custom Ban DM message set" + } + field { + name = "Auto-invite Moderator Role" + value = when (arguments.autoInviteModeratorRole) { + true -> "Enabled" + false -> "Disabled" + null -> "Disabled" + } + } + footer { + text = "Configured by ${user.asUserOrNull()?.username}" + } +} + +suspend fun EmbedBuilder.loggingEmbed(arguments: LoggingArgs, guild: GuildBehavior?, user: UserBehavior) { + title = "Configuration: Logging" + field { + name = "Message Delete Logs" + value = if (arguments.enableMessageDeleteLogs && arguments.messageLogs != null) { + arguments.messageLogs!!.mention + } else { + "Disabled" + } + } + field { + name = "Message Edit Logs" + value = if (arguments.enableMessageEditLogs && arguments.messageLogs != null) { + arguments.messageLogs!!.mention + } else { + "Disabled" + } + } + field { + name = "Member Logs" + value = if (arguments.enableMemberLogging && arguments.memberLog != null) { + arguments.memberLog!!.mention + } else { + "Disabled" + } + } + + field { + name = "Public Member logs" + value = if (arguments.enablePublicMemberLogging && arguments.publicMemberLog != null) { + arguments.publicMemberLog!!.mention + } else { + "Disabled" + } + } + if (arguments.enableMemberLogging && arguments.publicMemberLog != null) { + val config = LoggingConfigCollection().getConfig(guild!!.id) + if (config != null) { + field { + name = "Join Message" + value = config.publicMemberLogData?.joinMessage.trimmedContents(256)!! + } + field { + name = "Leave Message" + value = config.publicMemberLogData?.leaveMessage.trimmedContents(256)!! + } + field { + name = "Ping on join" + value = config.publicMemberLogData?.pingNewUsers.toString() + } + } + } + + footer { + text = "Configured by ${user.asUserOrNull()?.username}" + icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingArgs.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingArgs.kt new file mode 100644 index 00000000..9ecb08d1 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingArgs.kt @@ -0,0 +1,43 @@ +package org.hyacinthbots.lilybot.extensions.logging.config + +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.boolean +import dev.kordex.core.commands.converters.impl.optionalChannel + +class LoggingArgs : Arguments() { + 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" + } + + val enablePublicMemberLogging by boolean { + name = "enable-public-member-logging" + description = + "Enable logging of members joining and leaving the guild with a public message and ping if enabled" + } + + val messageLogs by optionalChannel { + name = "message-logs" + description = "The channel for logging message deletions" + } + + val memberLog by optionalChannel { + name = "member-log" + description = "The channel for logging members joining and leaving the guild" + } + + val publicMemberLog by optionalChannel { + name = "public-member-log" + description = "The channel for the public logging of members joining and leaving the guild" + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingCommand.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingCommand.kt new file mode 100644 index 00000000..91c25685 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/LoggingCommand.kt @@ -0,0 +1,165 @@ +package org.hyacinthbots.lilybot.extensions.logging.config + +import dev.kord.common.entity.Permission +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.getChannelOfOrNull +import dev.kord.core.behavior.interaction.modal +import dev.kord.core.entity.channel.TextChannel +import dev.kord.rest.builder.message.embed +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.application.slash.SlashCommand +import dev.kordex.core.utils.botHasPermissions +import dev.kordex.modules.dev.unsafe.annotations.UnsafeAPI +import dev.kordex.modules.dev.unsafe.commands.slash.InitialSlashCommandResponse +import dev.kordex.modules.dev.unsafe.extensions.unsafeSubCommand +import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection +import org.hyacinthbots.lilybot.database.entities.LoggingConfigData +import org.hyacinthbots.lilybot.database.entities.PublicMemberLogData +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.extensions.config.utils.loggingEmbed +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms + +@OptIn(UnsafeAPI::class) +suspend fun SlashCommand<*, *, *>.loggingCommand() = unsafeSubCommand(::LoggingArgs) { + name = "logging" + description = "Configure Lily's logging system" + + initialResponse = InitialSlashCommandResponse.None + + requirePermission(Permission.ManageGuild) + + check { + anyGuild() + hasPermission(Permission.ManageGuild) + } + + action { + val loggingConfig = LoggingConfigCollection().getConfig(guild!!.id) + if (loggingConfig != null) { + ackEphemeral() + respondEphemeral { + content = "You already have a logging configuration set. " + + "Please clear it before attempting to set a new one." + } + return@action + } + + if (arguments.enableMemberLogging && arguments.memberLog == null) { + ackEphemeral() + respondEphemeral { + content = "You must specify a channel to log members joining and leaving to!" + } + return@action + } else if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && + arguments.messageLogs == null + ) { + ackEphemeral() + respondEphemeral { content = "You must specify a channel to log deleted/edited messages to!" } + return@action + } else if (arguments.enablePublicMemberLogging && arguments.publicMemberLog == null) { + ackEphemeral() + respondEphemeral { + content = "You must specify a channel to publicly log members joining and leaving to!" + } + return@action + } + + val memberLog: TextChannel? + if (arguments.enableMemberLogging && arguments.memberLog != null) { + memberLog = guild!!.getChannelOfOrNull(arguments.memberLog!!.id) + if (memberLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + ackEphemeral() + respondEphemeral { + 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 + } + } + + val messageLog: TextChannel? + if ((arguments.enableMessageDeleteLogs || arguments.enableMessageEditLogs) && arguments.messageLogs != null) { + messageLog = guild!!.getChannelOfOrNull(arguments.messageLogs!!.id) + if (messageLog?.botHasPermissions(Permission.ViewChannel, Permission.SendMessages) != true) { + ackEphemeral() + respondEphemeral { + 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 + } + } + + val publicMemberLog: TextChannel? + if (arguments.enablePublicMemberLogging && arguments.publicMemberLog != null) { + publicMemberLog = guild!!.getChannelOfOrNull(arguments.publicMemberLog!!.id) + if (publicMemberLog?.botHasPermissions( + Permission.ViewChannel, + Permission.SendMessages + ) != true + ) { + ackEphemeral() + respondEphemeral { + content = "The public member log you've selected is invalid, or I can't view it. " + + "Please attempt to resolve this and try again." + } + return@action + } + } + + var publicMemberLogData: PublicMemberLogData? = null + if (arguments.enablePublicMemberLogging) { + val modalObj = PublicLoggingModal() + + this@unsafeSubCommand.componentRegistry.register(modalObj) + + event.interaction.modal( + modalObj.title, + modalObj.id + ) { + modalObj.applyToBuilder(this, getLocale(), null) + } + + modalObj.awaitCompletion { modalSubmitInteraction -> + interactionResponse = modalSubmitInteraction?.deferEphemeralMessageUpdate() + } + + publicMemberLogData = PublicMemberLogData( + modalObj.ping.value == "yes", + modalObj.joinMessage.value, + modalObj.leaveMessage.value + ) + } + + LoggingConfigCollection().setConfig( + LoggingConfigData( + guild!!.id, + arguments.enableMessageDeleteLogs, + arguments.enableMessageEditLogs, + arguments.messageLogs?.id, + arguments.enableMemberLogging, + arguments.memberLog?.id, + arguments.enablePublicMemberLogging, + arguments.publicMemberLog?.id, + publicMemberLogData + ) + ) + + if (!arguments.enablePublicMemberLogging) { + ackEphemeral() + } + respondEphemeral { embed { loggingEmbed(arguments, guild, user) } } + + val utilityLog = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild!!) + + if (utilityLog == null) { + respondEphemeral { + content = "Consider setting a utility config to log changes to configurations." + } + return@action + } + + utilityLog.createMessage { embed { loggingEmbed(arguments, guild, user) } } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/PublicLoggingModal.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/PublicLoggingModal.kt new file mode 100644 index 00000000..28928317 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/config/PublicLoggingModal.kt @@ -0,0 +1,24 @@ +package org.hyacinthbots.lilybot.extensions.logging.config + +import dev.kordex.core.components.forms.ModalForm + +class PublicLoggingModal : ModalForm() { + override var title = "Public logging configuration" + + val joinMessage = paragraphText { + label = "What would you like sent when a user joins" + placeholder = "Welcome to the server!" + required = true + } + + val leaveMessage = paragraphText { + label = "What would you like sent when a user leaves" + placeholder = "Adiós amigo!" + required = true + } + + val ping = lineText { + label = "Type `yes` to ping new users when they join" + placeholder = "Defaults to false if input is invalid or not `yes`" + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/GuildLogging.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/GuildLogging.kt similarity index 88% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/GuildLogging.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/GuildLogging.kt index c51e7b21..35a84898 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/config/GuildLogging.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/GuildLogging.kt @@ -1,10 +1,10 @@ -package org.hyacinthbots.lilybot.extensions.config +package org.hyacinthbots.lilybot.extensions.logging.events -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.event import dev.kord.core.event.guild.GuildCreateEvent import dev.kord.core.event.guild.GuildDeleteEvent import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.GuildLeaveTimeCollection import java.util.concurrent.CancellationException diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MemberLogging.kt similarity index 75% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MemberLogging.kt index fc899de9..49c10cff 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MemberLogging.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MemberLogging.kt @@ -1,12 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.events +package org.hyacinthbots.lilybot.extensions.logging.events -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.event -import com.kotlindiscord.kord.extensions.utils.botHasPermissions import dev.kord.common.entity.Permission import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.channel.createMessage @@ -15,9 +8,20 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.event.guild.MemberJoinEvent import dev.kord.core.event.guild.MemberLeaveEvent import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.guildFor +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.botHasPermissions import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection +import org.hyacinthbots.lilybot.database.collections.ModerationActionCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.extensions.moderation.utils.ModerationAction +import org.hyacinthbots.lilybot.utils.baseModerationEmbed +import org.hyacinthbots.lilybot.utils.dmNotificationStatusEmbedField import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms import org.hyacinthbots.lilybot.utils.requiredConfigs @@ -117,6 +121,21 @@ class MemberLogging : Extension() { color = DISCORD_RED } + // Check if the member was kicked, and send a log if they were. + val kickData = ModerationActionCollection().getAction(ModerationAction.KICK, event.guildId, event.user.id) + if (kickData != null) { + val targetUser = event.kord.getUser(kickData.targetUserId)!! + val actioner = kickData.data.actioner?.let { event.kord.getUser(it) }!! + getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, event.guild)?.createEmbed { + title = "Kicked a user" + description = "${targetUser.mention} has been kicked" + image = kickData.data.imageUrl + baseModerationEmbed(kickData.data.reason, targetUser, actioner) + dmNotificationStatusEmbedField(kickData.data.dmOutcome != null, kickData.data.dmOutcome) + timestamp = Clock.System.now() + } + } + if (config != null && config.enablePublicMemberLogs) { var publicLog = guildFor(event)?.getChannelOfOrNull(config.publicMemberLog!!) val permissions = publicLog?.botHasPermissions(Permission.SendMessages, Permission.EmbedLinks) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageDelete.kt similarity index 89% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageDelete.kt index e2516609..9c602189 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageDelete.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageDelete.kt @@ -1,12 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.events +package org.hyacinthbots.lilybot.extensions.logging.events -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.asChannelOfOrNull import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.channel.createMessage @@ -15,6 +8,13 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.event.message.MessageBulkDeleteEvent import dev.kord.rest.builder.message.create.UserMessageCreateBuilder import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_PINK +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.modules.pluralkit.api.PKMessage +import dev.kordex.modules.pluralkit.events.ProxiedMessageDeleteEvent +import dev.kordex.modules.pluralkit.events.UnProxiedMessageDeleteEvent import io.ktor.client.request.forms.ChannelProvider import io.ktor.util.cio.toByteReadChannel import kotlinx.datetime.Clock diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageEdit.kt similarity index 75% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageEdit.kt index 7b839516..ecb05524 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/MessageEdit.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/logging/events/MessageEdit.kt @@ -1,20 +1,21 @@ -package org.hyacinthbots.lilybot.extensions.events +package org.hyacinthbots.lilybot.extensions.logging.events -import com.kotlindiscord.kord.extensions.DISCORD_YELLOW -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.components.components -import com.kotlindiscord.kord.extensions.components.linkButton -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 com.kotlindiscord.kord.extensions.utils.getJumpUrl import dev.kord.core.behavior.channel.asChannelOfOrNull import dev.kord.core.behavior.channel.createMessage import dev.kord.core.entity.Message import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.components.components +import dev.kordex.core.components.linkButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.getJumpUrl +import dev.kordex.core.utils.isNullOrBot +import dev.kordex.modules.pluralkit.api.PKMessage +import dev.kordex.modules.pluralkit.events.ProxiedMessageUpdateEvent +import dev.kordex.modules.pluralkit.events.UnProxiedMessageUpdateEvent import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.attachmentsAndProxiedMessageInfo @@ -40,10 +41,9 @@ class MessageEdit : Extension() { check { anyGuild() requiredConfigs(ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) - failIf { - val message = event.message.asMessageOrNull() - message?.author?.isBot == true || event.old?.content == message?.content - } + failIf(event.message.asMessageOrNull()?.author.isNullOrBot()) + failIf(event.old?.content == event.message.asMessageOrNull()?.content) + failIf(event.old.trimmedContents() == null) } action { onMessageEdit(event.getMessageOrNull(), event.old, null) @@ -59,9 +59,8 @@ class MessageEdit : Extension() { check { anyGuild() requiredConfigs(ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED, ConfigOptions.MESSAGE_LOG) - failIf { - event.old?.content == event.message.asMessageOrNull()?.content - } + failIf(event.old?.content == event.message.asMessageOrNull()?.content) + failIf(event.old.trimmedContents() == null) } action { onMessageEdit(event.getMessageOrNull(), event.old, event.pkMessage) diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationActions.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationActions.kt deleted file mode 100644 index 86ca719d..00000000 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationActions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.hyacinthbots.lilybot.extensions.moderation - -enum class ModerationActions { - BAN, - SOFT_BAN, - KICK, - WARN, - TIMEOUT -} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ClearCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ClearCommands.kt similarity index 89% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ClearCommands.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ClearCommands.kt index db8aa988..9ad4cc7c 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ClearCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ClearCommands.kt @@ -1,17 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.moderation - -import com.kotlindiscord.kord.extensions.DISCORD_BLACK -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSlashCommandContext -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.int -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalInt -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalUser -import com.kotlindiscord.kord.extensions.commands.converters.impl.snowflake -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand +package org.hyacinthbots.lilybot.extensions.moderation.commands + import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.common.entity.Snowflake @@ -20,6 +8,18 @@ import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.entity.User import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kordex.core.DISCORD_BLACK +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.EphemeralSlashCommandContext +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.int +import dev.kordex.core.commands.converters.impl.optionalInt +import dev.kordex.core.commands.converters.impl.optionalUser +import dev.kordex.core.commands.converters.impl.snowflake +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/LockingCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/LockingCommands.kt similarity index 76% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/LockingCommands.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/LockingCommands.kt index abd3c9d8..b7994e75 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/LockingCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/LockingCommands.kt @@ -1,27 +1,31 @@ -package org.hyacinthbots.lilybot.extensions.moderation - -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingString -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalChannel -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.types.EphemeralInteractionContext +package org.hyacinthbots.lilybot.extensions.moderation.commands + +import dev.kord.common.DiscordBitSet import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.core.behavior.channel.asChannelOfOrNull import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.behavior.channel.editMemberPermission import dev.kord.core.behavior.channel.editRolePermission import dev.kord.core.behavior.edit import dev.kord.core.entity.channel.Channel import dev.kord.core.entity.channel.TextChannel import dev.kord.core.entity.channel.ThreadParentChannel import dev.kord.core.entity.channel.thread.TextChannelThread +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.defaultingString +import dev.kordex.core.commands.converters.impl.optionalChannel +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.types.EphemeralInteractionContext import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.database.collections.LockedChannelCollection +import org.hyacinthbots.lilybot.database.entities.LockedChannelData import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.botHasChannelPerms import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms @@ -49,15 +53,9 @@ class LockingCommands : Extension() { check { anyGuild() - requiredConfigs( - ConfigOptions.MODERATION_ENABLED - ) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) - requireBotPermissions( - Permission.ManageChannels, - Permission.ManageRoles, - Permission.SendMessages - ) + requireBotPermissions(Permission.ManageChannels, Permission.ManageRoles) botHasChannelPerms(Permissions(Permission.ManageChannels)) } @@ -66,12 +64,28 @@ class LockingCommands : Extension() { val channelParent = getChannelParent(channelArg) val targetChannel = getTargetChannel(channelParent, channelArg) - val channelPerms = targetChannel!!.getPermissionOverwritesForRole(guild!!.id) - if (channelPerms != null && channelPerms.denied.contains(Permission.SendMessages)) { - respond { content = "This channel is already locked!" } + val currentChannelPerms = targetChannel?.getPermissionOverwritesForRole(guild!!.id) + if (currentChannelPerms == null) { + respond { + content = "There was an error getting the permissions for this channel. Please try again." + } return@action } + if (LockedChannelCollection().getLockedChannel(guild!!.id, targetChannel.id) != null) { + respond { content = "This channel is already locked" } + return@action + } + + LockedChannelCollection().addLockedChannel( + LockedChannelData( + guildId = guild!!.id, + channelId = targetChannel.id, + allowed = currentChannelPerms.data.allowed.code.value, + denied = currentChannelPerms.data.denied.code.value + ) + ) + val everyoneRole = guild!!.getRoleOrNull(guild!!.id) if (everyoneRole == null) { respond { content = "I was unable to get the `@everyone` role. Please try again." } @@ -94,6 +108,11 @@ class LockingCommands : Extension() { denied += Permission.UseApplicationCommands } + // Explicitly allow Lily send messages, so she can unlock the server again + targetChannel.editMemberPermission(this@ephemeralSlashCommand.kord.selfId) { + allowed += Permission.SendMessages + } + respond { content = "${targetChannel.mention} has been locked." } val actionLog = @@ -119,15 +138,9 @@ class LockingCommands : Extension() { check { anyGuild() - requiredConfigs( - ConfigOptions.MODERATION_ENABLED - ) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) - requireBotPermissions( - Permission.ManageChannels, - Permission.ManageRoles, - Permission.SendMessages - ) + requireBotPermissions(Permission.ManageChannels, Permission.ManageRoles, Permission.SendMessages) } action { @@ -187,11 +200,7 @@ class LockingCommands : Extension() { anyGuild() requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) - requireBotPermissions( - Permission.ManageChannels, - Permission.ManageRoles, - Permission.SendMessages - ) + requireBotPermissions(Permission.ManageChannels, Permission.ManageRoles) botHasChannelPerms(Permissions(Permission.ManageChannels)) } @@ -209,30 +218,36 @@ class LockingCommands : Extension() { return@action } - val channelPerms = targetChannel!!.getPermissionOverwritesForRole(guild!!.id) + val channelPerms = targetChannel?.getPermissionOverwritesForRole(guild!!.id) if (channelPerms == null) { respond { content = "This channel is not locked!" } return@action } - if (!channelPerms.denied.contains(Permission.SendMessages)) { + val lockedChannel = LockedChannelCollection().getLockedChannel(guild!!.id, targetChannel.id) + if (lockedChannel == null) { respond { content = "This channel is not locked!" } return@action } targetChannel.editRolePermission(guild!!.id) { - denied -= Permission.SendMessages - denied -= Permission.SendMessagesInThreads - denied -= Permission.AddReactions - denied -= Permission.UseApplicationCommands + denied = Permissions.Builder(DiscordBitSet(lockedChannel.denied)).build() + allowed = Permissions.Builder(DiscordBitSet(lockedChannel.allowed)).build() + } + + // Remove the explicit allow to avoid any issues with the servers permission system + targetChannel.editMemberPermission(this@ephemeralSlashCommand.kord.selfId) { + allowed -= Permission.SendMessages } targetChannel.createEmbed { title = "Channel Unlocked" description = "This channel has been unlocked by a moderator.\n" + - "Please be aware of the rules when continuing discussion." + "Please be aware of the rules when continuing discussion." color = DISCORD_GREEN } + LockedChannelCollection().removeLockedChannel(guild!!.id, targetChannel.id) + respond { content = "${targetChannel.mention} has been unlocked." } val actionLog = @@ -258,15 +273,9 @@ class LockingCommands : Extension() { check { anyGuild() - requiredConfigs( - ConfigOptions.MODERATION_ENABLED - ) + requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(Permission.ModerateMembers) - requireBotPermissions( - Permission.ManageChannels, - Permission.ManageRoles, - Permission.SendMessages - ) + requireBotPermissions(Permission.ManageChannels, Permission.ManageRoles, Permission.SendMessages) } action { @@ -334,9 +343,9 @@ class LockingCommands : Extension() { * @since 4.8.0 */ private suspend inline fun EphemeralInteractionContext.getTargetChannel( - channelParent: TextChannel?, - channelArg: Channel? - ): TextChannel? { + channelParent: TextChannel?, + channelArg: Channel? + ): TextChannel? { val targetChannel = channelParent ?: channelArg?.asChannelOfOrNull() if (targetChannel == null) { respond { diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModUtilities.kt similarity index 91% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModUtilities.kt index de71d44a..1654b12f 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ModUtilities.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModUtilities.kt @@ -1,30 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_BLACK -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.DISCORD_WHITE -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.checks.hasPermission -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.forms.ModalForm -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.utils.getJumpUrl -import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler -import com.kotlindiscord.kord.extensions.utils.scheduling.Task +package org.hyacinthbots.lilybot.extensions.moderation.commands + import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions @@ -40,6 +15,31 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.interaction.followup.EphemeralFollowupMessage import dev.kord.rest.builder.message.embed import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.DISCORD_BLACK +import dev.kordex.core.DISCORD_BLURPLE +import dev.kordex.core.DISCORD_WHITE +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.guildFor +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.defaultingColor +import dev.kordex.core.commands.converters.impl.optionalBoolean +import dev.kordex.core.commands.converters.impl.optionalChannel +import dev.kordex.core.commands.converters.impl.optionalColour +import dev.kordex.core.commands.converters.impl.optionalString +import dev.kordex.core.commands.converters.impl.snowflake +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.components.forms.ModalForm +import dev.kordex.core.components.linkButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.utils.getJumpUrl +import dev.kordex.core.utils.scheduling.Scheduler +import dev.kordex.core.utils.scheduling.Task import kotlinx.coroutines.flow.toList import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.AutoThreadingCollection diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModerationCommands.kt similarity index 66% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModerationCommands.kt index 5ef381bf..274285a8 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/ModerationCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/ModerationCommands.kt @@ -1,32 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.moderation - -import com.kotlindiscord.kord.extensions.DISCORD_BLACK -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.annotations.DoNotChain -import com.kotlindiscord.kord.extensions.checks.hasPermissions -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingOptionalDuration -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingString -import com.kotlindiscord.kord.extensions.commands.converters.impl.int -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalAttachment -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalInt -import com.kotlindiscord.kord.extensions.commands.converters.impl.user -import com.kotlindiscord.kord.extensions.components.components -import com.kotlindiscord.kord.extensions.components.ephemeralStringSelectMenu -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralMessageCommand -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api.PluralKit -import com.kotlindiscord.kord.extensions.time.TimestampType -import com.kotlindiscord.kord.extensions.time.toDiscord -import com.kotlindiscord.kord.extensions.utils.dm -import com.kotlindiscord.kord.extensions.utils.isNullOrBot -import com.kotlindiscord.kord.extensions.utils.kordExUserAgent -import com.kotlindiscord.kord.extensions.utils.timeout -import com.kotlindiscord.kord.extensions.utils.timeoutUntil -import com.kotlindiscord.kord.extensions.utils.toDuration +package org.hyacinthbots.lilybot.extensions.moderation.commands + import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.common.entity.Snowflake @@ -36,19 +9,60 @@ import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.behavior.interaction.followup.edit import dev.kord.core.behavior.reply +import dev.kord.core.entity.Guild import dev.kord.core.entity.Message import dev.kord.core.entity.User import dev.kord.core.entity.interaction.followup.EphemeralFollowupMessage import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.builder.message.embed import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.DISCORD_BLACK +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.annotations.DoNotChain +import dev.kordex.core.checks.hasPermissions +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.coalescingDuration +import dev.kordex.core.commands.converters.impl.coalescingOptionalDuration +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.defaultingString +import dev.kordex.core.commands.converters.impl.int +import dev.kordex.core.commands.converters.impl.optionalAttachment +import dev.kordex.core.commands.converters.impl.optionalBoolean +import dev.kordex.core.commands.converters.impl.user +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralStringSelectMenu +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralMessageCommand +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.pagination.EphemeralResponsePaginator +import dev.kordex.core.pagination.pages.Page +import dev.kordex.core.pagination.pages.Pages +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import dev.kordex.core.utils.dm +import dev.kordex.core.utils.isNullOrBot +import dev.kordex.core.utils.kordExUserAgent +import dev.kordex.core.utils.scheduling.Scheduler +import dev.kordex.core.utils.scheduling.Task +import dev.kordex.core.utils.timeout +import dev.kordex.core.utils.timeoutUntil +import dev.kordex.core.utils.toDuration +import dev.kordex.modules.pluralkit.api.PluralKit import kotlinx.datetime.Clock import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone import kotlinx.datetime.plus +import org.hyacinthbots.lilybot.database.collections.ModerationActionCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection +import org.hyacinthbots.lilybot.database.collections.TemporaryBanCollection import org.hyacinthbots.lilybot.database.collections.WarnCollection +import org.hyacinthbots.lilybot.database.entities.ActionData +import org.hyacinthbots.lilybot.database.entities.TemporaryBanData +import org.hyacinthbots.lilybot.database.entities.TimeData import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.extensions.moderation.utils.ModerationAction import org.hyacinthbots.lilybot.utils.HYACINTH_GITHUB import org.hyacinthbots.lilybot.utils.baseModerationEmbed import org.hyacinthbots.lilybot.utils.dmNotificationStatusEmbedField @@ -65,8 +79,15 @@ class ModerationCommands : Extension() { "For more information about the warn system, please see [this document]" + "($HYACINTH_GITHUB/LilyBot/blob/main/docs/commands.md#name-warn)" + /** The scheduler that will track the time for un-banning in temp bans. */ + private val tempBanScheduler = Scheduler() + + /** The task that will run the [tempBanScheduler]. */ + private lateinit var tempBanTask: Task + @OptIn(DoNotChain::class) override suspend fun setup() { + tempBanTask = tempBanScheduler.schedule(120, repeat = true, callback = ::removeTempBans) ephemeralMessageCommand { name = "Moderate" locking = true @@ -118,23 +139,23 @@ class ModerationCommands : Extension() { placeholder = "Select action..." maximumChoices = 1 // Prevent selecting multiple options at once - option(label = "Ban user", ModerationActions.BAN.name) { + option("Ban user", ModerationAction.BAN.name) { description = "Ban the user that sent this message" } - option("Soft-ban", ModerationActions.SOFT_BAN.name) { + option("Soft-ban", ModerationAction.SOFT_BAN.name) { description = "Soft-ban the user that sent this message" } - option("Kick", ModerationActions.KICK.name) { + option("Kick", ModerationAction.KICK.name) { description = "Kick the user that sent this message" } - option("Timeout", ModerationActions.TIMEOUT.name) { + option("Timeout", ModerationAction.TIMEOUT.name) { description = "Timeout the user that sent this message" } - option("Warn", ModerationActions.WARN.name) { + option("Warn", ModerationAction.WARN.name) { description = "Warn the user that sent this message" } @@ -150,7 +171,7 @@ class ModerationCommands : Extension() { val modConfig = ModerationConfigCollection().getConfig(guild!!.id) when (option) { - ModerationActions.BAN.name -> { + ModerationAction.BAN.name -> { val dm = sender.dm { embed { title = "You have been banned from ${guild?.asGuildOrNull()?.name}" @@ -159,6 +180,18 @@ class ModerationCommands : Extension() { color = DISCORD_GREEN } } + ModerationActionCollection().addAction( + ModerationAction.BAN, guild!!.id, senderId, + ActionData( + user.id, + null, + null, + "Quick banned $reasonSuffix", + dm != null, + true, + null + ) + ) sender.ban { reason = @@ -174,7 +207,7 @@ class ModerationCommands : Extension() { "for sending this message." } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { channel.createEmbed { title = "Banned." description = "${sender.mention} user was banned " + @@ -183,27 +216,13 @@ class ModerationCommands : Extension() { } } - loggingChannel.createEmbed { - title = "Banned a user" - description = "${ - sender.mention - } has been banned!" - baseModerationEmbed( - "Quick banned via moderate menu $reasonSuffix", - sender, - user - ) - dmNotificationStatusEmbedField(dm, true) - timestamp = Clock.System.now() - } - menuMessage?.edit { content = "Banned a user." components { removeAll() } } } - ModerationActions.SOFT_BAN.name -> { + ModerationAction.SOFT_BAN.name -> { val dm = sender.dm { embed { title = "You have been soft-banned from ${guild?.asGuildOrNull()?.name}" @@ -213,11 +232,28 @@ class ModerationCommands : Extension() { } } + ModerationActionCollection().addAction( + ModerationAction.SOFT_BAN, guild!!.id, senderId, + ActionData( + user.id, + null, + null, + "Quick banned $reasonSuffix", + dm != null, + true, + null + ) + ) + sender.ban { reason = "Quick soft-banned $reasonSuffix" } + ModerationActionCollection().shouldIgnoreAction( + ModerationAction.SOFT_BAN, guild!!.id, senderId + ) + guild!!.unban(senderId, "Quick soft-ban unban") if (modConfig?.publicLogging != null && modConfig.publicLogging == true) { @@ -229,7 +265,7 @@ class ModerationCommands : Extension() { "for sending this message." } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { channel.createEmbed { title = "Soft-Banned." description = "${sender.mention} user was soft-banned " + @@ -238,27 +274,13 @@ class ModerationCommands : Extension() { } } - loggingChannel.createEmbed { - title = "Soft-Banned a user" - description = "${ - sender.mention - } has been soft-banned!" - baseModerationEmbed( - "Quick soft-banned via moderate menu $reasonSuffix", - sender, - user - ) - dmNotificationStatusEmbedField(dm, true) - timestamp = Clock.System.now() - } - menuMessage?.edit { content = "Soft-Banned a user." components { removeAll() } } } - ModerationActions.KICK.name -> { + ModerationAction.KICK.name -> { val dm = sender.dm { embed { title = "You have been kicked from ${guild?.asGuildOrNull()?.name}" @@ -277,7 +299,7 @@ class ModerationCommands : Extension() { "for sending this message." } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { channel.createEmbed { title = "Kicked." description = "${sender.mention} user was kicked " + @@ -286,19 +308,18 @@ class ModerationCommands : Extension() { } } - loggingChannel.createEmbed { - title = "Kicked a user" - description = "${ - sender.mention - } has been kicked!" - baseModerationEmbed( + ModerationActionCollection().addAction( + ModerationAction.KICK, guild!!.id, senderId, + ActionData( + user.id, + null, + null, "Quick kicked via moderate menu $reasonSuffix", - sender, - user + dm != null, + true, + null ) - dmNotificationStatusEmbedField(dm, true) - timestamp = Clock.System.now() - } + ) menuMessage?.edit { content = "Kicked a user." @@ -306,7 +327,7 @@ class ModerationCommands : Extension() { } } - ModerationActions.TIMEOUT.name -> { + ModerationAction.TIMEOUT.name -> { val timeoutTime = ModerationConfigCollection().getConfig(guild!!.id)?.quickTimeoutLength if (timeoutTime == null) { @@ -337,7 +358,7 @@ class ModerationCommands : Extension() { "${timeoutTime.interval()} for sending this message." } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { channel.createEmbed { title = "Timed-out." description = "${sender.mention} user was timed-out for " + @@ -346,24 +367,18 @@ class ModerationCommands : Extension() { } } - loggingChannel.createEmbed { - title = "Timed-out a user" - description = "${ - sender.mention - } has be timed-out!" - baseModerationEmbed( + ModerationActionCollection().addAction( + ModerationAction.TIMEOUT, guild!!.id, senderId, + ActionData( + user.id, + null, + TimeData(timeoutTime, null, null, null), "Quick timed-out via moderate menu $reasonSuffix", - sender, - user + dm != null, + null, + null ) - dmNotificationStatusEmbedField(dm, true) - field { - name = "Length" - value = modConfig?.quickTimeoutLength.interval() - ?: "Failed to load timeout length" - } - timestamp = Clock.System.now() - } + ) menuMessage?.edit { content = "Timed-out a user." @@ -371,7 +386,7 @@ class ModerationCommands : Extension() { } } - ModerationActions.WARN.name -> { + ModerationAction.WARN.name -> { WarnCollection().setWarn(senderId, guild!!.id, false) val strikes = WarnCollection().getWarn(senderId, guild!!.id)?.strikes @@ -435,7 +450,7 @@ class ModerationCommands : Extension() { "for sending this message." } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { channel.createEmbed { title = "Warned." description = "${sender.mention} user was warned " + @@ -474,7 +489,8 @@ class ModerationCommands : Extension() { val modConfig = ModerationConfigCollection().getConfig(guild!!.id) var dmStatus: Message? = null - if (arguments.dm) { + val dmControl = if (arguments.dm != null) arguments.dm!! else modConfig?.dmDefault == true + if (dmControl) { dmStatus = arguments.userArgument.dm { embed { title = "You have been banned from ${guild?.asGuildOrNull()?.name}" @@ -486,111 +502,187 @@ class ModerationCommands : Extension() { } else { arguments.reason } + }\n${ + if (arguments.softBan) { + "You were soft-banned. You are free to rejoin without the need to be unbanned" + } else { + "" + } }" } } } - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Banned a User" - description = "${arguments.userArgument.mention} has been banned!" - baseModerationEmbed(arguments.reason, arguments.userArgument, user) - image = arguments.image?.url - dmNotificationStatusEmbedField(dmStatus, arguments.dm) - timestamp = Clock.System.now() - field { - name = "Days of messages deleted:" - value = arguments.messages.toString() - inline = false - } - } - } + + ModerationActionCollection().addAction( + if (arguments.softBan) ModerationAction.SOFT_BAN else ModerationAction.BAN, + guild!!.id, arguments.userArgument.id, + ActionData( + user.id, + arguments.messages, + null, + arguments.reason, + dmStatus != null, + arguments.dm, + arguments.image?.url + ) + ) if (modConfig?.publicLogging == true) { event.interaction.channel.createEmbed { - title = "Banned a user" - description = "${arguments.userArgument.mention} has been banned!" + if (arguments.softBan) { + title = "Soft-Banned a user" + description = "${arguments.userArgument.mention} has been soft-banned!" + } else { + title = "Banned a user" + description = "${arguments.userArgument.mention} has been banned!" + } color = DISCORD_BLACK } } guild?.ban(arguments.userArgument.id) { - reason = arguments.reason - deleteMessageDuration = DateTimePeriod(days = arguments.messages).toDuration(TimeZone.UTC) + reason = arguments.reason + if (arguments.softBan) " **SOFT-BAN**" else "" + deleteMessageDuration = if (arguments.softBan && arguments.messages == 0) { + DateTimePeriod(days = 3).toDuration(TimeZone.UTC) + } else { + DateTimePeriod(days = arguments.messages).toDuration(TimeZone.UTC) + } + } + + if (arguments.softBan) { + ModerationActionCollection().declareActionToIgnore( + ModerationAction.UNBAN, guild?.id!!, arguments.userArgument.id + ) + guild?.unban(arguments.userArgument.id, "User was soft-banned. **SOFT-BAN**") } respond { - content = "Banned ${arguments.userArgument.mention}" + content = if (arguments.softBan) "Soft-banned " else "Banned " + arguments.userArgument.mention } } } - ephemeralSlashCommand(::SoftBanArgs) { - name = "soft-ban" - description = "Soft-bans a user." - - requirePermission(Permission.BanMembers) - - check { - modCommandChecks(Permission.BanMembers) - requireBotPermissions(Permission.BanMembers) - } + ephemeralSlashCommand { + name = "temp-ban" + description = "The parent command for temporary ban commands" - action { - isBotOrModerator(event.kord, arguments.userArgument, guild, "soft-ban") ?: return@action + ephemeralSubCommand(::TempBanArgs) { + name = "add" + description = "Temporarily bans a user" - // The discord limit for deleting days of messages in a ban is 7, so we should catch invalid inputs. - if (arguments.messages != null && (arguments.messages!! > 7 || arguments.messages!! < 0)) { - respond { - content = "Invalid `delete-message-days` parameter! This number must be between 0 and 7 days!" - } - return@action + requirePermission(Permission.BanMembers) + check { + modCommandChecks(Permission.BanMembers) + requireBotPermissions(Permission.BanMembers) } - val modConfig = ModerationConfigCollection().getConfig(guild!!.id) - var dmStatus: Message? = null - if (arguments.dm) { - dmStatus = arguments.userArgument.dm { - embed { - title = "You have been soft-banned from ${guild?.fetchGuild()?.name}" - description = "**Reason:**\n${arguments.reason}\n\n" + - "You are free to rejoin without the need to be unbanned" + action { + isBotOrModerator(event.kord, arguments.userArgument, guild, "temp-ban add") + val now = Clock.System.now() + val duration = now.plus(arguments.duration, TimeZone.UTC) + val modConfig = ModerationConfigCollection().getConfig(guild!!.id) + var dmStatus: Message? = null + val dmControl = if (arguments.dm != null) arguments.dm!! else modConfig?.dmDefault == true + if (dmControl) { + dmStatus = arguments.userArgument.dm { + embed { + title = "You have been temporarily banned from ${guild?.fetchGuild()?.name}" + description = "**Reason:**\n${arguments.reason}\n\n" + + "You are banned until $duration" + } } } - } - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Soft-Banned a User" - description = "${arguments.userArgument.mention} has been soft-banned!" - baseModerationEmbed(arguments.reason, arguments.userArgument, user) - image = arguments.image?.url - dmNotificationStatusEmbedField(dmStatus, arguments.dm) - timestamp = Clock.System.now() - field { - name = "Days of messages deleted:" - value = "${arguments.messages ?: "3"}" - inline = false + + ModerationActionCollection().addAction( + ModerationAction.TEMP_BAN, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, + arguments.messages, + TimeData(arguments.duration, duration), + arguments.reason, + dmStatus != null, + arguments.dm, + arguments.image?.url + ) + ) + + if (modConfig?.publicLogging == true) { + event.interaction.channel.createEmbed { + title = "Temp Banned a user" + description = "${arguments.userArgument.mention} has been Temp Banned!" + color = DISCORD_BLACK } } - } - if (modConfig?.publicLogging == true) { - event.interaction.channel.createEmbed { - title = "Soft-Banned a user" - description = "${arguments.userArgument.mention} has been soft-banned!" - color = DISCORD_BLACK + TemporaryBanCollection().setTempBan( + TemporaryBanData(guild!!.id, arguments.userArgument.id, user.id, now, duration) + ) + + guild?.ban(arguments.userArgument.id) { + reason = arguments.reason + " **TEMPORARY-BAN**" + deleteMessageDuration = DateTimePeriod(days = arguments.messages).toDuration(TimeZone.UTC) + } + + respond { + content = "Temporarily banned ${arguments.userArgument.mention}" } } + } - guild?.ban(arguments.userArgument.id) { - reason = arguments.reason + " **SOFT-BAN**" - deleteMessageDuration = DateTimePeriod(days = arguments.messages ?: 3).toDuration(TimeZone.UTC) + ephemeralSubCommand { + name = "view-all" + description = "View all temporary bans for this guild" + + requirePermission(Permission.BanMembers) + + check { + modCommandChecks(Permission.BanMembers) + requireBotPermissions(Permission.BanMembers) } - guild?.unban(arguments.userArgument.id, "Soft-ban unban") + action { + val pagesObj = Pages() + val tempBans = TemporaryBanCollection().getTempBansForGuild(guild!!.id) + if (tempBans.isEmpty()) { + pagesObj.addPage( + Page { + description = "There are no temporary bans in this guild." + } + ) + } else { + tempBans.chunked(4).forEach { tempBan -> + var content = "" + tempBan.forEach { + content = """ + User: ${this@ephemeralSubCommand.kord.getUser(it.bannedUserId)?.username} + Moderator: ${guild?.getMemberOrNull(it.moderatorUserId)?.username} + Start Time: ${it.startTime.toDiscord(TimestampType.ShortDateTime)} (${ + it.startTime.toDiscord(TimestampType.RelativeTime) + }) + End Time: ${it.endTime.toDiscord(TimestampType.ShortDateTime)} (${ + it.endTime.toDiscord(TimestampType.RelativeTime) + }) + --- + """.trimIndent() + } - respond { - content = "Soft-banned ${arguments.userArgument.mention}" + pagesObj.addPage( + Page { + title = "Temporary bans for ${guild?.asGuildOrNull()?.name ?: "this guild"}" + description = content + } + ) + } + } + + val paginator = EphemeralResponsePaginator( + pages = pagesObj, + owner = event.interaction.user, + timeoutSeconds = 300, + interaction = interactionResponse + ) + + paginator.send() } } } @@ -607,23 +699,24 @@ class ModerationCommands : Extension() { } action { - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Unbanned a user" - description = "${arguments.userArgument.mention} has been unbanned!\n${ - arguments.userArgument.id - } (${arguments.userArgument.username})" - field { - name = "Reason:" - value = arguments.reason - } - footer { - text = user.asUserOrNull()?.username ?: "Unable to get user username" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - timestamp = Clock.System.now() - color = DISCORD_GREEN - } + val tempBan = TemporaryBanCollection().getUserTempBan(this.getGuild()!!.id, arguments.userArgument.id) + if (tempBan == null) { + ModerationActionCollection().addAction( + ModerationAction.UNBAN, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, null, null, arguments.reason, null, null, null + ) + ) + guild?.unban(arguments.userArgument.id, arguments.reason) + } else { + ModerationActionCollection().addAction( + ModerationAction.UNBAN, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, null, null, arguments.reason + "**TEMPORARY-BAN", null, null, null + ) + ) + guild?.unban(arguments.userArgument.id, arguments.reason + "**TEMPORARY-BAN**") + TemporaryBanCollection().removeTempBan(guild!!.id, arguments.userArgument.id) } respond { content = "Unbanned user." @@ -647,7 +740,8 @@ class ModerationCommands : Extension() { val modConfig = ModerationConfigCollection().getConfig(guild!!.id) var dmStatus: Message? = null - if (arguments.dm) { + val dmControl = if (arguments.dm != null) arguments.dm!! else modConfig?.dmDefault == true + if (dmControl) { dmStatus = arguments.userArgument.dm { embed { title = "You have been kicked from ${guild?.fetchGuild()?.name}" @@ -655,16 +749,12 @@ class ModerationCommands : Extension() { } } } - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Kicked a user" - description = "${arguments.userArgument.mention} has been kicked!" - image = arguments.image?.url - baseModerationEmbed(arguments.reason, arguments.userArgument, user) - dmNotificationStatusEmbedField(dmStatus, arguments.dm) - timestamp = Clock.System.now() - } - } + ModerationActionCollection().addAction( + ModerationAction.KICK, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, null, null, arguments.reason, dmStatus != null, arguments.dm, arguments.image?.url + ) + ) if (modConfig?.publicLogging == true) { event.interaction.channel.createEmbed { @@ -701,7 +791,8 @@ class ModerationCommands : Extension() { isBotOrModerator(event.kord, arguments.userArgument, guild, "timeout") ?: return@action var dmStatus: Message? = null - if (arguments.dm) { + val dmControl = if (arguments.dm != null) arguments.dm!! else modConfig?.dmDefault == true + if (dmControl) { dmStatus = arguments.userArgument.dm { embed { title = "You have been timed out in ${guild?.fetchGuild()?.name}" @@ -712,20 +803,19 @@ class ModerationCommands : Extension() { } } - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Timeout" - image = arguments.image?.url - baseModerationEmbed(arguments.reason, arguments.userArgument, user) - dmNotificationStatusEmbedField(dmStatus, arguments.dm) - timestamp = Clock.System.now() - field { - name = "Duration:" - value = duration.toDiscord(TimestampType.Default) + " (${durationArg.interval()})" - inline = false - } - } - } + ModerationActionCollection().addAction( + ModerationAction.TIMEOUT, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, + null, + TimeData(durationArg, duration, Clock.System.now(), duration), + arguments.reason, + dmStatus != null, + arguments.dm, + arguments.image?.url + ) + ) + if (modConfig?.publicLogging == true) { event.interaction.channel.createEmbed { title = "Timeout" @@ -761,8 +851,10 @@ class ModerationCommands : Extension() { } action { + val config = ModerationConfigCollection().getConfig(guild!!.id) var dmStatus: Message? = null - if (arguments.dm) { + val dmControl = if (arguments.dm != null) arguments.dm!! else config?.dmDefault == true + if (dmControl) { dmStatus = arguments.userArgument.dm { embed { title = "Timeout removed in ${guild!!.asGuildOrNull()?.name}" @@ -770,23 +862,13 @@ class ModerationCommands : Extension() { } } } - getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, guild!!)?.createMessage { - embed { - title = "Timeout Removed" - dmNotificationStatusEmbedField(dmStatus, arguments.dm) - field { - name = "User:" - value = "${arguments.userArgument.username} \n${arguments.userArgument.id}" - inline = false - } - footer { - text = "Requested by ${user.asUserOrNull()?.username}" - icon = user.asUserOrNull()?.avatar?.cdnUrl?.toUrl() - } - timestamp = Clock.System.now() - color = DISCORD_BLACK - } - } + + ModerationActionCollection().addAction( + ModerationAction.REMOVE_TIMEOUT, guild!!.id, arguments.userArgument.id, + ActionData( + user.id, null, null, null, dmStatus != null, arguments.dm, null + ) + ) arguments.userArgument.asMemberOrNull(guild!!.id)?.edit { timeoutUntil = null @@ -826,7 +908,9 @@ class ModerationCommands : Extension() { var dmStatus: Message? = null - if (arguments.dm) { + val dmControl = if (arguments.dm != null) arguments.dm!! else config.dmDefault == true + + if (dmControl) { val warnText = if (config.autoPunishOnWarn == false) { "No moderation action has been taken.\n $warnSuffix" } else { @@ -862,7 +946,7 @@ class ModerationCommands : Extension() { title = "Warning" image = arguments.image?.url baseModerationEmbed(arguments.reason, arguments.userArgument, user) - dmNotificationStatusEmbedField(dmStatus, arguments.dm) + dmNotificationStatusEmbedField(dmStatus, dmControl) timestamp = Clock.System.now() field { name = "Total strikes" @@ -964,6 +1048,43 @@ class ModerationCommands : Extension() { } } + private suspend fun removeTempBans() { + val tempBans = TemporaryBanCollection().getAllTempBans() + val dueTempBans = + tempBans.filter { it.endTime.toEpochMilliseconds() - Clock.System.now().toEpochMilliseconds() <= 0 } + + for (it in dueTempBans) { + var guild: Guild? + try { + guild = kord.getGuildOrNull(it.guildId) + } catch (_: KtorRequestException) { + TemporaryBanCollection().removeTempBan(it.guildId, it.bannedUserId) + continue + } + + if (guild == null) { + TemporaryBanCollection().removeTempBan(it.guildId, it.bannedUserId) + continue + } + + ModerationActionCollection().addAction( + ModerationAction.UNBAN, guild.id, it.bannedUserId, + ActionData( + it.moderatorUserId, + null, + TimeData(null, null, it.startTime, it.endTime), + "**temporary-ban-expire**", + null, + null, + null + ) + ) + + guild.unban(it.bannedUserId, "Temporary Ban expired") + TemporaryBanCollection().removeTempBan(it.guildId, it.bannedUserId) + } + } + inner class BanArgs : Arguments() { /** The user to ban. */ val userArgument by user { @@ -984,11 +1105,17 @@ class ModerationCommands : Extension() { defaultValue = "No reason provided" } + /** Weather to softban this user or not. */ + val softBan by defaultingBoolean { + name = "soft-ban" + description = "Weather to soft-ban this user (unban them once messages are deleted)" + defaultValue = false + } + /** Whether to DM the user or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" description = "Whether to send a direct message to the user about the ban" - defaultValue = true } /** An image that the user wishes to provide for context to the ban. */ @@ -998,20 +1125,26 @@ class ModerationCommands : Extension() { } } - inner class SoftBanArgs : Arguments() { - /** The user to soft-ban. */ + inner class TempBanArgs : Arguments() { + /** The user to ban. */ val userArgument by user { name = "user" - description = "Person to Soft ban" + description = "Person to ban" } - /** The number of days worth of messages to delete, defaults to 3 days. */ - val messages by optionalInt { + /** The number of days worth of messages to delete. */ + val messages by int { name = "delete-message-days" description = "The number of days worth of messages to delete" } - /** The reason for the soft-ban. */ + /** The duration of the temporary ban. */ + val duration by coalescingDuration { + name = "duration" + description = "The duration of the temporary ban." + } + + /** The reason for the ban. */ val reason by defaultingString { name = "reason" description = "The reason for the ban" @@ -1019,13 +1152,12 @@ class ModerationCommands : Extension() { } /** Whether to DM the user or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" - description = "Whether to send a direct message to the user about the soft-ban" - defaultValue = true + description = "Whether to send a direct message to the user about the ban" } - /** An image that the user wishes to provide for context to the soft-ban. */ + /** An image that the user wishes to provide for context to the ban. */ val image by optionalAttachment { name = "image" description = "An image you'd like to provide as extra context for the action" @@ -1062,10 +1194,9 @@ class ModerationCommands : Extension() { } /** Whether to DM the user or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" description = "Whether to send a direct message to the user about the kick" - defaultValue = true } /** An image that the user wishes to provide for context to the kick. */ @@ -1096,10 +1227,9 @@ class ModerationCommands : Extension() { } /** Whether to DM the user or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" description = "Whether to send a direct message to the user about the timeout" - defaultValue = true } /** An image that the user wishes to provide for context to the kick. */ @@ -1117,10 +1247,9 @@ class ModerationCommands : Extension() { } /** Whether to DM the user about the timeout removal or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" description = "Whether to dm the user about this or not" - defaultValue = true } } @@ -1139,10 +1268,9 @@ class ModerationCommands : Extension() { } /** Whether to DM the user or not. */ - val dm by defaultingBoolean { + val dm by optionalBoolean { name = "dm" description = "Whether to send a direct message to the user about the warning" - defaultValue = true } /** An image that the user wishes to provide for context to the kick. */ diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/Report.kt similarity index 88% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/Report.kt index 19dcfe21..a78b5fe3 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/Report.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/commands/Report.kt @@ -1,17 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.moderation - -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.commands.Arguments -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.forms.ModalForm -import com.kotlindiscord.kord.extensions.components.linkButton -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralMessageCommand -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.utils.getJumpUrl +package org.hyacinthbots.lilybot.extensions.moderation.commands + import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Snowflake import dev.kord.core.behavior.UserBehavior @@ -21,6 +9,18 @@ import dev.kord.core.entity.Message import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.rest.builder.message.create.FollowupMessageCreateBuilder import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.components.forms.ModalForm +import dev.kordex.core.components.linkButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralMessageCommand +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.utils.getJumpUrl import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationArgs.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationArgs.kt new file mode 100644 index 00000000..f5cb47fb --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationArgs.kt @@ -0,0 +1,56 @@ +package org.hyacinthbots.lilybot.extensions.moderation.config + +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.boolean +import dev.kordex.core.commands.converters.impl.coalescingOptionalDuration +import dev.kordex.core.commands.converters.impl.optionalBoolean +import dev.kordex.core.commands.converters.impl.optionalChannel +import dev.kordex.core.commands.converters.impl.optionalRole +import dev.kordex.core.commands.converters.impl.optionalString + +class ModerationArgs : Arguments() { + val enabled by boolean { + name = "enable-moderation" + description = "Whether to enable the moderation system" + } + + val moderatorRole by optionalRole { + name = "moderator-role" + description = "The role of your moderators, used for pinging in message logs." + } + + val modActionLog by optionalChannel { + name = "action-log" + description = "The channel used to store moderator actions." + } + + val quickTimeoutLength by coalescingOptionalDuration { + name = "quick-timeout-length" + description = "The length of timeouts to use for quick timeouts" + } + + val warnAutoPunishments by optionalBoolean { + name = "warn-auto-punishments" + description = "Whether to automatically punish users for reach a certain threshold on warns" + } + + val logPublicly by optionalBoolean { + name = "log-publicly" + description = "Whether to log moderation publicly or not." + } + + val dmDefault by optionalBoolean { + name = "dm-default" + description = "The default value for whether to DM a user in a ban action or not." + } + + val banDmMessage by optionalString { + name = "ban-dm-message" + description = "A custom message to send to users when they are banned." + } + + val autoInviteModeratorRole by optionalBoolean { + name = "auto-invite-moderator-role" + description = "Silently ping moderators to invite them to new threads." + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationCommand.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationCommand.kt new file mode 100644 index 00000000..9660b247 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/config/ModerationCommand.kt @@ -0,0 +1,131 @@ +package org.hyacinthbots.lilybot.extensions.moderation.config + +import dev.kord.common.entity.Permission +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.getChannelOfOrNull +import dev.kord.core.entity.channel.TextChannel +import dev.kord.rest.builder.message.embed +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.application.slash.SlashCommand +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.utils.botHasPermissions +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.extensions.config.utils.moderationEmbed +import org.hyacinthbots.lilybot.utils.canPingRole +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms + +suspend fun SlashCommand<*, *, *>.moderationCommand() = + ephemeralSubCommand(::ModerationArgs) { + name = "moderation" + description = "Configure Lily's moderation system" + + requirePermission(Permission.ManageGuild) + + check { + anyGuild() + hasPermission(Permission.ManageGuild) + } + + action { + val moderationConfig = ModerationConfigCollection().getConfig(guild!!.id) + if (moderationConfig != null) { + respond { + content = "You already have a moderation configuration set. " + + "Please clear it before attempting to set a new one." + } + return@action + } + + if (!arguments.enabled) { + ModerationConfigCollection().setConfig( + ModerationConfigData( + guild!!.id, + false, + null, + null, + null, + null, + null, + null, + null, + null + ) + ) + respond { + content = "Moderation system disabled." + } + return@action + } + + if ( + arguments.moderatorRole != null && arguments.modActionLog == null || + arguments.moderatorRole == null && arguments.modActionLog != null + ) { + respond { + content = + "You must set both the moderator role and the action log channel to use the moderation configuration." + } + return@action + } + + if (!canPingRole(arguments.moderatorRole, guild!!.id, this@moderationCommand.kord)) { + 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 + } + + val modActionLog: TextChannel? + 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 + } + } + + respond { + embed { + moderationEmbed(arguments, user) + } + } + + ModerationConfigCollection().setConfig( + ModerationConfigData( + guild!!.id, + arguments.enabled, + arguments.modActionLog?.id, + arguments.moderatorRole?.id, + arguments.quickTimeoutLength, + arguments.warnAutoPunishments, + arguments.logPublicly, + arguments.dmDefault, + arguments.banDmMessage, + arguments.autoInviteModeratorRole + ) + ) + + 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(arguments, user) + } + } + } + } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/events/ModerationEvents.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/events/ModerationEvents.kt new file mode 100644 index 00000000..8b4d551d --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/events/ModerationEvents.kt @@ -0,0 +1,304 @@ +package org.hyacinthbots.lilybot.extensions.moderation.events + +import dev.kord.core.behavior.UserBehavior +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.event.guild.BanAddEvent +import dev.kord.core.event.guild.BanRemoveEvent +import dev.kord.core.event.guild.MemberUpdateEvent +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import dev.kordex.core.utils.timeoutUntil +import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.database.collections.ModerationActionCollection +import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.extensions.moderation.utils.ModerationAction +import org.hyacinthbots.lilybot.utils.baseModerationEmbed +import org.hyacinthbots.lilybot.utils.dmNotificationStatusEmbedField +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.interval + +class ModerationEvents : Extension() { + override val name: String = "moderation-events" + + override suspend fun setup() { + event { + check { anyGuild() } + action { + // Do not log if the moderation system is disabled + if (ModerationConfigCollection().getConfig(event.guildId)?.enabled != true) return@action + // If the ban doesn't exist then... ???? + event.getBanOrNull() ?: return@action + var existingAction = ModerationActionCollection().getAction( + ModerationAction.BAN, event.guildId, event.user.id + ) + if (existingAction == null) { + existingAction = ModerationActionCollection().getAction( + ModerationAction.SOFT_BAN, event.guildId, event.user.id + ) + } + + if (existingAction == null) { + existingAction = ModerationActionCollection().getAction( + ModerationAction.TEMP_BAN, event.guildId, event.user.id + ) + } + + if (existingAction != null && existingAction.targetUserId != event.user.id) { + return@action + } + + if (existingAction != null) { + getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, event.getGuild())?.createEmbed { + title = when (existingAction.actionType) { + ModerationAction.BAN -> "Banned a user" + ModerationAction.SOFT_BAN -> "Soft-banned a user" + ModerationAction.TEMP_BAN -> "Temp-banned a user" + else -> null // Theoretically this should never occur but the compiler cries otherwise + } + + description = "${event.user.mention} has been ${ + if (existingAction.data.reason?.contains("quick ban", true) == false) { + when (existingAction.actionType) { + ModerationAction.BAN -> "banned" + ModerationAction.SOFT_BAN -> "soft-banned" + ModerationAction.TEMP_BAN -> "temporarily banned" + else -> null // Again should theoretically never occur, but compiler + } + } else { + when (existingAction.actionType) { + ModerationAction.BAN -> existingAction.data.reason + ModerationAction.SOFT_BAN -> existingAction.data.reason + else -> null + } + } + }" + baseModerationEmbed( + existingAction.data.reason, + event.user, + UserBehavior(existingAction.data.actioner!!, kord) + ) + image = existingAction.data.imageUrl + dmNotificationStatusEmbedField(existingAction.data.dmOutcome, existingAction.data.dmOverride) + timestamp = Clock.System.now() + if (existingAction.data.deletedMessages != null) { + field { + name = "Days of messages deleted" + value = + if (existingAction.actionType == ModerationAction.SOFT_BAN && + existingAction.data.deletedMessages == 0 + ) { + "3" + } else { + existingAction.data.deletedMessages.toString() + } + inline = false + } + } + if (existingAction.data.timeData != null) { + field { + name = "Duration:" + value = + existingAction.data.timeData.durationInst?.toDiscord(TimestampType.Default) + + " (${existingAction.data.timeData.durationDtp?.interval()})" + } + } + } + + ModerationActionCollection().removeAction( + existingAction.actionType, + event.guildId, + event.user.id + ) + } else { + getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, event.getGuild())?.createEmbed { + title = "Banned a User" + description = "${event.user.mention} has been banned!" + baseModerationEmbed(event.getBan().reason, event.user, null) + timestamp = Clock.System.now() + } + } + } + } + event { + check { anyGuild() } + action { + // Do not log if the moderation system is disabled + if (ModerationConfigCollection().getConfig(event.guildId)?.enabled != true) return@action + val ignore = ModerationActionCollection().shouldIgnoreAction( + ModerationAction.UNBAN, + event.guildId, + event.user.id + ) + if (ignore != null && ignore) { + return@action + } + + val existingAction = + ModerationActionCollection().getAction(ModerationAction.UNBAN, event.guildId, event.user.id) + if (existingAction != null && existingAction.targetUserId != event.user.id) { + return@action + } + + if (existingAction != null) { + val isTempUnban = existingAction.data.reason?.contains("**temporary-ban**", true) == true + + getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, event.getGuild())?.createEmbed { + title = if (isTempUnban) "Temporary ban removed" else "Unbanned a user" + description = "${event.user.mention} has " + + "${if (isTempUnban) "had their temporary ban removed!" else "been unbanned!"}\n" + + "${event.user.id} (${event.user.username})" + if (existingAction.data.reason?.contains("**temporary-ban-expire**") == false) { + field { + name = "Reason:" + value = existingAction.data.reason.toString() + } + } else { + field { + name = "Initial Ban date:" + value = existingAction.data.timeData?.start?.toDiscord(TimestampType.ShortDateTime) + .toString() + } + } + + if (existingAction.data.actioner != null) { + val user = UserBehavior(existingAction.data.actioner, kord).asUserOrNull() + footer { + text = user?.username ?: "Unable to get username" + icon = user?.avatar?.cdnUrl?.toUrl() + } + } + timestamp = Clock.System.now() + color = DISCORD_GREEN + } + ModerationActionCollection().removeAction( + existingAction.actionType, + event.guildId, + event.user.id + ) + } + } + } + event { + check { anyGuild() } + action { + // Do not log if the moderation system is disabled + if (ModerationConfigCollection().getConfig(event.guildId)?.enabled != true) return@action + val channel = getLoggingChannelWithPerms(ConfigOptions.ACTION_LOG, event.guild) + if (event.old?.timeoutUntil != event.member.timeoutUntil) { + var existingAction = + ModerationActionCollection().getAction(ModerationAction.TIMEOUT, event.guildId, event.member.id) + + if (existingAction == null) { + existingAction = + ModerationActionCollection().getAction( + ModerationAction.REMOVE_TIMEOUT, + event.guildId, + event.member.id + ) + } + + if (existingAction != null) { + val data = existingAction.data + val isRemove = existingAction.actionType == ModerationAction.REMOVE_TIMEOUT + val user = UserBehavior(existingAction.targetUserId, kord).asUserOrNull() + val actioner = data.actioner?.let { UserBehavior(it, kord) }?.asUserOrNull() + channel?.createEmbed { + if (isRemove) { + title = "Timeout removed" + dmNotificationStatusEmbedField(data.dmOutcome, data.dmOverride) + field { + name = "User:" + value = "${user?.username} (${user?.id})" + } + footer { + text = "Requested by ${actioner?.username}" + icon = user?.avatar?.cdnUrl?.toUrl() + } + timestamp = Clock.System.now() + color = DISCORD_GREEN + } else { + title = "Timeout" + image = data.imageUrl + baseModerationEmbed(data.reason, event.member, user) + dmNotificationStatusEmbedField(data.dmOutcome, data.dmOverride) + timestamp = data.timeData?.start + color = DISCORD_RED + field { + name = "Duration" + value = data.timeData?.durationInst?.toDiscord(TimestampType.Default) + + " (${data.timeData?.durationDtp.interval()})" + } + } + } + + ModerationActionCollection().removeAction( + existingAction.actionType, + event.guild.id, + event.member.id + ) + } else { + channel?.createEmbed { + if (event.member.timeoutUntil != null) { + title = "Timeout" + color = DISCORD_RED + field { + name = "Duration" + value = event.member.timeoutUntil?.toDiscord(TimestampType.Default) ?: "0" + } + } else { + title = "Removed timeout" + color = DISCORD_GREEN + } + field { + name = "User" + value = event.member.mention + "(${event.member.id})" + } + description = "via Discord menus" + timestamp = Clock.System.now() + } + } + } else { + val newRoles = mutableListOf() + val oldRoles = mutableListOf() + event.member.roleBehaviors.forEach { newRoles.add(it.mention) } + event.old?.roleBehaviors?.forEach { oldRoles.add(it.mention) } + channel?.createEmbed { + title = "Member updated" + description = "${event.member.username} has been updated" + if (event.member.nickname != event.old?.nickname) { + field { + name = "Nickname change" + value = "Old: ${event.old?.nickname}\nNew: ${event.member.nickname}" + } + } + if (event.member.isOwner() != event.old?.isOwner()) { + field { + name = "Became server owner" + } + } + if (event.member.roles != event.old?.roles) { + field { + name = "New Roles" + value = newRoles.joinToString(", ") + inline = true + } + field { + name = "Old roles" + value = oldRoles.joinToString(", ") + inline = true + } + } + color = DISCORD_YELLOW + } + } + } + } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/utils/ModerationAction.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/utils/ModerationAction.kt new file mode 100644 index 00000000..7d655770 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/moderation/utils/ModerationAction.kt @@ -0,0 +1,13 @@ +package org.hyacinthbots.lilybot.extensions.moderation.utils + +enum class ModerationAction { + BAN, + SOFT_BAN, + TEMP_BAN, + UNBAN, + KICK, + TIMEOUT, + REMOVE_TIMEOUT, + WARN, + REMOVE_WARN +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/AutoThreading.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/AutoThreading.kt similarity index 77% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/AutoThreading.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/AutoThreading.kt index 1cf17ece..6ce9928b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/AutoThreading.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/AutoThreading.kt @@ -1,28 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.events - -import com.kotlindiscord.kord.extensions.DISCORD_BLACK -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.channel -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalRole -import com.kotlindiscord.kord.extensions.components.forms.ModalForm -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.extra.pluralkit.api.PKMessage -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.PKMessageCreateEvent -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.ProxiedMessageCreateEvent -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.events.UnProxiedMessageCreateEvent -import com.kotlindiscord.kord.extensions.modules.unsafe.annotations.UnsafeAPI -import com.kotlindiscord.kord.extensions.modules.unsafe.extensions.unsafeSubCommand -import com.kotlindiscord.kord.extensions.modules.unsafe.types.InitialSlashCommandResponse -import com.kotlindiscord.kord.extensions.modules.unsafe.types.ackEphemeral -import com.kotlindiscord.kord.extensions.modules.unsafe.types.respondEphemeral -import com.kotlindiscord.kord.extensions.utils.delete -import com.kotlindiscord.kord.extensions.utils.respond +package org.hyacinthbots.lilybot.extensions.threads + import dev.kord.common.entity.ArchiveDuration import dev.kord.common.entity.ChannelType import dev.kord.common.entity.MessageType @@ -44,7 +21,29 @@ import dev.kord.core.entity.channel.thread.ThreadChannel import dev.kord.core.event.channel.thread.ThreadChannelCreateEvent import dev.kord.core.supplier.EntitySupplyStrategy import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_BLACK +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.channel +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.optionalRole +import dev.kordex.core.components.forms.ModalForm +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.delete +import dev.kordex.core.utils.respond +import dev.kordex.modules.dev.unsafe.annotations.UnsafeAPI +import dev.kordex.modules.dev.unsafe.commands.slash.InitialSlashCommandResponse +import dev.kordex.modules.dev.unsafe.extensions.unsafeSubCommand +import dev.kordex.modules.pluralkit.api.PKMessage +import dev.kordex.modules.pluralkit.events.PKMessageCreateEvent +import dev.kordex.modules.pluralkit.events.ProxiedMessageCreateEvent +import dev.kordex.modules.pluralkit.events.UnProxiedMessageCreateEvent import kotlinx.datetime.Clock +import org.hyacinthbots.docgenerator.subCommandAdditionalDocumentation import org.hyacinthbots.lilybot.database.collections.AutoThreadingCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.ThreadsCollection @@ -135,7 +134,8 @@ class AutoThreading : Extension() { contentAwareNaming = arguments.contentAwareNaming, mention = arguments.mention, creationMessage = message, - addModsAndRole = arguments.addModsAndRole + addModsAndRole = arguments.addModsAndRole, + extraRoleIds = mutableListOf() ) ) @@ -257,7 +257,7 @@ class AutoThreading : Extension() { var responseContent: String? = null autoThreads.forEach { responseContent += "\n<#${it.channelId}>" - if (responseContent!!.length > 4080) { + if (responseContent.length > 4080) { responseContent += "(List trimmed.)" return@forEach } @@ -311,6 +311,16 @@ class AutoThreading : Extension() { "None" } } + if (autoThread.extraRoleIds.isNotEmpty()) { + var mentions = "" + autoThread.extraRoleIds.forEach { + mentions += "${guild!!.getRoleOrNull(it)?.mention} " + } + field { + name = "Extra Ping Roles" + value = mentions + } + } field { name = "Prevent duplicates" value = autoThread.preventDuplicates.toString() @@ -339,6 +349,103 @@ class AutoThreading : Extension() { } } } + + ephemeralSubCommand(::ExtraRolesArgs) { + name = "add-roles" + description = "Add extra to threads in auto-threaded channels" + + subCommandAdditionalDocumentation { + extraInformation = "This command will add roles to be pinged alongside the default ping role " + + "for this auto-threaded channel" + } + + requirePermission(Permission.ManageChannels) + + check { + anyGuild() + hasPermission(Permission.ManageChannels) + requireBotPermissions(Permission.SendMessages) + botHasChannelPerms(Permissions(Permission.SendMessages)) + } + + action { + val autoThread = AutoThreadingCollection().getSingleAutoThread(event.interaction.channelId) + if (autoThread == null) { + respond { + content = "**Error:** This is not an auto-threaded channel!" + } + return@action + } + + if (!canPingRole(arguments.role, guild!!.id, this@ephemeralSubCommand.kord)) { + respond { + content = "Lily cannot mention this role. Please fix the role's permissions and try again." + } + return@action + } + + if (autoThread.extraRoleIds.contains(arguments.role!!.id)) { + respond { + content = "This role has already been added." + } + return@action + } + + val updatedRoles = autoThread.extraRoleIds + updatedRoles.add(arguments.role!!.id) + + AutoThreadingCollection().updateExtraRoles(event.interaction.channelId, updatedRoles) + + respond { + content = "Role (${arguments.role!!.mention}) added" + } + } + } + + ephemeralSubCommand(::ExtraRolesArgs) { + name = "remove-roles" + description = "Remove extra from threads in auto-threaded channels" + + subCommandAdditionalDocumentation { + extraInformation = "This command will remove roles that have been added to be pinged alongside " + + "the default ping role for this auto-threaded channel" + } + + requirePermission(Permission.ManageChannels) + + check { + anyGuild() + hasPermission(Permission.ManageChannels) + requireBotPermissions(Permission.SendMessages) + botHasChannelPerms(Permissions(Permission.SendMessages)) + } + + action { + val autoThread = AutoThreadingCollection().getSingleAutoThread(event.interaction.channelId) + if (autoThread == null) { + respond { + content = "**Error:** This is not an auto-threaded channel!" + } + return@action + } + + if (!autoThread.extraRoleIds.contains(arguments.role!!.id)) { + respond { + content = "This role has not been added." + } + return@action + } + + val updatedRoles = autoThread.extraRoleIds + updatedRoles.remove(arguments.role!!.id) + + AutoThreadingCollection().updateExtraRoles(event.interaction.channelId, updatedRoles) + + respond { + content = "Role (${arguments.role!!.mention}) removed" + } + } + } } event { @@ -456,6 +563,13 @@ class AutoThreading : Extension() { } } + inner class ExtraRolesArgs : Arguments() { + val role by optionalRole { + name = "role" + description = "A role to invite to threads in this channel" + } + } + inner class AutoThreadingViewArgs : Arguments() { val channel by channel { name = "channel" @@ -571,14 +685,18 @@ class AutoThreading : Extension() { if (moderatorRoleId != null) { moderatorRole = inputThread.guild.getRole(moderatorRoleId) } + var mentions = "" + inputOptions.extraRoleIds.forEach { + mentions += inputThread.guild.getRole(it).mention + } if (moderatorRole != null && moderatorRole.mentionable && inputOptions.addModsAndRole) { threadMessage.edit { - content = role.mention + moderatorRole.mention + content = role.mention + mentions + moderatorRole.mention } } else { threadMessage.edit { - content = role.mention + content = role.mention + mentions } } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ModThreadInviting.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ModThreadInviting.kt similarity index 81% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ModThreadInviting.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ModThreadInviting.kt index e4b6cd97..cc07b5b3 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/events/ModThreadInviting.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ModThreadInviting.kt @@ -1,12 +1,12 @@ -package org.hyacinthbots.lilybot.extensions.events +package org.hyacinthbots.lilybot.extensions.threads -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.event import dev.kord.core.behavior.channel.createMessage import dev.kord.core.behavior.edit import dev.kord.core.event.channel.thread.ThreadChannelCreateEvent import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event import org.hyacinthbots.lilybot.database.collections.AutoThreadingCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.utils.canPingRole @@ -20,7 +20,7 @@ class ModThreadInviting : Extension() { anyGuild() failIf { event.channel.ownerId == kord.selfId || - event.channel.member != null + event.channel.member != null } } @@ -35,7 +35,7 @@ class ModThreadInviting : Extension() { val config = ModerationConfigCollection().getConfig(channel.guildId) ?: return@action - if (!config.enabled || config.role == null) return@action + if (!config.enabled || config.role == null || config.autoInviteModeratorRole != true) return@action val moderatorRole = channel.guild.getRoleOrNull(config.role) ?: return@action diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ThreadControl.kt similarity index 92% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ThreadControl.kt index 9c212578..64aff562 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/ThreadControl.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/threads/ThreadControl.kt @@ -4,22 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_FUCHSIA -import com.kotlindiscord.kord.extensions.checks.isInThread -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSlashCommandContext -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.member -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.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.event -import com.kotlindiscord.kord.extensions.utils.hasPermission +package org.hyacinthbots.lilybot.extensions.threads + import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions @@ -36,6 +22,20 @@ import dev.kord.core.entity.interaction.response.EphemeralMessageInteractionResp import dev.kord.core.event.channel.thread.ThreadUpdateEvent import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_FUCHSIA +import dev.kordex.core.checks.isInThread +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.EphemeralSlashCommandContext +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.member +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.hasPermission import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.ThreadsCollection diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GalleryChannel.kt similarity index 88% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GalleryChannel.kt index b1b0b083..b4c9c903 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GalleryChannel.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GalleryChannel.kt @@ -1,17 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -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.utils.delete -import com.kotlindiscord.kord.extensions.utils.permissionsForMember -import com.kotlindiscord.kord.extensions.utils.respond +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.entity.MessageType import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions @@ -22,6 +10,18 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.event.message.MessageCreateEvent import dev.kord.core.exception.EntityNotFoundException import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.guildFor +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.delete +import dev.kordex.core.utils.permissionsForMember +import dev.kordex.core.utils.respond import kotlinx.coroutines.delay import org.hyacinthbots.lilybot.database.collections.GalleryChannelCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions @@ -222,7 +222,7 @@ class GalleryChannel : Extension() { // Delete the explanation after 3 seconds. If an exception is thrown, the // message has already been deleted response.delete(2.5.seconds.inWholeMilliseconds) - } catch (e: EntityNotFoundException) { + } catch (_: EntityNotFoundException) { // The message that we're attempting to delete has already been deleted. } } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Github.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Github.kt similarity index 87% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Github.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Github.kt index db957cc4..9171d11d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Github.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Github.kt @@ -1,19 +1,19 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.application.slash.publicSubCommand -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.string -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import com.kotlindiscord.kord.extensions.sentry.BreadcrumbType +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.entity.Permission import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.application.slash.publicSubCommand +import dev.kordex.core.commands.converters.impl.int +import dev.kordex.core.commands.converters.impl.optionalString +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.publicSlashCommand +import dev.kordex.core.sentry.BreadcrumbType import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.GithubCollection import org.hyacinthbots.lilybot.github @@ -69,10 +69,6 @@ class Github : Extension() { } // Clarify the input is formatted correctly, inform the user if not. if (!repository.contains("/")) { - sentry.breadcrumb(BreadcrumbType.Error) { - category = "extensions.util.Github.issue.InputCheck" - message = "Input missing /" - } respond { embed { title = "Make sure your repository input is formatted like this:" @@ -93,7 +89,7 @@ class Github : Extension() { } else { try { github.getRepository(repository)?.getIssue(arguments.issue) - } catch (e: GHFileNotFoundException) { + } catch (_: GHFileNotFoundException) { respond { embed { title = "Unable to find issue number! Make sure this issue exists" @@ -102,7 +98,7 @@ class Github : Extension() { return@action } } - } catch (e: IOException) { + } catch (_: IOException) { val iterator: PagedIterator? = github.searchIssues() ?.q("${arguments.issue} repo:$repository") ?.order(GHDirection.DESC) @@ -112,7 +108,7 @@ class Github : Extension() { // Run a quick check on the iterator, in case the repository owner org/user doesn't exist try { iterator!!.hasNext() - } catch (e: GHException) { + } catch (_: GHException) { respond { embed { title = "Unable to access repository, make sure this repository exists!" @@ -124,11 +120,6 @@ class Github : Extension() { if (iterator.hasNext()) { issue = iterator.next() } else { - sentry.breadcrumb(BreadcrumbType.Error) { - category = "extensions.util.Github.issue.getIssue" - message = "Unable to find issue" - } - respond { embed { title = "Invalid issue number. Make sure this issue exists!" @@ -140,7 +131,7 @@ class Github : Extension() { val num = issue!!.number try { issue = github.getRepository(repository)?.getIssue(num) - } catch (e: GHFileNotFoundException) { + } catch (_: GHFileNotFoundException) { respond { embed { title = "Unable to find issue number! Make sure this issue exists" @@ -167,10 +158,6 @@ class Github : Extension() { merged = pull.isMerged draft = pull.isDraft } catch (ioException: IOException) { - sentry.breadcrumb(BreadcrumbType.Error) { - category = "extensions.util.Github.issue.CheckPRStatus" - message = "Error initializing PR wtf" - } ioException.printStackTrace() title = "Error!" description = "Error occurred initializing Pull Request. How did this happen?" @@ -245,7 +232,7 @@ class Github : Extension() { inline = false } } - } catch (ioException: IOException) { + } catch (_: IOException) { field { name = "Author:" value = "Unknown Author" @@ -266,7 +253,7 @@ class Github : Extension() { labels.add(label.name) } - if (labels.size > 0) { + if (labels.isNotEmpty()) { field { name = "Labels:" value = labels.joinToString(", ") @@ -295,12 +282,8 @@ class Github : Extension() { } return@action } - // Clarify the input is formatted correctly, inform the user if not + if (!repository.contains("/")) { - sentry.breadcrumb(BreadcrumbType.Error) { - category = "extensions.util.Github.repository.InputCheck" - message = "Input missing /" - } respond { embed { title = "Make sure your input is formatted like this:" @@ -321,11 +304,7 @@ class Github : Extension() { } else { github.getRepository(repository) } - sentry.breadcrumb(BreadcrumbType.Info) { - category = "extensions.util.Github.repository.getRepository" - message = "Repository found" - } - } catch (exception: IOException) { + } catch (_: IOException) { sentry.breadcrumb(BreadcrumbType.Error) { category = "extensions.util.Github.repository.getRepository" message = "Repository not found" @@ -402,15 +381,7 @@ class Github : Extension() { } else { github.getUser(arguments.username) } - sentry.breadcrumb(BreadcrumbType.Info) { - category = "extensions.util.Github.user.getUser" - message = "User found" - } - } catch (exception: IOException) { - sentry.breadcrumb(BreadcrumbType.Error) { - category = "extensions.util.Github.user.getUser" - message = "Unable to find user" - } + } catch (_: IOException) { respond { embed { title = "Invalid Username. Make sure this user exists!" @@ -550,7 +521,7 @@ class Github : Extension() { } else { github.getRepository(arguments.defaultRepo) } - } catch (e: IOException) { + } catch (_: IOException) { respond { content = "GitHub repository not found! Please make sure this repository exists" } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GuildAnnouncements.kt similarity index 87% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GuildAnnouncements.kt index 26ad758b..08e5e67b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/GuildAnnouncements.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/GuildAnnouncements.kt @@ -1,13 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalSnowflake -import com.kotlindiscord.kord.extensions.components.components -import com.kotlindiscord.kord.extensions.components.ephemeralButton -import com.kotlindiscord.kord.extensions.components.forms.ModalForm -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.Color import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission @@ -15,6 +7,14 @@ import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.behavior.interaction.followup.edit import dev.kord.core.entity.interaction.followup.EphemeralFollowupMessage import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.optionalSnowflake +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.components.forms.ModalForm +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.extensions.config.ConfigOptions import org.hyacinthbots.lilybot.utils.TEST_GUILD_ID diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/InfoCommands.kt similarity index 50% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/InfoCommands.kt index fad8ef8f..35a61b85 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/InfoCommands.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/InfoCommands.kt @@ -1,18 +1,12 @@ -package org.hyacinthbots.lilybot.extensions.util +package org.hyacinthbots.lilybot.extensions.utility.commands -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.components.components -import com.kotlindiscord.kord.extensions.components.linkButton -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import com.kotlindiscord.kord.extensions.time.TimestampType -import com.kotlindiscord.kord.extensions.time.toDiscord import dev.kord.rest.builder.message.create.MessageCreateBuilder import dev.kord.rest.builder.message.embed -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import org.hyacinthbots.lilybot.database.collections.UptimeCollection -import org.hyacinthbots.lilybot.internal.BuildInfo +import dev.kordex.core.DISCORD_BLURPLE +import dev.kordex.core.components.components +import dev.kordex.core.components.linkButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.publicSlashCommand import org.hyacinthbots.lilybot.utils.HYACINTH_GITHUB /** @@ -44,7 +38,7 @@ class InfoCommands : Extension() { title = "What is LilyBot?" description = "Lily is a FOSS multi-purpose bot for Discord created by " + "the HyacinthBots organization. " + - "Use `/info` to learn more, or `/invite` to get an invite link." + "Use `/about` to learn more, or `/invite` to get an invite link." field { name = "How do I configure Lily?" @@ -86,77 +80,6 @@ class InfoCommands : Extension() { } } - /** - * A command that creates an embed providing basic info about Lily and uptime data. - * - * @author NoComment1105 - * @author tempest15 - * @since 4.4.0 - */ - publicSlashCommand { - name = "info" - description = "Learn about Lily, and get uptime data!" - - action { - respond { - embed { - thumbnail { - url = event.kord.getSelf().avatar?.cdnUrl!!.toUrl() - } - title = "Info about LilyBot" - description = "Lily is a FOSS multi-purpose bot for Discord created by " + - "the HyacinthBots organization. " + - "Use `/help` for support or `/invite` to get an invite link." - - field { - name = "How can I support the continued development of Lily?" - value = "Lily is developed primarily by NoComment#6411 and tempest#4510 " + - "in our free time. Neither of us have resources to invest in hosting, " + - "so financial donations via [Buy Me a Coffee]" + - "(https://buymeacoffee.com/Hyacinthbots) help keep Lily afloat. Currently, we run" + - "lily on a Hetzner cloud server, which we can afford in our current situation. " + - "We will also have domain costs for our website.\n\n" + - "Contributions of code & documentation are also incredibly appreciated, " + - "and you can read our [contributing guide]" + - "($HYACINTH_GITHUB/LilyBot/blob/main/CONTRIBUTING.md) " + - "or [development guide]" + - "($HYACINTH_GITHUB/LilyBot/blob/main/docs/development-guide.md) " + - "to get started." - } - field { - name = "Version" - // To avoid IntelliJ shouting about build errors, use https://plugins.jetbrains.com/plugin/9407-pebble - value = - "${BuildInfo.LILY_VERSION} (${BuildInfo.BUILD_ID})" - inline = true - } - field { - name = "Up Since" - value = "${ - UptimeCollection().get()?.onTime?.toLocalDateTime(TimeZone.UTC) - ?.time.toString().split(".")[0] - } ${UptimeCollection().get()?.onTime?.toLocalDateTime(TimeZone.UTC)?.date} UTC\n " + - "(${UptimeCollection().get()?.onTime?.toDiscord(TimestampType.RelativeTime) ?: "??"})" - inline = true - } - field { - name = "Useful links" - value = - "Website: Coming Soon™️\n" + - "GitHub: ${HYACINTH_GITHUB}\n" + - "Buy Me a Coffee: https://buymeacoffee.com/HyacinthBots\n" + - "Twitter: https://twitter.com/HyacinthBots\n" + - "Email: `hyacinthbots@outlook.com`\n" + - "Discord: https://discord.gg/hy2329fcTZ" - } - color = DISCORD_BLURPLE - } - - buttons() - } - } - } - /** * A command that responds to the user with a link to invite Lily to their server. * diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/NewsChannelPublishing.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/NewsChannelPublishing.kt similarity index 88% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/NewsChannelPublishing.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/NewsChannelPublishing.kt index 936bf597..7f79935d 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/NewsChannelPublishing.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/NewsChannelPublishing.kt @@ -1,19 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.DISCORD_YELLOW -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.channelType -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.channel -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.pagination.EphemeralResponsePaginator -import com.kotlindiscord.kord.extensions.pagination.pages.Page -import com.kotlindiscord.kord.extensions.pagination.pages.Pages +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.Locale import dev.kord.common.asJavaLocale import dev.kord.common.entity.ChannelType @@ -23,6 +9,20 @@ import dev.kord.core.behavior.channel.asChannelOfOrNull import dev.kord.core.behavior.channel.createEmbed import dev.kord.core.entity.channel.NewsChannel import dev.kord.core.event.message.MessageCreateEvent +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.channelType +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.channel +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.event +import dev.kordex.core.pagination.EphemeralResponsePaginator +import dev.kordex.core.pagination.pages.Page +import dev.kordex.core.pagination.pages.Pages import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.NewsChannelPublishingCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/PublicUtilities.kt similarity index 89% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/PublicUtilities.kt index b9dd03e3..9b51cc7e 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/PublicUtilities.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/PublicUtilities.kt @@ -1,21 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.DISCORD_YELLOW -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -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.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import com.kotlindiscord.kord.extensions.utils.dm -import com.kotlindiscord.kord.extensions.utils.getTopRole -import com.kotlindiscord.kord.extensions.utils.hasPermission +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission import dev.kord.core.behavior.channel.createEmbed @@ -26,6 +10,22 @@ import dev.kord.core.entity.Message import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.rest.builder.message.embed import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.guildFor +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.publicSlashCommand +import dev.kordex.core.utils.dm +import dev.kordex.core.utils.getTopRole +import dev.kordex.core.utils.hasPermission import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection import org.hyacinthbots.lilybot.extensions.config.ConfigOptions @@ -281,7 +281,7 @@ class PublicUtilities : Extension() { } } } - } catch (e: KtorRequestException) { + } catch (_: KtorRequestException) { // Avoid hard failing on permission error, since the public won't know what it means respond { content = "Error sending message to moderators. Please ask the moderators to check" + diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Reminders.kt similarity index 91% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Reminders.kt index a212ee6d..99aa2d62 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Reminders.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Reminders.kt @@ -1,31 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.stringChoice -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.application.slash.publicSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.boolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingDuration -import com.kotlindiscord.kord.extensions.commands.converters.impl.coalescingOptionalDuration -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.long -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.publicSlashCommand -import com.kotlindiscord.kord.extensions.pagination.EphemeralResponsePaginator -import com.kotlindiscord.kord.extensions.pagination.pages.Page -import com.kotlindiscord.kord.extensions.pagination.pages.Pages -import com.kotlindiscord.kord.extensions.time.TimestampType -import com.kotlindiscord.kord.extensions.time.toDiscord -import com.kotlindiscord.kord.extensions.utils.botHasPermissions -import com.kotlindiscord.kord.extensions.utils.dm -import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler -import com.kotlindiscord.kord.extensions.utils.scheduling.Task -import com.kotlindiscord.kord.extensions.utils.toDuration +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.common.entity.Snowflake @@ -38,6 +12,32 @@ import dev.kord.core.event.interaction.ChatInputCommandInteractionCreateEvent import dev.kord.rest.builder.message.create.MessageCreateBuilder import dev.kord.rest.builder.message.embed import dev.kord.rest.request.KtorRequestException +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.guildFor +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.converters.impl.stringChoice +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.application.slash.publicSubCommand +import dev.kordex.core.commands.converters.impl.boolean +import dev.kordex.core.commands.converters.impl.coalescingDuration +import dev.kordex.core.commands.converters.impl.coalescingOptionalDuration +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.long +import dev.kordex.core.commands.converters.impl.optionalString +import dev.kordex.core.commands.converters.impl.user +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.publicSlashCommand +import dev.kordex.core.pagination.EphemeralResponsePaginator +import dev.kordex.core.pagination.pages.Page +import dev.kordex.core.pagination.pages.Pages +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import dev.kordex.core.utils.botHasPermissions +import dev.kordex.core.utils.dm +import dev.kordex.core.utils.scheduling.Scheduler +import dev.kordex.core.utils.scheduling.Task +import dev.kordex.core.utils.toDuration import kotlinx.datetime.Clock import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone @@ -384,7 +384,7 @@ class Reminders : Extension() { } } - ReminderCollection().removeReminder(user.id, arguments.reminder) + ReminderCollection().removeReminder(arguments.user.id, arguments.reminder) markReminderCompleteOrCancelled( reminder.guildId, reminder.channelId, reminder.messageId, wasCancelled = true, diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/RoleMenu.kt similarity index 93% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/RoleMenu.kt index 4f83ff5e..5ce716c2 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/RoleMenu.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/RoleMenu.kt @@ -1,26 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_BLACK -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.checks.hasPermissions -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.EphemeralSlashCommandContext -import com.kotlindiscord.kord.extensions.commands.application.slash.ephemeralSubCommand -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingBoolean -import com.kotlindiscord.kord.extensions.commands.converters.impl.defaultingColor -import com.kotlindiscord.kord.extensions.commands.converters.impl.role -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.ephemeralStringSelectMenu -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.utils.getJumpUrl -import com.kotlindiscord.kord.extensions.utils.getTopRole +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.entity.ButtonStyle import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions @@ -35,6 +14,27 @@ import dev.kord.core.entity.Message import dev.kord.core.entity.Role import dev.kord.core.event.interaction.GuildButtonInteractionCreateEvent import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_BLACK +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.checks.hasPermissions +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.EphemeralSlashCommandContext +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.defaultingColor +import dev.kordex.core.commands.converters.impl.role +import dev.kordex.core.commands.converters.impl.snowflake +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.components.components +import dev.kordex.core.components.ephemeralButton +import dev.kordex.core.components.ephemeralStringSelectMenu +import dev.kordex.core.components.linkButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.event +import dev.kordex.core.utils.getJumpUrl +import dev.kordex.core.utils.getTopRole import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.toList @@ -100,13 +100,13 @@ class RoleMenu : Extension() { } // While we don't normally edit in components, in this case we need the message ID. - menuMessage!!.edit { + menuMessage.edit { val components = components { ephemeralButton { label = "Select roles" style = ButtonStyle.Primary - id = "role-menu${menuMessage!!.id}" + id = "role-menu${menuMessage.id}" action { } } @@ -116,7 +116,7 @@ class RoleMenu : Extension() { } RoleMenuCollection().setRoleMenu( - menuMessage!!.id, + menuMessage.id, channel.id, guild!!.id, mutableListOf(arguments.initialRole.id) @@ -154,7 +154,7 @@ class RoleMenu : Extension() { components { linkButton { label = "Jump to role menu" - url = menuMessage!!.getJumpUrl() + url = menuMessage.getJumpUrl() } } } @@ -371,7 +371,7 @@ class RoleMenu : Extension() { roles.add(newRole.id) } else { - utilsLogger.debug("skipped creating new roles") + utilsLogger.debug { "skipped creating new roles" } roles.add(existingRole.id) } } @@ -707,7 +707,7 @@ class RoleMenu : Extension() { return@action } - if (!config!!.subscribableRoles.contains(arguments.role.id)) { + if (!config.subscribableRoles.contains(arguments.role.id)) { respond { content = "That is not a subscribable role." } diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StartupHooks.kt similarity index 82% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StartupHooks.kt index 186b1bcc..3fc602b7 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StartupHooks.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StartupHooks.kt @@ -1,16 +1,16 @@ -package org.hyacinthbots.lilybot.extensions.util +package org.hyacinthbots.lilybot.extensions.utility.commands -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -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.core.behavior.channel.createEmbed import dev.kord.core.behavior.getChannelOfOrNull import dev.kord.core.entity.channel.NewsChannel import dev.kord.core.event.gateway.ReadyEvent +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import dev.kordex.core.utils.scheduling.Scheduler +import dev.kordex.core.utils.scheduling.Task import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.Cleanups import org.hyacinthbots.lilybot.database.collections.UptimeCollection diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StatusPing.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StatusPing.kt similarity index 70% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StatusPing.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StatusPing.kt index 2422aacd..bfac3ca0 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/StatusPing.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/StatusPing.kt @@ -1,11 +1,11 @@ -package org.hyacinthbots.lilybot.extensions.util +package org.hyacinthbots.lilybot.extensions.utility.commands -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.utils.scheduling.Scheduler -import com.kotlindiscord.kord.extensions.utils.scheduling.Task +import dev.kordex.core.extensions.Extension +import dev.kordex.core.utils.scheduling.Scheduler +import dev.kordex.core.utils.scheduling.Task +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.client.HttpClient import io.ktor.client.request.get -import mu.KotlinLogging import org.hyacinthbots.lilybot.utils.ENV import kotlin.time.Duration.Companion.seconds diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Tags.kt similarity index 93% rename from src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt rename to src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Tags.kt index 61893e06..20c1a808 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/util/Tags.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/commands/Tags.kt @@ -1,24 +1,5 @@ -package org.hyacinthbots.lilybot.extensions.util - -import com.kotlindiscord.kord.extensions.DISCORD_BLURPLE -import com.kotlindiscord.kord.extensions.DISCORD_GREEN -import com.kotlindiscord.kord.extensions.DISCORD_RED -import com.kotlindiscord.kord.extensions.DISCORD_YELLOW -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.commands.Arguments -import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.optionalStringChoice -import com.kotlindiscord.kord.extensions.commands.application.slash.converters.impl.stringChoice -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalString -import com.kotlindiscord.kord.extensions.commands.converters.impl.optionalUser -import com.kotlindiscord.kord.extensions.commands.converters.impl.string -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.extensions.ephemeralSlashCommand -import com.kotlindiscord.kord.extensions.extensions.publicSlashCommand -import com.kotlindiscord.kord.extensions.pagination.EphemeralResponsePaginator -import com.kotlindiscord.kord.extensions.pagination.pages.Page -import com.kotlindiscord.kord.extensions.pagination.pages.Pages -import com.kotlindiscord.kord.extensions.utils.suggestStringMap +package org.hyacinthbots.lilybot.extensions.utility.commands + import dev.kord.common.Locale import dev.kord.common.asJavaLocale import dev.kord.common.entity.Permission @@ -30,6 +11,25 @@ import dev.kord.core.behavior.getChannelOfOrNull import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.rest.builder.message.EmbedBuilder import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_BLURPLE +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.application.slash.converters.impl.optionalStringChoice +import dev.kordex.core.commands.application.slash.converters.impl.stringChoice +import dev.kordex.core.commands.converters.impl.optionalString +import dev.kordex.core.commands.converters.impl.optionalUser +import dev.kordex.core.commands.converters.impl.string +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.ephemeralSlashCommand +import dev.kordex.core.extensions.publicSlashCommand +import dev.kordex.core.pagination.EphemeralResponsePaginator +import dev.kordex.core.pagination.pages.Page +import dev.kordex.core.pagination.pages.Pages +import dev.kordex.core.utils.suggestStringMap import kotlinx.datetime.Clock import org.hyacinthbots.lilybot.database.collections.TagsCollection import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityArgs.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityArgs.kt new file mode 100644 index 00000000..36c30a76 --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityArgs.kt @@ -0,0 +1,29 @@ +package org.hyacinthbots.lilybot.extensions.utility.config + +import dev.kordex.core.commands.Arguments +import dev.kordex.core.commands.converters.impl.defaultingBoolean +import dev.kordex.core.commands.converters.impl.optionalChannel + +class UtilityArgs : Arguments() { + val utilityLogChannel by optionalChannel { + name = "utility-log" + description = "The channel to log various utility actions too." + } + val logChannelUpdates by defaultingBoolean { + name = "log-channel-updates" + description = "Whether to log changes made to channels in this guild." + defaultValue = false + } + val logEventUpdates by defaultingBoolean { + name = "log-event-updates" + description = "Whether to log changes made to scheduled events in this guild." + } + val logInviteUpdates by defaultingBoolean { + name = "log-invite-updates" + description = "Whether to log changes made to invites in this guild." + } + val logRoleUpdates by defaultingBoolean { + name = "log-role-updates" + description = "Whether to log changes made to roles in this guild." + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityCommand.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityCommand.kt new file mode 100644 index 00000000..f3f77feb --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/config/UtilityCommand.kt @@ -0,0 +1,74 @@ +package org.hyacinthbots.lilybot.extensions.utility.config + +import dev.kord.common.entity.Permission +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.behavior.getChannelOfOrNull +import dev.kord.core.entity.channel.TextChannel +import dev.kord.rest.builder.message.embed +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.commands.application.slash.SlashCommand +import dev.kordex.core.commands.application.slash.ephemeralSubCommand +import dev.kordex.core.utils.botHasPermissions +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection +import org.hyacinthbots.lilybot.database.entities.UtilityConfigData +import org.hyacinthbots.lilybot.extensions.config.utils.utilityEmbed + +suspend fun SlashCommand<*, *, *>.utilityCommand() = ephemeralSubCommand(::UtilityArgs) { + name = "utility" + description = "Configure Lily's utility settings" + + requirePermission(Permission.ManageGuild) + + check { + anyGuild() + hasPermission(Permission.ManageGuild) + } + + action { + val utilityConfig = UtilityConfigCollection().getConfig(guild!!.id) + + if (utilityConfig != null) { + respond { + content = "You already have a utility configuration set. " + + "Please clear it before attempting to set a new one." + } + 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 + } + } + + respond { + embed { + utilityEmbed(arguments, user) + } + } + + UtilityConfigCollection().setConfig( + UtilityConfigData( + guild!!.id, + arguments.utilityLogChannel?.id, + arguments.logChannelUpdates, + arguments.logEventUpdates, + arguments.logInviteUpdates, + arguments.logRoleUpdates + ) + ) + + utilityLog?.createMessage { + embed { + utilityEmbed(arguments, user) + } + } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/events/UtilityEvents.kt b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/events/UtilityEvents.kt new file mode 100644 index 00000000..fe84294c --- /dev/null +++ b/src/main/kotlin/org/hyacinthbots/lilybot/extensions/utility/events/UtilityEvents.kt @@ -0,0 +1,677 @@ +package org.hyacinthbots.lilybot.extensions.utility.events + +import dev.kord.common.entity.ChannelType +import dev.kord.common.entity.ForumTag +import dev.kord.common.entity.GuildScheduledEventPrivacyLevel +import dev.kord.common.entity.optional.value +import dev.kord.core.behavior.GuildBehavior +import dev.kord.core.behavior.channel.asChannelOfOrNull +import dev.kord.core.behavior.channel.createEmbed +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.channel.Category +import dev.kord.core.entity.channel.Channel +import dev.kord.core.entity.channel.ForumChannel +import dev.kord.core.event.channel.ChannelCreateEvent +import dev.kord.core.event.channel.ChannelDeleteEvent +import dev.kord.core.event.channel.ChannelUpdateEvent +import dev.kord.core.event.channel.thread.ThreadChannelCreateEvent +import dev.kord.core.event.channel.thread.ThreadChannelDeleteEvent +import dev.kord.core.event.guild.GuildScheduledEventCreateEvent +import dev.kord.core.event.guild.GuildScheduledEventDeleteEvent +import dev.kord.core.event.guild.GuildScheduledEventEvent +import dev.kord.core.event.guild.GuildScheduledEventUpdateEvent +import dev.kord.core.event.guild.InviteCreateEvent +import dev.kord.core.event.guild.InviteDeleteEvent +import dev.kord.core.event.role.RoleCreateEvent +import dev.kord.core.event.role.RoleDeleteEvent +import dev.kord.core.event.role.RoleUpdateEvent +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.embed +import dev.kordex.core.DISCORD_GREEN +import dev.kordex.core.DISCORD_RED +import dev.kordex.core.DISCORD_YELLOW +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.extensions.Extension +import dev.kordex.core.extensions.event +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import kotlinx.datetime.Clock +import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection +import org.hyacinthbots.lilybot.extensions.config.ConfigOptions +import org.hyacinthbots.lilybot.utils.afterDot +import org.hyacinthbots.lilybot.utils.formatPermissionSet +import org.hyacinthbots.lilybot.utils.getLoggingChannelWithPerms +import org.hyacinthbots.lilybot.utils.ifNullOrEmpty +import kotlin.collections.forEach + +/** A String identifier to use for the permission map to get allowed permissions. */ +private const val ALLOWED = "Allowed" + +/** A String identifier to use for the permission map to get denied permissions. */ +private const val DENIED = "Denied" + +class UtilityEvents : Extension() { + override val name: String = "utility-events" + + override suspend fun setup() { + event { + check { + failIf { + // We get more detail from the specific event + event.channel.type != ChannelType.PublicGuildThread || event.channel.type != ChannelType.PrivateThread + } + } + action { + val guildId = event.channel.data.guildId.value + // Do not log if the channel logging option is false + if (guildId?.let { UtilityConfigCollection().getConfig(it)?.logChannelUpdates } == false) return@action + val guild = guildId?.let { GuildBehavior(it, event.kord) } + val perms = formatPermissionsForDisplay(guild, event.channel) + var allowed = perms.getValue(ALLOWED) + var denied = perms.getValue(DENIED) + + if (allowed.isBlank()) allowed = "None overrides set" + if (denied.isBlank()) denied = "None overrides set" + + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild!!)?.createEmbed { + title = "${writeChannelType(event.channel.type)} Created" + description = "${event.channel.mention} (${event.channel.data.name.value}) was created." + field { + name = "Allowed Permissions" + value = allowed + inline = true + } + field { + name = "Denied Permissions" + value = denied + inline = true + } + timestamp = Clock.System.now() + color = DISCORD_GREEN + } + } + } + event { + action { + val guildId = event.channel.data.guildId.value + // Do not log if the channel logging option is false + if (guildId?.let { UtilityConfigCollection().getConfig(it)?.logChannelUpdates } == false) return@action + val guild = guildId?.let { GuildBehavior(it, event.kord) } + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild!!)?.createEmbed { + title = "${writeChannelType(event.channel.type)} Deleted" + description = "`${event.channel.data.name.value}` was deleted." + timestamp = Clock.System.now() + color = DISCORD_RED + } + } + } + event { + check { + failIf { + // Data from old threads is almost always null, so we should not try to display changes + event.channel.type != ChannelType.PublicGuildThread || event.channel.type != ChannelType.PrivateThread + } + } + action { + val guildId = event.channel.data.guildId.value + // Do not log if the channel logging option is false + if (guildId?.let { UtilityConfigCollection().getConfig(it)?.logChannelUpdates } == false) return@action + val guild = guildId?.let { GuildBehavior(it, event.kord) } + val oldPerms = formatPermissionsForDisplay(guild, event.old) + val newPerms = formatPermissionsForDisplay(guild, event.channel) + val oldAllowed = oldPerms.getValue(ALLOWED) + val oldDenied = oldPerms.getValue(DENIED) + val newAllowed = newPerms.getValue(ALLOWED) + val newDenied = newPerms.getValue(DENIED) + val oldData = event.old?.data + val newData = event.channel.data + val oldAppliedTags = mutableListOf() + newData.appliedTags.value?.forEach { tag -> + event.old?.asChannelOrNull()?.data?.availableTags?.value?.filter { it.id == tag } + ?.get(0)?.name?.let { oldAppliedTags.add(it) } + } + val newAppliedTags = mutableListOf() + newData.appliedTags.value?.forEach { tag -> + event.channel.asChannelOrNull()?.data?.availableTags?.value?.filter { it.id == tag } + ?.get(0)?.name?.let { newAppliedTags.add(it) } + } + + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild!!)?.createEmbed { + title = "${writeChannelType(event.channel.type)} Updated" + oldNewEmbedField( + "Type Change", writeChannelType(event.old?.type), writeChannelType(event.channel.type) + ) + oldNewEmbedField("Name Change", oldData?.name?.value, newData.name.value) + oldNewEmbedField("Topic Changed", oldData?.topic?.value, newData.topic.value) + oldNewEmbedField( + "Parent Category Change", + kord.getChannelOf(oldData?.parentId.value!!)?.mention, + kord.getChannelOf(newData.parentId.value!!)?.mention + ) + if (event.channel.data.nsfw != event.old?.data?.nsfw) { + field { + name = "NSFW Setting" + value = event.channel.data.nsfw.discordBoolean.toString() + } + } + oldNewEmbedField("Position Changed", oldData?.position.value, newData.position.value) + oldNewEmbedField( + "Slowmode time changed", + oldData?.rateLimitPerUser?.value?.toString() ?: "0", + newData.rateLimitPerUser.value?.toString() ?: "0" + ) + oldNewEmbedField("Bitrate changed", oldData?.bitrate.value, newData.bitrate.value) + oldNewEmbedField("User limit changed", oldData?.userLimit.value ?: 0, newData.userLimit.value ?: 0) + oldNewEmbedField( + "Region Changed", + oldData?.rtcRegion?.value ?: "Automatic", + newData.rtcRegion.value ?: "Automatic" + ) + oldNewEmbedField( + "Video Quality Changed", + oldData?.videoQualityMode?.value.afterDot(), + newData.videoQualityMode.value.afterDot() + ) + oldNewEmbedField( + "Default Auto-Archive Duration", + oldData?.defaultAutoArchiveDuration?.value?.duration.toString(), + newData.defaultAutoArchiveDuration.value?.duration.toString() + ) + oldNewEmbedField( + "Default Sort Changed", + oldData?.defaultSortOrder?.value.afterDot(), + newData.defaultSortOrder.value.afterDot() + ) + oldNewEmbedField( + "Default Layout Changed", + oldData?.defaultForumLayout?.value.afterDot(), + newData.defaultForumLayout.value.afterDot() + ) + oldNewEmbedField( + "Available tags Changed", + formatAvailableTags(oldData?.availableTags?.value), + formatAvailableTags(newData.availableTags.value) + ) + oldNewEmbedField( + "Applied tags Changed", + oldAppliedTags.joinToString(", "), + newAppliedTags.joinToString(", ") + ) + oldNewEmbedField( + "Default Reaction Emoji Changed", + oldData?.defaultReactionEmoji?.value?.emojiName, + newData.defaultReactionEmoji.value?.emojiName + ) + oldNewEmbedField( + "Default Thread Slowmode Changed", + oldData?.defaultThreadRateLimitPerUser?.value.toString(), + newData.defaultThreadRateLimitPerUser.value.toString() + ) + if (oldAllowed != newAllowed) { + field { + name = "New Allowed Permissions" + value = newAllowed + inline = true + } + field { + name = "Old Allowed Permissions" + value = oldAllowed + inline = true + } + } + if (oldDenied != newDenied) { + field { + name = "New Denied Permissions" + value = newDenied + inline = false + } + field { + name = "Old Denied Permissions" + value = oldDenied + inline = true + } + } + color = DISCORD_YELLOW + timestamp = Clock.System.now() + } + } + } + event { + check { anyGuild() } + action { + // Do not log if event updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logEventUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Scheduled Event Created!" + description = "An event has been created!" + guildEventEmbed(event, guild) + // This appears to exist in the front end but the api doesn't have it anywhere, api payloads contain + // a recurrence field that would fill this but the api doesn't mention it +// field { +// name = "Repeating Frequency" +// value = event.scheduledEvent. +// } + field { + name = "Guild Members only" + value = if (event.scheduledEvent.privacyLevel == GuildScheduledEventPrivacyLevel.GuildOnly) { + "True" + } else { + "False" + } + } + color = DISCORD_GREEN + footer { + text = + "Created by ${event.scheduledEvent.creatorId?.let { guild.getMemberOrNull(it) }?.username}" + icon = + event.scheduledEvent.creatorId?.let { guild.getMemberOrNull(it) }?.avatar?.cdnUrl?.toUrl() + } + } + } + } + event { + check { anyGuild() } + action { + // Do not log if event updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logEventUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Scheduled Event Deleted!" + description = "An event has been deleted" + guildEventEmbed(event, guild) + color = DISCORD_RED + footer { + text = + "Originally created by ${event.scheduledEvent.creatorId?.let { guild.getMemberOrNull(it) }?.username}" + icon = + event.scheduledEvent.creatorId?.let { guild.getMemberOrNull(it) }?.avatar?.cdnUrl?.toUrl() + } + } + } + } + event { + check { anyGuild() } + action { + // Do not log if event updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logEventUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + val oldEvent = event.oldEvent + val newEvent = event.scheduledEvent + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Scheduled Event Updated!" + description = "An event has been updated" + oldNewEmbedField("Name Changed", oldEvent?.name, newEvent.name) + oldNewEmbedField("Description Changed", oldEvent?.description, newEvent.description) + oldNewEmbedField( + "Location Changed", + oldEvent?.channelId?.let { guild.getChannelOrNull(it) }?.mention ?: "Unable to get channel", + newEvent.channelId?.let { guild.getChannelOrNull(it) }?.mention ?: "Unable to get channel" + ) + oldNewEmbedField( + "Start time changed", + oldEvent?.scheduledStartTime?.toDiscord(TimestampType.ShortDateTime), + newEvent.scheduledStartTime.toDiscord(TimestampType.ShortDateTime) + ) + color = DISCORD_YELLOW + } + } + } + event { + check { anyGuild() } + action { + // Do not log if invite updates are disabled + if (event.guildId?.let { UtilityConfigCollection().getConfig(it) }?.logInviteUpdates == false) return@action + val guild = event.guildId?.let { GuildBehavior(it, kord) } ?: return@action + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Invite link created" + description = "An invite has been created" + field { + name = "Code" + value = event.code + } + field { + name = "Target Channel" + value = event.channel.mention + } + field { + name = "Max uses" + value = event.maxUses.toString() + } + field { + name = "Duration of Invite" + value = event.maxAge.toIsoString() + } + field { + name = "Temporary Membership invite" + value = event.isTemporary.toString() + } + footer { + text = "Created by ${event.getInviterAsMemberOrNull()?.mention}" + icon = event.getInviterAsMemberOrNull()?.avatar?.cdnUrl?.toUrl() + } + timestamp = Clock.System.now() + color = DISCORD_GREEN + } + } + } + event { + check { anyGuild() } + action { + // Do not log if invite updates are disabled + if (event.guildId?.let { UtilityConfigCollection().getConfig(it) }?.logInviteUpdates == false) return@action + val guild = event.guildId?.let { GuildBehavior(it, kord) } ?: return@action + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Invite link deleted" + description = "An invite has been deleted" + field { + name = "Code" + value = event.code + } + field { + name = "Target Channel" + value = event.channel.mention + } + timestamp = Clock.System.now() + color = DISCORD_RED + } + } + } + event { + action { + // Do not log if role updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logRoleUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Created a Role" + description = "A new role has been created" + field { + name = "Role name" + value = event.role.name + inline = true + } + field { + name = "Role Color" + value = event.role.color.rgb.toString() + inline = true + } + field { + name = "Position" + value = event.role.rawPosition.toString() + inline = true + } + field { + name = "Display separately?" + value = event.role.hoisted.toString() + inline = true + } + field { + name = "Mentionable" + value = event.role.mentionable.toString() + inline = true + } + field { + name = "Icon" + value = event.role.icon?.cdnUrl?.toUrl() ?: "No icon" + inline = true + } + field { + name = "Emoji" + value = event.role.unicodeEmoji ?: "No emoji" + inline = true + } + field { + name = "Managed by integration?" + value = event.role.managed.toString() + inline = true + } + field { + name = "Permissions" + value = formatPermissionSet(event.role.permissions) + } + color = DISCORD_GREEN + timestamp = Clock.System.now() + } + } + } + event { + action { + // Do not log if role updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logRoleUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild)?.createEmbed { + title = "Role deleted" + description = "A role has been deleted" + field { + name = "Role name" + value = event.role?.name ?: "Unable to get name" + } + color = DISCORD_RED + timestamp = Clock.System.now() + } + } + } + event { + action { + // Do not log if role updates are disabled + if (UtilityConfigCollection().getConfig(event.guildId)?.logRoleUpdates == false) return@action + val guild = GuildBehavior(event.guildId, kord) + val channel = getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, guild) + channel?.createMessage { + embed { + title = "Updated a Role" + description = "A role has been updated" + oldNewEmbedField("Name changed", event.old?.name, event.role.name) + oldNewEmbedField("Display separately setting changed", event.old?.hoisted, event.role.hoisted) + oldNewEmbedField("Mentionable setting changed", event.old?.mentionable, event.role.mentionable) + oldNewEmbedField("Position changed", event.old?.getPosition(), event.role.getPosition()) + oldNewEmbedField( + "Icon changed", + event.old?.icon?.cdnUrl?.toUrl() ?: "No icon", + event.role.icon?.cdnUrl?.toUrl() ?: "No icon" + ) + oldNewEmbedField( + "Emoji changed", event.old?.unicodeEmoji ?: "No icon", event.role.unicodeEmoji ?: "No icon" + ) + oldNewEmbedField( + "Permissions changed", + event.old?.permissions?.let { formatPermissionSet(it) } ?: "Unable to get permissions", + formatPermissionSet(event.role.permissions) + ) + color = DISCORD_GREEN + timestamp = Clock.System.now() + } + if (event.old?.color != event.role.color) { + embed { + description = "Old color" + color = if (event.old?.color?.rgb != 0) event.old?.color else null + } + embed { + description = "New color" + color = event.role.color + } + } + } + } + } + event { + check { anyGuild() } + action { + // Do not log if the channel logging option is false + if (UtilityConfigCollection().getConfig(event.channel.guild.id)?.logChannelUpdates == false) return@action + val appliedTags = mutableListOf() + event.channel.appliedTags.forEach { tag -> + event.channel.parent.asChannelOfOrNull() + ?.availableTags?.filter { it.id == tag }?.get(0)?.name?.let { appliedTags.add(it) } + } + + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, event.channel.guild)?.createEmbed { + title = "${writeChannelType(event.channel.type)} Created" + description = "${event.channel.mention} (${event.channel.data.name.value}) was created." + field { + name = "Parent Channel" + value = "${event.channel.parent.mention} (`${event.channel.parent.asChannelOrNull()?.name}`)" + } + field { + name = "Archive duration" + value = event.channel.autoArchiveDuration.duration.toString() + } + if (appliedTags.isNotEmpty()) { + field { + name = "Applied tags" + value = appliedTags.joinToString(", ") + } + } + timestamp = Clock.System.now() + color = DISCORD_GREEN + } + } + } + event { + check { anyGuild() } + action { + // Do not log if the channel logging option is false + if (UtilityConfigCollection().getConfig(event.channel.guild.id)?.logChannelUpdates == false) return@action + getLoggingChannelWithPerms(ConfigOptions.UTILITY_LOG, event.channel.guild)?.createEmbed { + title = "${writeChannelType(event.channel.type)} Deleted" + description = "`${event.channel.data.name.value}` was deleted." + timestamp = Clock.System.now() + color = DISCORD_RED + } + } + } + // No thread update event because the old object is almost always null, meaning that displaying the changes is + // effectively pointless because no original values are available. + } + + /** + * Compare two values to see if they've changed and format an embed field indicating the old value versus the new + * value. + * + * @param detailName The title for the embed field, the information on what has changed + * @param oldValue The original value + * @param newValue The new value + * @author NoComment1105 + * @since 5.0.0 + */ + private fun EmbedBuilder.oldNewEmbedField(detailName: String, oldValue: String?, newValue: String?) { + if (newValue != oldValue) { + field { + name = detailName + value = "Old: $oldValue\nNew: $newValue" + } + } + } + + /** + * A version of [oldNewEmbedField] that takes integers and converts them itself. + * + * @see oldNewEmbedField + * @author NoComment1105 + * @since 5.0.0 + */ + private fun EmbedBuilder.oldNewEmbedField(detailName: String, oldValue: Int?, newValue: Int?) = + oldNewEmbedField(detailName, oldValue.toString(), newValue.toString()) + + /** + * A version of [oldNewEmbedField] that takes booleans and converts them itself. + * + * @see oldNewEmbedField + * @author NoComment1105 + * @since 5.0.0 + */ + private fun EmbedBuilder.oldNewEmbedField(detailName: String, oldValue: Boolean?, newValue: Boolean?) = + oldNewEmbedField(detailName, oldValue.toString(), newValue.toString()) + + /** + * Writes a [ChannelType] into a String to use as a reasonable title. + * + * @param type The type of the channel + * @return A String for the channel title + * @author NoComment1105 + * @since 5.0.0 + */ + private fun writeChannelType(type: ChannelType?): String? = when (type) { + ChannelType.GuildCategory -> "Category" + ChannelType.GuildNews -> "Announcement Channel" + ChannelType.GuildForum -> "Forum Channel" + ChannelType.GuildStageVoice -> "Stage Channel" + ChannelType.GuildText -> "Text Channel" + ChannelType.GuildVoice -> "Voice Channel" + ChannelType.PublicGuildThread -> "Thread" + ChannelType.PrivateThread -> "Private Thread" + else -> null + } + + /** + * Formats the permission overwrites for a [channel] to a string map of allowed and denied permissions. + * + * @param guild The guild the channel is in + * @param channel The channel object to get the permissions for + * @return A [Map] of strings for allowed and denied permissions + * @author NoComment1105 + * @since 5.0.0 + */ + private suspend inline fun formatPermissionsForDisplay( + guild: GuildBehavior?, + channel: Channel? + ): Map { + val map = mutableMapOf() + map[ALLOWED] = "" + map[DENIED] = "" + channel?.data?.permissionOverwrites?.value?.forEach { + map[ALLOWED] += "${guild!!.getRoleOrNull(it.id)?.mention}: ${formatPermissionSet(it.allow)}\n" + map[DENIED] += "${guild.getRoleOrNull(it.id)?.mention}: ${formatPermissionSet(it.deny)}\n" + } + return map + } + + /** + * Formats the Available tags ([ForumTag]) into a readable bullet pointed display for the update embed. + * + * @param tagList A List of [ForumTag]s to format + * @return The formated string from [tagList] + * @author NoComment1105 + * @since 5.0.0 + */ + private fun formatAvailableTags(tagList: List?): String { + var tagString = "" + tagList?.forEach { + tagString += "\n* Name: ${it.name}\n* Moderated: ${it.moderated}\n" + + "* Emoji: ${if (it.emojiId != null) "" else it.emojiName}\n---" + } + return tagString.ifNullOrEmpty { "None" } + } + + /** + * Fills out the content for Guild event updates to avoid repeating code. + * + * @param event The event instance + * @param guild The guild for the event + * @author NoComment1105 + * @since 5.0.0 + */ + private suspend fun EmbedBuilder.guildEventEmbed(event: GuildScheduledEventEvent, guild: GuildBehavior) { + field { + name = "Event Name" + value = event.scheduledEvent.name + } + field { + name = "Event Description" + value = event.scheduledEvent.description ?: "No description provided" + } + field { + name = "Event location" + value = if (event.scheduledEvent.channelId != null) { + guild.getChannelOrNull(event.scheduledEvent.channelId!!)?.mention ?: "Unable to get channel" + } else { + "External event, no channel." + } + } + field { + name = "Start time" + value = event.scheduledEvent.scheduledStartTime.toDiscord(TimestampType.ShortDateTime) + } + image = event.scheduledEvent.image?.cdnUrl?.toUrl().ifNullOrEmpty { "No image" } + } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/ResponseHelper.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/ResponseHelper.kt index d79f7d77..5c86d07b 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/ResponseHelper.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/ResponseHelper.kt @@ -1,11 +1,11 @@ package org.hyacinthbots.lilybot.utils -import com.kotlindiscord.kord.extensions.modules.extra.pluralkit.api.PKMessage import dev.kord.core.behavior.UserBehavior import dev.kord.core.entity.Guild import dev.kord.core.entity.Message import dev.kord.core.entity.User import dev.kord.rest.builder.message.EmbedBuilder +import dev.kordex.modules.pluralkit.api.PKMessage /** * This is the base moderation embed for all moderation actions. This should be posted to the action log of a guild. @@ -17,7 +17,7 @@ import dev.kord.rest.builder.message.EmbedBuilder * @author NoComment1105 * @since 3.0.0 */ -suspend inline fun EmbedBuilder.baseModerationEmbed(reason: String?, targetUser: User?, commandUser: UserBehavior) { +suspend inline fun EmbedBuilder.baseModerationEmbed(reason: String?, targetUser: User?, commandUser: UserBehavior?) { field { name = "User:" value = "${targetUser?.username ?: "Unable to get user"}\n${targetUser?.id ?: ""}" @@ -28,9 +28,11 @@ suspend inline fun EmbedBuilder.baseModerationEmbed(reason: String?, targetUser: value = reason ?: "No reason provided" inline = false } - footer { - text = "Requested by ${commandUser.asUserOrNull()?.username}" - icon = commandUser.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + if (commandUser != null) { + footer { + text = "Requested by ${commandUser.asUserOrNull()?.username}" + icon = commandUser.asUserOrNull()?.avatar?.cdnUrl?.toUrl() + } } } @@ -57,6 +59,28 @@ fun EmbedBuilder.dmNotificationStatusEmbedField(dm: Message?, override: Boolean) } } +/** + * This function uses a success variable and checks to see if it succeeded in sending the user a DM. + * + * @param success Whether the DM was a success or not + * @param override Whether the DM was forcefully disabled by the command runner. + * @author NoComment1105 + * @since 5.0.0 + */ +fun EmbedBuilder.dmNotificationStatusEmbedField(success: Boolean?, override: Boolean?) { + field { + name = "User Notification:" + value = if (success != null && success) { + "User notified with direct message" + } else if (override != null && !override) { + "DM Notification Disabled" + } else { + "Failed to notify user with direct message" + } + inline = false + } +} + /** * This function removed duplicated code from MessageDelete and MessageEdit. * It holds attachment and PluralKit info fields for the logging embeds. diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_ConfigUtils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_ConfigUtils.kt index aac26629..2ea5b0e6 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_ConfigUtils.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_ConfigUtils.kt @@ -1,8 +1,8 @@ package org.hyacinthbots.lilybot.utils -import com.kotlindiscord.kord.extensions.checks.guildFor -import com.kotlindiscord.kord.extensions.checks.types.CheckContext import dev.kord.common.entity.Snowflake +import dev.kordex.core.checks.guildFor +import dev.kordex.core.checks.types.CheckContext import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.UtilityConfigCollection @@ -28,15 +28,29 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO fail("There are no config options provided in the code. Please inform the developers immediately!") } + val moderationConfig = ModerationConfigCollection().getConfig(guildFor(event)!!.id) + if (moderationConfig == null) { + fail("Unable to access moderation config for this guild! Please inform a member of staff.") + return + } + + 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.") + return + } + + val utilityConfig = UtilityConfigCollection().getConfig(guildFor(event)!!.id) + if (utilityConfig == null) { + fail("Unable to access logging config for this guild! Please inform a member of staff.") + return + } + // Look at the config options and check the presence of the config in the database. for (option in configOptions) { when (option) { ConfigOptions.MODERATION_ENABLED -> { - val moderationConfig = ModerationConfigCollection().getConfig(guildFor(event)!!.id) - if (moderationConfig == null) { - fail("Unable to access moderation config for this guild! Please inform a member of staff.") - break - } else if (!moderationConfig.enabled) { + if (!moderationConfig.enabled) { fail("Moderation is disabled for this guild!") break } else { @@ -45,11 +59,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.MODERATOR_ROLE -> { - val moderationConfig = ModerationConfigCollection().getConfig(guildFor(event)!!.id) - if (moderationConfig == null) { - fail("Unable to access moderation config for this guild! Please inform a member of staff.") - break - } else if (moderationConfig.role == null) { + if (moderationConfig.role == null) { fail("A moderator role has not been set for this guild!") break } else { @@ -58,11 +68,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.ACTION_LOG -> { - val moderationConfig = ModerationConfigCollection().getConfig(guildFor(event)!!.id) - if (moderationConfig == null) { - fail("Unable to access moderation config for this guild! Please inform a member of staff.") - break - } else if (moderationConfig.channel == null) { + if (moderationConfig.channel == null) { fail("An action log has not been set for this guild!") break } else { @@ -71,11 +77,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.LOG_PUBLICLY -> { - val moderationConfig = ModerationConfigCollection().getConfig(guildFor(event)!!.id) - if (moderationConfig == null) { - fail("Unable to access moderation config for this guild! Please inform a member of staff.") - break - } else if (moderationConfig.publicLogging == null) { + if (moderationConfig.publicLogging == null) { fail("Public logging has not been enabled for this guild!") break } else { @@ -84,11 +86,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } 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.enableMessageDeleteLogs) { + if (!loggingConfig.enableMessageDeleteLogs) { fail("Message delete logging is disabled for this guild!") break } else { @@ -97,11 +95,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } 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) { + if (!loggingConfig.enableMessageEditLogs) { fail("Message edit logging is disabled for this guild!") break } else { @@ -110,11 +104,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.MESSAGE_LOG -> { - 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.messageChannel == null) { + if (loggingConfig.messageChannel == null) { fail("A message log has not been set for this guild!") break } else { @@ -123,11 +113,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.MEMBER_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.enableMemberLogs) { + if (!loggingConfig.enableMemberLogs) { fail("Member logging is disabled for this guild!") break } else { @@ -136,11 +122,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.MEMBER_LOG -> { - 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.memberLog == null) { + if (loggingConfig.memberLog == null) { fail("A member log has not been set for this guild") break } else { @@ -149,11 +131,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO } ConfigOptions.UTILITY_LOG -> { - val utilityConfig = UtilityConfigCollection().getConfig(guildFor(event)!!.id) - if (utilityConfig == null) { - fail("Unable to access utility config for this guild! Please inform a member of staff.") - break - } else if (utilityConfig.utilityLogChannel == null) { + if (utilityConfig.utilityLogChannel == null) { fail("A utility log has not been set for this guild") break } else { @@ -174,7 +152,7 @@ suspend inline fun CheckContext<*>.requiredConfigs(vararg configOptions: ConfigO */ suspend inline fun configIsUsable(guildId: Snowflake, option: ConfigOptions): Boolean { when (option) { - ConfigOptions.MODERATION_ENABLED -> return ModerationConfigCollection().getConfig(guildId)?.enabled ?: false + ConfigOptions.MODERATION_ENABLED -> return ModerationConfigCollection().getConfig(guildId)?.enabled == true ConfigOptions.MODERATOR_ROLE -> { val moderationConfig = ModerationConfigCollection().getConfig(guildId) ?: return false @@ -192,18 +170,17 @@ suspend inline fun configIsUsable(guildId: Snowflake, option: ConfigOptions): Bo } ConfigOptions.MESSAGE_DELETE_LOGGING_ENABLED -> - return LoggingConfigCollection().getConfig(guildId)?.enableMessageDeleteLogs ?: false + return LoggingConfigCollection().getConfig(guildId)?.enableMessageDeleteLogs == true ConfigOptions.MESSAGE_EDIT_LOGGING_ENABLED -> - return LoggingConfigCollection().getConfig(guildId)?.enableMessageEditLogs ?: false + return LoggingConfigCollection().getConfig(guildId)?.enableMessageEditLogs == true 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_LOGGING_ENABLED -> return LoggingConfigCollection().getConfig(guildId)?.enableMemberLogs == true ConfigOptions.MEMBER_LOG -> { val loggingConfig = LoggingConfigCollection().getConfig(guildId) ?: return false diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt index 0bcc99a4..33d960a4 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Constants.kt @@ -1,8 +1,8 @@ package org.hyacinthbots.lilybot.utils -import com.kotlindiscord.kord.extensions.utils.env -import com.kotlindiscord.kord.extensions.utils.envOrNull import dev.kord.common.entity.Snowflake +import dev.kordex.core.utils.env +import dev.kordex.core.utils.envOrNull /** The Bot Token. */ val BOT_TOKEN = env("TOKEN") diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt index a6a7fab3..bbb4e912 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_PermissionUtils.kt @@ -1,13 +1,5 @@ package org.hyacinthbots.lilybot.utils -import com.kotlindiscord.kord.extensions.checks.anyGuild -import com.kotlindiscord.kord.extensions.checks.channelFor -import com.kotlindiscord.kord.extensions.checks.hasPermission -import com.kotlindiscord.kord.extensions.checks.types.CheckContext -import com.kotlindiscord.kord.extensions.checks.types.CheckContextWithCache -import com.kotlindiscord.kord.extensions.types.EphemeralInteractionContext -import com.kotlindiscord.kord.extensions.utils.botHasPermissions -import com.kotlindiscord.kord.extensions.utils.getTopRole import dev.kord.common.entity.Permission import dev.kord.common.entity.Permissions import dev.kord.core.Kord @@ -20,6 +12,14 @@ import dev.kord.core.entity.channel.GuildMessageChannel import dev.kord.core.entity.channel.NewsChannel import dev.kord.core.entity.channel.TextChannel import dev.kord.core.entity.channel.thread.ThreadChannel +import dev.kordex.core.checks.anyGuild +import dev.kordex.core.checks.channelFor +import dev.kordex.core.checks.hasPermission +import dev.kordex.core.checks.types.CheckContext +import dev.kordex.core.checks.types.CheckContextWithCache +import dev.kordex.core.types.EphemeralInteractionContext +import dev.kordex.core.utils.botHasPermissions +import dev.kordex.core.utils.getTopRole import kotlinx.coroutines.flow.toList import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection @@ -131,18 +131,7 @@ suspend inline fun CheckContext<*>.botHasChannelPerms(permissions: Permissions) val eventChannel = channelFor(event)?.asChannelOrNull() ?: return - val permissionsSet: MutableSet = mutableSetOf() - var count = 0 - permissions.values.forEach { _ -> - permissionsSet.add( - permissions.values.toString() - .split(",")[count] - .split(".")[1] - .replace("[", "`") - .replace("]", "`") - ) - count++ - } + val permissionsSet: String = formatPermissionSet(permissions) /* Use `TextChannel` when the channel is a Text channel */ if (eventChannel is TextChannel) { @@ -187,7 +176,7 @@ suspend inline fun CheckContext<*>.botHasChannelPerms(permissions: Permissions) * point the check fails, null is returned. This should be handled with an elvis operator to return the action in the * code. * - * @param kord The kord instance so the self of the bot can be gotten if needed + * @param kord The kord instance so the self of the bot can be got if needed * @param user The target user in the command * @param guild The guild the command was run in * @param commandName The name of the command. Used for the responses and error message @@ -268,3 +257,26 @@ suspend fun CheckContextWithCache<*>.modCommandChecks(actionPermission: Permissi requiredConfigs(ConfigOptions.MODERATION_ENABLED) hasPermission(actionPermission) } + +/** + * Formats [Permissions] into a readable string list, returning "None" if there are no permissions there. + * + * @param permissions The [Permissions] to format + * @return A string containing the permissions + * @author NoComment1105 + * @since 5.0.0 + */ +fun formatPermissionSet(permissions: Permissions): String { + val permissionsSet: MutableSet = mutableSetOf() + var count = 0 + permissions.values.forEach { _ -> + permissionsSet.add( + permissions.values.toString() + .split(",")[count] + .split(".")[1] + .replace("]", "") + ) + count++ + } + return permissionsSet.toString().replace("[", "").replace("]", "").ifEmpty { "None" } +} diff --git a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt index 22d5b62d..33700ddd 100644 --- a/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt +++ b/src/main/kotlin/org/hyacinthbots/lilybot/utils/_Utils.kt @@ -1,9 +1,5 @@ package org.hyacinthbots.lilybot.utils -import com.kotlindiscord.kord.extensions.builders.ExtensibleBotBuilder -import com.kotlindiscord.kord.extensions.extensions.Extension -import com.kotlindiscord.kord.extensions.utils.hasPermission -import com.kotlindiscord.kord.extensions.utils.loadModule import dev.kord.common.entity.Permission import dev.kord.common.entity.Snowflake import dev.kord.core.Kord @@ -11,12 +7,16 @@ import dev.kord.core.behavior.GuildBehavior import dev.kord.core.behavior.RoleBehavior import dev.kord.core.entity.Message import dev.kord.core.supplier.EntitySupplyStrategy +import dev.kordex.core.builders.ExtensibleBotBuilder +import dev.kordex.core.extensions.Extension +import dev.kordex.core.utils.hasPermission +import dev.kordex.core.utils.loadModule +import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.flow.count import kotlinx.coroutines.runBlocking import kotlinx.datetime.DateTimePeriod import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import mu.KotlinLogging import org.hyacinthbots.lilybot.database.Database import org.hyacinthbots.lilybot.database.collections.ConfigMetaCollection import org.hyacinthbots.lilybot.database.collections.GalleryChannelCollection @@ -24,6 +24,7 @@ import org.hyacinthbots.lilybot.database.collections.GithubCollection import org.hyacinthbots.lilybot.database.collections.GuildLeaveTimeCollection import org.hyacinthbots.lilybot.database.collections.LoggingConfigCollection import org.hyacinthbots.lilybot.database.collections.MainMetaCollection +import org.hyacinthbots.lilybot.database.collections.ModerationActionCollection import org.hyacinthbots.lilybot.database.collections.ModerationConfigCollection import org.hyacinthbots.lilybot.database.collections.NewsChannelPublishingCollection import org.hyacinthbots.lilybot.database.collections.ReminderCollection @@ -237,6 +238,7 @@ suspend inline fun ExtensibleBotBuilder.database(migrate: Boolean) { single { GuildLeaveTimeCollection() } bind GuildLeaveTimeCollection::class single { LoggingConfigCollection() } bind LoggingConfigCollection::class single { MainMetaCollection() } bind MainMetaCollection::class + single { ModerationActionCollection() } bind ModerationActionCollection::class single { ModerationConfigCollection() } bind ModerationConfigCollection::class single { NewsChannelPublishingCollection() } bind NewsChannelPublishingCollection::class single { ReminderCollection() } bind ReminderCollection::class @@ -259,3 +261,12 @@ suspend inline fun ExtensibleBotBuilder.database(migrate: Boolean) { } } } + +/** + * Takes a value [T], converts it to a string and returns the part after a full-stop/period. + * Generally used on class values, i.e. ArchiveDuration.Day to return just "Day" + * + * @author NoComment1105 + * @since 5.0.0 + */ +fun T?.afterDot() = this.toString().substringAfter(".")