├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ ├── ci.yaml
│ └── gradle_plugin_ci.yml
├── .gitignore
├── .gitpod.yml
├── .idea
├── .gitignore
├── GradleUpdaterPlugin.xml
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── discord.xml
├── encodings.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── jsonSchemas.xml
├── kotlinc.xml
├── runConfigurations
│ └── Bump_Versions.xml
├── scopes
│ └── Not_kts.xml
└── vcs.xml
├── .sdkmanrc
├── Dockerfile
├── LICENSE
├── PLUGINS.md
├── README.md
├── SETUP.md
├── api
├── annotations
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── plugin
│ │ └── api
│ │ └── Annotations.kt
├── build.gradle.kts
└── src
│ └── main
│ ├── kotlin
│ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── plugin
│ │ └── api
│ │ ├── AboutExtensionPoint.kt
│ │ ├── EnvironmentConfig.kt
│ │ ├── ModuleExtensionPoint.kt
│ │ ├── Plugin.kt
│ │ ├── PluginSystem.kt
│ │ ├── config
│ │ └── Config.kt
│ │ ├── io
│ │ └── Database.kt
│ │ ├── module
│ │ ├── MikBotModule.kt
│ │ └── SubCommandModule.kt
│ │ ├── owner
│ │ └── OwnerApi.kt
│ │ ├── settings
│ │ └── SettingsApi.kt
│ │ └── util
│ │ ├── ActionInteraction.kt
│ │ ├── AllShardsReadyEvent.kt
│ │ ├── ArgumentUtil.kt
│ │ ├── AutoCompleteUtil.kt
│ │ ├── CheckUtil.kt
│ │ ├── CommandUtil.kt
│ │ ├── ComponentLiveMessage.kt
│ │ ├── Confirmation.kt
│ │ ├── EffectiveAvatar.kt
│ │ ├── EmbedUtil.kt
│ │ ├── ErrorUtil.kt
│ │ ├── EventHandlerUtil.kt
│ │ ├── ExtensionFinder.kt
│ │ ├── IKnowWhatIAmDoing.kt
│ │ ├── ListPaginator.kt
│ │ ├── LocaleUtil.kt
│ │ ├── LoomDispatcher.kt
│ │ ├── MapArgument.kt
│ │ ├── MessageBuilder.kt
│ │ ├── MessageSearchUtil.kt
│ │ ├── MessageUtil.kt
│ │ ├── PathUtil.kt
│ │ ├── SortedArguments.kt
│ │ ├── TranslationUtil.kt
│ │ └── UserCommands.kt
│ └── resources
│ └── translations
│ └── mikbot
│ ├── strings.properties
│ ├── strings_de_DE.properties
│ ├── strings_fr.properties
│ ├── strings_it_IT.properties
│ ├── strings_pl.properties
│ └── strings_vi.properties
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ ├── java
│ └── DummyFileCollection.java
│ └── kotlin
│ ├── Git.kt
│ ├── KtorUtil.kt
│ ├── Project.kt
│ ├── mikbot-module.gradle.kts
│ ├── mikbot-publishing.gradle.kts
│ └── mikbot-template.gradle.kts
├── clients
├── README.md
├── haste-client
│ ├── build.gradle.kts
│ └── src
│ │ ├── main
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── schlaubi
│ │ │ └── mikbot
│ │ │ └── haste
│ │ │ ├── Haste.kt
│ │ │ ├── HasteClient.kt
│ │ │ └── HasteResponse.kt
│ │ └── test
│ │ └── kotlin
│ │ └── HasteTest.kt
├── image-color-client-kord
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── KordExtensions.kt
└── image-color-client
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── kotlin
│ └── ImageColorClient.kt
├── core
├── README.md
├── database-i18n
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── kotlin
│ │ └── dev
│ │ │ └── schlaubi
│ │ │ └── mikbot
│ │ │ └── core
│ │ │ └── i18n
│ │ │ └── database
│ │ │ ├── DatabaseI18NPlugin.kt
│ │ │ ├── LanguageDatabase.kt
│ │ │ ├── gdpr
│ │ │ └── GDPR.kt
│ │ │ └── settings
│ │ │ ├── DatabaseI18NSettingsExtension.kt
│ │ │ └── LanguageCommand.kt
│ │ └── resources
│ │ └── translations
│ │ └── database-i18n
│ │ ├── strings.properties
│ │ ├── strings_de_DE.properties
│ │ ├── strings_fr.properties
│ │ ├── strings_it_IT.properties
│ │ ├── strings_nb_NO.properties
│ │ ├── strings_pl.properties
│ │ └── strings_vi.properties
├── game-animator
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── Config.kt
│ │ ├── GameAnimatorPlugin.kt
│ │ └── api
│ │ └── GameAnimatorExtensionPoint.kt
├── gdpr
│ ├── .test-env
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── kotlin
│ │ └── dev
│ │ │ └── schlaubi
│ │ │ └── mikbot
│ │ │ └── core
│ │ │ └── gdpr
│ │ │ ├── CoreDataPoints.kt
│ │ │ ├── DeleteCommand.kt
│ │ │ ├── GDPRModule.kt
│ │ │ ├── GDPRPlugin.kt
│ │ │ ├── InfoCommand.kt
│ │ │ ├── RequestCommand.kt
│ │ │ └── api
│ │ │ ├── DataPoint.kt
│ │ │ └── GDPRExtensionPoint.kt
│ │ └── resources
│ │ └── translations
│ │ └── gdpr
│ │ ├── strings.properties
│ │ ├── strings_de_DE.properties
│ │ ├── strings_fr.properties
│ │ ├── strings_it_IT.properties
│ │ ├── strings_nb_NO.properties
│ │ ├── strings_pl.properties
│ │ └── strings_vi.properties
├── ktor
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── util_plugins
│ │ └── ktor
│ │ ├── KtorPlugin.kt
│ │ ├── Redoc.kt
│ │ ├── Types.kt
│ │ └── api
│ │ ├── Config.kt
│ │ ├── KtorExtensionPoint.kt
│ │ └── URLUtil.kt
├── kubernetes
│ ├── Dockerfile
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ ├── main
│ │ ├── kotlin
│ │ │ └── dev
│ │ │ │ └── schlaubi
│ │ │ │ └── mikbot
│ │ │ │ └── core
│ │ │ │ └── health
│ │ │ │ ├── Config.kt
│ │ │ │ ├── KubernetesAPIServer.kt
│ │ │ │ ├── KubernetesPlugin.kt
│ │ │ │ ├── Rebalancer.kt
│ │ │ │ ├── ShardCalculator.kt
│ │ │ │ ├── check
│ │ │ │ ├── DatabaseHealthCheck.kt
│ │ │ │ ├── HealthCheck.kt
│ │ │ │ └── KordHealthCheck.kt
│ │ │ │ ├── ratelimit
│ │ │ │ ├── DistributedRateLimiter.kt
│ │ │ │ └── Setup.kt
│ │ │ │ └── routes
│ │ │ │ └── HealthRoutes.kt
│ │ └── resources
│ │ │ └── translations
│ │ │ └── kubernetes
│ │ │ ├── strings.properties
│ │ │ └── strings_de_DE.properties
│ │ └── test
│ │ └── kotlin
│ │ └── ShardingCalculatorTest.kt
└── redeploy-hook
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── kotlin
│ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── core
│ │ └── redeploy_hook
│ │ ├── Config.kt
│ │ ├── RedeployCommand.kt
│ │ ├── RedeployHookPlugin.kt
│ │ └── api
│ │ └── RedeployExtensionPoint.kt
│ └── resources
│ └── translations
│ └── redeploy-hook
│ ├── strings.properties
│ └── strings_de_DE.properties
├── dev.docker-compose.yml
├── docker-compose.yaml
├── gradle-plugin
├── README.md
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ ├── java
│ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── gradle
│ │ └── PatchPropertiesTask.java
│ └── kotlin
│ └── dev
│ └── schlaubi
│ └── mikbot
│ └── gradle
│ ├── Assembly.kt
│ ├── BuildRepositoryExtension.kt
│ ├── InstallBotTask.kt
│ ├── InstallPluginsToTestBotTask.kt
│ ├── LicenseChecker.kt
│ ├── MakeRepositoryIndexTask.kt
│ ├── MikBotPluginGradlePlugin.kt
│ ├── MikBotRepositories.kt
│ ├── MikBotVersionExtraction.kt
│ ├── PluginInfo.kt
│ ├── Publishing.kt
│ ├── RunBotTask.kt
│ ├── TransientDependencies.kt
│ ├── Util.kt
│ └── extension
│ ├── Extension.kt
│ └── Extensions.kt
├── gradle.properties
├── gradle
├── gradle-daemon-jvm.properties
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── kotlin-js-store
└── yarn.lock
├── music
├── README.md
├── api
│ ├── README.md
│ ├── requests
│ │ ├── http-client.env.json
│ │ └── player.http
│ ├── server
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ └── main
│ │ │ └── kotlin
│ │ │ ├── Config.kt
│ │ │ ├── ErrorHandler.kt
│ │ │ ├── MusicAPI.kt
│ │ │ ├── StateWatcher.kt
│ │ │ ├── TypeConverters.kt
│ │ │ ├── authentication
│ │ │ ├── Provider.kt
│ │ │ ├── Route.kt
│ │ │ └── Utils.kt
│ │ │ ├── documentation
│ │ │ ├── Documenter.kt
│ │ │ └── RouteFunctions.kt
│ │ │ └── player
│ │ │ ├── ChannelRoute.kt
│ │ │ ├── Guilds.kt
│ │ │ ├── PlayerRoute.kt
│ │ │ ├── QueueRoute.kt
│ │ │ ├── SearchRoute.kt
│ │ │ └── WebSocket.kt
│ └── types
│ │ ├── README.md
│ │ ├── build.gradle.kts
│ │ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ ├── Authentication.kt
│ │ ├── Documentation.kt
│ │ ├── Events.kt
│ │ ├── PlayerState.kt
│ │ ├── Queue.kt
│ │ ├── Routes.kt
│ │ ├── SchedulerSettings.kt
│ │ └── Track.kt
├── build.gradle.kts
├── commands
│ ├── README.md
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── dev
│ │ └── schlaubi
│ │ └── mikmusic
│ │ ├── MusicCommandsPlugin.kt
│ │ ├── commands
│ │ ├── ClearCommand.kt
│ │ ├── Commands.kt
│ │ ├── FixCommand.kt
│ │ ├── MoveCommand.kt
│ │ ├── NextCommand.kt
│ │ ├── NowPlayingCommand.kt
│ │ ├── PauseCommand.kt
│ │ ├── PlayCommand.kt
│ │ ├── QueueCommand.kt
│ │ ├── RadioCommand.kt
│ │ ├── RemoveCommand.kt
│ │ ├── ReplayCommand.kt
│ │ ├── SchedulerCommands.kt
│ │ ├── SeekCommand.kt
│ │ ├── SkipCommand.kt
│ │ ├── StopCommand.kt
│ │ └── VolumeCommand.kt
│ │ ├── context
│ │ └── PlayMessageAction.kt
│ │ ├── core
│ │ └── settings
│ │ │ ├── MusicSettingsExtension.kt
│ │ │ └── commands
│ │ │ ├── DjModeCommand.kt
│ │ │ ├── FixMusicChannel.kt
│ │ │ ├── LeaveTimeoutCommand.kt
│ │ │ └── SponsorBlockCommand.kt
│ │ └── playlist
│ │ ├── Playlist.kt
│ │ ├── PlaylistDatabase.kt
│ │ ├── commands
│ │ ├── AddCommand.kt
│ │ ├── Common.kt
│ │ ├── DeleteCommand.kt
│ │ ├── ListCommand.kt
│ │ ├── LoadCommand.kt
│ │ ├── RemoveCommand.kt
│ │ ├── RenameCommand.kt
│ │ ├── SaveCommand.kt
│ │ ├── SongsCommand.kt
│ │ └── ToggleVisibilityCommand.kt
│ │ └── gdpr
│ │ └── PlaylistGdprExtensionPoint.kt
├── lyrics
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── kotlin
│ │ ├── APIServer.kt
│ │ ├── Config.kt
│ │ ├── KaraokeCommand.kt
│ │ ├── LyricsCommand.kt
│ │ ├── LyricsPlugin.kt
│ │ └── events
│ │ │ └── Events.kt
│ │ └── resources
│ │ └── translations
│ │ └── lyrics
│ │ ├── strings.properties
│ │ └── strings_de_DE.properties
└── player
│ ├── build.gradle.kts
│ ├── docker-compose.yml
│ └── src
│ ├── main
│ ├── kotlin
│ │ └── dev
│ │ │ └── schlaubi
│ │ │ └── mikmusic
│ │ │ ├── autocomplete
│ │ │ └── AutoCompleteArgument.kt
│ │ │ ├── checks
│ │ │ ├── MusicQuizCheck.kt
│ │ │ ├── PlayingCheck.kt
│ │ │ └── VoiceChannelCheck.kt
│ │ │ ├── core
│ │ │ ├── Config.kt
│ │ │ ├── MusicModule.kt
│ │ │ ├── MusicPlugin.kt
│ │ │ ├── audio
│ │ │ │ ├── LavalinkManager.kt
│ │ │ │ └── LavalinkServer.kt
│ │ │ └── settings
│ │ │ │ ├── GuildSettings.kt
│ │ │ │ ├── MusicSettingsDatabase.kt
│ │ │ │ └── UserSettings.kt
│ │ │ ├── gdpr
│ │ │ └── MusicGdprExtension.kt
│ │ │ ├── musicchannel
│ │ │ ├── MusicChannelCommand.kt
│ │ │ ├── MusicChannelSettingsExtension.kt
│ │ │ ├── MusicChannelUI.kt
│ │ │ └── MusicInteractionModule.kt
│ │ │ ├── player
│ │ │ ├── AutoPlay.kt
│ │ │ ├── MusicPlayer.kt
│ │ │ ├── PersistentPlayerState.kt
│ │ │ ├── Queue.kt
│ │ │ ├── VoiceStateWatcher.kt
│ │ │ └── queue
│ │ │ │ ├── FriendlyException.kt
│ │ │ │ ├── QueueResponse.kt
│ │ │ │ ├── SongSearch.kt
│ │ │ │ └── TrackFinder.kt
│ │ │ ├── redeploy
│ │ │ └── RedeployExtension.kt
│ │ │ └── util
│ │ │ ├── Common.kt
│ │ │ ├── JsonElementSerializer.kt
│ │ │ ├── LinkedListSerializer.kt
│ │ │ ├── QueuedTrackUtl.kt
│ │ │ ├── SpotifyUtil.kt
│ │ │ ├── TrackSerialization.kt
│ │ │ ├── TrackUtil.kt
│ │ │ ├── VideoFormatter.kt
│ │ │ └── YouTubeUtil.kt
│ └── resources
│ │ └── translations
│ │ └── music
│ │ ├── strings.properties
│ │ ├── strings_de_DE.properties
│ │ ├── strings_fr.properties
│ │ ├── strings_it_IT.properties
│ │ ├── strings_nb_NO.properties
│ │ ├── strings_pl.properties
│ │ └── strings_vi.properties
│ └── test
│ └── kotlin
│ └── QueueTest.kt
├── plugin-processor
├── build.gradle.kts
└── src
│ └── main
│ ├── kotlin
│ └── dev
│ │ └── schlaubi
│ │ └── mikbot
│ │ └── plugin
│ │ └── processor
│ │ ├── PluginProcessor.kt
│ │ └── PluginProcessorProvider.kt
│ └── resources
│ └── META-INF
│ └── services
│ └── com.google.devtools.ksp.processing.SymbolProcessorProvider
├── rebuild-plugin-dependency-list.ps1
├── rebuild-plugin-dependency-list.sh
├── renovate.json
├── runtime
├── build.gradle.kts
├── plugins.txt
└── src
│ ├── README.md
│ └── main
│ ├── kotlin
│ └── dev
│ │ └── schlaubi
│ │ └── musicbot
│ │ ├── Launcher.kt
│ │ ├── core
│ │ ├── AboutCommand.kt
│ │ ├── Bot.kt
│ │ ├── io
│ │ │ ├── DatabaseImpl.kt
│ │ │ └── DurationSerializer.kt
│ │ ├── plugin
│ │ │ ├── DependencyCheckingExtensionFinder.kt
│ │ │ ├── KtorHttpFileDownloader.kt
│ │ │ ├── MikBotPluginRepository.kt
│ │ │ ├── MikbotPluginFactory.kt
│ │ │ ├── PluginLoader.kt
│ │ │ ├── PluginTranslationProvider.kt
│ │ │ ├── PluginUpdater.kt
│ │ │ └── Utils.kt
│ │ └── sentry
│ │ │ └── SentryExtensionPoint.kt
│ │ └── module
│ │ ├── owner
│ │ └── OwnerModuleImpl.kt
│ │ └── settings
│ │ └── SettingsModuleImpl.kt
│ └── resources
│ ├── logback.xml
│ └── translations
│ └── settings
│ ├── strings_de_DE.properties
│ └── strings_en_GB.properties
├── scripts
└── bump_versions.main.kts
├── settings.gradle.kts
└── test.http
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 120
10 | tab_width = 4
11 | trim_trailing_whitespace = true
12 |
13 | [{*.yaml,*.yml}]
14 | indent_size = 2
15 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 | #
3 | # The above will handle all files NOT found below
4 | # https://help.github.com/articles/dealing-with-line-endings/
5 | # https://github.com/Danimoth/gitattributes
6 | # These are explicitly windows files and should use crlf
7 | *.bat text eol=crlf
8 |
9 | *.jar binary
10 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | types:
9 | - opened
10 | - synchronize
11 |
12 | permissions: write-all
13 |
14 | jobs:
15 | mikbot:
16 | uses: mikbot/mikbot-workflow/.github/workflows/mikbot-workflow.yml@v1.6.1
17 | with:
18 | run-maven-publish: true
19 | update-binary-repository: true
20 | docker-name: "ghcr.io/drschlaubi/mikmusic/bot"
21 | secrets:
22 | GCP_ACCOUNT_KEY: ${{ secrets.GCP_ACCOUNT_KEY }}
23 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
24 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
25 | BUILDCACHE_USER: ${{ secrets.BUILDCACHE_USER }}
26 | BUILDCACHE_PASSWORD: ${{ secrets.BUILDCACHE_PASSWORD }}
27 | DOCKER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 |
--------------------------------------------------------------------------------
/.github/workflows/gradle_plugin_ci.yml:
--------------------------------------------------------------------------------
1 | name: Gradle Plugin CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths:
8 | - gradle/libs.versions.toml
9 | - gradle-plugin
10 | pull_request:
11 | paths:
12 | - gradle-plugin/**
13 | types:
14 | - opened
15 | - synchronize
16 | workflow_dispatch:
17 |
18 |
19 | env:
20 | BUILD_PLUGIN_CI: true
21 |
22 | jobs:
23 | build:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v4
27 | - name: Set up JDK 22
28 | uses: actions/setup-java@v4
29 | with:
30 | distribution: 'oracle'
31 | java-version: 22
32 | - uses: gradle/actions/setup-gradle@v4
33 | - name: Test with Gradle
34 | run: ./gradlew gradle-plugin:check
35 | - name: Login to Gradle Plugin Portal
36 | if: "github.event_name == 'push' || github.event_name == 'workflow_dispatch'"
37 | env:
38 | GRADLE_CONFIG: ${{ secrets.GRADLE_CONFIG }}
39 | run: echo "$GRADLE_CONFIG" > ~/.gradle/gradle.properties
40 | - name: Update dependency list
41 | run: ./rebuild-plugin-dependency-list.sh
42 | - name: Gradle Publish
43 | if: "github.event_name == 'push' || github.event_name == 'workflow_dispatch'"
44 | run: ./gradlew :gradle-plugin:publishPlugins
45 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | # Default Ktor plugin port
2 | ports:
3 | - port: 8080
4 | visibility: public
5 |
6 | tasks:
7 | - before: sdk env install && echo "sdkman_auto_env=true" >> ~/.sdkman/etc/config # install needed toolchain and enable it
8 | init: ./gradlew classes testClasses # populate build and dependency cache
9 | - command: docker-compose -f dev.docker-compose.yaml up -d # start dev environment
10 |
11 | jetbrains:
12 | intellij:
13 | # enable pre-indexing
14 | prebuilds:
15 | version: stable
16 | # Plugins recommended for use with the project
17 | plugins:
18 | - me.schlaubi.gradleupdater
19 | - com.intellij.grazie.pro
20 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | /dataSources.xml
10 | artifacts/
11 |
--------------------------------------------------------------------------------
/.idea/GradleUpdaterPlugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/discord.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/jsonSchemas.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Bump_Versions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/scopes/Not_kts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.sdkmanrc:
--------------------------------------------------------------------------------
1 | # Enable auto-env through the sdkman_auto_env config
2 | # Add key=value pairs of SDKs to use below
3 | java=18.0.1-tem
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:23-jre-alpine
2 |
3 | WORKDIR /usr/app
4 | COPY runtime/build/install/mikmusic ./
5 |
6 | LABEL org.opencontainers.image.source="https://github.com/DRSchlaubi/mikbot"
7 |
8 | ENTRYPOINT ["bin/mikmusic"]
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Michael Rittmeister
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/api/annotations/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | }
5 |
6 | group = "dev.schlaubi"
7 | version = mikbotVersion
8 |
9 | kotlin {
10 | explicitApi()
11 | }
12 |
--------------------------------------------------------------------------------
/api/annotations/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/Annotations.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api
2 |
3 | /**
4 | * **DO NOT USE THIS API IN PLUGINS!!**
5 | */
6 | @MustBeDocumented
7 | @RequiresOptIn
8 | @Retention(AnnotationRetention.BINARY)
9 | public annotation class InternalAPI
10 |
11 | /**
12 | * Class used to mark the main class of a plugin.
13 | */
14 | @MustBeDocumented
15 | @Retention(AnnotationRetention.SOURCE)
16 | @Target(AnnotationTarget.CLASS)
17 | public annotation class PluginMain
18 |
--------------------------------------------------------------------------------
/api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import dev.kordex.gradle.plugins.kordex.InternalAPI
2 | import dev.kordex.gradle.plugins.kordex.base.KordExExtension
3 | import dev.kordex.gradle.plugins.kordex.helpers.I18nHelper
4 | import dev.kordex.gradle.plugins.kordex.i18n.KordExI18nSettings
5 |
6 | plugins {
7 | `mikbot-module`
8 | `mikbot-publishing`
9 | `mikbot-template`
10 | java
11 | }
12 |
13 | group = "dev.schlaubi.mikbot"
14 | version = mikbotVersion
15 |
16 | kotlin {
17 | explicitApi()
18 | }
19 |
20 | dependencies {
21 | // Api base
22 | api(projects.api.annotations)
23 | // Bot
24 | api("dev.kord:kord-common-jvm:${libs.versions.kord.get()}")
25 | api("dev.kord:kord-rest-jvm:${libs.versions.kord.get()}")
26 | api("dev.kord:kord-gateway-jvm:${libs.versions.kord.get()}")
27 | api("dev.kord:kord-core-jvm:${libs.versions.kord.get()}")
28 | api(libs.kordex)
29 | api(libs.kordex.unsafe)
30 | api(libs.kordx.emoji) {
31 | exclude("dev.kord")
32 | }
33 | api(libs.kotlinx.coroutines.jdk8)
34 | api(libs.kmongo.coroutine.serialization)
35 | api(libs.pf4j)
36 |
37 | // Util
38 | api(libs.stdx.full)
39 |
40 | // Logging
41 | api(libs.logback.classic)
42 | }
43 |
44 | template {
45 | className = "MikBotInfo"
46 | packageName = "dev.schlaubi.mikbot.plugin.api"
47 | }
48 |
49 | val kordExExtension = extensions.create("kordex").apply {
50 | i18n {
51 | classPackage = "dev.schlaubi.mikbot.plugin.api"
52 | className = "MikBotTranslations"
53 | translationBundle = "mikbot"
54 | }
55 | }
56 |
57 | @Suppress("INVISIBLE_MEMBER")
58 | @OptIn(InternalAPI::class)
59 | I18nHelper.apply(project, kordExExtension.i18n)
60 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/AboutExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api
2 |
3 | import dev.kordex.core.builders.AboutBuilder
4 | import org.pf4j.ExtensionPoint
5 |
6 | /**
7 | * Allows to modify the about command.
8 | */
9 | public interface AboutExtensionPoint : ExtensionPoint {
10 | /**
11 | * Applies this extensions settings to the about page.
12 | */
13 | public suspend fun AboutBuilder.apply()
14 | }
15 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/EnvironmentConfig.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api
2 |
3 | import dev.schlaubi.envconf.Config
4 |
5 | /**
6 | * Alias to [Config] so you don't have to use named import.
7 | *
8 | * @see dev.schlaubi.mikbot.plugin.api.config.Config
9 | */
10 | public typealias EnvironmentConfig = Config
11 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/ModuleExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api
2 |
3 | import dev.kordex.core.extensions.Extension
4 | import dev.schlaubi.mikbot.plugin.api.module.MikBotModule
5 | import org.pf4j.ExtensionPoint
6 | import kotlin.reflect.KClass
7 |
8 | /**
9 | * Extension point for the module [T].
10 | */
11 | public interface ModuleExtensionPoint : ExtensionPoint {
12 | /**
13 | * Applies instructions to the module.
14 | */
15 | public suspend fun T.apply()
16 | }
17 |
18 | @InternalAPI
19 | public abstract class ModuleExtensionPointImpl(context: PluginContext) : MikBotModule(context) {
20 | protected abstract val extensionClazz: KClass>
21 |
22 | @Suppress("UNCHECKED_CAST", "RedundantModalityModifier")
23 | open override suspend fun setup() {
24 | context.pluginSystem.getExtensions(extensionClazz).forEach {
25 | with(it) {
26 | (this@ModuleExtensionPointImpl as T).apply()
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/PluginSystem.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api
2 |
3 | import dev.kord.core.Kord
4 | import dev.kord.core.event.Event
5 | import dev.schlaubi.mikbot.plugin.api.module.MikBotModule
6 | import org.pf4j.ExtensionPoint
7 | import kotlin.reflect.KClass
8 |
9 | /**
10 | * Internal [PluginSystem] instance.
11 | */
12 | @InternalAPI
13 | public lateinit var _pluginSystem: PluginSystem
14 |
15 | /**
16 | * Global instance for [PluginSystem].
17 | *
18 | * @see MikBotModule
19 | */
20 | @OptIn(InternalAPI::class)
21 | @Deprecated("Replaced by PluginContext", ReplaceWith("PluginContext.pluginSystem"))
22 | public val pluginSystem: PluginSystem get() = _pluginSystem
23 |
24 | /**
25 | * API for plugin related actions.
26 | *
27 | * @see pluginSystem
28 | */
29 | public interface PluginSystem {
30 | /**
31 | * Retrieves all extensions of the [extension point][type].
32 | */
33 | public fun getExtensions(type: KClass): List
34 |
35 | /**
36 | * Translates [key] from [bundleName] with [replacements].
37 | */
38 | public fun translate(
39 | key: String,
40 | bundleName: String,
41 | locale: String? = null,
42 | replacements: Array = emptyArray(),
43 | ): String
44 |
45 | /**
46 | * Emits [event] on [Kord.events].
47 | */
48 | public suspend fun emitEvent(event: Event)
49 | }
50 |
51 | /**
52 | * Retrieves all extensions of the [extension point][T].
53 | */
54 | public inline fun PluginSystem.getExtensions(): List = getExtensions(T::class)
55 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/io/Database.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.io
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.IKnowWhatIAmDoing
4 | import org.litote.kmongo.coroutine.CoroutineClient
5 | import org.litote.kmongo.coroutine.CoroutineCollection
6 | import org.litote.kmongo.coroutine.CoroutineDatabase
7 |
8 | /**
9 | * API for Database.
10 | *
11 | * @see getCollection
12 | */
13 | public interface Database {
14 |
15 | /**
16 | * The [CoroutineClient] used for the bot.
17 | */
18 | @IKnowWhatIAmDoing
19 | public val client: CoroutineClient
20 |
21 | /**
22 | * The [CoroutineDatabase] used for all bot collections.
23 | */
24 | public val database: CoroutineDatabase
25 | }
26 |
27 | /**
28 | * Gets a collection, with a specific default document class.
29 | *
30 | * @param name the name of the collection
31 | * @param the type of the class to use instead of `Document`.
32 | * @return the collection
33 | **/
34 | public inline fun Database.getCollection(name: String): CoroutineCollection =
35 | database.getCollection(name)
36 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/module/MikBotModule.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.module
2 |
3 | import dev.kordex.core.extensions.Extension
4 | import dev.schlaubi.mikbot.plugin.api.PluginContext
5 | import dev.schlaubi.mikbot.plugin.api.Plugin
6 |
7 | /**
8 | * Implementation of [Extension] which stores a [PluginContext].
9 | *
10 | * @property context the [PluginContext] which registered this module
11 | * @see Plugin.add
12 | */
13 | public abstract class MikBotModule(public val context: PluginContext) : Extension()
14 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/owner/OwnerApi.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.owner
2 |
3 | import dev.kordex.core.commands.application.slash.SlashCommand
4 | import dev.kord.common.entity.ApplicationIntegrationType
5 | import dev.kord.common.entity.InteractionContextType
6 | import dev.kord.common.entity.Permission
7 | import dev.schlaubi.mikbot.plugin.api.*
8 | import dev.schlaubi.mikbot.plugin.api.config.Config
9 | import kotlinx.coroutines.CoroutineScope
10 |
11 | /**
12 | * Module which houses commands reserved for bot owners.
13 | *
14 | * @see ownerOnly
15 | */
16 | @OptIn(InternalAPI::class)
17 | public abstract class OwnerModule @InternalAPI constructor(context: PluginContext) :
18 | ModuleExtensionPointImpl(context), CoroutineScope
19 |
20 | /**
21 | * The Extension point for Owner module customization.
22 | */
23 | public interface OwnerExtensionPoint : ModuleExtensionPoint {
24 | /**
25 | * Applies instructions to the owner module.
26 | */
27 | public override suspend fun OwnerModule.apply()
28 | }
29 |
30 | /**
31 | * Configures this command, to only be usable by administrators of [Config.OWNER_GUILD].
32 | */
33 | public fun SlashCommand<*, *, *>.ownerOnly() {
34 | guildId = Config.OWNER_GUILD ?: error("Cannot register owner command without OWNER_GUILD value")
35 | requirePermission(Permission.Administrator)
36 | allowedContexts.add(InteractionContextType.Guild)
37 | allowedInstallTypes.add(ApplicationIntegrationType.GuildInstall)
38 | }
39 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/settings/SettingsApi.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.settings
2 |
3 | import dev.kord.common.entity.InteractionContextType
4 | import dev.kord.common.entity.Permission
5 | import dev.kordex.core.commands.application.slash.SlashCommand
6 | import dev.schlaubi.mikbot.plugin.api.InternalAPI
7 | import dev.schlaubi.mikbot.plugin.api.ModuleExtensionPoint
8 | import dev.schlaubi.mikbot.plugin.api.ModuleExtensionPointImpl
9 | import dev.schlaubi.mikbot.plugin.api.PluginContext
10 |
11 | /**
12 | * Extension for settings.
13 | *
14 | * @see guildAdminOnly
15 | */
16 | @OptIn(InternalAPI::class)
17 | public abstract class SettingsModule @InternalAPI constructor(context: PluginContext) :
18 | ModuleExtensionPointImpl(context)
19 |
20 | public interface SettingsExtensionPoint : ModuleExtensionPoint {
21 | public override suspend fun SettingsModule.apply()
22 | }
23 |
24 | public fun SlashCommand<*, *, *>.guildAdminOnly() {
25 | requirePermission(Permission.ManageGuild)
26 | allowedContexts.add(InteractionContextType.Guild)
27 | }
28 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/ActionInteraction.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.behavior.interaction.ActionInteractionBehavior
4 | import dev.kord.core.behavior.interaction.respondEphemeral
5 | import dev.kord.core.behavior.interaction.respondPublic
6 | import dev.kord.core.behavior.interaction.response.EphemeralMessageInteractionResponseBehavior
7 | import dev.kord.core.behavior.interaction.response.PublicMessageInteractionResponseBehavior
8 | import dev.kord.core.entity.interaction.Interaction
9 | import dev.kord.rest.builder.message.create.InteractionResponseCreateBuilder
10 |
11 | /**
12 | * Acknowledges an interaction and responds with [EphemeralMessageInteractionResponseBehavior] with ephemeral flag.
13 | *
14 | * @param block [InteractionResponseCreateBuilder] used to a create an ephemeral response.
15 | * @return [EphemeralMessageInteractionResponseBehavior] ephemeral response to the interaction.
16 | */
17 | public suspend inline fun Interaction.respondEphemeral(block: InteractionResponseCreateBuilder.() -> Unit): EphemeralMessageInteractionResponseBehavior =
18 | (this as ActionInteractionBehavior).respondEphemeral(block)
19 |
20 | /**
21 | * Acknowledges an interaction and responds with [PublicMessageInteractionResponseBehavior].
22 | *
23 | * @param block [InteractionResponseCreateBuilder] used to create a public response.
24 | * @return [PublicMessageInteractionResponseBehavior] public response to the interaction.
25 | */
26 | public suspend inline fun Interaction.respondPublic(block: InteractionResponseCreateBuilder.() -> Unit): PublicMessageInteractionResponseBehavior =
27 | (this as ActionInteractionBehavior).respondPublic(block)
28 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/AllShardsReadyEvent.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.common.annotation.KordPreview
4 | import dev.kord.core.Kord
5 | import dev.kord.core.event.Event
6 |
7 | /**
8 | * Event fired when all shards are first connected to Discord.
9 | */
10 | @OptIn(KordPreview::class)
11 | public class AllShardsReadyEvent(
12 | override val kord: Kord,
13 | override val shard: Int,
14 | override val customContext: Any? = null,
15 | ) : Event
16 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/ArgumentUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.ExtensibleBot
4 | import dev.kordex.core.commands.CommandContext
5 | import dev.kordex.core.koin.KordExKoinComponent
6 | import dev.schlaubi.mikbot.plugin.api.io.Database
7 | import org.koin.core.component.KoinComponent
8 |
9 | /**
10 | * Simple accessor for [ExtensibleBot] for all [KoinComponents][KoinComponent]
11 | */
12 | public val CommandContext.bot: ExtensibleBot
13 | get() = getKoin().get()
14 |
15 | /**
16 | * Simple accessor for [Database] for all [KoinComponents][KoinComponent]
17 | */
18 | public val KordExKoinComponent.database: Database
19 | get() = getKoin().get()
20 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/AutoCompleteUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.entity.interaction.OptionValue
4 |
5 | public val OptionValue<*>.safeInput: String get() = value?.toString() ?: ""
6 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/CheckUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.checks.types.CheckContext
4 | import dev.kord.core.event.Event
5 |
6 | /**
7 | * Only executes this series of checks if the context is still passing.
8 | */
9 | public inline fun CheckContext.ifPassing(block: CheckContext.() -> Unit) {
10 | if (passed) block()
11 | }
12 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/CommandUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.checks.anyGuild
4 | import dev.kordex.core.commands.Command
5 | import dev.kordex.core.commands.CommandContext
6 | import dev.kord.core.Kord
7 | import dev.kord.core.behavior.GuildBehavior
8 | import dev.kord.core.behavior.MemberBehavior
9 | import kotlinx.coroutines.runBlocking
10 |
11 | /**
12 | * Accessor to [GuildBehavior] for contexts having the [anyGuild] check applied.
13 | */
14 | public val CommandContext.safeGuild: GuildBehavior
15 | get() = runBlocking { getGuild() } ?: error("This command required a guild check")
16 |
17 | /**
18 | * Accessor to [MemberBehavior] for contexts having the [anyGuild] check applied.
19 | */
20 | public val CommandContext.safeMember: MemberBehavior
21 | get() = runBlocking { getMember() } ?: error("This command required a guild check")
22 |
23 | /**
24 | * Adds [Command.kord] to [CommandContext] as implicit receivers are blocked.
25 | */
26 | public inline val CommandContext.kord: Kord
27 | get() = command.kord
28 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/ComponentLiveMessage.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.common.annotation.KordPreview
4 | import dev.kord.common.entity.Snowflake
5 | import dev.kord.core.entity.Message
6 | import dev.kord.core.event.Event
7 | import dev.kord.core.event.interaction.ComponentInteractionCreateEvent
8 | import dev.kord.core.live.AbstractLiveKordEntity
9 | import dev.kord.core.live.on
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Job
12 | import kotlinx.coroutines.SupervisorJob
13 | import kotlinx.coroutines.plus
14 |
15 | public fun Message.componentLive(coroutineScope: CoroutineScope = kord + SupervisorJob()): ComponentLiveMessage =
16 | ComponentLiveMessage(this, coroutineScope)
17 |
18 | @OptIn(KordPreview::class)
19 | public class ComponentLiveMessage(private val message: Message, coroutineScope: CoroutineScope = message.kord + SupervisorJob()) :
20 | AbstractLiveKordEntity(message.kord, coroutineScope) {
21 | override val id: Snowflake
22 | get() = message.id
23 |
24 | override fun filter(event: Event): Boolean =
25 | (event as? ComponentInteractionCreateEvent)?.interaction?.message?.id == id
26 |
27 | override fun update(event: Event): Unit = Unit
28 |
29 | public fun onInteraction(consumer: suspend ComponentInteractionCreateEvent.() -> Unit): Job =
30 | on(this, consumer)
31 | }
32 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/EffectiveAvatar.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.entity.User
4 |
5 | public val User.effectiveAvatar: String
6 | get() = avatar?.cdnUrl?.toUrl() ?: defaultAvatar.cdnUrl.toUrl()
7 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/EmbedUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.rest.builder.message.EmbedBuilder
4 | import kotlin.contracts.ExperimentalContracts
5 | import kotlin.contracts.InvocationKind
6 | import kotlin.contracts.contract
7 |
8 | /**
9 | * Creates an [EmbedBuilder] and applies [builder] to it.
10 | */
11 | @OptIn(ExperimentalContracts::class)
12 | public inline fun embed(builder: EmbedBuilder.() -> Unit): EmbedBuilder {
13 | contract {
14 | callsInPlace(builder, InvocationKind.EXACTLY_ONCE)
15 | }
16 |
17 | return EmbedBuilder().also(builder)
18 | }
19 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/ErrorUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.DiscordRelayedException
4 | import dev.kordex.core.i18n.types.Key
5 |
6 | /**
7 | * Throws a [DiscordRelayedException] with [message] and therefore sends it to the user.
8 | */
9 | public fun discordError(message: Key): Nothing = throw DiscordRelayedException(message)
10 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/EventHandlerUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.checks.types.CheckContext
4 | import dev.kord.core.behavior.interaction.ActionInteractionBehavior
5 | import dev.kord.core.behavior.interaction.respondEphemeral
6 | import dev.kord.core.behavior.reply
7 | import dev.kord.core.event.Event
8 | import dev.kord.core.event.interaction.InteractionCreateEvent
9 | import dev.kord.core.event.message.MessageCreateEvent
10 | import kotlinx.coroutines.coroutineScope
11 | import kotlinx.coroutines.launch
12 |
13 | /**
14 | * Responds with [CheckContext.message] if the check failed.
15 | */
16 | @JvmName("respondIfFailedInInteraction")
17 | public suspend fun CheckContext.respondIfFailed(): Unit = respondIfFailed {
18 | (event.interaction as? ActionInteractionBehavior)?.respondEphemeral { content = it }
19 | }
20 |
21 | /**
22 | * Responds with [CheckContext.message] if the check failed.
23 | */
24 | @JvmName("respondIfFailedInMessageChannel")
25 | public suspend fun CheckContext.respondIfFailed(): Unit = respondIfFailed {
26 | coroutineScope {
27 | launch { event.message.deleteAfterwards() }
28 | event.message.reply { content = it }.deleteAfterwards()
29 | }
30 | }
31 |
32 | @JvmName("respondIfFailedGeneric")
33 | private suspend fun CheckContext.respondIfFailed(respond: suspend (String) -> Unit) {
34 | if (!passed && message != null) {
35 | val content = message ?: return
36 | respond(content)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/ExtensionFinder.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.ExtensibleBot
4 | import dev.kordex.core.extensions.Extension
5 |
6 | /**
7 | * Allows to lazily access other extensions in an [Extension].
8 | *
9 | * Example:
10 | * ```kotlin
11 | * val musicModule: MusicModule by extension()
12 | * ```
13 | */
14 | public inline fun Extension.extension(): Lazy = bot.extension()
15 |
16 | /**
17 | * Allows to lazily access other extensions in an [Extension].
18 | *
19 | * Example:
20 | * ```kotlin
21 | * val musicModule: MusicModule by extension()
22 | * ```
23 | */
24 | public inline fun ExtensibleBot.extension(): Lazy = lazy { findExtension()!! }
25 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/IKnowWhatIAmDoing.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | @RequiresOptIn(message = "You could be using this API wrong. Only use it when you know what you're doing!")
4 | @Retention(AnnotationRetention.BINARY)
5 | @Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
6 | public annotation class IKnowWhatIAmDoing
7 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/LocaleUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.common.Locale
4 | import dev.kord.core.entity.User
5 | import dev.kord.core.entity.channel.GuildChannel
6 | import dev.kordex.core.ExtensibleBot
7 | import java.util.Locale as JLocale
8 |
9 | /** Resolve the locale for this command context. **/
10 | public suspend fun ExtensibleBot.getLocale(channel: GuildChannel, user: User): JLocale {
11 | var locale: JLocale? = null
12 |
13 | @Suppress("LoopToCallChain") // False positive
14 | for (resolver in settings.i18nBuilder.localeResolvers) {
15 | val result = resolver(channel.guild, channel, user, null)
16 |
17 | if (result != null) {
18 | locale = result
19 | break
20 | }
21 | }
22 |
23 | return locale ?: settings.i18nBuilder.defaultLocale
24 | }
25 |
26 | /**
27 | * This converts the language codes used by Discord (e.g `de`) to the ones used by [JLocale] like `de_DE`.
28 | *
29 | * If [Locale.country] is already specified, it will just use the already specified version
30 | */
31 | public fun Locale.convertToISO(): Locale = when {
32 | !country.isNullOrBlank() -> this
33 | language == "cs" -> copy(country = "CZ")
34 | language == "da" -> copy(country = "DK")
35 | language == "el" -> copy(country = "GR")
36 | language == "hi" -> copy(country = "IN")
37 | language == "ja" -> copy(country = "JP")
38 | language == "uk" -> copy(country = "UA")
39 | language == "vi" -> copy(country = "VN")
40 | else -> Locale(language, language.uppercase(JLocale.ENGLISH))
41 | }
42 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/LoomDispatcher.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.asCoroutineDispatcher
6 | import kotlinx.coroutines.withContext
7 | import java.util.concurrent.Executors
8 |
9 | /**
10 | * Coroutine dispatcher using [Executors.newVirtualThreadPerTaskExecutor] for dispatching.
11 | */
12 | public val loomDispatcher: CoroutineDispatcher = Executors.newVirtualThreadPerTaskExecutor()
13 | .asCoroutineDispatcher()
14 |
15 | /**
16 | * Executes the oncoming block using [loomDispatcher].
17 | */
18 | public suspend fun blocking(block: suspend CoroutineScope.() -> T): T = withContext(loomDispatcher, block)
19 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/MapArgument.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.commands.CommandContext
4 | import dev.kordex.core.commands.converters.Converter
5 | import dev.kordex.core.i18n.types.Key
6 | import dev.kordex.parser.StringParser
7 |
8 | public fun Converter.map(
9 | mapper: (OutputType) -> B
10 | ): Converter =
11 | object : Converter() {
12 | private var parsedValue: Any? = null
13 |
14 | @Suppress("UNUSED_PARAMETER") // the setter is irrelevant here
15 | override var parsed: B
16 | @Suppress("UNCHECKED_CAST")
17 | get() = mapper(this@map.parsed)
18 | set(value) {}
19 |
20 | override val signatureType: Key
21 | get() = TODO("Not supported")
22 |
23 | override suspend fun parse(parser: StringParser?, context: CommandContext, named: NamedInputType?): ResultType {
24 | val result = this@map.parse(parser, context, named)
25 | if (this@map.parseSuccess) {
26 | parseSuccess = true
27 | }
28 | parsedValue = mapper(this@map.parsed)
29 | return result
30 | }
31 |
32 | override suspend fun validate(context: CommandContext) {
33 | super.validate(context)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/MessageBuilder.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.behavior.interaction.followup.FollowupMessageBehavior
4 | import dev.kord.rest.builder.message.create.MessageCreateBuilder
5 | import dev.kord.rest.builder.message.modify.MessageModifyBuilder
6 | import dev.kordex.core.commands.CommandContext
7 |
8 | /**
9 | * This is a message builder block (aka. the thing a [MessageSender] takes).
10 | */
11 | public typealias MessageBuilder = suspend MessageCreateBuilder.() -> Unit
12 |
13 | /**
14 | * This is a message edit builder block (aka. the thing a [MessageEditor] takes).
15 | */
16 | public typealias MessageEditBuilder = suspend MessageModifyBuilder.() -> Unit
17 |
18 | /**
19 | * Function which can send an interaction message (e.g. [respond]), which can be edited.
20 | */
21 | public typealias EditableMessageSender = suspend (MessageBuilder) -> FollowupMessageBehavior
22 |
23 | /**
24 | * Function which can send an interaction message (e.g. [respond]), which cannot be edited.
25 | */
26 | public typealias MessageSender = suspend (MessageBuilder) -> Any
27 |
28 | /**
29 | * Function that edits an existing message.
30 | */
31 | public typealias MessageEditor = suspend (MessageEditBuilder) -> Unit
32 |
33 | /**
34 | * Function translating the key in a group (e.g. [CommandContext.translate]).
35 | */
36 | public typealias Translator = suspend (key: String, group: String) -> String
37 |
38 | /**
39 | * A function creating a confirmation (e.g. [confirmation]).
40 | */
41 | public typealias ConfirmationSender = suspend (MessageBuilder) -> Confirmation
42 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/MessageSearchUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.entity.Message
4 |
5 | /**
6 | * Returns the first attachment url or the message content.
7 | */
8 | public val Message.attachmentOrContentQuery: String
9 | get() = attachments.firstOrNull()?.url ?: content
10 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/MessageUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kord.core.behavior.MessageBehavior
4 | import kotlinx.coroutines.delay
5 | import kotlin.time.Duration
6 | import kotlin.time.Duration.Companion.seconds
7 |
8 | /**
9 | * Deletes this [MessageBehavior] after [duration].
10 | */
11 | public suspend fun MessageBehavior.deleteAfterwards(duration: Duration = 3.seconds) {
12 | delay(duration)
13 | delete()
14 | }
15 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/PathUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | private val driveLetterRegex = "[A-Z]:".toRegex()
4 |
5 | public fun String.ensurePath(): String =
6 | if (isWindows()) {
7 | val driveLetter = driveLetterRegex.find(this)
8 | if (driveLetter != null) {
9 | drop(driveLetter.range.first)
10 | } else {
11 | this
12 | }
13 | } else {
14 | this
15 | }
16 |
17 | private fun isWindows() = System.getProperty("os.name").startsWith("Windows")
18 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/TranslationUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.i18n.types.Key
4 | import dev.kordex.core.i18n.withContext
5 | import dev.kordex.core.types.TranslatableContext
6 |
7 | /**
8 | * Translates [key] with [replacements].
9 | */
10 | public suspend fun TranslatableContext.translate(key: Key, vararg replacements: Any?): String =
11 | key.withContext(this).translate(*replacements)
12 |
--------------------------------------------------------------------------------
/api/src/main/kotlin/dev/schlaubi/mikbot/plugin/api/util/UserCommands.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.plugin.api.util
2 |
3 | import dev.kordex.core.commands.application.ApplicationCommand
4 | import dev.kord.common.entity.ApplicationIntegrationType
5 | import dev.kord.common.entity.InteractionContextType
6 |
7 | public fun ApplicationCommand<*>.executableEverywhere() {
8 | allowedInstallTypes.addAll(ApplicationIntegrationType.entries)
9 | allowedContexts.addAll(InteractionContextType.entries)
10 | }
11 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings.properties:
--------------------------------------------------------------------------------
1 | general.yes=Yes
2 | general.no=No
3 | general.aborted=Aborted process!
4 | checks.owner.failed=So yeah funny story here, you pleb aren't allowed to use this command **BUT!!** whilst you here, I would like to mention to you, that you shouldn't buy iPhones.
5 | general.true=True
6 | general.false=False
7 | general.unset=Not set
8 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings_de_DE.properties:
--------------------------------------------------------------------------------
1 | general.yes=Ja
2 | general.no=Nein
3 | general.aborted=Prozess Abgebrochen!
4 | checks.owner.failed=Lol. Was traust du pleb dich eigentlich. Diesen Command auszuführen. Kauf dir kein iPhone. Weil du gerade da bist: Support me on patreon. ()
5 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings_fr.properties:
--------------------------------------------------------------------------------
1 | general.aborted=Processus interrompu!
2 | general.yes=Oui
3 | general.no=Non
4 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings_it_IT.properties:
--------------------------------------------------------------------------------
1 | general.yes=Sì
2 | general.no=No
3 | general.aborted=Processo interrotto!
4 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings_pl.properties:
--------------------------------------------------------------------------------
1 | general.yes=Tak
2 | general.no=Nie
3 | general.aborted=Proces przerwany!
4 |
--------------------------------------------------------------------------------
/api/src/main/resources/translations/mikbot/strings_vi.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/api/src/main/resources/translations/mikbot/strings_vi.properties
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
2 | import dev.schlaubi.mikbot.gradle.addRepositories
3 |
4 | plugins {
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | subprojects {
9 | addRepositories()
10 | }
11 |
12 | mikbotPlugin {
13 | license = "MIT License"
14 | provider = "Mikbot Official Plugins"
15 |
16 | i18n {
17 | classPackage = "dev.schlaubi.mikbot.translations"
18 | }
19 | }
20 |
21 | pluginPublishing {
22 | repositoryUrl = "https://storage.googleapis.com/mikbot-plugins"
23 | targetDirectory = rootProject.file("ci-repo")
24 | currentRepository = rootProject.file("ci-repo-old")
25 | projectUrl = "https://github.com/DRSchlaubi/mikbot"
26 | }
27 |
28 | tasks {
29 | task("buildDockerImage") {
30 | dependsOn(":runtime:installDist")
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | groovy
6 | `kotlin-dsl`
7 | }
8 |
9 | repositories {
10 | mavenCentral()
11 | gradlePluginPortal()
12 |
13 | maven("https://releases-repo.kordex.dev")
14 | }
15 |
16 | dependencies {
17 | implementation(kotlin("gradle-plugin-api", libs.versions.kotlin.get()))
18 | implementation(kotlin("gradle-plugin", libs.versions.kotlin.get()))
19 | implementation("dev.schlaubi", "gradle-plugin", "1.0.0")
20 | implementation("com.google.devtools.ksp", "com.google.devtools.ksp.gradle.plugin", libs.versions.ksp.get())
21 | implementation("org.jlleitschuh.gradle", "ktlint-gradle", "12.1.1")
22 | implementation("com.github.gmazzo", "gradle-buildconfig-plugin", "3.1.0")
23 | implementation("gradle.plugin.com.google.cloud.artifactregistry", "artifactregistry-gradle-plugin", "2.2.2")
24 | implementation(gradleApi())
25 | implementation(localGroovy())
26 | }
27 |
28 | tasks {
29 | withType {
30 | compilerOptions {
31 | jvmTarget = JvmTarget.JVM_19
32 | }
33 | }
34 | }
35 |
36 | java {
37 | sourceCompatibility = JavaVersion.VERSION_19
38 | }
39 |
--------------------------------------------------------------------------------
/buildSrc/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "buildSrc"
2 |
3 | dependencyResolutionManagement {
4 | @Suppress("UnstableApiUsage")
5 | versionCatalogs {
6 | create("libs") {
7 | from(files("../gradle/libs.versions.toml"))
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Git.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 | import java.io.ByteArrayOutputStream
3 |
4 | fun Project.getGitCommit(): String {
5 | return execCommand("git", "rev-parse", "--short", "HEAD")
6 | ?: System.getenv("GITHUB_SHA") ?: ""
7 | }
8 |
9 | fun Project.getGitBranch(): String = execCommand("git", "rev-parse", "--abbrev-ref", "HEAD") ?: "unknown"
10 |
11 | internal fun Project.execCommand(vararg command: String): String? {
12 | val output = ByteArrayOutputStream()
13 | try {
14 | providers.exec {
15 | commandLine("git", *command)
16 | standardOutput = output
17 | errorOutput = output
18 | workingDir = rootDir
19 | }.result.get().rethrowFailure()
20 | } catch (e: Exception) {
21 | return null
22 | }
23 | return output.toString().trim()
24 | }
25 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/KtorUtil.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.artifacts.dsl.DependencyHandler
2 | import org.gradle.api.provider.Provider
3 | import org.gradle.api.provider.ProviderConvertible
4 | import org.gradle.kotlin.dsl.DependencyHandlerScope
5 | import org.gradle.kotlin.dsl.accessors.runtime.addConfiguredDependencyTo
6 | import org.gradle.kotlin.dsl.exclude
7 |
8 |
9 | fun DependencyHandlerScope.ktorDependency(dependency: ProviderConvertible<*>) = ktorDependency(dependency.asProvider())
10 | fun DependencyHandler.ktorDependency(dependency: Provider<*>) =
11 | addConfiguredDependencyTo(this, "implementation", dependency) {
12 | exclude(module = "ktor-server-core")
13 | }
14 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Project.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 | import org.gradle.api.artifacts.VersionCatalogsExtension
3 | import org.gradle.kotlin.dsl.getByType
4 |
5 | object Project {
6 | // Mikbot version (not core plugins)
7 | const val version = "3.17.0"
8 | }
9 |
10 | val Project.mikbotVersion: String
11 | get() = extensions.getByType().named("libs").findVersion("api")
12 | .get().requiredVersion + "-SNAPSHOT"
13 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mikbot-module.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("jvm")
3 | }
4 |
5 | val experimentalAnnotations =
6 | listOf("kotlin.RequiresOptIn", "kotlin.time.ExperimentalTime", "kotlin.contracts.ExperimentalContracts")
7 |
8 | tasks {
9 | withType {
10 | useJUnitPlatform()
11 | }
12 | }
13 |
14 | kotlin {
15 | jvmToolchain(22)
16 |
17 | compilerOptions {
18 | freeCompilerArgs.addAll(experimentalAnnotations.map { "-opt-in=$it" })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/mikbot-template.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.github.gmazzo.gradle.plugins.BuildConfigField
2 |
3 | plugins {
4 | com.github.gmazzo.buildconfig apply false
5 | }
6 |
7 | abstract class TemplateExtension {
8 | @get:Input
9 | abstract val packageName: Property
10 |
11 | @get:Input
12 | abstract val className: Property
13 | }
14 |
15 | extensions.create("template")
16 |
17 | val extension = project.extensions.findByName("template") as TemplateExtension
18 |
19 | buildConfig {
20 | packageName = extension.packageName
21 | className = extension.className
22 |
23 | buildConfigField("String", "VERSION", "\"${project.version}\"")
24 | // buildConfigField("String", "BRANCH", provider { "\"${project.getGitBranch()}\"" })
25 | // buildConfigField("String", "COMMIT", provider { "\"${project.getGitCommit()}\"" })
26 | }
27 |
--------------------------------------------------------------------------------
/clients/README.md:
--------------------------------------------------------------------------------
1 | # clients
2 |
3 | Collection of API Clients used by the bot
4 |
--------------------------------------------------------------------------------
/clients/haste-client/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | alias(libs.plugins.kotlinx.serialization)
5 | }
6 |
7 | version = mikbotVersion
8 |
9 | dependencies {
10 | implementation(libs.ktor.client.okhttp)
11 | implementation(libs.ktor.client.content.negotiation)
12 | implementation(libs.ktor.serialization.kotlinx.json)
13 | testImplementation(kotlin("test"))
14 | testImplementation(libs.kotlinx.coroutines.test)
15 | }
16 |
17 | kotlin {
18 | explicitApi()
19 | }
20 |
--------------------------------------------------------------------------------
/clients/haste-client/src/main/kotlin/dev/schlaubi/mikbot/haste/Haste.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.haste
2 |
3 | public class Haste(public val key: String, public val url: String, private val hasteClient: HasteClient) {
4 | public suspend fun getContent(): String {
5 | return hasteClient.getHasteContent(key)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/clients/haste-client/src/main/kotlin/dev/schlaubi/mikbot/haste/HasteClient.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.haste
2 |
3 | import io.ktor.client.*
4 | import io.ktor.client.call.*
5 | import io.ktor.client.engine.okhttp.*
6 | import io.ktor.client.plugins.contentnegotiation.*
7 | import io.ktor.client.request.*
8 | import io.ktor.http.*
9 | import io.ktor.serialization.kotlinx.json.*
10 | import kotlinx.serialization.json.Json
11 |
12 | /**
13 | * Client for interacting with a [hastebin](https://github.com/toptal/haste-server) compatible server.
14 | *
15 | * @property url the base url of the haste server. e.g. https://pasta.with-rice.by.devs-from.asia/
16 | */
17 | public class HasteClient(private val url: String) {
18 |
19 | private val httpClient: HttpClient = HttpClient(OkHttp) {
20 | install(ContentNegotiation) {
21 | val json = Json {
22 | ignoreUnknownKeys = true
23 | }
24 | json(json)
25 | }
26 | }
27 |
28 | /**
29 | * Creates a new haste.
30 | *
31 | * @param content the content to save as a haste.
32 | * @return the created haste.
33 | */
34 | public suspend fun createHaste(content: String): Haste {
35 | val (key) = httpClient.post(URLBuilder(url).appendPathSegments("documents").build()) {
36 | setBody(content)
37 | }.body()
38 | val hasteUrl = URLBuilder(url).appendPathSegments(key).buildString()
39 | return Haste(key, hasteUrl, this)
40 | }
41 |
42 | /**
43 | * Fetches a haste by its key.
44 | *
45 | * @param key the key of the haste to fetch.
46 | * @return the haste's content
47 | */
48 | public suspend fun getHasteContent(key: String): String {
49 | return httpClient.get(URLBuilder(url).appendPathSegments("raw", key).build()).body()
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/clients/haste-client/src/main/kotlin/dev/schlaubi/mikbot/haste/HasteResponse.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.haste
2 |
3 | import kotlinx.serialization.ExperimentalSerializationApi
4 | import kotlinx.serialization.Serializable
5 | import kotlinx.serialization.json.JsonNames
6 |
7 | @OptIn(ExperimentalSerializationApi::class)
8 | @Serializable
9 | internal data class HasteResponse(@JsonNames("path") val key: String)
10 |
--------------------------------------------------------------------------------
/clients/haste-client/src/test/kotlin/HasteTest.kt:
--------------------------------------------------------------------------------
1 | import dev.schlaubi.mikbot.haste.HasteClient
2 | import kotlinx.coroutines.ExperimentalCoroutinesApi
3 | import kotlinx.coroutines.test.runTest
4 | import kotlin.test.Ignore
5 | import kotlin.test.Test
6 | import kotlin.test.assertEquals
7 |
8 | @Ignore
9 | internal class HasteTest {
10 | @OptIn(ExperimentalCoroutinesApi::class)
11 | @Test
12 | fun `Create a haste`() = runTest {
13 | val client = HasteClient("https://pasta.with-rice.by.devs-from.asia/")
14 | val testContent = "uwu"
15 | val haste = client.createHaste(testContent)
16 | val content = haste.getContent()
17 | assertEquals(testContent, content)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/clients/image-color-client-kord/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | }
5 |
6 | group = "dev.nycode"
7 | version = mikbotVersion
8 |
9 | dependencies {
10 | api(projects.clients.imageColorClient)
11 | api(libs.kord.rest)
12 | }
13 |
14 | kotlin {
15 | explicitApi()
16 | }
17 |
--------------------------------------------------------------------------------
/clients/image-color-client-kord/src/main/kotlin/KordExtensions.kt:
--------------------------------------------------------------------------------
1 | package dev.nycode.imagecolor.kord
2 |
3 | import dev.nycode.imagecolor.Image
4 | import dev.nycode.imagecolor.ImageFormat
5 | import dev.kord.rest.Image as KordImage
6 |
7 | public fun KordImage.toImage(): Image = Image(data, format.asImageFormat())
8 |
9 | public fun KordImage.Format.asImageFormat(): ImageFormat {
10 | return when (extension) {
11 | "jpeg" -> ImageFormat.JPEG
12 | "png" -> ImageFormat.PNG
13 | "webp" -> ImageFormat.WEBP
14 | "gif" -> ImageFormat.GIF
15 | else -> error("Invalid image format: $extension")
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/clients/image-color-client/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | alias(libs.plugins.kotlinx.serialization)
5 | }
6 |
7 | group = "dev.nycode"
8 | version = mikbotVersion
9 |
10 | dependencies {
11 | implementation(libs.ktor.client.okhttp)
12 | implementation(libs.ktor.client.content.negotiation)
13 | implementation(libs.ktor.serialization.kotlinx.json)
14 | }
15 |
16 | kotlin {
17 | explicitApi()
18 | }
19 |
--------------------------------------------------------------------------------
/core/README.md:
--------------------------------------------------------------------------------
1 | # core
2 |
3 | This module acts as a parent to all core functionality of the bot.
4 | In general all of these modules should be in any installation of the bot
5 |
6 | # Modules
7 |
8 | - [database-i18n](database-i18n) - Implementation of i18n API backed by database
9 | - [game-animator](game-animator) - MOTD plugin for bot's presence
10 | - [gdpr](gdpr) - GDPR compliance
11 | - [redeploy-hook](redeploy-hook) - /redeploy command backed by webhook
12 |
--------------------------------------------------------------------------------
/core/database-i18n/README.md:
--------------------------------------------------------------------------------
1 | # database-i18n
2 |
3 | Implementation of the bots i18n-system backed by a database
4 |
5 | Currently, this is the only available implementation. If you want to make your own, take a look
6 | at [this](https://kordex.netlify.app/latest/concepts/i18n/)
7 |
--------------------------------------------------------------------------------
/core/database-i18n/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | alias(libs.plugins.kotlinx.serialization)
4 | com.google.devtools.ksp
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | group = "dev.schlaubi.mikbot"
9 | version = mikbotVersion
10 |
11 | dependencies {
12 | optionalPlugin(projects.core.gdpr)
13 | }
14 |
15 | mikbotPlugin {
16 | description = "Implementation of the bots i18n-system backed by a database"
17 | }
18 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/kotlin/dev/schlaubi/mikbot/core/i18n/database/DatabaseI18NPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.i18n.database
2 |
3 | import dev.kordex.core.builders.ExtensibleBotBuilder
4 | import dev.schlaubi.mikbot.plugin.api.Plugin
5 | import dev.schlaubi.mikbot.plugin.api.PluginContext
6 | import dev.schlaubi.mikbot.plugin.api.PluginMain
7 |
8 | @PluginMain
9 | class DatabaseI18NPlugin(wrapper: PluginContext) : Plugin(wrapper) {
10 | override suspend fun ExtensibleBotBuilder.apply() {
11 | i18n {
12 | localeResolver { _, _, user, _ ->
13 | user?.let {
14 | LanguageDatabase.collection.findOneById(it.id)?.locale
15 | }
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/kotlin/dev/schlaubi/mikbot/core/i18n/database/LanguageDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.i18n.database
2 |
3 | import dev.kordex.core.koin.KordExKoinComponent
4 | import dev.kord.common.entity.Snowflake
5 | import dev.schlaubi.mikbot.plugin.api.io.getCollection
6 | import dev.schlaubi.mikbot.plugin.api.util.database
7 | import kotlinx.serialization.Contextual
8 | import kotlinx.serialization.SerialName
9 | import kotlinx.serialization.Serializable
10 | import java.util.*
11 |
12 | object LanguageDatabase : KordExKoinComponent {
13 | val collection = database.getCollection("language_users")
14 | }
15 |
16 | @Serializable
17 | data class LangaugeUser(@SerialName("_id") val id: Snowflake, @Contextual val locale: Locale)
18 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/kotlin/dev/schlaubi/mikbot/core/i18n/database/gdpr/GDPR.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.i18n.database.gdpr
2 |
3 | import dev.kord.core.entity.User
4 | import dev.schlaubi.mikbot.core.gdpr.api.DataPoint
5 | import dev.schlaubi.mikbot.core.gdpr.api.GDPRExtensionPoint
6 | import dev.schlaubi.mikbot.core.gdpr.api.PermanentlyStoredDataPoint
7 | import dev.schlaubi.mikbot.core.i18n.database.LanguageDatabase
8 | import dev.schlaubi.mikbot.translations.DatabaseI18nTranslations
9 | import org.pf4j.Extension
10 |
11 | @Extension
12 | class DatabaseI18NGDPR : GDPRExtensionPoint {
13 | override fun provideDataPoints(): List = listOf(languageDataPoint)
14 | }
15 |
16 | val languageDataPoint: PermanentlyStoredDataPoint = LanguageDataPoint
17 |
18 | private object LanguageDataPoint : PermanentlyStoredDataPoint(DatabaseI18nTranslations.Gdpr.name, DatabaseI18nTranslations.Gdpr.description) {
19 | override suspend fun deleteFor(user: User) {
20 | LanguageDatabase.collection.deleteOneById(user.id)
21 | }
22 |
23 | override suspend fun requestFor(user: User): List {
24 | val language = LanguageDatabase.collection.findOneById(user.id)?.locale?.let { it.getDisplayName(it) }
25 | return listOf(language.toString())
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/kotlin/dev/schlaubi/mikbot/core/i18n/database/settings/DatabaseI18NSettingsExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.i18n.database.settings
2 |
3 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsExtensionPoint
4 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule
5 | import org.pf4j.Extension
6 |
7 | @Extension
8 | class DatabaseI18NSettingsExtension : SettingsExtensionPoint {
9 | override suspend fun SettingsModule.apply() {
10 | languageCommand()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings.properties:
--------------------------------------------------------------------------------
1 | gdpr.description=In order to provide the Bot in your language, it will store your selected language
2 | gdpr.name=Language
3 | commands.language.arguments.language.name=language
4 | commands.language.arguments.language.description=The language you want to use
5 | commands.language.arguments.language.german=German
6 | commands.language.arguments.language.english=English
7 | commands.language.arguments.language.italian=Italian
8 | commands.language.arguments.language.french=French
9 | commands.language.name=language
10 | commands.language.description=Changes the language of the bot (overrides Discord Client language)
11 | commands.language.changed=Your language was changed to `{0}`.
12 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_de_DE.properties:
--------------------------------------------------------------------------------
1 | gdpr.description=Um den Bot in deiner Sprache zur Verfügung stellen zu können, wird diese gespeichert
2 | gdpr.name=Sprache
3 | commands.language.arguments.language.name=sprache
4 | commands.language.arguments.language.description=Die Sprache die du benutzen willst
5 | commands.language.arguments.language.german=Deutsch
6 | commands.language.arguments.language.english=Englisch (English)
7 | commands.language.arguments.language.italian=Italienisch
8 | commands.language.arguments.language.french=Französisch
9 | commands.language.description=Ändert die Sprache des Bots (überschreibt Discord Sprache)
10 | commands.language.changed=Deine Sprache wurde zu `{0}` gewechselt.
11 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_fr.properties:
--------------------------------------------------------------------------------
1 | gdpr.description=Afin de fournir le Bot dans votre langue, il enregistrera la langue s\u00E9lectionn\u00E9e
2 | gdpr.name=Langue
3 | commands.language.arguments.language.name=langue
4 | commands.language.arguments.language.description=La langue que vous voulez utiliser
5 | commands.language.arguments.language.german=Allemand
6 | commands.language.arguments.language.english=Anglais (English)
7 | commands.language.arguments.language.italian=Italien
8 | commands.language.description=Change la langue du bot (remplace la langue du client Discord)
9 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_it_IT.properties:
--------------------------------------------------------------------------------
1 | gdpr.description=Per provvedere il Bot nella tua lingua, memorizzer\u00E0 la lingua selezionata
2 | gdpr.name=Lingua
3 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_nb_NO.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/core/database-i18n/src/main/resources/translations/database-i18n/strings_nb_NO.properties
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_pl.properties:
--------------------------------------------------------------------------------
1 | commands.language.arguments.language.english=Angielski
2 | commands.language.arguments.language.italian=W\u0142oski
3 | commands.language.description=Zmienia j\u0119zyk bota (zast\u0119puje j\u0119zyk klienta Discorda)
4 | gdpr.name=J\u0119zyk
5 | gdpr.description=Aby bot m\u00F3g\u0142 si\u0119 porozumiewa\u0107 w Twoim j\u0119zyku, b\u0119dzie przechowywa\u0107 wyb\u00F3r j\u0119zyka
6 | commands.language.arguments.language.description=J\u0119zyk, kt\u00F3rego chcesz u\u017Cywa\u0107
7 | commands.language.arguments.language.name=j\u0119zyk
8 | commands.language.arguments.language.german=Niemiecki
9 |
--------------------------------------------------------------------------------
/core/database-i18n/src/main/resources/translations/database-i18n/strings_vi.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/core/database-i18n/src/main/resources/translations/database-i18n/strings_vi.properties
--------------------------------------------------------------------------------
/core/game-animator/README.md:
--------------------------------------------------------------------------------
1 | # game-animator
2 | Plugin changing the bots presence every 30 seconds
3 |
4 | ## Configuration
5 | This plugin adds the following new env variables.
6 | ```
7 | GAMES=p: some funny games,w: unfunny funny compilations on YouTube,l: to silence,p: lästert über aktuelle Musik,p: lästert über aktuelle Musik,p: Würde lieber Justin Bieber hören,p: Würde lieber Justin Bieber hören
8 | ```
9 |
10 | ### Game structure
11 | `: `
12 |
13 | #### Game types
14 | - `p: ` -> Playing
15 | - `l: ` -> Listening
16 | - `s: ` -> Streaming
17 |
--------------------------------------------------------------------------------
/core/game-animator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | org.jetbrains.kotlin.jvm
4 | com.google.devtools.ksp
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | group = "dev.schlaubi.mikbot"
9 | version = mikbotVersion
10 |
11 | mikbotPlugin {
12 | description = "Plugin changing the bots presence every 30 seconds"
13 | }
14 |
--------------------------------------------------------------------------------
/core/game-animator/src/main/kotlin/Config.kt:
--------------------------------------------------------------------------------
1 | /* ktlint-disable package-name */
2 | package dev.schlaubi.mikbot.core.game_animator
3 |
4 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
5 |
6 | object Config : EnvironmentConfig("") {
7 | val GAMES by getEnv(emptyList()) { it.split(",").map(Game.Companion::parse) }
8 | }
9 |
--------------------------------------------------------------------------------
/core/game-animator/src/main/kotlin/api/GameAnimatorExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | /* ktlint-disable package-name */
2 | package dev.schlaubi.mikbot.core.game_animator.api
3 |
4 | import org.pf4j.ExtensionPoint
5 |
6 | /**
7 | * Extension point for game animator plugin.
8 | */
9 | interface GameAnimatorExtensionPoint : ExtensionPoint {
10 | /**
11 | * Replace variables in a game before sending it to Discord.
12 | */
13 | suspend fun String.replaceVariables(): String
14 | }
15 |
--------------------------------------------------------------------------------
/core/gdpr/.test-env:
--------------------------------------------------------------------------------
1 | MONGO_URL=mongodb://bot:bot@localhost
2 |
--------------------------------------------------------------------------------
/core/gdpr/README.md:
--------------------------------------------------------------------------------
1 | # GDPR
2 |
3 | Plugin adding functionality to comply with the [GDPR](https://gdpr.eu/)
4 |
5 | ## API
6 |
7 | This plugin provides an API for other plugins, so they can comply with the GDPR more easily.
8 |
9 | The API consists of so called `DataPoints`, each data point represents one type of data collected by the providing
10 | plugin. Depending on what type of data point it is, it needs to provide methods of requesting and deleting the data.
11 |
12 | ### Example Extension
13 |
14 | In order to register your data points, please refer to the `GDPRExtensionPoint` and
15 | the [PF4J Documentation](https://pf4j.org/doc/extensions.html)
16 |
17 | ```kotlin
18 | @Extension
19 | class MyGDPRExtension : GDPRExtensionPoint {
20 | override fun provideDataPoints(): List = listOf(dataPoint1, dataPoint2, dataPoint3)
21 | }
22 | ```
23 |
24 | For an example data point, you can check
25 | the [database-i18n plugin](https://github.com/DRSchlaubi/mikmusic/blob/3dc82da7ef5dca15c6e75268cae2935cad52f3f7/core/database-i18n/src/main/kotlin/dev/schlaubi/mikbot/core/i18n/database/gdpr/GDPR.kt#L17-L26)
26 |
27 | You can also refer to the [DataPoint.kt file](https://github.com/DRSchlaubi/mikbot/blob/main/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/api/DataPoint.kt)
28 | to learn more about data points and their different types
29 |
--------------------------------------------------------------------------------
/core/gdpr/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | com.google.devtools.ksp
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | group = "dev.schlaubi.mikbot"
9 | version = mikbotVersion
10 |
11 | mikbotPlugin {
12 | description = "Plugin adding functionality to comply with the GDPR"
13 | }
14 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/CoreDataPoints.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr
2 |
3 | import dev.kord.core.entity.User
4 | import dev.schlaubi.mikbot.core.gdpr.api.AnonymizedData
5 | import dev.schlaubi.mikbot.core.gdpr.api.PermanentlyStoredDataPoint
6 | import dev.schlaubi.mikbot.translations.GdprTranslations
7 |
8 | val SentryDataPoint = AnonymizedData(
9 | GdprTranslations.Gdpr.Sentry.description,
10 | GdprTranslations.Gdpr.Sentry.Sharing.description,
11 | )
12 |
13 | object UserIdDataPoint : PermanentlyStoredDataPoint(
14 | GdprTranslations.Gdpr.Userid.name,
15 | GdprTranslations.Gdpr.Userid.description,
16 | ) {
17 | override suspend fun deleteFor(user: User) {
18 | // not required, as this data point itself doesn't store anything
19 | // and just exists for descriptive purposes
20 | }
21 |
22 | override suspend fun requestFor(user: User): List = listOf(user.id.toString())
23 | }
24 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/DeleteCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.confirmation
4 | import dev.schlaubi.mikbot.plugin.api.util.translate
5 | import dev.schlaubi.mikbot.translations.GdprTranslations
6 |
7 | fun GDPRModule.deleteCommand() = ephemeralSubCommand {
8 | name = GdprTranslations.Commands.Gdpr.Delete.name
9 | description = GdprTranslations.Commands.Gdpr.Delete.description
10 |
11 | action {
12 | val (confirmed) = confirmation {
13 | content = translate(GdprTranslations.Commands.Gdpr.Delete.confirm)
14 | }
15 |
16 | if (!confirmed) {
17 | return@action
18 | }
19 |
20 | val discordUser = user.asUser()
21 |
22 | interactiveDataPoints.forEach {
23 | it.deleteFor(discordUser)
24 | }
25 |
26 | respond {
27 | content = translate(GdprTranslations.Commands.Gdpr.Delete.success)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/GDPRModule.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr
2 |
3 | import dev.kordex.core.commands.application.slash.SlashCommand
4 | import dev.schlaubi.mikbot.core.gdpr.api.DataPoint
5 | import dev.schlaubi.mikbot.core.gdpr.api.GDPRExtensionPoint
6 | import dev.schlaubi.mikbot.core.gdpr.api.PermanentlyStoredDataPoint
7 | import dev.schlaubi.mikbot.plugin.api.PluginContext
8 | import dev.schlaubi.mikbot.plugin.api.getExtensions
9 | import dev.schlaubi.mikbot.plugin.api.module.SubCommandModule
10 | import dev.schlaubi.mikbot.plugin.api.util.executableEverywhere
11 | import dev.schlaubi.mikbot.translations.GdprTranslations
12 |
13 | class GDPRModule(context: PluginContext) : SubCommandModule(context) {
14 | override val name: String = "gdpr"
15 | override val commandName = GdprTranslations.Commands.Gdpr.name
16 |
17 | val dataPoints: List =
18 | context.pluginSystem.getExtensions()
19 | .flatMap(GDPRExtensionPoint::provideDataPoints) + listOf(
20 | UserIdDataPoint,
21 | SentryDataPoint
22 | )
23 |
24 | val interactiveDataPoints = dataPoints.filterIsInstance()
25 |
26 | override fun SlashCommand<*, *, *>.commandSettings() {
27 | executableEverywhere()
28 | }
29 |
30 | override suspend fun overrideSetup() {
31 | infoCommand()
32 | requestCommand()
33 | deleteCommand()
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/GDPRPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr
2 |
3 | import dev.kordex.core.builders.ExtensionsBuilder
4 | import dev.schlaubi.mikbot.plugin.api.Plugin
5 | import dev.schlaubi.mikbot.plugin.api.PluginContext
6 | import dev.schlaubi.mikbot.plugin.api.PluginMain
7 |
8 | @PluginMain
9 | class GDPRPlugin(wrapper: PluginContext) : Plugin(wrapper) {
10 | override fun ExtensionsBuilder.addExtensions() {
11 | add(::GDPRModule)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/api/DataPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr.api
2 |
3 | import dev.kord.core.entity.User
4 | import dev.kordex.core.i18n.types.Key
5 |
6 | /**
7 | * Top-level representation of a type of personal data, that is being collected.
8 | *
9 | * @property module the i18n key of the module providing this data point
10 | * @property descriptionKey i18n key describing which data is collected and why
11 | * @property sharingDescriptionKey optional description key describing how and why this data is shared.
12 | * `null` mean this data isn't shared.
13 | */
14 | sealed class DataPoint {
15 | abstract val descriptionKey: Key
16 | abstract val sharingDescriptionKey: Key?
17 | }
18 |
19 | /**
20 | * Abstract data point of data which is permanently stored within the bots own database (e.g. settings).
21 | *
22 | * @property displayNameKey i18n key for the name display in /gdpr request
23 | */
24 | abstract class PermanentlyStoredDataPoint(
25 | val displayNameKey: Key,
26 | override val descriptionKey: Key,
27 | override val sharingDescriptionKey: Key? = null
28 | ) : DataPoint() {
29 | /**
30 | * Deletes data matching this [DataPoint] for [user].
31 | */
32 | abstract suspend fun deleteFor(user: User)
33 |
34 | /**
35 | * Requests a List of string representations of all data sets matching this [DataPoint] for [user].
36 | */
37 | abstract suspend fun requestFor(user: User): List
38 | }
39 |
40 | /**
41 | * Data which is stores anonymized.
42 | */
43 | class AnonymizedData(
44 | override val descriptionKey: Key,
45 | override val sharingDescriptionKey: Key?
46 | ) : DataPoint()
47 |
48 | /**
49 | * Data which is only stored in memory for processing reasons and deleted immediately after it was needed.
50 | */
51 | class ProcessedData(
52 | override val descriptionKey: Key,
53 | override val sharingDescriptionKey: Key?
54 | ) : DataPoint()
55 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/kotlin/dev/schlaubi/mikbot/core/gdpr/api/GDPRExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.gdpr.api
2 |
3 | import org.pf4j.ExtensionPoint
4 |
5 | /**
6 | * Extension point for GDPR functionalities.
7 | */
8 | interface GDPRExtensionPoint : ExtensionPoint {
9 | /**
10 | * Provides the [DataPoints][DataPoint] of this module.
11 | */
12 | fun provideDataPoints(): List
13 | }
14 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/resources/translations/gdpr/strings_it_IT.properties:
--------------------------------------------------------------------------------
1 | commands.gdpr.info.title=MikMusic Elaborazione dei dati & politica sulla privacy
2 | commands.gdpr.info.data_processing=Dati elaborati
3 | commands.gdpr.delete.confirm=Questo canceller\u00E0 tutte le tue impostazioni del bot, le playlist e le statistiche di gioco. Vuoi procedere?
4 | commands.gdpr.delete.success=I tuoi dati sono stati cancellati con successo.
5 | commands.gdpr.request.title=Dati memorizzati in modo persistente per: {0}
6 | commands.gdpr.request.id=ID utente
7 | gdpr.sentry.sharing.description=Questi dati saranno condivisi con il nostro fornitore di monitoraggio degli errori [Sentry](https://sentry.io/privacy/)
8 | gdpr.userid.name=ID utente
9 | gdpr.userid.description=Alcune funzioni del bot potrebbero memorizzare il tuo ID utente, per collegare i dati a te.
10 | gdpr.general.data_sharing=**Condivisione dei dati**:
11 | gdpr.general.processed_data.explainer=Questi dati sono memorizzati solo fino a quando sono stati elaborati e non memorizzati in modo permanente
12 | gdpr.general.persistent_data.explainer=Questi dati sono memorizzati in modo permanente dopo essere stati raccolti durante il tuo utilizzo, puoi cancellare questi dati con `/gdpr delete` e richiederli con `/gdpr request`.
13 | commands.gdpr.info.stored_data=Dati memorizzati in modo persistente
14 | commands.gdpr.info.anonymized_data=Dati anonimizzati
15 | # GDPR
16 | gdpr.sentry.description=Questo Bot potrebbe memorizzare dati su di te per scopi di tracciamento degli errori\nQuesti dati saranno sempre privati di qualsiasi informazione personale che possa essere ricondotta a te.\nLe informazioni sull'errore possono includere:\n- Tipo di canale in cui si \u00E8 verificato l'errore\n- Permessi del bot in quel canale\n- Permessi del bot su quel server\n- Il contenuto della richiesta di Discord eventualmente fallita
17 |
--------------------------------------------------------------------------------
/core/gdpr/src/main/resources/translations/gdpr/strings_nb_NO.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/core/gdpr/src/main/resources/translations/gdpr/strings_nb_NO.properties
--------------------------------------------------------------------------------
/core/gdpr/src/main/resources/translations/gdpr/strings_vi.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/core/gdpr/src/main/resources/translations/gdpr/strings_vi.properties
--------------------------------------------------------------------------------
/core/ktor/README.md:
--------------------------------------------------------------------------------
1 | # ktor
2 |
3 | An API to have a webserver in multiple plugins on the same port powered by [Ktor](https://ktor.io)
4 |
5 | # Usage
6 |
7 | ```kotlin
8 | @Extension
9 | class ExampleServer : KtorExtensionPoint {
10 | override fun Application.apply() {
11 | // install plugins or routing here
12 | }
13 |
14 | fun StatusPagesConfig.apply() {
15 | // Add additional StatusPages plugin configuration here
16 | }
17 |
18 | fun provideSerializersModule(): SerializersModule = SerializersModule {
19 | contextual(SomeSerializer)
20 | }
21 |
22 | fun JsonBuilder.apply() {
23 | // Do anything but serializerModule configuration here use provideSerializersModule() instead
24 | }
25 | }
26 | ```
27 |
28 | The plugin bundles `ktor-resources`, so it is recommended to use Ktor's [type-safe routing](https://ktor.io/docs/type-safe-routing.html)
29 |
--------------------------------------------------------------------------------
/core/ktor/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | com.google.devtools.ksp
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | group = "dev.schlaubi"
9 | version = mikbotVersion
10 |
11 | dependencies {
12 | // Verification Server
13 | api(libs.ktor.server.netty)
14 | api(libs.ktor.server.resources)
15 | api(libs.ktor.server.status.pages)
16 | api(libs.ktor.server.cors)
17 | api(libs.ktor.server.content.negotiation)
18 | api(libs.ktor.serialization.kotlinx.json)
19 | api(libs.ktor.server.html.builder)
20 | api(libs.ktor.server.websockets)
21 | api(libs.kompendium.core)
22 | api(libs.kompendium.resources)
23 | }
24 |
25 | kotlin {
26 | compilerOptions {
27 | freeCompilerArgs.add("-opt-in=dev.schlaubi.mikbot.plugin.api.InternalAPI")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/core/ktor/src/main/kotlin/dev/schlaubi/mikbot/util_plugins/ktor/Redoc.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.util_plugins.ktor
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.response.*
5 | import io.ktor.server.routing.*
6 |
7 | fun Route.configureRedoc() {
8 | get("docs") {
9 | call.respondText(ContentType.Text.Html) {
10 | //language=HTML
11 | """
12 |
13 |
14 |
15 | Redoc
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | """.trimIndent()
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/ktor/src/main/kotlin/dev/schlaubi/mikbot/util_plugins/ktor/api/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.util_plugins.ktor.api
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 | import dev.schlaubi.mikbot.plugin.api.InternalAPI
5 | import io.ktor.http.*
6 |
7 | /**
8 | * Configuration of the Ktor web server.
9 | */
10 | object Config : EnvironmentConfig("") {
11 | /**
12 | * The port of the web server.
13 | */
14 | val WEB_SERVER_PORT by getEnv(8080) { it.toInt() }
15 |
16 | /**
17 | * The host of the web server.
18 | */
19 | val WEB_SERVER_HOST by getEnv("127.0.0.1")
20 |
21 | /**
22 | * The web server url
23 | */
24 | @InternalAPI
25 | val WEB_SERVER_URL by getEnv(Url("http://localhost:8080")) { Url(it) }
26 | }
27 |
--------------------------------------------------------------------------------
/core/ktor/src/main/kotlin/dev/schlaubi/mikbot/util_plugins/ktor/api/KtorExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.util_plugins.ktor.api
2 |
3 | import io.bkbn.kompendium.core.plugin.NotarizedApplication
4 | import io.bkbn.kompendium.json.schema.definition.JsonSchema
5 | import io.bkbn.kompendium.oas.OpenApiSpec
6 | import io.ktor.server.application.*
7 | import io.ktor.server.plugins.statuspages.*
8 | import kotlinx.serialization.json.JsonBuilder
9 | import kotlinx.serialization.modules.EmptySerializersModule
10 | import kotlinx.serialization.modules.SerializersModule
11 | import org.pf4j.ExtensionPoint
12 | import kotlin.reflect.KType
13 |
14 | /**
15 | * Ktor plugin extension point.
16 | */
17 | interface KtorExtensionPoint : ExtensionPoint {
18 | /**
19 | * Customizes the Ktor application of the bot
20 | */
21 | fun Application.apply()
22 |
23 | /**
24 | * Customizes the Ktor's StatusPages feature
25 | */
26 | fun StatusPagesConfig.apply() {}
27 |
28 | /**
29 | * Provides the serializers module for this extenion.
30 | */
31 | fun provideSerializersModule(): SerializersModule = EmptySerializersModule()
32 |
33 | /**
34 | * Add extension specific [JsonBuilder] options.
35 | */
36 | fun JsonBuilder.apply() {}
37 |
38 | /**
39 | * Provides [NotarizedApplication.Config.customTypes] for this extension.
40 | */
41 | fun provideCustomTypes(): Map = emptyMap()
42 |
43 | /**
44 | * Provides [NotarizedApplication] configuration for this extension.
45 | */
46 | fun NotarizedApplication.Config.apply() {}
47 |
48 | /**
49 | * Configures the base [OpenApiSpec].
50 | */
51 | fun OpenApiSpec.apply(): OpenApiSpec = this
52 | }
53 |
--------------------------------------------------------------------------------
/core/ktor/src/main/kotlin/dev/schlaubi/mikbot/util_plugins/ktor/api/URLUtil.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.util_plugins.ktor.api
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.resources.*
6 | import kotlin.contracts.InvocationKind
7 | import kotlin.contracts.contract
8 |
9 | /**
10 | * Builds on URL based on [Config.WEB_SERVER_URL].
11 | */
12 | inline fun buildBotUrl(urlBuilder: URLBuilder.() -> Unit): Url {
13 | contract {
14 | callsInPlace(urlBuilder, InvocationKind.EXACTLY_ONCE)
15 | }
16 |
17 | return URLBuilder(Config.WEB_SERVER_URL).apply(urlBuilder).build()
18 | }
19 |
20 | /**
21 | * Build URL of [resource].
22 | */
23 | inline fun Application.buildBotUrl(resource: T, urlBuilder: URLBuilder.() -> Unit = {}): String {
24 | contract {
25 | callsInPlace(urlBuilder, InvocationKind.EXACTLY_ONCE)
26 | }
27 |
28 | val builder = URLBuilder(Config.WEB_SERVER_URL)
29 | href(resource, builder.apply(urlBuilder))
30 |
31 | return builder.buildString()
32 | }
33 |
--------------------------------------------------------------------------------
/core/kubernetes/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=$TARGETOS/$TARGETARCH eclipse-temurin:22-jre-alpine
2 |
3 | WORKDIR /usr/app
4 | COPY build/install/bot-kubernetes .
5 |
6 | ENTRYPOINT ["/usr/app/bin/mikmusic"]
7 |
--------------------------------------------------------------------------------
/core/kubernetes/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | alias(libs.plugins.kotlinx.serialization)
5 | com.google.devtools.ksp
6 | dev.schlaubi.mikbot.`gradle-plugin`
7 | `jvm-test-suite`
8 | }
9 |
10 | group = "dev.schlaubi.mikbot"
11 | version = mikbotVersion
12 |
13 | repositories {
14 | maven("https://jitpack.io")
15 | }
16 |
17 | dependencies {
18 | optionalPlugin(projects.core.redeployHook)
19 | implementation(libs.ktor.server.netty)
20 | implementation(libs.ktor.server.resources)
21 |
22 | implementation(libs.kubernetes.client)
23 | implementation(libs.kotlin.jsonpatch)
24 | implementation(libs.bucket4j)
25 | implementation(libs.lettuce.core)
26 |
27 | testImplementation(kotlin("test-junit5"))
28 | testImplementation(projects.api)
29 | testImplementation(libs.kord.core)
30 | }
31 |
32 | testing {
33 | suites {
34 | @Suppress("UnstableApiUsage")
35 | named("test") {
36 | useJUnitJupiter()
37 | }
38 | }
39 | }
40 |
41 | mikbotPlugin {
42 | description = "Plugin providing an /healthz endpoint used for health checking."
43 | }
44 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 |
5 | object Config : EnvironmentConfig() {
6 | val ENABLE_SCALING by getEnv(false, String::toBooleanStrict)
7 | val POD_ID by getEnv(transform = String::toInt)
8 | val SHARDS_PER_POD by getEnv(2, String::toInt)
9 | val TOTAL_SHARDS by getEnv(transform = String::toInt)
10 | val KUBERNETES_PORT by getEnv(8081, String::toInt)
11 | val STATEFUL_SET_NAME by this
12 | val NAMESPACE by getEnv("default")
13 | val CONTAINER_NAME by this
14 | val REDIS_URL by this
15 | }
16 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/KubernetesAPIServer.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health
2 |
3 | import dev.schlaubi.mikbot.core.health.check.HealthCheck
4 | import dev.schlaubi.mikbot.core.health.routes.HealthRoutes
5 | import dev.schlaubi.mikbot.core.redeploy_hook.api.RedeployExtensionPoint
6 | import dev.schlaubi.mikbot.plugin.api.PluginContext
7 | import dev.schlaubi.mikbot.plugin.api.getExtensions
8 | import io.ktor.http.*
9 | import io.ktor.server.application.*
10 | import io.ktor.server.engine.*
11 | import io.ktor.server.netty.*
12 | import io.ktor.server.resources.*
13 | import io.ktor.server.response.*
14 | import io.ktor.server.routing.*
15 | import mu.KotlinLogging
16 |
17 | fun startServer(checks: List, context: PluginContext) =
18 | embeddedServer(Netty, Config.KUBERNETES_PORT) {
19 | install(Resources)
20 |
21 | routing {
22 | get {
23 | if (checks.all { it.isSuccessful() }) {
24 | call.respond(HttpStatusCode.OK)
25 | } else {
26 | call.respond(HttpStatusCode.InternalServerError)
27 | }
28 | }
29 |
30 | if (context.pluginWrapper.pluginManager.getPlugin("redeploy-hook") != null) {
31 | val redeployHooks = context.pluginSystem.getExtensions()
32 | get {
33 | redeployHooks.forEach { it.beforeRedeploy() }
34 | }
35 | }
36 | }
37 | }.start(wait = false)
38 |
39 | private val logger = KotlinLogging.logger {}
40 |
41 | private suspend inline fun HealthCheck.isSuccessful(): Boolean {
42 | logger.debug { "Running health check ${this::class.qualifiedName}" }
43 | return checkHealth()
44 | }
45 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/ShardCalculator.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health
2 |
3 | import dev.kord.gateway.builder.Shards
4 | import io.github.oshai.kotlinlogging.KotlinLogging
5 |
6 | private val LOG = KotlinLogging.logger { }
7 |
8 | fun calculateShards(shardsPerPod: Int = Config.SHARDS_PER_POD, totalShards: Int = Config.TOTAL_SHARDS, podId: Int = Config.POD_ID): Shards {
9 | val firstShard = shardsPerPod * podId
10 | val lastShard = (firstShard + (shardsPerPod - 1)).coerceAtMost(totalShards - 1)
11 |
12 | LOG.debug { "Determined shards for $podId ($firstShard..$lastShard)" }
13 | return Shards(totalShards, firstShard..lastShard)
14 | }
15 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/check/DatabaseHealthCheck.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health.check
2 |
3 | import com.mongodb.ReadPreference
4 | import dev.kordex.core.koin.KordExKoinComponent
5 | import dev.schlaubi.mikbot.plugin.api.config.Config
6 | import dev.schlaubi.mikbot.plugin.api.util.IKnowWhatIAmDoing
7 | import dev.schlaubi.mikbot.plugin.api.util.database
8 | import org.pf4j.Extension
9 |
10 | @Extension
11 | class DatabaseHealthCheck : HealthCheck, KordExKoinComponent {
12 | @OptIn(IKnowWhatIAmDoing::class)
13 | override suspend fun checkHealth(): Boolean {
14 | if (Config.MONGO_DATABASE != null && Config.MONGO_URL != null) {
15 | val cluster = database.client.client.clusterDescription
16 | return cluster.hasReadableServer(ReadPreference.nearest())
17 | && cluster.hasWritableServer()
18 | }
19 | return true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/check/HealthCheck.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health.check
2 |
3 | import dev.kordex.core.builders.ExtensionsBuilder
4 | import org.pf4j.ExtensionPoint
5 |
6 | interface HealthCheck : ExtensionPoint {
7 | /**
8 | * Runs the health check.
9 | *
10 | * @return if the health check succeeded
11 | */
12 | suspend fun checkHealth(): Boolean
13 |
14 | /**
15 | * Register an optional extension.
16 | */
17 | fun ExtensionsBuilder.addExtensions() = Unit
18 | }
19 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/check/KordHealthCheck.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health.check
2 |
3 | import dev.kord.core.Kord
4 | import dev.kordex.core.builders.ExtensionsBuilder
5 | import dev.kordex.core.extensions.event
6 | import dev.kordex.core.koin.KordExKoinComponent
7 | import dev.schlaubi.mikbot.plugin.api.util.AllShardsReadyEvent
8 | import io.github.oshai.kotlinlogging.KotlinLogging
9 | import kotlinx.coroutines.isActive
10 | import org.koin.core.component.inject
11 | import org.pf4j.Extension
12 | import dev.kordex.core.extensions.Extension as KordExtension
13 |
14 | private val LOG = KotlinLogging.logger { }
15 |
16 | /**
17 | * Whether the node is ready.
18 | */
19 | internal var ready = false
20 | private set
21 |
22 | @Extension
23 | class KordHealthCheck : HealthCheck, KordExKoinComponent {
24 |
25 | private val kord by inject()
26 |
27 | override suspend fun checkHealth(): Boolean =
28 | ready && kord.gateway.gateways.all { it.value.isActive } && kord.isActive
29 |
30 | override fun ExtensionsBuilder.addExtensions() {
31 | add(::ShardMonitor)
32 | }
33 | }
34 |
35 | private class ShardMonitor() : KordExtension() {
36 | override val name: String = "Shard monitor"
37 |
38 | override suspend fun setup() {
39 | event {
40 | action {
41 | LOG.debug { "All shards are ready, returning 200 on health checks from now on" }
42 | ready = true
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/ratelimit/Setup.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health.ratelimit
2 |
3 | import dev.kord.core.builder.kord.KordBuilder
4 | import dev.kord.rest.request.KtorRequestHandler
5 | import dev.schlaubi.mikbot.core.health.Config
6 | import io.github.bucket4j.redis.lettuce.Bucket4jLettuce
7 | import io.lettuce.core.RedisClient
8 | import io.lettuce.core.codec.ByteArrayCodec
9 | import io.lettuce.core.codec.RedisCodec
10 | import io.lettuce.core.codec.StringCodec
11 |
12 | fun KordBuilder.setupDistributedRateLimiter() {
13 | val connection = RedisClient.create(Config.REDIS_URL)
14 | .connect(RedisCodec.of(StringCodec.UTF8, ByteArrayCodec.INSTANCE))
15 | val proxyManager = Bucket4jLettuce
16 | .casBasedBuilder(connection).build()
17 |
18 | val rateLimiter = DistributedRateLimiter(proxyManager)
19 |
20 | requestHandler {
21 | KtorRequestHandler(it.token, rateLimiter)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/kotlin/dev/schlaubi/mikbot/core/health/routes/HealthRoutes.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.health.routes
2 |
3 | import io.ktor.resources.*
4 | import kotlinx.serialization.Serializable
5 |
6 | @Resource("/")
7 | class HealthRoutes {
8 | @Resource("/healthz") // this is not a typo. See https://stackoverflow.com/questions/43380939/where-does-the-convention-of-using-healthz-for-application-health-checks-come-f
9 | class Health(val health: HealthRoutes)
10 |
11 | @Resource("/kubernetes/pre-stop")
12 | @Serializable
13 | class PreStop(val parent: HealthRoutes = HealthRoutes())
14 | }
15 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/resources/translations/kubernetes/strings.properties:
--------------------------------------------------------------------------------
1 | commands.rebalance.name=rebalance
2 | commands.rebalance.description=Re-balances the bot's replicas
3 | commands.rebalance.already_balanced=The bot is already in perfect balance
4 | commands.rebalance.done=Re-balance successfully triggered
5 | commands.rebalance.arguments.force_to.name=force_to
6 | commands.rebalance.arguments.force_to.description=Forces the controller to use this count
7 |
--------------------------------------------------------------------------------
/core/kubernetes/src/main/resources/translations/kubernetes/strings_de_DE.properties:
--------------------------------------------------------------------------------
1 | commands.rebalance.name=neu-aufteilen
2 | commands.rebalance.description=Lagert die Replicas erneut aus
3 | commands.rebalance.already_balanced=Der Bot ist bereits perfekt ausgelagert
4 | commands.rebalance.done=Neu-Auslagerung ausgelöst!
5 | commands.rebalance.arguments.force_to.description=Zwingt den Controller dieser Replica Zahl zu benutzen
6 | commands.rebalance.arguments.force_to.name=erzwingen_auf
7 |
--------------------------------------------------------------------------------
/core/redeploy-hook/README.md:
--------------------------------------------------------------------------------
1 | # redeploy-hook
2 | Plugin adding a /redeploy command, backed by a webhook
3 |
4 | ## Configuration
5 | This plugin adds the following new env variables.
6 | ```shell
7 | REDEPLOY_HOST=<>
8 | REDEPLOY_TOKEN=<>
9 | ```
10 |
11 | ### Setup
12 |
13 | Section inspired by [Devcordbot](https://github.com/devcordde/DevcordBot)
14 |
15 | Service installation: https://github.com/adnanh/webhook#installation
16 |
17 | Env vars `REDEPLOY_HOST`,`REDEPLOY_TOKEN`,`OWNER_GUILD` and `BOT_OWNERS` need to be set
18 |
19 | hooks.json
20 |
21 | ```json
22 | {
23 | "id": "redeploy-mikmusic",
24 | "execute-command": "/usr/bin/sh",
25 | "pass-arguments-to-command": [
26 | {
27 | "source": "string",
28 | "name": "/path/to/mikmusic/redeploy.sh"
29 | }
30 | ],
31 | "command-working-directory": "/path/to/mikmusic",
32 | "trigger-rule": {
33 | "match": {
34 | "type": "value",
35 | "value": "YOUR_SECRET_TOKEN",
36 | "parameter": {
37 | "source": "header",
38 | "name": "Redeploy-Token"
39 | }
40 | }
41 | }
42 | }
43 | ```
44 |
45 | redeploy.sh
46 |
47 | ```shell
48 | #!/usr/bin/env sh
49 | docker-compose pull && docker-compose up -d
50 | ```
51 |
--------------------------------------------------------------------------------
/core/redeploy-hook/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | com.google.devtools.ksp
4 | dev.schlaubi.mikbot.`gradle-plugin`
5 | }
6 |
7 | group = "dev.schlaubi.mikbot"
8 | version = mikbotVersion
9 |
10 | mikbotPlugin {
11 | description = "Plugin adding a /redeploy command, backed by a webhook"
12 | }
13 |
--------------------------------------------------------------------------------
/core/redeploy-hook/src/main/kotlin/dev/schlaubi/mikbot/core/redeploy_hook/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.redeploy_hook
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 |
5 | object Config : EnvironmentConfig("") {
6 | val REDEPLOY_HOST by getEnv().optional()
7 | val REDEPLOY_TOKEN by getEnv().optional()
8 | }
9 |
--------------------------------------------------------------------------------
/core/redeploy-hook/src/main/kotlin/dev/schlaubi/mikbot/core/redeploy_hook/RedeployHookPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.redeploy_hook
2 |
3 | import dev.schlaubi.mikbot.plugin.api.Plugin
4 | import dev.schlaubi.mikbot.plugin.api.PluginContext
5 | import dev.schlaubi.mikbot.plugin.api.PluginMain
6 | import dev.schlaubi.mikbot.plugin.api.owner.OwnerExtensionPoint
7 | import dev.schlaubi.mikbot.plugin.api.owner.OwnerModule
8 | import org.pf4j.Extension
9 |
10 | @PluginMain
11 | class RedeployHookPlugin(wrapper: PluginContext) : Plugin(wrapper)
12 |
13 | @Extension
14 | class RedeployHookOwnerExtension : OwnerExtensionPoint {
15 | override suspend fun OwnerModule.apply() {
16 | redeployCommand()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/core/redeploy-hook/src/main/kotlin/dev/schlaubi/mikbot/core/redeploy_hook/api/RedeployExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.core.redeploy_hook.api
2 |
3 | import org.pf4j.ExtensionPoint
4 |
5 | interface RedeployExtensionPoint : ExtensionPoint {
6 | suspend fun beforeRedeploy()
7 | }
8 |
--------------------------------------------------------------------------------
/core/redeploy-hook/src/main/resources/translations/redeploy-hook/strings.properties:
--------------------------------------------------------------------------------
1 | commands.redeploy.name=redeploy
2 | commands.redeploy.description=Redeploys the bot
3 | commands.redeploy.success=Bot is going to restart now
4 | commands.redeploy.not_satisfied=U suck
5 |
--------------------------------------------------------------------------------
/core/redeploy-hook/src/main/resources/translations/redeploy-hook/strings_de_DE.properties:
--------------------------------------------------------------------------------
1 | commands.redeploy.name=neustart
2 | commands.redeploy.description=Startet den Bot neu
3 | commands.redeploy.success=Bot wird jetzt redeployed
4 | commands.redeploy.not_satisfied=Du sockst (U suck)
5 |
--------------------------------------------------------------------------------
/dev.docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | mongo:
3 | image: mongo
4 | environment:
5 | MONGO_INITDB_ROOT_USERNAME: bot
6 | MONGO_INITDB_ROOT_PASSWORD: bot
7 | volumes:
8 | - mongo-data:/data/db
9 | ports:
10 | - "27017:27017"
11 | image_color_service:
12 | image: ghcr.io/mikbot/image-color-service
13 | ports:
14 | - "4567:8080"
15 | volumes:
16 | mongo-data: { }
17 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | mongo:
3 | image: mongo
4 | environment:
5 | MONGO_INITDB_ROOT_USERNAME: bot
6 | MONGO_INITDB_ROOT_PASSWORD: bot
7 | volumes:
8 | - mongo-data:/data/db
9 | bot:
10 | image: ghcr.io/drschlaubi/mikmusic/bot:latest # or your own distro, see PLUGINS.md for more
11 | depends_on:
12 | - mongo
13 | volumes:
14 | - ./plugins:/usr/app/plugins
15 | ports:
16 | # only needed for verification_mode
17 | - "127.0.0.1:7725:8080"
18 | volumes:
19 | mongo-data: { }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "gradle-plugin"
2 |
3 | dependencyResolutionManagement {
4 | @Suppress("UnstableApiUsage")
5 | versionCatalogs {
6 | create("libs") {
7 | from(files("../gradle/libs.versions.toml"))
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/dev/schlaubi/mikbot/gradle/BuildRepositoryExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.gradle
2 |
3 | import org.gradle.api.file.DirectoryProperty
4 | import org.gradle.api.provider.Property
5 | import org.gradle.api.tasks.Input
6 | import org.gradle.api.tasks.InputDirectory
7 |
8 | abstract class BuildRepositoryExtension {
9 | /**
10 | * The directory to save the repository to.
11 | */
12 | @get:InputDirectory
13 | abstract val targetDirectory: DirectoryProperty
14 |
15 | /**
16 | * Directory representing the current repository content (defaults to [targetDirectory]).
17 | */
18 | @get:InputDirectory
19 | abstract val currentRepository: DirectoryProperty
20 |
21 | /**
22 | * The URL were the repository is hosted (used for URLs in plugins.json).
23 | */
24 | @get:Input
25 | abstract val repositoryUrl: Property
26 |
27 | /**
28 | * The URL of this project.
29 | */
30 | @get:Input
31 | abstract val projectUrl: Property
32 | }
33 |
34 |
35 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/dev/schlaubi/mikbot/gradle/InstallPluginsToTestBotTask.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.gradle
2 |
3 | import dev.schlaubi.mikbot.gradle.extension.pluginId
4 | import org.gradle.api.DefaultTask
5 | import org.gradle.api.file.Directory
6 | import org.gradle.api.file.FileSystemOperations
7 | import org.gradle.api.file.RegularFileProperty
8 | import org.gradle.api.provider.Provider
9 | import org.gradle.api.tasks.InputFile
10 | import org.gradle.api.tasks.OutputDirectory
11 | import org.gradle.api.tasks.TaskAction
12 | import javax.inject.Inject
13 |
14 | abstract class InstallPluginsToTestBotTask : DefaultTask() {
15 |
16 | @get:InputFile
17 | abstract val pluginArchive: RegularFileProperty
18 |
19 | @get:OutputDirectory
20 | val outputDirectory: Provider = project.layout.buildDirectory.dir("test-bot/plugins")
21 |
22 | @get:Inject
23 | abstract val fs: FileSystemOperations
24 |
25 | @TaskAction
26 | fun install() {
27 | val result = fs.copy {
28 | from(pluginArchive)
29 | into(outputDirectory)
30 | rename { "plugin-${project.pluginId}.zip" }
31 | }
32 |
33 | didWork = result.didWork
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/dev/schlaubi/mikbot/gradle/LicenseChecker.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.gradle
2 |
3 | import com.github.jk1.license.LicenseReportExtension
4 | import com.github.jk1.license.LicenseReportPlugin
5 | import com.github.jk1.license.render.JsonReportRenderer
6 | import org.gradle.api.Project
7 | import org.gradle.api.tasks.SourceSetContainer
8 | import org.gradle.kotlin.dsl.*
9 |
10 | fun Project.configureLicenseChecker() {
11 | apply()
12 | val output = layout.buildDirectory.dir("generated/mikbot")
13 |
14 | configure {
15 | outputDir = output.get().asFile.absolutePath
16 | renderers = arrayOf(JsonReportRenderer("license-report.json", true))
17 | configurations = arrayOf("runtimeClasspath")
18 | }
19 |
20 | the().named("main") {
21 | resources.srcDir(output)
22 | }
23 |
24 | with(tasks) {
25 | named("processResources") {
26 | dependsOn("generateLicenseReport")
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/dev/schlaubi/mikbot/gradle/MikBotVersionExtraction.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikbot.gradle
2 |
3 | import org.gradle.api.Project
4 |
5 | // great name i know
6 | fun Project.extractMikBotVersionFromProjectApiDependency(): String = MikBotPluginInfo.VERSION
7 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/dev/schlaubi/mikbot/gradle/PluginInfo.kt:
--------------------------------------------------------------------------------
1 | // It is applied, some issues with included Gradle builds make IntelliJ think it's not
2 | package dev.schlaubi.mikbot.gradle
3 |
4 | import kotlinx.serialization.KSerializer
5 | import kotlinx.serialization.Serializable
6 | import kotlinx.serialization.descriptors.PrimitiveKind
7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.encoding.Decoder
10 | import kotlinx.serialization.encoding.Encoder
11 | import java.text.SimpleDateFormat
12 | import java.util.*
13 |
14 |
15 | @Serializable
16 | internal data class PluginInfo(
17 | val id: String,
18 | val name: String,
19 | val description: String,
20 | val projectUrl: String,
21 | val releases: List
22 | )
23 |
24 | @Serializable
25 | internal data class PluginRelease(
26 | val version: String,
27 | @Serializable(with = DateSerializer::class)
28 | val date: Date,
29 | val requires: String? = null,
30 | val url: String,
31 | val sha512sum: String? = null
32 | )
33 |
34 | internal object DateSerializer : KSerializer {
35 | private val format = SimpleDateFormat("MMM dd, yyyy, h:mm:ss a", Locale.ENGLISH)
36 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
37 | override fun deserialize(decoder: Decoder): Date = format.parse(decoder.decodeString())
38 |
39 | override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(format.format(value))
40 | }
41 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 | # Gradle: We're really fast
3 | # Also Gradle: drives with the handbreak on
4 | org.gradle.parallel=true
5 | org.gradle.caching=true
6 | # Currently waiting on
7 | # https://github.com/GoogleCloudPlatform/artifact-registry-maven-tools/issues/85
8 | # https://github.com/jk1/Gradle-License-Report/issues/255
9 | #org.gradle.configuration-cache=true
10 |
--------------------------------------------------------------------------------
/gradle/gradle-daemon-jvm.properties:
--------------------------------------------------------------------------------
1 | #This file is generated by updateDaemonJvm
2 | toolchainVersion=22
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DRSchlaubi/mikbot/d30c1849f6f6ceed028c4ce3afc418f5bcc97a2e/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/music/api/README.md:
--------------------------------------------------------------------------------
1 | # api
2 |
3 | This offers a REST API for interacting with the music module.
4 |
5 | ## Documentation
6 |
7 | Rest endpoints are documented here: https://musikus.gutikus.schlau.bi/docs
8 |
9 | ### WebSocket API
10 |
11 | A WebSocket is available to receive real time events at: `wss://host/music/events`
12 |
13 | #### Events
14 |
15 | #### Event Structure
16 |
17 | | Name | Type | Description | For type |
18 | |------------|---------------------------|---------------------------------------|------------------------------------|
19 | | `guild_id` | snowflake | the id of the guild the event is for | All |
20 | | `type` | [event type](#event-type) | the type of the event | All |
21 | | `queue` | list of [queued tracks]() | the queue of the player | `player_update` and `queue_update` |
22 | | `state` | [player state]() | the state of the player | `player_update` |
23 | | `state` | [voice_state]() | the voice state of the logged in user | `voice_state_update` |
24 |
25 | #### Event type
26 |
27 | The following event types exist:
28 |
29 | | Name | Description |
30 | |----------------------|---------------------------------------------------------------|
31 | | `player_update` | Sent when the player updates (currentTrack, volume, position) |
32 | | `queue_update` | Sent when the queue gets updated |
33 | | `voice_state_update` | Sent when the bots or the users voice state changes |
34 |
--------------------------------------------------------------------------------
/music/api/requests/http-client.env.json:
--------------------------------------------------------------------------------
1 | {
2 | "dev": {
3 | "Security": {
4 | "Auth": {
5 | "auth": {
6 | "Type": "OAuth2",
7 | "Auth URL": "http://localhost:8080/music/auth/authorize",
8 | "Grant Type": "Authorization Code",
9 | "Token URL": "http://localhost:8080/music/auth/token",
10 | "Redirect URL": "http://localhost/login.php",
11 | "Client ID": "535129406650318860",
12 | "State": "random_state_2134321321"
13 | }
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/music/api/requests/player.http:
--------------------------------------------------------------------------------
1 | GET http://localhost:8080/music/players
2 | Authorization: Bearer {{$auth.token("auth")}}
3 |
4 | ###
5 | GET http://localhost:8080/music/players/1277696743823114353
6 | Authorization: Bearer {{$auth.token("auth")}}
7 |
8 | ###
9 | GET http://localhost:8080/music/search?query=spsearch:Gathering Storm pentakill
10 | Authorization: Bearer {{$auth.token("auth")}}
11 |
12 | ###
13 |
14 | PATCH http://localhost:8080/music/players/809471441719787602
15 | Authorization: Bearer {{$auth.token("auth")}}
16 | Content-Type: application/json
17 |
18 | {
19 | "channel": "1199813828913930250",
20 | "track": "QAABzgMAD0dhdGhlcmluZyBTdG9ybQAJUGVudGFraWxsAAAAAAAEOpoAFjJlS2FiN0Z3NWxDVm5WTTRHMTZONkIAAQA1aHR0cHM6Ly9vcGVuLnNwb3RpZnkuY29tL3RyYWNrLzJlS2FiN0Z3NWxDVm5WTTRHMTZONkIBAEBodHRwczovL2kuc2Nkbi5jby9pbWFnZS9hYjY3NjE2ZDAwMDBiMjczMjcxZGI4MmRjOGFiMzZhNThlZGIwNGQ3AQAMUVpINlMxOTAwMzU0AAdzcG90aWZ5AQARSUlJOiBMb3N0IENoYXB0ZXIBADVodHRwczovL29wZW4uc3BvdGlmeS5jb20vYWxidW0vNmpIc25ZcVNtaU1xQnZ6Rzhxcll1RQEANmh0dHBzOi8vb3Blbi5zcG90aWZ5LmNvbS9hcnRpc3QvMnFjR1RCNXMydDlvMnc5U3JJNzE5cwABAGtodHRwczovL3Auc2Nkbi5jby9tcDMtcHJldmlldy9jYTIwNWEzYjU3NTg3YjllYWVmMDI3YWIyODU1MzQ0ZDJjZTlhM2MyP2NpZD1mNmE0MDc3NjU4MDk0M2E3YmM1MTczMTI1YTFlODgzMgAAAAAAAAAAAA"
21 | }
22 |
23 | ###
24 | DELETE http://localhost:8080/music/players/809471441719787602
25 | Authorization: Bearer {{$auth.token("auth")}}
26 |
27 | ###
28 |
29 | WEBSOCKET ws://localhost:8080/music/events?api_key={{$auth.token("auth")}}
30 |
--------------------------------------------------------------------------------
/music/api/server/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 |
4 | com.google.devtools.ksp
5 | alias(libs.plugins.kotlinx.serialization)
6 | dev.schlaubi.mikbot.`gradle-plugin`
7 | }
8 |
9 | dependencies {
10 | plugin(projects.core.ktor)
11 | plugin(projects.music.player)
12 |
13 | ktorDependency(libs.ktor.server.auth.jwt)
14 | }
15 |
16 | mikbotPlugin {
17 | pluginId = "music-api"
18 | description = "Adds a rest API for player functionality"
19 | }
20 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 |
5 | object Config : EnvironmentConfig() {
6 | val DISCORD_CLIENT_ID by this
7 | val DISCORD_CLIENT_SECRET by this
8 | val JWT_SECRET by getEnv("spukyscaryskeletons")
9 | }
10 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/ErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api
2 |
3 | import com.auth0.jwt.exceptions.JWTVerificationException
4 | import dev.schlaubi.mikmusic.api.types.UnexclusiveSchedulingOptions
5 | import io.ktor.http.*
6 | import io.ktor.server.auth.*
7 | import io.ktor.server.plugins.statuspages.*
8 | import io.ktor.server.response.*
9 |
10 | fun StatusPagesConfig.installErrorHandler() {
11 | exception { call, cause ->
12 | call.respond(HttpStatusCode.BadRequest, cause.errorCode ?: cause.message ?: "")
13 | }
14 |
15 | exception { call, _ ->
16 | call.respond(HttpStatusCode.Forbidden)
17 | }
18 |
19 | exception { call, _ ->
20 | call.respond(HttpStatusCode.BadRequest, "Scheduling options need to be exclusive")
21 | }
22 |
23 | exception { call, _ ->
24 | call.respond(HttpStatusCode.Unauthorized)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/authentication/Provider.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.authentication
2 |
3 | import com.auth0.jwt.JWT
4 | import com.auth0.jwt.algorithms.Algorithm
5 | import dev.kord.common.entity.Snowflake
6 | import dev.schlaubi.mikbot.util_plugins.ktor.api.buildBotUrl
7 | import dev.schlaubi.mikmusic.api.Config
8 | import io.ktor.server.application.*
9 | import io.ktor.server.auth.*
10 | import io.ktor.server.auth.jwt.*
11 | import io.ktor.server.routing.*
12 |
13 | private const val MUSIC_API_AUTH = "music_api_auth"
14 |
15 | val ApplicationCall.authenticatedUser: DiscordUserPrincipal
16 | get() = principal() ?: error("Request not authenticated")
17 |
18 | data class DiscordUserPrincipal(val userId: Snowflake, val discordToken: String)
19 |
20 | fun Route.withAuthentication(build: Route.() -> Unit) = authenticate(MUSIC_API_AUTH, build = build)
21 |
22 | val accessTokenVerifier = JWT.require(Algorithm.HMAC256(Config.JWT_SECRET))
23 | .withIssuer(buildBotUrl { }.toString())
24 | .withAudience(Config.DISCORD_CLIENT_ID)
25 | .withClaimPresence("discord_token")
26 | .build()
27 |
28 | fun Application.authentication() {
29 | val plugin = pluginOrNull(Authentication) ?: install(Authentication)
30 | plugin.configure {
31 | jwt(MUSIC_API_AUTH) {
32 | realm = MUSIC_API_AUTH
33 | verifier(accessTokenVerifier)
34 |
35 | validate {
36 | DiscordUserPrincipal(Snowflake(it.payload.subject), it.payload.getClaim("discord_token").asString())
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/authentication/Utils.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.authentication
2 |
3 | import com.auth0.jwt.JWT
4 | import com.auth0.jwt.JWTCreator
5 | import com.auth0.jwt.algorithms.Algorithm
6 | import dev.kord.common.entity.DiscordUser
7 | import dev.kord.common.entity.Snowflake
8 | import dev.schlaubi.mikbot.util_plugins.ktor.api.buildBotUrl
9 | import dev.schlaubi.mikmusic.api.Config
10 | import kotlinx.serialization.Serializable
11 | import java.time.Duration
12 | import java.time.Instant
13 |
14 | @Serializable
15 | data class DiscordAuthorization(val user: DiscordUser)
16 |
17 | fun newAccessToken(discordToken: String, userId: Snowflake, expiresIn: Long): String = newKey(userId) {
18 | withExpiresAt(Instant.now() + Duration.ofSeconds(expiresIn))
19 | withAudience(Config.DISCORD_CLIENT_ID)
20 | withClaim("discord_token", discordToken)
21 | }
22 |
23 | fun newRefreshToken(userId: Snowflake, discordRefreshToken: String): String = newKey(userId) {
24 | withClaim("discord_refresh_token", discordRefreshToken)
25 | }
26 |
27 | private fun newKey(userId: Snowflake, additionalSettings: JWTCreator.Builder.() -> Unit) = JWT.create()
28 | .withIssuer(buildBotUrl { }.toString())
29 | .withSubject(userId.toString())
30 | .apply(additionalSettings)
31 | .sign(Algorithm.HMAC256(Config.JWT_SECRET))
32 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/documentation/RouteFunctions.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.documentation
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.resources.*
5 | import io.ktor.server.routing.*
6 | import io.ktor.utils.io.*
7 |
8 | @KtorDsl
9 | inline fun Route.documentedPost(noinline body: suspend RoutingContext.(R) -> Unit) =
10 | documentedRoute(HttpMethod.Post, body)
11 |
12 | @KtorDsl
13 | inline fun Route.documentedGet(noinline body: suspend RoutingContext.(R) -> Unit) =
14 | documentedRoute(HttpMethod.Get, body)
15 |
16 | @KtorDsl
17 | inline fun Route.documentedPatch(noinline body: suspend RoutingContext.(R) -> Unit) =
18 | documentedRoute(HttpMethod.Patch, body)
19 |
20 | @KtorDsl
21 | inline fun Route.documentedDelete(noinline body: suspend RoutingContext.(R) -> Unit) =
22 | documentedRoute(HttpMethod.Delete, body)
23 |
24 | @KtorDsl
25 | inline fun Route.documentedPut(noinline body: suspend RoutingContext.(R) -> Unit) =
26 | documentedRoute(HttpMethod.Put, body)
27 |
28 | inline fun Route.documentedRoute(
29 | method: HttpMethod,
30 | noinline body: suspend RoutingContext.(R) -> Unit,
31 | ) {
32 | resource {
33 | applyDocs(method)
34 | method(method) {
35 | handle(body)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/player/ChannelRoute.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.player
2 |
3 | import dev.kord.core.entity.channel.VoiceChannel
4 | import dev.schlaubi.mikmusic.api.ForbiddenException
5 | import dev.schlaubi.mikmusic.api.authentication.authenticatedUser
6 | import dev.schlaubi.mikmusic.api.documentation.documentedGet
7 | import dev.schlaubi.mikmusic.api.types.Channel
8 | import dev.schlaubi.mikmusic.api.types.Routes
9 | import io.ktor.server.response.*
10 | import io.ktor.server.routing.*
11 | import kotlinx.coroutines.flow.filterIsInstance
12 | import kotlinx.coroutines.flow.map
13 | import kotlinx.coroutines.flow.toList
14 |
15 | fun Route.channels() {
16 | getChannels()
17 | }
18 |
19 | private fun Route.getChannels() = documentedGet { (id) ->
20 | val (userId) = call.authenticatedUser
21 |
22 | val guild = kord.getGuild(id)
23 | if (guild.getMemberOrNull(userId) == null) {
24 | throw ForbiddenException()
25 | }
26 |
27 | val channels = guild.channels
28 | .filterIsInstance()
29 | .map { Channel(it.id, it.guildId, it.name) }
30 | .toList()
31 |
32 | call.respond(channels)
33 | }
34 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/player/Guilds.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.player
2 |
3 | import dev.kord.common.entity.DiscordPartialGuild
4 | import dev.schlaubi.mikmusic.api.authentication.authenticatedUser
5 | import dev.schlaubi.mikmusic.api.httpClient
6 | import io.ktor.client.call.*
7 | import io.ktor.client.request.*
8 | import io.ktor.server.routing.*
9 |
10 | suspend fun RoutingCall.fetchGuilds(): List {
11 | val (_, token) = authenticatedUser
12 | return httpClient.get("https://discord.com/api/v10/users/@me/guilds") {
13 | bearerAuth(token)
14 | }.body>()
15 | }
16 |
--------------------------------------------------------------------------------
/music/api/server/src/main/kotlin/player/SearchRoute.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.player
2 |
3 | import dev.kordex.core.ExtensibleBot
4 | import dev.kordex.core.koin.KordExContext
5 | import dev.schlaubi.lavakord.plugins.lavasearch.rest.search
6 | import dev.schlaubi.mikbot.plugin.api.util.extension
7 | import dev.schlaubi.mikmusic.api.documentation.documentedGet
8 | import dev.schlaubi.mikmusic.api.types.Routes
9 | import dev.schlaubi.mikmusic.core.audio.LavalinkManager
10 | import io.ktor.server.response.*
11 | import io.ktor.server.routing.*
12 |
13 | fun Route.searchRoute() {
14 | val bot by KordExContext.get().inject()
15 | val lavalinkManager by bot.extension()
16 |
17 | documentedGet { (query) ->
18 | call.respond(lavalinkManager.newNode().search(query))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/music/api/types/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.*
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | alias(libs.plugins.kotlinx.serialization)
6 | `maven-publish`
7 | signing
8 | com.google.cloud.artifactregistry.`gradle-plugin`
9 | }
10 |
11 | kotlin {
12 | jvm()
13 |
14 | sourceSets {
15 | commonMain {
16 | dependencies {
17 | api(libs.kord.common)
18 | api(libs.lavalink.protocol)
19 | implementation(libs.ktor.resources)
20 | implementation(libs.kotlinx.serialization.json)
21 | api(libs.lavakord.sponsorblock)
22 | }
23 | }
24 | }
25 | }
26 |
27 | publishing {
28 | repositories {
29 | maven("artifactregistry://europe-west3-maven.pkg.dev/mik-music/mikbot") {
30 | credentials {
31 | username = "_json_key_base64"
32 | password = System.getenv("GOOGLE_KEY")?.toByteArray()?.let {
33 | Base64.getEncoder().encodeToString(it)
34 | }
35 | }
36 |
37 | authentication {
38 | create("basic")
39 | }
40 | }
41 | }
42 | }
43 |
44 | signing {
45 | val signingKey = System.getenv("SIGNING_KEY")?.toString()
46 | val signingPassword = System.getenv("SIGNING_KEY_PASSWORD")?.toString()
47 | if (signingKey != null && signingPassword != null) {
48 | useInMemoryPgpKeys(String(Base64.getDecoder().decode(signingKey)), signingPassword)
49 | publishing.publications.forEach { sign(it) }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/Authentication.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.types
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class OAuth2AccessTokenResponse(
8 | @SerialName("access_token")
9 | val accessToken: String,
10 | @SerialName("token_type")
11 | val tokenType: String,
12 | @SerialName("expires_in")
13 | val expiresIn: Long,
14 | @SerialName("refresh_token")
15 | val refreshToken: String,
16 | )
17 |
18 | @Serializable
19 | data class OAuth2TokenRequest(
20 | val code: String? = null,
21 | @SerialName("refresh_token")
22 | val refreshToken: String? = null,
23 | @SerialName("grant_type")
24 | val grantType: String,
25 | val state: String? = null,
26 | )
27 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/Documentation.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalSerializationApi::class)
2 |
3 | package dev.schlaubi.mikmusic.api.types
4 |
5 | import kotlinx.serialization.ExperimentalSerializationApi
6 | import kotlinx.serialization.SerialInfo
7 | import kotlin.reflect.KClass
8 |
9 | object EmptyBody
10 |
11 | @SerialInfo
12 | @MustBeDocumented
13 | @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
14 | annotation class Description(val value: String)
15 |
16 | @MustBeDocumented
17 | @Target()
18 | annotation class HttpVerb(
19 | val description: String,
20 | val summary: String,
21 | val response: HttpBody,
22 | val request: HttpBody = HttpBody("", status = -1),
23 | val errors: Array = [],
24 | val auth: String = "BearerAuth"
25 | ) {
26 | annotation class HttpBody(
27 | val description: String,
28 | val mediaType: String = "application/json",
29 | val body: KClass<*> = EmptyBody::class,
30 | val status: Int = 200,
31 | val typeParameters: Array> = [],
32 | )
33 | }
34 |
35 | @SerialInfo
36 | @MustBeDocumented
37 | @Target(AnnotationTarget.CLASS)
38 | annotation class Post(val descriptor: HttpVerb)
39 |
40 | @SerialInfo
41 | @MustBeDocumented
42 | @Target(AnnotationTarget.CLASS)
43 | annotation class Get(val descriptor: HttpVerb)
44 |
45 | @SerialInfo
46 | @MustBeDocumented
47 | @Target(AnnotationTarget.CLASS)
48 | annotation class Delete(val descriptor: HttpVerb)
49 |
50 | @SerialInfo
51 | @MustBeDocumented
52 | @Target(AnnotationTarget.CLASS)
53 | annotation class Patch(val descriptor: HttpVerb)
54 |
55 | @SerialInfo
56 | @MustBeDocumented
57 | @Target(AnnotationTarget.CLASS)
58 | annotation class Put(val descriptor: HttpVerb)
59 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/Events.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.types
2 |
3 | import dev.kord.common.entity.Snowflake
4 | import kotlinx.serialization.SerialName
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | sealed interface Event {
9 | val guildId: Snowflake
10 | }
11 |
12 | @Serializable
13 | @SerialName("player_update")
14 | data class PlayerUpdateEvent(
15 | val state: PlayerState,
16 | val queue: List,
17 | val time: Long,
18 | override val guildId: Snowflake,
19 | ) : Event
20 |
21 | @Serializable
22 | @SerialName("queue_update")
23 | data class QueueUpdateEvent(val queue: List, override val guildId: Snowflake) : Event
24 |
25 | @Serializable
26 | @SerialName("voice_state_update")
27 | data class VoiceStateUpdateEvent(
28 | override val guildId: Snowflake,
29 | val state: Player.VoiceState,
30 | ) : Event
31 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/Queue.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.types
2 |
3 | import dev.kord.common.entity.optional.Optional
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class QueueAddRequest(
8 | val tracks: List,
9 | val schedulerSettings: Optional = Optional.Missing(),
10 | val top: Boolean,
11 | )
12 |
13 | @Serializable
14 | data class QueueRemoveRequest(
15 | val start: Int,
16 | val end: Int? = null,
17 | )
18 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/SchedulerSettings.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.types
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | class UnexclusiveSchedulingOptions() : RuntimeException()
6 |
7 | interface SchedulingOptions {
8 | val shuffle: Boolean?
9 | val loop: Boolean?
10 | val loopQueue: Boolean?
11 | }
12 |
13 | @Serializable
14 | data class SchedulerSettings(
15 | override val loopQueue: Boolean? = null,
16 | override val loop: Boolean? = null,
17 | override val shuffle: Boolean? = null,
18 | val volume: Float? = null,
19 | ) : SchedulingOptions {
20 |
21 | init {
22 | if ((loopQueue == true) xor (loop == true) xor (shuffle == true)) {
23 | throw UnexclusiveSchedulingOptions()
24 | }
25 | }
26 |
27 | fun merge(parent: SchedulerSettings) = SchedulerSettings(
28 | parent.loopQueue ?: loopQueue,
29 | parent.loop ?: loop,
30 | parent.shuffle ?: shuffle,
31 | parent.volume ?: volume
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/music/api/types/src/commonMain/kotlin/Track.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.api.types
2 |
3 | import dev.arbjerg.lavalink.protocol.v4.Track
4 | import dev.kord.common.entity.Snowflake
5 | import dev.schlaubi.lavakord.plugins.sponsorblock.model.YouTubeChapter
6 | import kotlinx.serialization.Contextual
7 | import kotlinx.serialization.SerialName
8 | import kotlinx.serialization.Serializable
9 | import kotlin.time.Duration
10 |
11 | @Serializable
12 | sealed class QueuedTrack {
13 |
14 | abstract val track: Track
15 | abstract val queuedBy: Snowflake
16 |
17 | abstract operator fun component1(): Track
18 | }
19 |
20 | @Serializable
21 | @SerialName("simple")
22 | data class SimpleQueuedTrack(@Contextual override val track: Track, override val queuedBy: Snowflake) : QueuedTrack()
23 |
24 | @Serializable
25 | @SerialName("chartered")
26 | data class ChapterQueuedTrack(
27 | @Contextual override val track: Track,
28 | override val queuedBy: Snowflake,
29 | val chapters: List
30 | ) : QueuedTrack() {
31 | var chapterIndex: Int = 0
32 | private set
33 | val chapter: YouTubeChapter
34 | get() = chapters[chapterIndex]
35 |
36 | val isOnLast: Boolean
37 | get() = chapterIndex >= chapters.lastIndex
38 |
39 | fun skipTo(startTime: Duration, name: String) {
40 | chapterIndex = chapters.indexOfFirst { it.start == startTime && it.name == name }
41 | }
42 |
43 | fun nextChapter(): YouTubeChapter {
44 | chapterIndex++
45 | return chapters[chapterIndex]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/music/build.gradle.kts:
--------------------------------------------------------------------------------
1 | subprojects {
2 | version = "4.4.2-SNAPSHOT"
3 |
4 | repositories {
5 | maven("https://maven.topi.wtf/releases")
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/music/commands/README.md:
--------------------------------------------------------------------------------
1 | # music-commands
2 |
3 | Commands for the [music](..) plugin, spun off so you can use music APIs without a full music bot system
4 |
--------------------------------------------------------------------------------
/music/commands/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | alias(libs.plugins.kotlinx.serialization)
4 | com.google.devtools.ksp
5 | dev.schlaubi.mikbot.`gradle-plugin`
6 | }
7 |
8 | dependencies {
9 | plugin(projects.music.player)
10 | optionalPlugin(projects.core.gdpr)
11 | }
12 |
13 | kotlin {
14 | compilerOptions {
15 | freeCompilerArgs.add("-Xcontext-receivers")
16 | }
17 | }
18 |
19 | mikbotPlugin {
20 | pluginId = "music-commands"
21 | description = "Plugin providing full music related commands"
22 | }
23 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/MusicCommandsPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic
2 |
3 | import dev.kordex.core.builders.ExtensionsBuilder
4 | import dev.schlaubi.mikbot.plugin.api.Plugin
5 | import dev.schlaubi.mikbot.plugin.api.PluginContext
6 | import dev.schlaubi.mikbot.plugin.api.PluginMain
7 | import dev.schlaubi.mikmusic.commands.commands
8 | import dev.schlaubi.mikmusic.context.playMessageAction
9 | import dev.schlaubi.mikmusic.core.MusicExtensionPoint
10 | import dev.schlaubi.mikmusic.core.MusicModule
11 | import dev.schlaubi.mikmusic.playlist.commands.PlaylistModule
12 | import org.pf4j.Extension
13 |
14 | @PluginMain
15 | class MusicCommandsPlugin(context: PluginContext) : Plugin(context) {
16 | override fun ExtensionsBuilder.addExtensions() {
17 | add(::PlaylistModule)
18 | }
19 | }
20 |
21 | @Extension
22 | class MusicCommandsExtension : MusicExtensionPoint {
23 | override suspend fun MusicModule.overrideSetup() {
24 | commands()
25 | playMessageAction()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/ClearCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.core.MusicModule
6 | import dev.schlaubi.mikmusic.core.musicControlContexts
7 |
8 | suspend fun MusicModule.clearCommand() = ephemeralControlSlashCommand {
9 | name = MusicTranslations.Commands.Clear.name
10 | description = MusicTranslations.Commands.Clear.description
11 | musicControlContexts()
12 |
13 | action {
14 | musicPlayer.queue.clear()
15 | musicPlayer.updateMusicChannelMessage()
16 | respond {
17 | content = translate(MusicTranslations.Commands.Clear.cleared)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/Commands.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikmusic.core.MusicModule
4 |
5 | suspend fun MusicModule.commands() {
6 | playCommand()
7 | pauseCommand()
8 | stopCommand()
9 | volumeCommand()
10 | queueCommand()
11 | skipCommand()
12 | schedulerCommands()
13 | nowPlayingCommand()
14 | moveCommand()
15 | removeCommand()
16 | seekCommand()
17 | replayCommand()
18 | clearCommand()
19 | fixCommand()
20 | nextCommand()
21 | radioCommand()
22 | }
23 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/NextCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.api.types.ChapterQueuedTrack
6 | import dev.schlaubi.mikmusic.checks.anyMusicPlaying
7 | import dev.schlaubi.mikmusic.core.MusicModule
8 | import dev.schlaubi.mikmusic.core.musicControlContexts
9 |
10 | suspend fun MusicModule.nextCommand() = ephemeralControlSlashCommand {
11 | name = MusicTranslations.Commands.Next.name
12 | description = MusicTranslations.Commands.Next.description
13 | musicControlContexts()
14 |
15 | check {
16 | anyMusicPlaying(this@nextCommand)
17 | }
18 |
19 | action {
20 | val chapterSong = musicPlayer.playingTrack as? ChapterQueuedTrack
21 | if (chapterSong == null || chapterSong.isOnLast) {
22 | if (musicPlayer.canSkip) {
23 | respond { content = translate(MusicTranslations.Commands.Skip.skipped) }
24 | return@action
25 | }
26 | musicPlayer.skip()
27 | respond { content = translate(MusicTranslations.Commands.Skip.skipped) }
28 | } else {
29 | musicPlayer.skipChapter()
30 | respond { content = translate(MusicTranslations.Commands.Next.skippedChapter) }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/PauseCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikbot.translations.MusicTranslations
4 | import dev.schlaubi.mikmusic.core.MusicModule
5 | import dev.schlaubi.mikmusic.core.musicControlContexts
6 |
7 | suspend fun MusicModule.pauseCommand() = ephemeralControlSlashCommand {
8 | name = MusicTranslations.Commands.Pause.name
9 | description = MusicTranslations.Commands.Pause.description
10 | musicControlContexts()
11 |
12 | action {
13 | musicPlayer.pause(!link.player.paused)
14 |
15 | respond { content = "Pause toggle" }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/PlayCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.kordex.core.commands.converters.impl.defaultingBoolean
4 | import dev.kordex.core.extensions.ephemeralSlashCommand
5 | import dev.schlaubi.mikbot.translations.MusicTranslations
6 | import dev.schlaubi.mikmusic.checks.joinSameChannelCheck
7 | import dev.schlaubi.mikmusic.core.MusicModule
8 | import dev.schlaubi.mikmusic.core.musicControlContexts
9 | import dev.schlaubi.mikmusic.player.queue.QueueArguments
10 | import dev.schlaubi.mikmusic.player.queue.queueTracks
11 |
12 | class PlayArguments : QueueArguments() {
13 | val search by defaultingBoolean {
14 | name = MusicTranslations.Commands.Play.Arguments.Search.name
15 | description = MusicTranslations.Commands.Play.Arguments.Search.description
16 | defaultValue = false
17 | }
18 | }
19 |
20 | suspend fun MusicModule.playCommand() {
21 | ephemeralSlashCommand(::PlayArguments) {
22 | musicControlContexts()
23 |
24 | name = MusicTranslations.Commands.Play.name
25 | description = MusicTranslations.Commands.Play.description
26 |
27 | check {
28 | joinSameChannelCheck(bot)
29 | }
30 |
31 | action {
32 | queueTracks(musicPlayer, arguments.search)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/ReplayCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.core.MusicModule
6 | import dev.schlaubi.mikmusic.core.musicControlContexts
7 |
8 | suspend fun MusicModule.replayCommand() = ephemeralControlSlashCommand {
9 | name = MusicTranslations.Commands.Replay.name
10 | description = MusicTranslations.Commands.Replay.description
11 | musicControlContexts()
12 |
13 | action {
14 | player.seekTo(0)
15 |
16 | respond {
17 | content = translate(MusicTranslations.Commands.Replay.success)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/SkipCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.kordex.core.commands.Arguments
4 | import dev.kordex.core.commands.converters.impl.defaultingInt
5 | import dev.schlaubi.mikbot.plugin.api.util.discordError
6 | import dev.schlaubi.mikbot.plugin.api.util.translate
7 | import dev.schlaubi.mikbot.translations.MusicTranslations
8 | import dev.schlaubi.mikmusic.core.MusicModule
9 | import dev.schlaubi.mikmusic.core.musicControlContexts
10 |
11 | class SkipArguments : Arguments() {
12 | val to by defaultingInt {
13 | name = MusicTranslations.Commands.Skip.Arguments.To.name
14 | description = MusicTranslations.Commands.Skip.Arguments.To.description
15 | defaultValue = 1
16 | }
17 | }
18 |
19 | suspend fun MusicModule.skipCommand() = ephemeralControlSlashCommand(::SkipArguments) {
20 | name = MusicTranslations.Commands.Skip.name
21 | description = MusicTranslations.Commands.Skip.description
22 | musicControlContexts()
23 |
24 | action {
25 | if (!musicPlayer.canSkip) {
26 | respond { content = translate(MusicTranslations.Commands.Skip.empty) }
27 | return@action
28 | }
29 | if (arguments.to < 1 && musicPlayer.hasAutoPlay) {
30 | discordError(MusicTranslations.Commands.skipsExceedAutoplayRange)
31 | }
32 | if (arguments.to > (musicPlayer.queuedTracks.size + musicPlayer.autoPlayTrackCount)) {
33 | respond {
34 | content = translate(MusicTranslations.Commands.Skip.exceedsRange)
35 | }
36 | return@action
37 | }
38 |
39 | musicPlayer.skip(arguments.to)
40 | respond { content = translate(MusicTranslations.Commands.Skip.skipped) }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/StopCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.core.MusicModule
6 | import dev.schlaubi.mikmusic.core.musicControlContexts
7 |
8 | suspend fun MusicModule.stopCommand() =
9 | ephemeralControlSlashCommand {
10 | name = MusicTranslations.Commands.Stop.name
11 | description = MusicTranslations.Commands.Stop.description
12 | musicControlContexts()
13 |
14 | action {
15 | musicPlayer.stop()
16 |
17 | respond { content = translate(MusicTranslations.Commands.Stop.stopped) }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/commands/VolumeCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.commands
2 |
3 | import dev.kordex.core.commands.Arguments
4 | import dev.kordex.core.commands.converters.impl.optionalInt
5 | import dev.schlaubi.mikbot.plugin.api.util.translate
6 | import dev.schlaubi.mikbot.translations.MusicTranslations
7 | import dev.schlaubi.mikmusic.core.MusicModule
8 | import dev.schlaubi.mikmusic.core.musicControlContexts
9 |
10 | class VolumeArguments : Arguments() {
11 | val volume by optionalInt {
12 | name = MusicTranslations.Commands.Volume.Arguments.Volume.name
13 | description = MusicTranslations.Commands.Volume.Arguments.Volume.description
14 | maxValue = 1000
15 | minValue = 0
16 | }
17 | }
18 |
19 | suspend fun MusicModule.volumeCommand() = ephemeralControlSlashCommand(::VolumeArguments) {
20 | name = MusicTranslations.Commands.Volume.name
21 | description = MusicTranslations.Commands.Volume.description
22 | musicControlContexts()
23 |
24 | action {
25 | val volume = arguments.volume
26 | if (volume != null) {
27 | musicPlayer.changeVolume(volume)
28 | respond { content = translate(MusicTranslations.Commands.Volume.set, volume.toString()) }
29 | } else {
30 | respond { content = translate(MusicTranslations.Commands.Volume.current, player.volume.toString()) }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/context/PlayMessageAction.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.context
2 |
3 | import dev.kordex.core.commands.application.message.EphemeralMessageCommandContext
4 | import dev.kordex.core.extensions.ephemeralMessageCommand
5 | import dev.schlaubi.mikbot.plugin.api.util.attachmentOrContentQuery
6 | import dev.schlaubi.mikbot.translations.MusicTranslations
7 | import dev.schlaubi.mikmusic.checks.joinSameChannelCheck
8 | import dev.schlaubi.mikmusic.core.MusicModule
9 | import dev.schlaubi.mikmusic.core.musicControlContexts
10 | import dev.schlaubi.mikmusic.player.MusicPlayer
11 | import dev.schlaubi.mikmusic.player.queue.SearchQuery
12 | import dev.schlaubi.mikmusic.player.queue.queueTracks
13 |
14 |
15 | suspend fun MusicModule.playMessageAction() = ephemeralMessageCommand {
16 | name = MusicTranslations.Context.Message.playAsTrack
17 |
18 | musicControlContexts()
19 |
20 | check {
21 | joinSameChannelCheck(bot)
22 | }
23 |
24 | action {
25 | val query = event.interaction.messages.values.first().attachmentOrContentQuery
26 |
27 | val arguments = SearchQuery(query)
28 |
29 | queue(arguments, musicPlayer)
30 | }
31 | }
32 |
33 | private suspend fun EphemeralMessageCommandContext<*>.queue(
34 | arguments: SearchQuery,
35 | musicPlayer: MusicPlayer
36 | ) = queueTracks(musicPlayer, true, arguments, { respond { it() } }) {
37 | editingPaginator { it() }
38 | }
39 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/MusicSettingsExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings
2 |
3 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsExtensionPoint
4 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule
5 | import dev.schlaubi.mikmusic.core.settings.commands.*
6 | import org.pf4j.Extension
7 |
8 | @Extension
9 | class MusicSettingsExtension : SettingsExtensionPoint {
10 | override suspend fun SettingsModule.apply() {
11 | djModeCommand()
12 | fixMusicChannel()
13 | leaveTimeoutCommand()
14 | sponsorBlockCommand()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/commands/DjModeCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings.commands
2 |
3 | import dev.kordex.core.commands.Arguments
4 | import dev.kordex.core.commands.converters.impl.optionalRole
5 | import dev.kordex.core.extensions.ephemeralSlashCommand
6 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule
7 | import dev.schlaubi.mikbot.plugin.api.settings.guildAdminOnly
8 | import dev.schlaubi.mikbot.plugin.api.util.safeGuild
9 | import dev.schlaubi.mikbot.plugin.api.util.translate
10 | import dev.schlaubi.mikbot.translations.MusicTranslations
11 | import dev.schlaubi.mikmusic.core.settings.MusicSettingsDatabase
12 |
13 | private class DjModeArguments : Arguments() {
14 | val djRole by optionalRole {
15 | name = MusicTranslations.Commands.DjMode.Arguments.Role.name
16 | description = MusicTranslations.Commands.DjMode.Arguments.Role.description
17 | }
18 | }
19 |
20 | suspend fun SettingsModule.djModeCommand() {
21 | ephemeralSlashCommand(::DjModeArguments) {
22 | name = MusicTranslations.Commands.DjMode.name
23 | description = MusicTranslations.Commands.DjMode.description
24 |
25 | guildAdminOnly()
26 |
27 |
28 | action {
29 | val role = arguments.djRole
30 |
31 | if (role == null) {
32 | respond {
33 | content = translate(MusicTranslations.Command.Djmode.disabled)
34 | }
35 | } else {
36 | respond {
37 | content = translate(MusicTranslations.Command.Djmode.enabled, role.name)
38 | }
39 | }
40 |
41 | MusicSettingsDatabase.guild.save(
42 | MusicSettingsDatabase.findGuild(safeGuild).copy(djMode = role != null, djRole = role?.id)
43 | )
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/commands/FixMusicChannel.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings.commands
2 |
3 | import dev.kordex.core.extensions.ephemeralSlashCommand
4 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule
5 | import dev.schlaubi.mikbot.plugin.api.util.safeGuild
6 | import dev.schlaubi.mikbot.plugin.api.util.translate
7 | import dev.schlaubi.mikbot.translations.MusicTranslations
8 | import dev.schlaubi.mikmusic.core.musicControlContexts
9 | import dev.schlaubi.mikmusic.core.settings.MusicSettingsDatabase
10 | import dev.schlaubi.mikmusic.util.musicModule
11 |
12 | suspend fun SettingsModule.fixMusicChannel() = ephemeralSlashCommand {
13 | name = MusicTranslations.Commands.FixMusicChannel.name
14 | description = MusicTranslations.Commands.FixMusicChannel.description
15 | musicControlContexts()
16 |
17 | action {
18 | val guildSettings = MusicSettingsDatabase.findGuild(safeGuild)
19 | if (guildSettings.musicChannelData == null) {
20 | respond {
21 | content = translate(MusicTranslations.Commands.FixMusicChannel.notEnabled)
22 | }
23 |
24 | return@action
25 | }
26 |
27 | musicModule.getMusicPlayer(safeGuild).updateMusicChannelMessage(force = true)
28 |
29 | respond {
30 | content = translate(MusicTranslations.Commands.FixMusicChannel.success)
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/Playlist.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist
2 |
3 | import dev.kord.common.entity.Snowflake
4 | import dev.schlaubi.lavakord.audio.Node
5 | import dev.schlaubi.lavakord.rest.decodeTracks
6 | import dev.schlaubi.mikmusic.util.EncodedTrack
7 | import dev.schlaubi.mikmusic.util.TrackListSerializer
8 | import kotlinx.serialization.Contextual
9 | import kotlinx.serialization.SerialName
10 | import kotlinx.serialization.Serializable
11 | import org.litote.kmongo.Id
12 |
13 | @Serializable
14 | data class Playlist(
15 | @SerialName("_id") @Contextual
16 | val id: Id,
17 | val authorId: Snowflake,
18 | val name: String,
19 | @Serializable(with = TrackListSerializer::class) val songs: List,
20 | val public: Boolean = false,
21 | val usages: Int = 0,
22 | ) {
23 | suspend fun getTracks(lavalink: Node) =
24 | lavalink.decodeTracks(songs.map(EncodedTrack::value))
25 | }
26 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/PlaylistDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist
2 |
3 | import dev.kordex.core.koin.KordExKoinComponent
4 | import dev.schlaubi.mikbot.plugin.api.io.getCollection
5 | import dev.schlaubi.mikbot.plugin.api.util.database
6 |
7 | object PlaylistDatabase : KordExKoinComponent {
8 | val collection = database.getCollection("playlists")
9 |
10 | suspend fun updatePlaylistUsages(playlist: Playlist) {
11 | collection.save(playlist.copy(usages = playlist.usages + 1))
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/DeleteCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
6 |
7 | class PlaylistDeleteArguments : PlaylistArguments()
8 |
9 | fun PlaylistModule.deleteCommand() = ephemeralSubCommand(::PlaylistDeleteArguments) {
10 | name = MusicTranslations.Commands.Playlist.Delete.name
11 | description = MusicTranslations.Commands.Playlist.Delete.description
12 |
13 | action {
14 | checkPermissions { (id, _, playlistName) ->
15 | PlaylistDatabase.collection.deleteOneById(id)
16 |
17 | respond {
18 | content = translate(MusicTranslations.Commands.Playlist.Delete.deleted, playlistName)
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/LoadCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.checks.joinSameChannelCheck
6 | import dev.schlaubi.mikmusic.core.musicControlContexts
7 | import dev.schlaubi.mikmusic.player.queue.SchedulingArguments
8 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
9 | import dev.schlaubi.mikmusic.util.mapToQueuedTrack
10 |
11 | class LoadArguments : SchedulingArguments(), PlaylistOptions {
12 | override val name by playlistName(onlyMine = false)
13 | }
14 |
15 | fun PlaylistModule.loadCommand() = ephemeralSubCommand(::LoadArguments) {
16 | name = MusicTranslations.Commands.Playlist.Load.name
17 | description = MusicTranslations.Commands.Playlist.Load.description
18 |
19 | musicControlContexts()
20 |
21 | check {
22 | joinSameChannelCheck(bot)
23 | }
24 |
25 | action {
26 | val playlist = getPlaylist()
27 | PlaylistDatabase.updatePlaylistUsages(playlist)
28 |
29 | musicPlayer.queueTrack(
30 | force = false, onTop = false,
31 | tracks = playlist.getTracks(musicPlayer.node).mapToQueuedTrack(user),
32 | schedulingOptions = arguments
33 | )
34 |
35 | respond {
36 | content = translate(MusicTranslations.Command.Playlist.Load.queued, playlist.songs.size, playlist.name)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/RemoveCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.kordex.core.commands.converters.impl.int
4 | import dev.schlaubi.mikbot.plugin.api.util.translate
5 | import dev.schlaubi.mikbot.translations.MusicTranslations
6 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
7 |
8 | class PlaylistRemoveArguments : PlaylistArguments() {
9 | val index by int {
10 | name = MusicTranslations.Commands.Playlist.Remove.Arguments.Index.name
11 | description = MusicTranslations.Commands.Playlist.Remove.Arguments.Index.description
12 | }
13 | }
14 |
15 | fun PlaylistModule.removeCommand() = ephemeralSubCommand(::PlaylistRemoveArguments) {
16 | name = MusicTranslations.Commands.Playlist.Remove.name
17 | description = MusicTranslations.Commands.Playlist.Remove.description
18 |
19 | action {
20 | checkPermissions { playlist ->
21 | val index = arguments.index - 1
22 | val item = playlist.songs.getOrNull(index)
23 | if (item == null) {
24 | respond {
25 | content = translate(MusicTranslations.Commands.Playlist.Remove.tooHighIndex)
26 | }
27 |
28 | return@action
29 | }
30 |
31 | PlaylistDatabase.collection.save(
32 | playlist.copy(
33 | songs = playlist.songs.toMutableList().apply {
34 | removeAt(index) // this might be a dupe, so we remove by index
35 | }
36 | )
37 | )
38 |
39 | respond {
40 | content = translate(MusicTranslations.Commands.Playlist.Remove.removed, item.toTrack(musicPlayer.node), playlist.name)
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/RenameCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.kordex.core.commands.converters.impl.string
4 | import dev.schlaubi.mikbot.plugin.api.util.translate
5 | import dev.schlaubi.mikbot.translations.MusicTranslations
6 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
7 |
8 | class PlaylistRenameArguments : PlaylistArguments() {
9 | val newName by string {
10 | name = MusicTranslations.Commands.Playlist.Rename.Arguments.NewName.name
11 | description = MusicTranslations.Commands.Playlist.Rename.Arguments.NewName.description
12 | }
13 | }
14 |
15 | fun PlaylistModule.renameCommand() = ephemeralSubCommand(::PlaylistRenameArguments) {
16 | name = MusicTranslations.Commands.Playlist.Rename.name
17 | description = MusicTranslations.Commands.Playlist.Rename.description
18 |
19 | action {
20 | checkPermissions { playlist ->
21 | checkName(arguments.newName, playlist.public) {
22 | PlaylistDatabase.collection.save(playlist.copy(name = arguments.newName))
23 |
24 | respond {
25 | content = translate(MusicTranslations.Commands.Playlist.Rename.renamed, playlist.name, arguments.newName)
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/SongsCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.arbjerg.lavalink.protocol.v4.Track
4 | import dev.schlaubi.mikbot.plugin.api.util.forList
5 | import dev.schlaubi.mikbot.plugin.api.util.translate
6 | import dev.schlaubi.mikbot.translations.MusicTranslations
7 | import dev.schlaubi.mikmusic.util.format
8 |
9 | class PlaylistSongsArguments : PlaylistArguments()
10 |
11 | fun PlaylistModule.songsCommand() = ephemeralSubCommand(::PlaylistSongsArguments) {
12 | name = MusicTranslations.Commands.Playlist.Songs.name
13 | description = MusicTranslations.Commands.Playlist.Songs.description
14 |
15 | action {
16 | val playlist = getPlaylist()
17 | if (playlist.songs.isEmpty()) {
18 | respond {
19 | content = translate(MusicTranslations.Commands.Playlist.Songs.isEmpty)
20 | }
21 | return@action
22 | }
23 |
24 | val tracks = playlist.getTracks(musicPlayer.node)
25 |
26 | editingPaginator {
27 | forList(
28 | user, tracks, Track::format,
29 | { current, total ->
30 | translate(
31 | MusicTranslations.Commands.Playlist.Songs.Paginator.title,
32 | arrayOf(playlist.name, current.toString(), total.toString())
33 | )
34 | }
35 | )
36 | }.send()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/commands/ToggleVisibilityCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.commands
2 |
3 | import dev.schlaubi.mikbot.plugin.api.util.translate
4 | import dev.schlaubi.mikbot.translations.MusicTranslations
5 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
6 |
7 | class PlaylistToggleVisibilityCommand : PlaylistArguments()
8 |
9 | fun PlaylistModule.toggleVisibilityCommand() = ephemeralSubCommand(::PlaylistToggleVisibilityCommand) {
10 | name = MusicTranslations.Commands.Playlist.ToggleVisibility.name
11 | description = MusicTranslations.Commands.Playlist.ToggleVisibility.description
12 |
13 | action {
14 | checkPermissions { playlist ->
15 | PlaylistDatabase.collection.save(playlist.copy(public = !playlist.public))
16 |
17 | respond {
18 | content = if (!playlist.public) {
19 | translate(MusicTranslations.Commands.Playlist.ToggleVisibility.on)
20 | } else {
21 | translate(MusicTranslations.Commands.Playlist.ToggleVisibility.off)
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/music/commands/src/main/kotlin/dev/schlaubi/mikmusic/playlist/gdpr/PlaylistGdprExtensionPoint.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.playlist.gdpr
2 |
3 | import dev.kord.core.entity.User
4 | import dev.schlaubi.mikbot.core.gdpr.api.DataPoint
5 | import dev.schlaubi.mikbot.core.gdpr.api.GDPRExtensionPoint
6 | import dev.schlaubi.mikbot.core.gdpr.api.PermanentlyStoredDataPoint
7 | import dev.schlaubi.mikbot.translations.MusicTranslations
8 | import dev.schlaubi.mikmusic.playlist.Playlist
9 | import dev.schlaubi.mikmusic.playlist.PlaylistDatabase
10 | import org.litote.kmongo.eq
11 | import org.pf4j.Extension
12 |
13 | @Extension
14 | class PlaylistGdprExtensionPoint : GDPRExtensionPoint {
15 | override fun provideDataPoints(): List = listOf(PlaylistDataPoint)
16 | }
17 |
18 | // Data point for stored playlists
19 | val PlaylistDataPoint: PermanentlyStoredDataPoint = PlaylistDataPointImpl
20 |
21 | private object PlaylistDataPointImpl :
22 | PermanentlyStoredDataPoint(MusicTranslations.Gdpr.Playlists.name, MusicTranslations.Gdpr.Playlists.description, null) {
23 | override suspend fun deleteFor(user: User) {
24 | PlaylistDatabase.collection.deleteMany(Playlist::authorId eq user.id)
25 | }
26 |
27 | override suspend fun requestFor(user: User): List =
28 | listOf("All Playlists: `/playlist list`", "Specific Playlist: `/playlist info `")
29 | }
30 |
--------------------------------------------------------------------------------
/music/lyrics/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | com.google.devtools.ksp
4 | dev.schlaubi.mikbot.`gradle-plugin`
5 | alias(libs.plugins.kotlinx.serialization)
6 | }
7 |
8 | dependencies {
9 | plugin(projects.music.player)
10 | plugin(projects.core.ktor)
11 | ktorDependency(libs.ktor.server.cors)
12 | }
13 |
14 | mikbotPlugin {
15 | pluginId = "music-lyrics"
16 | description = "Plugin providing lyrics for the music plugin"
17 | bundle = "lyrics"
18 | }
19 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/kotlin/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.lyrics
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 |
5 | object Config : EnvironmentConfig() {
6 | val LYRICS_WEB_URL by getEnv("http://localhost:3001")
7 | }
8 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/kotlin/KaraokeCommand.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.lyrics
2 |
3 | import dev.kordex.core.extensions.Extension
4 | import dev.kordex.core.extensions.ephemeralSlashCommand
5 | import dev.schlaubi.lavakord.plugins.lyrics.rest.requestLyrics
6 | import dev.schlaubi.lyrics.protocol.TimedLyrics
7 | import dev.schlaubi.mikbot.plugin.api.util.discordError
8 | import dev.schlaubi.mikbot.plugin.api.util.translate
9 | import dev.schlaubi.mikbot.translations.LyricsTranslations
10 | import dev.schlaubi.mikmusic.checks.musicQuizAntiCheat
11 | import dev.schlaubi.mikmusic.util.musicModule
12 |
13 | suspend fun Extension.karaokeCommand() = ephemeralSlashCommand {
14 | name = LyricsTranslations.Commands.Karaoke.name
15 | description = LyricsTranslations.Commands.Karaoke.description
16 |
17 | check {
18 | musicQuizAntiCheat(musicModule)
19 | }
20 |
21 | action {
22 | val player = with(musicModule) { player }
23 |
24 | val lyrics = runCatching { player.requestLyrics() }.getOrNull()
25 |
26 | if (lyrics !is TimedLyrics) {
27 | discordError(LyricsTranslations.Commands.Karaoke.notAvailable)
28 | }
29 |
30 | val token = requestToken(user.id)
31 |
32 | respond {
33 | content = translate(LyricsTranslations.Commands.Karaoke.success, "${Config.LYRICS_WEB_URL}?apiKey=$token")
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/kotlin/LyricsPlugin.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.lyrics
2 |
3 | import dev.kordex.core.builders.ExtensionsBuilder
4 | import dev.schlaubi.mikbot.plugin.api.Plugin
5 | import dev.schlaubi.mikbot.plugin.api.PluginContext
6 | import dev.schlaubi.mikbot.plugin.api.PluginMain
7 | import dev.schlaubi.mikbot.plugin.api.module.MikBotModule
8 |
9 | @PluginMain
10 | class LyricsPlugin(context: PluginContext) : Plugin(context) {
11 | override fun ExtensionsBuilder.addExtensions() {
12 | add(::LyricsModule)
13 | }
14 | }
15 |
16 | class LyricsModule(context: PluginContext) : MikBotModule(context) {
17 | override val name: String = "lyrics"
18 |
19 | override suspend fun setup() {
20 | lyricsCommand()
21 | karaokeCommand()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/kotlin/events/Events.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.lyrics.events
2 |
3 | import kotlinx.datetime.Instant
4 | import kotlinx.serialization.ExperimentalSerializationApi
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.json.JsonClassDiscriminator
8 |
9 | @OptIn(ExperimentalSerializationApi::class)
10 | @Serializable
11 | @JsonClassDiscriminator("type")
12 | sealed interface Event
13 |
14 | @SerialName("player_update")
15 | @Serializable
16 | data class PlayerStateUpdateEvent(
17 | val playing: Boolean,
18 | val position: Long,
19 | val timestamp: Instant
20 | ) : Event {
21 | override fun equals(other: Any?): Boolean {
22 | if (this === other) return true
23 | if (javaClass != other?.javaClass) return false
24 |
25 | other as PlayerStateUpdateEvent
26 |
27 | if (playing != other.playing) return false
28 | if (position != other.position) return false
29 |
30 | return true
31 | }
32 |
33 | override fun hashCode(): Int {
34 | var result = playing.hashCode()
35 | result = 31 * result + position.hashCode()
36 | return result
37 | }
38 | }
39 |
40 | @SerialName("player_stopped")
41 | @Serializable
42 | data object PlayerStoppedEvent : Event
43 |
44 | @SerialName("next_track")
45 | @Serializable
46 | data class NextTrackEvent(val startPosition: Long) : Event
47 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/resources/translations/lyrics/strings.properties:
--------------------------------------------------------------------------------
1 | command.lyrics.source=Source: {0}
2 | commands.karaoke.name=karaoke
3 | commands.karaoke.description=Allows having a karaoke party
4 | commands.karaoke.not_available=We do not have karaoke-capable lyrics for this track :(
5 | commands.karaoke.success=Please open [this website]({0}) to have karaoke fun
6 | command.lyrics.no_lyrics=We couldn't find lyrics for this song. This might be caused by the YouTube video title not matching up with the actual song title, so you can try running `/lyrics song_name: ` and choose a more exact title.
7 | command.lyrics.no_song_playing=There is currently no song playing
8 | commands.lyrics.arguments.song_name.description=The name of the song to search for, if no one is playing
9 | commands.lyrics.arguments.song_name.name=song-name
10 | commands.lyrics.name=lyrics
11 | commands.lyrics.description=Displays the lyrics for the current song or the specified query
12 |
--------------------------------------------------------------------------------
/music/lyrics/src/main/resources/translations/lyrics/strings_de_DE.properties:
--------------------------------------------------------------------------------
1 | command.lyrics.source=Quelle: {0}
2 | commands.karaoke.description=Lässt dich eine Karaoke party haben
3 | commands.karaoke.not_available=Wir haben keine Karaoke fähigen Songtextre für dieses Lied :(
4 | commands.karaoke.success=Bitte öffne [diese Website]({0}) um Karaoke spaß zu haben
5 | command.lyrics.no_lyrics=Wir konnten keinen Songtext für diesen Song finden. Versuche es doch noch einmal mit `/lyrics song_name: `. Vielleicht klappt's ja mit einer anderen Schreibweise.
6 | command.lyrics.no_song_playing=Es wird derzeit kein Song abgespielt
7 | commands.lyrics.description=Zeigt den Songtext eines spezifischen oder des derzeitigen Liedes
8 | commands.lyrics.arguments.song_name.description=Der Name des Songs, dessen Songtextes angezeigt werden soll.
9 | commands.lyrics.arguments.song_name.name=lied-name
10 | commands.lyrics.name=liedtext
11 |
--------------------------------------------------------------------------------
/music/player/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `mikbot-module`
3 | `mikbot-publishing`
4 | alias(libs.plugins.kotlinx.serialization)
5 | com.google.devtools.ksp
6 | dev.schlaubi.mikbot.`gradle-plugin`
7 | `jvm-test-suite`
8 | }
9 |
10 | group = "dev.schlaubi.mikbot"
11 |
12 | dependencies {
13 | api(projects.music.api.types) {
14 | exclude(group = "io.ktor")
15 | }
16 | api(libs.lavakord.kord)
17 | api(libs.lavakord.sponsorblock)
18 | api(libs.lavakord.lavsrc)
19 | api(libs.lavakord.lavasearch)
20 | api(libs.lavakord.lyrics)
21 |
22 | // Plattform support
23 | implementation(libs.google.apis.youtube)
24 |
25 | // redeploy support
26 | optionalPlugin(projects.core.redeployHook)
27 |
28 | // GDPR support
29 | optionalPlugin(projects.core.gdpr)
30 |
31 | // Image Color Client
32 | api(projects.clients.imageColorClient)
33 | api(projects.clients.imageColorClientKord)
34 |
35 | implementation(libs.ktor.client.logging)
36 |
37 | testImplementation(kotlin("test-junit5"))
38 | testImplementation(libs.stdx.full)
39 | }
40 |
41 | kotlin {
42 | compilerOptions {
43 | freeCompilerArgs.add("-Xcontext-receivers")
44 | }
45 | }
46 |
47 | testing {
48 | suites {
49 | named("test") {
50 | useJUnitJupiter()
51 | }
52 | }
53 | }
54 |
55 | mikbotPlugin {
56 | pluginId = "music-player"
57 | description = "Plugin providing full music functionality for the bot"
58 | bundle = "music"
59 | }
60 |
61 | publishing {
62 | publications {
63 | named("maven") {
64 | artifactId = "mikbot-music-player"
65 | }
66 | }
67 | }
68 |
69 | fun DependencyHandlerScope.lavakordDependency(provider: Provider<*>) = implementation(provider) {
70 | exclude(module = "ktor-resources-jvm")
71 | }
72 |
--------------------------------------------------------------------------------
/music/player/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | color_service:
4 | image: ghcr.io/nycodeghg/image-color-service:0.1.1
5 | ports:
6 | - 9090:8080
7 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/checks/MusicQuizCheck.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.checks
2 |
3 | import dev.kordex.core.checks.guildFor
4 | import dev.kordex.core.checks.types.CheckContext
5 | import dev.schlaubi.mikbot.translations.MusicTranslations
6 | import dev.schlaubi.mikmusic.core.MusicModule
7 |
8 | suspend fun CheckContext<*>.musicQuizAntiCheat(musicModule: MusicModule) {
9 | failIf(MusicTranslations.Commands.NowPlaying.cheatAttempt) {
10 | val musicPlayer = musicModule.getMusicPlayer(guildFor(event)!!)
11 | musicPlayer.disableMusicChannel
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/checks/PlayingCheck.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.checks
2 |
3 | import dev.kord.core.event.interaction.InteractionCreateEvent
4 | import dev.kordex.core.checks.guildFor
5 | import dev.kordex.core.checks.types.CheckContext
6 | import dev.schlaubi.mikbot.translations.MusicTranslations
7 | import dev.schlaubi.mikmusic.core.MusicModule
8 |
9 | suspend fun CheckContext.anyMusicPlaying(musicModule: MusicModule) {
10 | if (!passed) {
11 | return
12 | }
13 |
14 | val guild = guildFor(event) ?: error("This check needs to also use anyGuild()")
15 | val player = musicModule.getMusicPlayer(guild)
16 | if (player.player.playingTrack == null) {
17 | fail(MusicTranslations.Music.Checks.notPlaying)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/core/Config.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core
2 |
3 | import dev.schlaubi.mikbot.plugin.api.EnvironmentConfig
4 |
5 | object Config : EnvironmentConfig("") {
6 | val ENABLE_MUSIC_CHANNEL_FEATURE: Boolean by getEnv(true, String::toBooleanStrict)
7 | val HAPPI_KEY by getEnv().optional()
8 | val YOUTUBE_API_KEY by this
9 | val SPOTIFY_CLIENT_ID by getEnv("")
10 | val SPOTIFY_CLIENT_SECRET by getEnv("")
11 | val IMAGE_COLOR_SERVICE_URL by getEnv().optional()
12 | val DEFAULT_SEARCH_PROVIDER by getEnv("ytsearch")
13 | }
14 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/core/audio/LavalinkServer.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.audio
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LavalinkServer(val url: String, val password: String)
7 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/GuildSettings.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings
2 |
3 | import dev.kord.common.entity.Snowflake
4 | import dev.schlaubi.mikmusic.api.types.SchedulerSettings
5 | import kotlinx.serialization.Contextual
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 | import kotlin.time.Duration
9 | import kotlin.time.Duration.Companion.seconds
10 |
11 | @Serializable
12 | data class GuildSettings(
13 | @SerialName("_id")
14 | val guildId: Snowflake,
15 | val djMode: Boolean = false,
16 | val djRole: Snowflake? = null,
17 | val announceSongs: Boolean = true,
18 | val musicChannelData: MusicChannelData? = null,
19 | val defaultSchedulerSettings: SchedulerSettings? = null,
20 | val useSponsorBlock: Boolean = true,
21 | @Contextual
22 | val leaveTimeout: Duration = 30.seconds
23 | )
24 |
25 | @Serializable
26 | data class MusicChannelData(
27 | val musicChannel: Snowflake,
28 | val musicChannelMessage: Snowflake
29 | )
30 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/MusicSettingsDatabase.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings
2 |
3 | import dev.kord.core.behavior.GuildBehavior
4 | import dev.kord.core.behavior.UserBehavior
5 | import dev.kordex.core.koin.KordExKoinComponent
6 | import dev.schlaubi.mikbot.plugin.api.io.getCollection
7 | import dev.schlaubi.mikbot.plugin.api.util.database
8 |
9 | object MusicSettingsDatabase : KordExKoinComponent {
10 | val user = database.getCollection("user_settings")
11 | val guild = database.getCollection("guild_settings")
12 |
13 | suspend fun findUser(user: UserBehavior) =
14 | this.user.findOneById(user.id) ?: UserSettings(user.id).also { this.user.save(it) }
15 |
16 | suspend fun findGuild(guild: GuildBehavior) =
17 | this.guild.findOneById(guild.id) ?: GuildSettings(guild.id).also { this.guild.save(it) }
18 | }
19 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/core/settings/UserSettings.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.core.settings
2 |
3 | import dev.kord.common.entity.Snowflake
4 | import dev.schlaubi.mikmusic.api.types.SchedulerSettings
5 | import kotlinx.serialization.SerialName
6 | import kotlinx.serialization.Serializable
7 |
8 | @Serializable
9 | data class UserSettings(
10 | @SerialName("_id")
11 | val id: Snowflake,
12 | val defaultSchedulerSettings: SchedulerSettings? = null
13 | )
14 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/gdpr/MusicGdprExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.gdpr
2 |
3 | import dev.schlaubi.mikbot.core.gdpr.api.AnonymizedData
4 | import dev.schlaubi.mikbot.core.gdpr.api.DataPoint
5 | import dev.schlaubi.mikbot.core.gdpr.api.GDPRExtensionPoint
6 | import dev.schlaubi.mikbot.core.gdpr.api.ProcessedData
7 | import dev.schlaubi.mikbot.translations.MusicTranslations
8 | import org.pf4j.Extension
9 |
10 | @Extension
11 | class MusicGdprExtension : GDPRExtensionPoint {
12 | override fun provideDataPoints(): List =
13 | listOf(AutoCompleteDataPoint, MusicChannelDataPoint)
14 | }
15 |
16 | // Data sent to Google for AutoComplete on search commands
17 | val AutoCompleteDataPoint =
18 | AnonymizedData(
19 | MusicTranslations.Gdpr.AutoComplete.description,
20 | MusicTranslations.Gdpr.AutoComplete.Sharing.description
21 | )
22 |
23 | // Data required for music-channel
24 | val MusicChannelDataPoint = ProcessedData(MusicTranslations.Gdpr.MusicChannel.description, null)
25 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/musicchannel/MusicChannelSettingsExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.musicchannel
2 |
3 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsExtensionPoint
4 | import dev.schlaubi.mikbot.plugin.api.settings.SettingsModule
5 | import org.pf4j.Extension
6 |
7 | @Extension
8 | class MusicChannelSettingsExtension : SettingsExtensionPoint {
9 | override suspend fun SettingsModule.apply() {
10 | musicChannel()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/PersistentPlayerState.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.player
2 |
3 | import dev.arbjerg.lavalink.protocol.v4.Filters
4 | import dev.kord.common.entity.Snowflake
5 | import dev.schlaubi.mikmusic.api.types.QueuedTrack
6 | import dev.schlaubi.mikmusic.api.types.SchedulerSettings
7 | import dev.schlaubi.mikmusic.util.QueuedTrackJsonSerializer
8 | import kotlinx.serialization.Contextual
9 | import kotlinx.serialization.Serializable
10 | import kotlin.time.Duration
11 |
12 | @Serializable
13 | data class PersistentPlayerState(
14 | val guildId: Snowflake,
15 | val channelId: Snowflake,
16 | val queue: List<@Serializable(with = QueuedTrackJsonSerializer::class) QueuedTrack>,
17 | @Contextual // this is a playingTrack which contains the current position
18 | val currentTrack: QueuedTrack?,
19 | val filters: Filters?,
20 | val schedulerOptions: SchedulerSettings,
21 | val paused: Boolean,
22 | val position: Duration,
23 | val autoPlayContext: AutoPlayContext?,
24 | val volume: Int,
25 | ) {
26 | constructor(musicPlayer: MusicPlayer) : this(
27 | Snowflake(musicPlayer.guildId),
28 | Snowflake(musicPlayer.lastChannelId!!),
29 | musicPlayer.queuedTracks,
30 | musicPlayer.playingTrack,
31 | musicPlayer.filters,
32 | SchedulerSettings(musicPlayer.loopQueue, musicPlayer.repeat, musicPlayer.shuffle),
33 | musicPlayer.player.paused,
34 | musicPlayer.player.positionDuration,
35 | musicPlayer.autoPlay,
36 | musicPlayer.player.volume
37 | )
38 | }
39 |
40 | fun SchedulerSettings.applyToPlayer(player: MusicPlayer) {
41 | if (shuffle != null) {
42 | player.shuffle = shuffle!!
43 | }
44 | if (loopQueue != null) {
45 | player.loopQueue = loopQueue!!
46 | }
47 | if (loop != null) {
48 | player.repeat = loop!!
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/VoiceStateWatcher.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.player
2 |
3 | import dev.kord.core.event.user.VoiceStateUpdateEvent
4 | import dev.kordex.core.extensions.event
5 | import dev.schlaubi.mikmusic.core.MusicModule
6 | import kotlinx.coroutines.flow.filter
7 | import kotlinx.coroutines.flow.toList
8 |
9 | suspend fun MusicModule.voiceStateWatcher() = event {
10 | check {
11 | failIf {
12 | val guild = event.state.getGuild()
13 | val channelId = event.state.channelId
14 | val voiceStates = guild.voiceStates.filter { it.channelId == channelId }.toList()
15 |
16 | voiceStates.size > 1 || voiceStates.none { it.userId == kord.selfId }
17 | }
18 | }
19 |
20 | action {
21 | getMusicPlayer(event.state.getGuild()).stop() // Leave if the bot is the last user in VC
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/player/queue/FriendlyException.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.player.queue
2 |
3 | import dev.arbjerg.lavalink.protocol.v4.Exception
4 |
5 | class FriendlyException(severity: Exception.Severity, message: String?) :
6 | RuntimeException("$severity: $message")
7 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/redeploy/RedeployExtension.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.redeploy
2 |
3 | import dev.kordex.core.ExtensibleBot
4 | import dev.kordex.core.koin.KordExKoinComponent
5 | import dev.schlaubi.mikbot.core.redeploy_hook.api.RedeployExtensionPoint
6 | import dev.schlaubi.mikmusic.core.MusicModule
7 | import org.koin.core.component.inject
8 | import org.pf4j.Extension
9 |
10 | @Extension
11 | class RedeployExtension : RedeployExtensionPoint, KordExKoinComponent {
12 | val bot by inject()
13 | override suspend fun beforeRedeploy() {
14 | val musicModule = bot.findExtension() ?: return
15 |
16 | musicModule.savePlayerStates()
17 | musicModule.disconnect()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/Common.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.util
2 |
3 | import dev.kordex.core.extensions.Extension
4 | import dev.schlaubi.mikmusic.core.MusicModule
5 |
6 | val Extension.musicModule: MusicModule get() = bot.findExtension()!!
7 |
--------------------------------------------------------------------------------
/music/player/src/main/kotlin/dev/schlaubi/mikmusic/util/JsonElementSerializer.kt:
--------------------------------------------------------------------------------
1 | package dev.schlaubi.mikmusic.util
2 |
3 | import dev.arbjerg.lavalink.protocol.v4.Track
4 | import dev.schlaubi.mikmusic.api.types.QueuedTrack
5 | import kotlinx.serialization.KSerializer
6 | import kotlinx.serialization.descriptors.PrimitiveKind
7 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
8 | import kotlinx.serialization.descriptors.SerialDescriptor
9 | import kotlinx.serialization.encoding.Decoder
10 | import kotlinx.serialization.encoding.Encoder
11 | import kotlinx.serialization.json.Json
12 |
13 | object QueuedTrackJsonSerializer : JsonElementSerializer(QueuedTrack.serializer())
14 | object TrackJsonSerializer : JsonElementSerializer